Entra ID (Microsoft identity platform v2) credential resolution, scopes, certificate client assertions, and the per-run token cache. Usually used indirectly through ::microsoft::graph::api/request.

Credential sources. Per-call opts win over context. When opts carries a primary credential (token, refresh-token, client-secret or private-key), the user and app credentials in context are ignored for that call; only the app identity (client-id, tenant-id, cloud) is still read from context when opts leaves it out, and a per-call refresh-token also borrows the context's client secret or certificate for the same client-id. Without a primary credential in opts, context is used with opts.tenant-id / opts.cloud / opts.scopes layered on top. Within one source the order is: access token, refresh token, certificate, client secret.

Per-call rules (resolve-from). A credential key (token, refresh-token, client-id, client-secret, private-key) that is in opts but blank or null is an error naming the key, never a fallback to context: a tenant whose token is missing must not run on the project's credentials. graph-url or login-url in opts need a primary credential in the same opts, and a per-call refresh token then borrows nothing from context: context credentials never go to a per-call URL. Credentials from opts are marked as secrets (::hot::ctx/mark-secret), so call records mask them; they are never written to context.

Credentialopts keysContext variables
Access tokentokenmicrosoft.access.token
Delegated refresh tokenrefresh-token, client-id (+ client-secret or private-key for confidential apps)microsoft.oauth.refresh-token, microsoft.client.id
App-only, certificateprivate-key, certificate or thumbprint / thumbprint-sha256, client-id, tenant-idmicrosoft.client.private-key, microsoft.client.certificate, microsoft.client.thumbprint, microsoft.client.id, microsoft.tenant.id
App-only, client secretclient-secret, client-id, tenant-idmicrosoft.client.secret, microsoft.client.id, microsoft.tenant.id
National cloudcloud (global, us-gov, us-dod, china), graph-url, login-urlmicrosoft.cloud, microsoft.graph.url, microsoft.login.url

Tenant. tenant-id is a tenant GUID, a verified domain (contoso.onmicrosoft.com), or for delegated sign-in common (default: work, school and personal accounts), organizations or consumers. App-only tokens need a real tenant.

Scopes. App-only tokens are always minted for https://graph.microsoft.com/.default (every application permission an admin consented). Delegated refresh tokens are redeemed for opts.scopes, else the microsoft.scopes context variable, else .default (every delegated permission the user consented); offline_access is always added so Entra returns a new refresh token. Short names (Files.Read.All) are prefixed with the Graph resource in national clouds.

Refresh-token rotation. Entra returns a new refresh token every time one is redeemed. The newest one is kept for the rest of the run (latest-refresh-token); persist it (for example in ::hot::store) and use it next time. Refresh tokens are valid for 90 days (24 hours for single-page apps) and the window slides each time you redeem the newest one.

Token cache. Minted tokens are cached in the run's execution context (::hot::ctx, stored as secrets) under a key derived from the credential identity and the sorted scope set, until expires_in minus 5 minutes. The cache lives for one run. Explicit access tokens are never cached. Every token a token endpoint returns is marked as a secret before it leaves this package.

Values

ASSERTION_LIFETIME_SECONDS

ASSERTION_LIFETIME_SECONDS: Int 600

BLANK_CHECKED_KEYS

BLANK_CHECKED_KEYS: Vec [
  "token",
  "refresh-token",
  "client-id",
  "client-secret",
  "private-key"
]

CACHE_PREFIX

CACHE_PREFIX: Str "microsoft.token-cache."

CLIENT_ASSERTION_TYPE

CLIENT_ASSERTION_TYPE: Str "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

CLIENT_AUTH_KEYS

CLIENT_AUTH_KEYS: Vec [
  "client-secret",
  "private-key",
  "certificate",
  "thumbprint",
  "thumbprint-sha256"
]

CREDENTIAL_KEYS

CREDENTIAL_KEYS: Vec [
  "token",
  "refresh-token",
  "client-id",
  "client-secret",
  "certificate",
  "private-key",
  "thumbprint",
  "thumbprint-sha256",
  "tenant-id",
  "cloud",
  "graph-url",
  "login-url",
  "scopes",
  "retry",
  "client-request-id",
  "headers",
  "api-version"
]

EXPIRY_MARGIN_SECONDS

EXPIRY_MARGIN_SECONDS: Int 300

IDENTITY_KEYS

IDENTITY_KEYS: Vec [
  "client-id",
  "tenant-id",
  "cloud",
  "graph-url",
  "login-url"
]

PRIMARY_KEYS

PRIMARY_KEYS: Vec [
  "token",
  "refresh-token",
  "client-secret",
  "private-key"
]

ROTATION_PREFIX

ROTATION_PREFIX: Str "microsoft.refresh-rotation."

SECRET_KEYS

SECRET_KEYS: Vec [
  "token",
  "refresh-token",
  "client-secret",
  "private-key"
]

TOKEN_RESPONSE_SECRETS

TOKEN_RESPONSE_SECRETS: Vec [
  "access_token",
  "refresh_token",
  "id_token"
]

URL_KEYS

URL_KEYS: Vec [
  "graph-url",
  "login-url"
]

Functions

access-token

fn (): Str
fn (opts: Map): Str

Resolve a Graph access token from opts and context. Returns the bearer token string, or a TokenError when no credentials are configured or the token request fails. Minted tokens are cached for the run.

token ::microsoft::graph::auth/access-token()

// Per call: one customer tenant, app-only with a certificate
token2 ::microsoft::graph::auth/access-token({
  tenant-id: "contoso.onmicrosoft.com",
  client-id: "11111111-2222-3333-4444-555555555555",
  private-key: ::hot::ctx/get("microsoft.client.private-key"),
  certificate: ::hot::ctx/get("microsoft.client.certificate")
})

app-scopes

fn (graph: Str): Vec

The scope app-only tokens are minted for: ["{graph}/.default"].

assertion-claims

fn (client-id: Str, audience: Str, now: Int): Map

The RFC 7523 claims of a client assertion (pure): aud (the token endpoint), iss = sub = the client id, a fresh jti, nbf = iat = now, exp 10 minutes later.

assertion-header

fn (thumbprints: Map): Map

The JOSE header of a certificate client assertion (pure): {alg: "RS256", typ: "JWT", x5t?, "x5t#S256"?}. thumbprints is {sha1?, sha256?} in hex; x5t is set from SHA-1 and x5t#S256 from SHA-256. Entra identifies the certificate by either.

authorize-endpoint

fn (login: Str, tenant: Str): Str

The v2 authorization endpoint for a tenant: {login}/{tenant}/oauth2/v2.0/authorize.

bearer

fn (creds: Map, scopes: Vec): Map

Resolve a bearer token for resolved credentials and scopes, using the per-run cache. Returns {token, cache-key, cached} (cached is true when the token came from the cache, so a 401 should evict and retry), or a TokenError.

blank-credential

fn (opts: Map): Str?

The first credential key opts has with a blank or null value, else null (pure).

build-client-credentials-body

fn (creds: Map, scopes: Vec, audience: Str): Map

The client_credentials grant form body (pure apart from signing a certificate assertion).

build-refresh-body

fn (creds: Map, refresh-token: Str, scopes: Vec, audience: Str): Map

The refresh_token grant form body (pure apart from signing a certificate assertion).

cache-entry

fn (tokens: Map, now: Int): Map

The cached form of a token response: {access-token, expires-at} (epoch ms), expiring EXPIRY_MARGIN_SECONDS (5 minutes) early.

certificate-thumbprints

fn (certificate-pem: Str): Map

The SHA-1 and SHA-256 thumbprints (lower-case hex) of a PEM certificate's DER encoding: {sha1, sha256} (pure). Returns a TokenError when the PEM has no CERTIFICATE block.

certificate-thumbprints-from

fn (source: Map): Map

The {sha1?, sha256?} thumbprints for a credential source: computed from certificate (PEM) when set, else thumbprint (SHA-1 hex) and thumbprint-sha256. Pure.

client-assertion

fn (creds: Map, audience: Str): Str

Build a signed client assertion for certificate credentials: creds is a resolved certificate credential (or any Map with client-id, private-key and thumbprints), audience the token endpoint URL.

::auth ::microsoft::graph::auth
assertion ::auth/client-assertion({
  client-id: "11111111-2222-3333-4444-555555555555",
  private-key: private-key-pem,
  thumbprints: ::auth/certificate-thumbprints(certificate-pem)
}, "https://login.microsoftonline.com/contoso.onmicrosoft.com/oauth2/v2.0/token")

client-auth-from

fn (source: Map): Map?

How a confidential client proves itself at the token endpoint (pure): {method: "certificate", private-key, thumbprints}, {method: "secret", client-secret}, or null for a public client. A certificate wins over a secret.

client-auth-identity

fn (auth: Map?): Str

client-auth-params

fn (client-auth: Map?, client-id: Str, audience: Str): Map

The client-authentication form fields for client-auth (pure apart from signing): client_secret, or client_assertion_type + client_assertion for a certificate, or {} for a public client. audience is the token endpoint.

client-credentials-token

fn (creds: Map, scopes: Vec): Map

Mint an app-only token for resolved certificate / client-secret credentials (no cache). Returns the token response or a TokenError.

cloud-config

fn (source: Map): Map

The endpoints for a credential source (pure): {name, graph, login}. cloud names a preset (global default, us-gov for GCC High, us-dod, china); graph-url / login-url override either root (a proxy, or a cloud without a preset). Returns an Err for an unknown cloud name.

::microsoft::graph::auth/cloud-config({cloud: "us-gov"})
// {name: "us-gov", graph: "https://graph.microsoft.us", login: "https://login.microsoftonline.us"}

compact

fn (m: Map): Map

Drop null and blank-string values from a Map.

credentials-from

fn (source: Map): Map

Turn a credential source Map (the opts shape) into resolved credentials (pure; no network): {kind: "token" | "refresh" | "certificate" | "client-secret", cloud, ...}. Returns a TokenError when nothing usable is set, the client id is missing, or an app-only credential names common/organizations/consumers instead of a tenant.

ctx-source

fn (): Map

The credential source read from context, in the same shape as opts, with blank values dropped.

Context Vars: microsoft.access.token, microsoft.oauth.refresh-token, microsoft.client.id, microsoft.client.secret, microsoft.client.certificate, microsoft.client.private-key, microsoft.client.thumbprint, microsoft.tenant.id, microsoft.cloud, microsoft.graph.url, microsoft.login.url

delegated-scopes

fn (opts: Map, graph: Str): Vec

The scopes a delegated refresh-token redemption asks for (pure apart from reading microsoft.scopes): opts.scopes, else the microsoft.scopes context variable, else {graph}/.default, always with offline_access so a new refresh token comes back.

Context Vars: microsoft.scopes

encode-segment

fn (value: Map): Str

evict-token

fn (cache-key: Str?): Bool

Drop a cached token (e.g. after a 401). No-op for a null key.

expand-scope

fn (scope: Str, graph: Str): Str

Expand one scope for a Graph root (pure). OpenID scopes (openid, profile, email, offline_access) and full URIs pass through; .default becomes {graph}/.default; a short permission name (Files.Read.All) stays short in the global cloud (Entra reads it as a Graph permission) and is prefixed with the Graph root elsewhere, as national clouds require.

first-line

fn (text: Any): Str?

has-primary-credential

fn (source: Map): Bool

True when source carries a primary credential: token, refresh-token, client-secret or private-key.

is-blank-value

fn (value: Any): Bool

True for null, "", and whitespace-only strings (context placeholders wire "" for unset env vars).

is-fresh

fn (entry: Any, now: Int): Bool

True when a cache entry exists and has not reached its (margin-adjusted) expiry.

is-multi-tenant-segment

fn (tenant: Str?): Bool

True for common, organizations and consumers (audiences, not tenants).

latest-refresh-token

fn (): Str?
fn (opts: Map): Str?

The newest refresh token for the call's delegated credentials: the rotated token Entra returned when it was last redeemed in this run, else the configured one. Null for other credential kinds. Persist it after a run that used a refresh token.

::microsoft::graph::users/me({refresh-token: stored, client-id: app-id})
newest ::microsoft::graph::auth/latest-refresh-token({refresh-token: stored, client-id: app-id})
::hot::store/put(tokens, user-id, newest)

mark-secrets

fn (m: Map, keys-to-mark: Vec): Map

Mark the values of keys-to-mark in m as secrets and return m unchanged.

mark-tokens

fn (tokens: Map): Map

Mark the access, refresh and id tokens of a token response as secrets and return the response unchanged.

mark-values

fn (values: Vec): Vec

Register the non-blank values as secrets for this run's call records (::hot::ctx/mark-secret).

merge-sources

fn (opts: Map, ctx: Map): Map

Pick the credential source for a call (pure; resolve-credentials passes the live context):

  • opts without a primary credential: context with opts on top (so opts.tenant-id or opts.cloud retarget the context's app)
  • opts with a primary credential: opts, plus the context's app identity (client-id, tenant-id, cloud, graph-url, login-url) where opts omits it; a per-call refresh-token with no client credential or URL of its own also borrows the context's client secret or certificate when the client id is the same

mint-token

fn (creds: Map, scopes: Vec): Map

Mint a fresh token response for resolved credentials (no cache). A delegated refresh records the rotated refresh token for the run.

missing-client-id

fn (what: Str): TokenError

normalize-scopes

fn (scopes: Any): Vec
fn (scopes: Any, graph: Str): Vec

Normalise a scope value to a de-duplicated Vec (pure). Accepts null (empty), a Vec, or a Str separated by spaces or commas; see expand-scope for the forms.

::microsoft::graph::auth/normalize-scopes("Files.Read, Sites.Read.All offline_access", "https://graph.microsoft.com")
// ["Files.Read", "Sites.Read.All", "offline_access"]

normalize-thumbprint

fn (thumbprint: Any): Str?

A hex thumbprint as the Entra portal shows it (upper case, maybe with : or spaces) in lower-case hex, or null for blank input.

now-ms

fn (): Int

parse-int

fn (value: Any): Int?

Parse an Int from an Int or a decimal-digit Str (surrounding whitespace allowed); null for anything else. Never halts on bad input.

pem-body

fn (pem: Str, label: Str): Str?

The base64 body of the first PEM block with the given label (e.g. "CERTIFICATE"), whitespace removed; null when absent (pure).

pick

fn (m: Map, ks: Vec): Map

redeem-refresh-token

fn (creds: Map, refresh-token: Str, scopes: Vec): Map

Redeem a refresh token for resolved refresh credentials and scopes (no cache). Returns Entra's token response, including the rotated refresh_token, or a TokenError.

resolve-credentials

fn (): Map
fn (opts: Map): Map

Resolve the credentials for a call from opts and context (see the namespace doc for precedence and the per-call rules). Credentials from opts are marked as secrets first, and never written to context.

resolve-from

fn (opts: Map, ctx: Map): Map

The one resolver, pure: credentials for a call from opts first, then ctx (a source Map, see ctx-source). An Err when a credential key is in opts but blank or null, or when graph-url / login-url is in opts without a primary credential; otherwise as credentials-from(merge-sources(opts, ctx)). Exposed for testing; resolve-credentials feeds it the context.

rotation-key

fn (creds: Map): Str

The context key under which the newest refresh token for an original refresh token is kept for the run.

sign-assertion

fn (header: Map, claims: Map, private-key-pem: Str): Str

Sign a client assertion JWT with RS256 (Entra's documented PS256 is not available in Hot; RS256 is accepted, and is what MSAL sends with a SHA-1 thumbprint). header and claims are Maps; returns the compact JWT or a TokenError.

thumbprint-header

fn (hex-thumbprint: Str, expected-length: Int): Str

Base64url-encode a hex thumbprint for the JWT x5t (SHA-1) or x5t#S256 (SHA-256) header (pure). Returns an Err for malformed hex.

token-cache-key

fn (creds: Map, scopes: Vec): Str?

The context key a minted token is cached under, or null for explicit access tokens (pure). Keyed by credential kind, cloud, tenant, client id, the client credential's fingerprint (a hash, never the secret), the refresh token's hash for delegated tokens, and the sorted scope set.

token-error

fn (message: Str): TokenError
fn (prefix: Str, payload: Any): TokenError

Build a TokenError. The 1-arity form wraps a plain message; the 2-arity form prefixes the payload of a failed ::oauth2 call ({status, body} or a transport error).

token-request

fn (url: Str, body: Any, prefix: Str): Map

token-scopes

fn (creds: Map, opts: Map): Vec

The scopes a token for resolved credentials is requested with: .default for app-only, delegated-scopes for refresh tokens, [] for explicit tokens.

token-url

fn (login: Str, tenant: Str): Str

The v2 token endpoint for a tenant: {login}/{tenant}/oauth2/v2.0/token.

trim-slash

fn (url: Str): Str

url-without-credential

fn (opts: Map): Str?

The first of graph-url / login-url that opts sets without a primary credential in the same opts, else null (pure).

Types

TokenError

TokenError type {
    message: Str,
    status: Int?,
    error-code: Str?,
    error-description: Str?,
    error-codes: Vec?,
    trace-id: Str?,
    correlation-id: Str?,
    body: Any?
}

A credential or token-endpoint failure. message is readable, e.g. "microsoft-graph: token request failed: invalid_grant (AADSTS70008: The provided authorization code or refresh token has expired due to inactivity.)". error-code and error-description carry Entra's OAuth error / error_description (first line only), error-codes the AADSTS numbers, and trace-id / correlation-id identify the request for Microsoft support.