---
title: Acquire an outbound access token
summary: Returns a cached OAuth client-credentials access token, refreshing it from the configured token endpoint when it is missing or near expiry.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.AuthService
reference_type: package
operation_id: OP-13
trigger: Consumer code calls `IAccessTokenClient.GetToken()` before calling a downstream API.
---

# Acquire an outbound access token

Returns a cached OAuth client-credentials access token, refreshing it from the configured token endpoint when it is missing or near expiry.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-13 |
| **Trigger** | Consumer code calls `IAccessTokenClient.GetToken()` before calling a downstream API. |
| **Inputs** | `TokenUrl`, `ClientId`, `ClientSecret`, `GrantType`, `Scopes` (all code-set properties), and the current `HttpContext` only on the refresh path. |
| **Outputs** | An OAuth access token string, returned from cache or from a fresh refresh; there is no post-refresh validity check before returning or caching it**. |
| **Primary components** | `OktaTokenClient.GetToken()` |
| **Entry point** | `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs` |

## Steps

1. If the cached token is missing or not `IsValidAndNotExpiring` (empty token, or expiring within 30 seconds), call `GetNewAccessToken()` ([BR-AUTH-017](../business-rules.md#br-auth-017-cached-okta-token-is-reused-until-near-expiry))
2. `GetNewAccessToken`: create a new `HttpClient`; set `Authorization: Basic base64(ClientId:ClientSecret)`; POST a form body of `grant_type`, `scope`, and `redirect_uri = {Request.Scheme}://{Request.Host}` to `TokenUrl` ([BR-AUTH-018](../business-rules.md#br-auth-018-outbound-token-requests-use-http-basic-client-credentials))
3. `response.EnsureSuccessStatusCode()`, which throws on a non-2xx response; deserialize with Newtonsoft and set `ExpiresAt = UtcNow + ExpiresIn` seconds.
4. Return `AccessToken`.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
    Start([GetToken called]) --> Valid{Cached token valid and not expiring?}
    Valid -->|Yes| Return([Return cached AccessToken])
    Valid -->|No| New[GetNewAccessToken]
    New --> Post["POST TokenUrl with Basic auth + form body"]
    Post --> Success{2xx response?}
    Success -->|Yes| Cache["Deserialize, cache token, set ExpiresAt"]
    Success -->|No| Throw([EnsureSuccessStatusCode throws])
    Cache --> Return
```

## Failure and edge-case behavior

- A non-2xx response from the token endpoint throws from `EnsureSuccessStatusCode()`.
- `HttpContext` is only read when building `redirect_uri` on the refresh path (step 2); a cache hit (step 1) returns without touching `HttpContext` at all. Calling this outside a request only throws `NullReferenceException` when a refresh is actually needed*.
- After a successful refresh, the deserialized token value is cached and returned without any additional validity check — the response is trusted as-is once `EnsureSuccessStatusCode()` passes.

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## Sources
- `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs`
