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.
| Credential | opts keys | Context variables |
|---|---|---|
| Access token | token | microsoft.access.token |
| Delegated refresh token | refresh-token, client-id (+ client-secret or private-key for confidential apps) | microsoft.oauth.refresh-token, microsoft.client.id |
| App-only, certificate | private-key, certificate or thumbprint / thumbprint-sha256, client-id, tenant-id | microsoft.client.private-key, microsoft.client.certificate, microsoft.client.thumbprint, microsoft.client.id, microsoft.tenant.id |
| App-only, client secret | client-secret, client-id, tenant-id | microsoft.client.secret, microsoft.client.id, microsoft.tenant.id |
| National cloud | cloud (global, us-gov, us-dod, china), graph-url, login-url | microsoft.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.
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.
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):
optswithout a primary credential: context withoptson top (soopts.tenant-idoropts.cloudretarget the context's app)optswith a primary credential:opts, plus the context's app identity (client-id,tenant-id,cloud,graph-url,login-url) whereoptsomits it; a per-callrefresh-tokenwith 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.