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

  1. Make a PKCE verifier, store it with your state, and redirect the user to authorize-url(...). offline_access is added for you, so Entra returns a refresh token.
  2. In your callback, exchange-code(...) turns ?code= into tokens.
  3. Store refresh_token per user, then pass it per call as {refresh-token, client-id} opts (plus client-secret for a web app), or set microsoft.oauth.refresh-token for a single-user app.
  4. 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

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 sent
  • code-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