Entra ID (Microsoft identity platform v2) OAuth helpers: the
delegated authorization-code flow with PKCE and offline_access,
refresh-token redemption (with rotation), and app-only client
credentials. Built on hot.dev/oauth2.
Delegated flow
- Make a PKCE verifier, store it with your
state, and redirect the user toauthorize-url(...).offline_accessis added for you, so Entra returns a refresh token. - In your callback,
exchange-code(...)turns?code=into tokens. - Store
refresh_tokenper user, then pass it per call as{refresh-token, client-id}opts (plusclient-secretfor a web app), or setmicrosoft.oauth.refresh-tokenfor a single-user app. - Entra rotates refresh tokens: every redemption returns a new one.
After a run, persist
::microsoft::graph::auth/latest-refresh-token(opts).
::oauth ::microsoft::graph::oauth
verifier ::oauth/pkce-verifier()
url ::oauth/authorize-url(client-id, "https://app.example.com/oauth/microsoft",
["Files.Read.All", "Sites.Read.All"], state, {code-verifier: verifier})
// callback handler:
tokens ::oauth/exchange-code(client-id, null, request.query.code,
"https://app.example.com/oauth/microsoft", {code-verifier: verifier})
tokens.refresh_token
Consent. Files.Read, Files.ReadWrite, Sites.Read.All and
User.Read delegated permissions are user-consentable in most
tenants; *.All application permissions and Sites.Selected always
need a tenant admin. admin-consent-url builds the admin-consent
link for app-only permissions.
Functions
admin-consent-url
fn (client-id: Str, redirect-uri: Str, state: Str): Str
fn (client-id: Str, redirect-uri: Str, state: Str, opts: Map): Str
The admin-consent URL for an app's configured application
permissions ({login}/{tenant}/v2.0/adminconsent, scope
{graph}/.default). A tenant admin opens it once per tenant;
Entra then redirects to redirect-uri with admin_consent=True and
tenant. opts.tenant-id defaults to organizations.
authorize-scopes
fn (scopes: Any, graph: Str, offline: Bool): Str
The scope value authorize-url sends (pure): the normalised scopes plus offline_access (unless offline is false), space-separated.
authorize-url
fn (client-id: Str, redirect-uri: Str, scopes: Str | Vec, state: Str): Str
fn (client-id: Str, redirect-uri: Str, scopes: Str | Vec, state: Str, opts: Map): Str
Build the Entra sign-in URL for the delegated authorization-code
flow. scopes is a Vec or space/comma-separated Str of delegated
permissions ("Files.Read.All Sites.Read.All"); offline_access is
added unless opts.offline is false. opts.tenant-id (default the
microsoft.tenant.id context variable, else common) and
opts.cloud pick the authority. Use PKCE (code-verifier) for
every client; it is required for public and single-page apps.
verifier ::microsoft::graph::oauth/pkce-verifier()
url ::microsoft::graph::oauth/authorize-url("11111111-2222-3333-4444-555555555555",
"https://app.example.com/oauth/microsoft", "Files.Read.All User.Read", "csrf-123",
{code-verifier: verifier, prompt: "select_account"})
build-authorize-params
fn (opts: Map): Map
The Entra-specific authorization parameters for authorize-url
(pure). Options (all optional):
code-verifier- a PKCE verifier; its S256 challenge is sentcode-challenge- a precomputed S256 challenge (instead)prompt-"select_account","consent","login","none"login-hint- pre-fill the account (UPN or email)domain-hint- skip home-realm discovery ("consumers","organizations", or a domain)response-mode-"query"(default) or"form_post"extra- any other parameters, merged last
client-credentials
fn (tenant-id: Str, client-id: Str, opts: Map): Map
Mint an app-only token (client credentials grant, no cache) for
{graph}/.default. opts needs client-secret, or private-key
plus certificate (or thumbprint), and takes cloud. Returns the
token response or a TokenError. Usually you let
::microsoft::graph::api/request do this for you.
endpoints
fn (opts: Map): Map
exchange-code
fn (client-id: Str, client-secret: Str?, code: Str, redirect-uri: Str): Map
fn (client-id: Str, client-secret: Str?, code: Str, redirect-uri: Str, opts: Map): Map
Exchange an authorization code for tokens. Returns Entra's token
response (access_token, expires_in, refresh_token, scope,
token_type, id_token when openid was requested; the tokens are
marked as secrets), or a ::microsoft::graph::auth/TokenError. opts.code-verifier
completes a PKCE flow; opts.tenant-id / opts.cloud must match
authorize-url. Pass a null client-secret for public clients;
opts.private-key + opts.certificate authenticate a confidential
client with a certificate instead of a secret.
granted-scopes
fn (tokens: Map): Vec
The scopes a token response says were granted (scope split on spaces). Check it after consent: users and admins can decline permissions.
missing-scopes
fn (tokens: Map, required: Str | Vec): Vec
The required scopes (Vec or Str) a token response did not grant, compared case-insensitively and ignoring a https://graph.microsoft.com/ prefix. Empty when everything was granted.
refresh-access-token
fn (client-id: Str, refresh-token: Str): Map
fn (client-id: Str, refresh-token: Str, opts: Map): Map
Redeem a refresh token (no cache) and return Entra's token response:
access_token, expires_in, scope, and the rotated
refresh_token; store the new one, it replaces the old. opts takes
client-secret (or private-key + certificate) for confidential
apps, scopes, tenant-id and cloud. A revoked or expired refresh
token yields error-code: "invalid_grant": send the user through
consent again.
tokens ::microsoft::graph::oauth/refresh-access-token(client-id, stored-refresh-token, {client-secret: secret})
::hot::store/put(tokens-store, user-id, tokens.refresh_token)
tenant-of
fn (opts: Map): Str