← All @molecule/* packages · App templates
@molecule/api-oauth-microsoftProvider bond · auth · API (Node) · v1.0.1 · Apache-2.0
Microsoft Identity Platform OAuth provider for molecule.dev.
npm install @molecule/api-oauth-microsoftnpm · Source on GitHub · Implements @molecule/api-oauth
@molecule/api-oauth-microsoft is a provider bond on the API (Node) side: it implements the auth core interface (@molecule/api-oauth) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
import { bond } from '@molecule/api-bond'
import * as microsoft from '@molecule/api-oauth-microsoft'
bond('oauth', microsoft.serverName, microsoft)Works with: @molecule/api-bond, @molecule/api-http, @molecule/api-oauth, @molecule/api-secrets
Secrets: OAUTH_MICROSOFT_CLIENT_ID, OAUTH_MICROSOFT_CLIENT_SECRET, OAUTH_MICROSOFT_TENANT_ID (optional)
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Microsoft Identity Platform OAuth provider for molecule.dev.
Implements the @molecule/api-oauth contract (serverName + verify)
for compatibility with the existing OAuth bond wiring, plus a richer
OAuthProvider surface (getAuthorizationUrl,
exchangeCodeForTokens, getUserInfo, refreshAccessToken,
verifyIdToken) for apps that need OpenID Connect, refresh-token
rotation, or Microsoft Graph access (Outlook, OneDrive, Teams).
Sign in to the Microsoft Entra admin center.
Navigate to Applications → App registrations → New registration.
OAUTH_MICROSOFT_TENANT_ID=common). For
single-tenant apps, choose Accounts in this organizational
directory only and set OAUTH_MICROSOFT_TENANT_ID to the
directory tenant id.http://localhost:3000 for development, your production
origin otherwise — must match the redirectUri passed to
getAuthorizationUrl and exchangeCodeForTokens).After registration:
Copy the Application (client) ID to your API's
OAUTH_MICROSOFT_CLIENT_ID environment variable.
Open Certificates & secrets → Client secrets → New client
secret, then copy the secret value (NOT the secret id) to
your API's OAUTH_MICROSOFT_CLIENT_SECRET environment variable.
For single-tenant apps, also set
OAUTH_MICROSOFT_TENANT_ID to the directory tenant id.
Under API permissions, add the delegated Microsoft Graph
permissions you need. The defaults granted by the
openid email profile User.Read scope suffice for sign-in.
Restart your API and/or rebuild your app so they pick up the environment variables.
Your users should now be able to log in via Microsoft!
import { bond } from '@molecule/api-bond'
import * as microsoft from '@molecule/api-oauth-microsoft'
bond('oauth', microsoft.serverName, microsoft)
provider
npm install @molecule/api-oauth-microsoft @molecule/api-bond @molecule/api-http @molecule/api-oauth @molecule/api-secrets
JsonWebKeyRFC 7517 JSON Web Key (RS256 subset).
interface JsonWebKey {
kty: string
use?: string
kid: string
n: string
e: string
alg?: string
x5c?: string[]
}
JwksResponseDiscovery response shape for the /discovery/v2.0/keys endpoint.
interface JwksResponse {
keys: JsonWebKey[]
}
MicrosoftIdTokenClaimsVerified ID token claims returned by verifyIdToken.
Only the validated subset of OpenID claims is exposed — the full raw
payload is included as claims for callers that need additional
fields, but only after signature/issuer/audience/expiry checks pass.
interface MicrosoftIdTokenClaims {
/** Subject identifier (Microsoft user object id). */
sub: string
/** Issuer URL. */
iss: string
/** Audience (the OAuth client ID). */
aud: string
/** Expiry (epoch seconds). */
exp: number
/** Issued-at (epoch seconds). */
iat: number
/** Email address, when present. */
email?: string
/** Preferred username (often the UPN), when present. */
preferred_username?: string
/** Display name, when present. */
name?: string
/** Tenant id from the token. */
tid?: string
/** Object id from the token. */
oid?: string
/** Full validated payload for additional non-standard claims. */
claims: Record<string, unknown>
}
MicrosoftOAuthConfigOptional configuration for the Microsoft OAuth provider.
tenantId overrides the path segment in Microsoft endpoints. Defaults
to "common" (multi-tenant + personal accounts). For single-tenant
apps, pass the directory tenant GUID.
interface MicrosoftOAuthConfig {
/**
* Microsoft tenant identifier. Defaults to `"common"`.
*
* Common values: `"common"`, `"organizations"`, `"consumers"`, or a
* tenant GUID such as `"00000000-0000-0000-0000-000000000000"`.
*/
tenantId?: string
/** Override OAuth client ID. Defaults to `process.env.OAUTH_MICROSOFT_CLIENT_ID`. */
clientId?: string
/** Override OAuth client secret. Defaults to `process.env.OAUTH_MICROSOFT_CLIENT_SECRET`. */
clientSecret?: string
/**
* Default OAuth scopes when none are passed to `getAuthorizationUrl`.
* Defaults to `'openid email profile User.Read'`.
*/
defaultScope?: string
}
MicrosoftTokenSetToken response from the Microsoft /token endpoint.
Microsoft rotates refresh tokens — always persist whatever
refresh_token value is returned, replacing the previous one.
interface MicrosoftTokenSet {
/** Bearer access token used for Microsoft Graph requests. */
accessToken: string
/** OpenID Connect ID token (JWT) for `verifyIdToken`. */
idToken?: string
/** Refresh token (rotated by Microsoft on every refresh). */
refreshToken?: string
/** Token type (typically `"Bearer"`). */
tokenType: string
/** Lifetime of `accessToken` in seconds, when reported. */
expiresIn?: number
/** Granted scope string (space-delimited). */
scope?: string
}
OAuthAuthorizeUrlParamsParameters for building a provider authorization (initiation) URL — the URL the user's browser is redirected to so the provider can authenticate them and send back an authorization code.
interface OAuthAuthorizeUrlParams {
/**
* Absolute URI the provider should redirect the user back to after
* authorization (the app origin, optionally with a path). When omitted,
* the builder leaves `redirect_uri` off the URL so the provider falls
* back to its registered callback URL.
*/
redirectUri?: string
/**
* The CSRF `state` parameter bound to the initiating session (stored in
* an httpOnly cookie by the initiation endpoint and validated by the
* login handler on callback).
*/
state: string
/**
* PKCE code challenge derived (S256) from the per-session code verifier.
* Omit only for providers that do not support PKCE.
*/
codeChallenge?: string
/**
* PKCE challenge method. Always prefer `'S256'`; `'plain'` exists only
* for providers that cannot hash.
*/
codeChallengeMethod?: 'S256' | 'plain'
}
OAuthProviderMicrosoft Identity Platform OAuth provider surface.
Higher-level than the core OAuthVerifier contract — exposes the full
OAuth 2.0 / OpenID Connect lifecycle (auth URL, token exchange,
refresh, id-token verification) needed by apps that integrate Outlook,
OneDrive, Teams, or any Graph API.
interface OAuthProvider {
/** OAuth server identifier (`"microsoft"`). */
readonly serverName: string
/**
* Build the authorization URL the user is redirected to.
* @param params - Authorization request parameters.
* @returns Fully-qualified URL to begin the OAuth flow.
*/
getAuthorizationUrl(params: { state: string; redirectUri: string; scope?: string }): string
/**
* Exchange an authorization code for an access/id/refresh token set.
*
* When the authorization request carried a PKCE `code_challenge`, the
* matching `codeVerifier` MUST be supplied — the Microsoft identity
* platform requires `code_verifier` at redemption for such codes and
* otherwise rejects with `invalid_grant` (AADSTS501481).
*
* @param code - Authorization code returned by Microsoft.
* @param redirectUri - Redirect URI matching the authorization request.
* @param codeVerifier - PKCE code verifier matching the authorization
* request's `code_challenge`, when PKCE was used.
* @returns The token set.
*/
exchangeCodeForTokens(
code: string,
redirectUri: string,
codeVerifier?: string,
): Promise<MicrosoftTokenSet>
/**
* Fetch normalized user info from Microsoft Graph (`/v1.0/me`).
* @param accessToken - Access token previously obtained.
* @returns Normalized user info.
*/
getUserInfo(accessToken: string): Promise<OAuthUserInfo>
/**
* Use a refresh token to obtain a fresh access/id/refresh token set.
*
* Microsoft rotates refresh tokens — the returned token set's
* `refreshToken` (when present) supersedes the input token.
* @param refreshToken - Existing refresh token.
* @returns New token set.
*/
refreshAccessToken(refreshToken: string): Promise<MicrosoftTokenSet>
/**
* Verify a Microsoft-issued ID token (RS256), validating signature
* via JWKS plus `iss`, `aud`, and `exp` claims.
*
* JWKS is fetched from the discovery endpoint and cached in memory
* for one hour.
*
* @param idToken - The compact JWS string to verify.
* @returns Validated claims.
*/
verifyIdToken(idToken: string): Promise<MicrosoftIdTokenClaims>
}
OAuthUserInfoNormalized user info returned by OAuth providers.
Matches the shape used across bonds — id/email/name/picture — derived
from OAuthUserProps so consumers do not need provider-specific shapes.
interface OAuthUserInfo {
/** Unique identifier from the provider. */
id: string
/** User's email address, when available. */
email?: string
/** User's display name, when available. */
name?: string
/** URL of the user's profile picture, when available. */
picture?: string
}
OAuthUserPropsThe properties returned when verifying an OAuth code.
interface OAuthUserProps {
/**
* An alphanumeric username derived from the OAuth provider.
*
* Format: `{provider_username}@{provider_name}`
*/
username: string
/**
* The user's display name from the OAuth provider.
*/
name?: string
/**
* The user's short biography / description from the OAuth provider
* (e.g. GitHub's `bio`, GitLab's `bio`, X's `description`). Omitted when
* the provider exposes no such field.
*/
bio?: string
/**
* URL of the user's profile image from the OAuth provider (e.g. Google's
* `picture`, GitHub/GitLab's `avatar_url`, X's `profile_image_url`).
* Omitted when the provider exposes none (Apple never does; Microsoft
* Graph only serves photos as a binary endpoint behind an extra scope).
*/
avatar?: string
/**
* The user's email address from the OAuth provider.
*/
email?: string
/**
* Whether the OAuth provider has affirmatively verified that the user
* controls this `email` mailbox.
*
* `true` MUST mean the provider proved mailbox ownership (e.g. Google's
* `email_verified`, Apple's `email_verified` ID-token claim). When the
* provider exposes no trustworthy verification signal in the profile data
* the verifier fetched, this MUST be `false`/`undefined` (never optimistically
* `true`) — consumers treat only an explicit `true` as verified.
*
* Consumers (e.g. the user resource's `logInOAuth` handler) use this to
* decide whether a provider-supplied email may be trusted over an existing,
* unverified local account — preventing an unverified squatter from blocking
* the verified mailbox owner.
*/
emailVerified?: boolean
/**
* The OAuth server identifier (e.g., 'github', 'google', 'twitter').
*/
oauthServer: string
/**
* Unique identifier for the user from the OAuth provider.
*/
oauthId: string
/**
* Raw user data from the OAuth provider.
*/
oauthData: Record<string, unknown>
}
OAuthAuthorizeUrlBuilderBuilds the provider's authorization URL for OAuth initiation
(GET /users/oauth/:provider → 302 to this URL). Implementations embed
their own client id, scopes, and authorize endpoint so no consumer ever
hardcodes provider knowledge.
type OAuthAuthorizeUrlBuilder = (params: OAuthAuthorizeUrlParams) => string | null
OAuthVerifierExchanges an OAuth authorization code for user profile information.
Implementations call the provider's token and user-info endpoints,
then return normalized OAuthUserProps for account creation or login.
Returning null means the provider AFFIRMATIVELY rejected the code
(e.g. GitHub's bad_verification_code, an expired/forged code) — the
consumer (logInOAuth) surfaces that as a clean 403 "verification
failed". A thrown error means an infrastructure failure (network,
provider outage) and surfaces as a 500. Implementations MUST NOT throw
for a rejected code — that would misreport a client mistake (or an
attack) as a server fault.
type OAuthVerifier = (
code: string,
codeVerifier?: string,
redirectUri?: string,
) => Promise<OAuthUserProps | null>
MicrosoftOAuthCodeRejectedErrorThrown by exchangeCodeForTokens when the Microsoft identity platform
v2.0 token endpoint AFFIRMATIVELY rejects the authorization code or PKCE
verifier — HTTP 400 with {"error":"invalid_grant"} (e.g. AADSTS70008
expired/already-redeemed code, AADSTS501481 code_verifier mismatch).
This is a client-side failure (or an attack), NOT an infrastructure
fault: verify catches this error and returns null per the
OAuthVerifier contract so the consumer responds with a clean 403
"verification failed" instead of a misleading 500.
allowedIssuers(tenantId, tokenTid)Acceptable issuer URLs for a given Microsoft tenant.
The configured tenantId decides whether the token's own tid may
resolve the accepted issuer:
tid is NEVER used to widen the accepted
issuer set — Microsoft's public-cloud signing keys are shared across all
tenants, so a validly-signed token from a different tenant would
otherwise pass a single-tenant pin (a cross-tenant authentication
bypass). The caller's separate tid-vs-config check (see
verifyMicrosoftIdToken) is what enforces the tenant; this function does
not silently trust an attacker-supplied tid.common / organizations / consumers):
Microsoft's v2.0 issuer is the templated form
https://login.microsoftonline.com/{tid}/v2.0, where {tid} is the
signing tenant. Here accepting the token's tid-derived issuer IS the
documented contract (the app intentionally allows any directory), so it
is permitted.function allowedIssuers(tenantId: string, tokenTid?: string): string[]
tenantId — The configured tenant id ("common" by default).tokenTid — The tid claim from the token, when present.Returns: A list of acceptable iss values.
clearJwksCache()Clear the in-memory JWKS cache (test-only / forced refresh).
function clearJwksCache(): void
createMicrosoftProvider(overrides)Build a Microsoft OAuth provider with optional config overrides.
function createMicrosoftProvider(overrides?: MicrosoftOAuthConfig): OAuthProvider
overrides — Optional configuration overrides.Returns: A typed OAuthProvider.
getAuthorizeUrl(params)Builds the Microsoft authorization URL for OAuth initiation
(GET /users/oauth/:provider 302s the browser here) per the core
OAuthAuthorizeUrlBuilder contract. Embeds this bond's client id,
default scopes (openid email profile User.Read — exactly what
verify's Microsoft Graph /me call needs), and the tenant-derived
authorize endpoint
(https://login.microsoftonline.com/<tenant>/oauth2/v2.0/authorize,
tenant from OAUTH_MICROSOFT_TENANT_ID, default common), so no
consumer hardcodes Microsoft knowledge.
Includes the caller's CSRF state, response_mode=query (the v2.0
auth-code flow's query-string callback), and — when a codeChallenge
is supplied — the PKCE code_challenge + code_challenge_method
(default S256). PKCE with a confidential client is supported and
recommended by the Microsoft identity platform: the client secret and
code_verifier are complementary, and exchangeCodeForTokens MUST
then be given the matching verifier (see AADSTS501481).
function getAuthorizeUrl({
redirectUri,
state,
codeChallenge,
codeChallengeMethod,
}: OAuthAuthorizeUrlParams): string | null
params — State, PKCE challenge, and optional redirect URI.Returns: The Microsoft authorize URL, or null when OAUTH_MICROSOFT_CLIENT_ID is unset (unconfigured).
getJwks(tenantId, options)Fetch (and cache) the JWKS document for a Microsoft tenant.
Cache TTL is one hour. Pass force: true (e.g., on a key-id miss)
to bypass the cache and refresh.
function getJwks(
tenantId: string,
options?: { force?: boolean; now?: () => number },
): Promise<JsonWebKey[]>
tenantId — Microsoft tenant identifier.options — Optional overrides.Returns: The list of JWKs from the tenant.
jwksUrlFor(tenantId)Build the JWKS discovery URL for a given Microsoft tenant.
function jwksUrlFor(tenantId: string): string
tenantId — Microsoft tenant identifier (e.g., "common").Returns: The fully-qualified JWKS endpoint URL.
resolveConfig(overrides)Resolve effective configuration by layering an explicit config over environment variables. Pure — does no I/O.
function resolveConfig(overrides?: MicrosoftOAuthConfig): {
tenantId: string
clientId: string
clientSecret: string
defaultScope: string
}
overrides — Partial configuration to merge over env defaults.Returns: The fully-resolved configuration.
sanitizeError(error, secrets)Strip client secrets and refresh tokens from an error before logging or rethrowing. Mutates and returns a new error-like object.
function sanitizeError(error: unknown, secrets: string[]): Error
error — The original error.secrets — Sensitive strings to redact.Returns: A sanitized error suitable for logging or surfacing.
verify(code, codeVerifier, redirectUri)Verifies a Microsoft OAuth code and responds with normalized
OAuthUserProps for compatibility with the core @molecule/api-oauth
OAuthVerifier contract — exchanges the code (passing the PKCE
verifier through to the token endpoint), fetches /me from Microsoft
Graph, and shapes the result.
Returns null (never throws) when Microsoft AFFIRMATIVELY rejects the
code or verifier — the token endpoint's HTTP 400 invalid_grant
(AADSTS70008 expired/redeemed code, AADSTS501481 verifier mismatch) —
so the consumer responds 403 "verification failed". Infrastructure
failures (network, outage, malformed 2xx) still throw and surface as 500.
function verify(
code: string,
codeVerifier?: string,
redirectUri?: string,
): Promise<{
username: string
name: string | undefined
email: string | undefined
emailVerified: false
oauthServer: 'microsoft'
oauthId: string
oauthData: Record<string, unknown>
} | null>
code — The authorization code from the OAuth callback.codeVerifier — PKCE code verifier matching the code_challenge sent by getAuthorizeUrl. Required by Microsoft at redemption whenever the authorization request carried a challenge.redirectUri — The redirect URI used in the authorization request. Falls back to APP_ORIGIN.Returns: Normalized OAuthUserProps, or null when the provider rejected the code.
verifyMicrosoftIdToken(idToken, config, options)Verify a Microsoft-issued ID token end-to-end (signature, issuer, audience, expiry).
function verifyMicrosoftIdToken(
idToken: string,
config: { tenantId: string; audience: string },
options?: { now?: () => number; refreshOnMiss?: boolean },
): Promise<MicrosoftIdTokenClaims>
idToken — Compact JWS.config — Tenant + audience config.options — Optional clock + JWKS refresh hooks (test seams).Returns: Validated claims.
verifyRs256Signature(jwk, signingInput, signature)Verify an RS256 signature against the supplied JWK.
function verifyRs256Signature(jwk: JsonWebKey, signingInput: string, signature: string): boolean
jwk — JSON Web Key with kty: "RSA" and alg: "RS256".signingInput — Concatenated ${header}.${payload} string.signature — Base64url-encoded signature segment.Returns: true if the signature verifies.
DEFAULT_SCOPEDefault OAuth scopes when none are supplied.
const DEFAULT_SCOPE: 'openid email profile User.Read'
GRAPH_ME_URLMicrosoft Graph endpoint for the signed-in user.
const GRAPH_ME_URL: 'https://graph.microsoft.com/v1.0/me'
oauthMicrosoftSecretDefinitionsSecret definitions required by the Microsoft OAuth bond.
const oauthMicrosoftSecretDefinitions: SecretDefinition[]
providerDefault Microsoft OAuth provider, configured from environment.
Use createMicrosoftProvider() with explicit config for tests or
multi-tenant apps that need per-request configuration.
const provider: OAuthProvider
serverNameThe OAuth server identifier for Microsoft.
const serverName: 'microsoft'
Implements @molecule/api-oauth interface.
Setup function to register this provider with the bond system:
import { bond } from '@molecule/api-bond'
import { serverName, verify, getAuthorizeUrl } from '@molecule/api-oauth-microsoft'
export function setupOauthMicrosoft(): void {
bond('oauth', serverName, { serverName, verify, getAuthorizeUrl })
}
Peer dependencies:
@molecule/api-bond ^1.0.1@molecule/api-http ^1.0.1@molecule/api-oauth ^1.0.1@molecule/api-secrets ^1.0.1OAUTH_MICROSOFT_CLIENT_ID (required) — Microsoft application (client) ID
OAUTH_MICROSOFT_CLIENT_SECRET (required) — Microsoft client secret
OAUTH_MICROSOFT_TENANT_ID (optional) — Microsoft directory (tenant) ID
common@molecule/api-bond@molecule/api-http@molecule/api-oauth@molecule/api-secretsID-token issuer / tenant validation contract. verifyMicrosoftIdToken
(and provider.verifyIdToken) validate iss against the issuer(s) implied
by the configured tenant (OAUTH_MICROSOFT_TENANT_ID / config.tenantId)
— never against the token's own tid. This matters because Microsoft's
public-cloud signing keys are shared across every tenant, so a
validly-signed token issued for a different directory would otherwise
satisfy a single-tenant configuration (a cross-tenant authentication
bypass). The rule:
tid must equal the
configured tenant. A token's self-asserted tid can NOT widen the
accepted issuer set.common / organizations / consumers (multi-tenant): any
directory's users may sign in by design, so the token's tid-derived
https://login.microsoftonline.com/{tid}/v2.0 issuer IS accepted — this
is the documented multi-tenant contract, not a widening of a pin.Pin to a single tenant by setting OAUTH_MICROSOFT_TENANT_ID to the
directory GUID; leave it common only when multi-tenant sign-in is
intended.
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
state parameter is generated on initiation and
verified on callback (CSRF protection): a mismatched or absent state is
rejected (403); the redirect_uri is validated against an allowlist so an
attacker cannot redirect the code elsewhere; and the client secret + tokens
stay server-side — grep the browser bundle and network tab to confirm the
secret never reaches the client (only the authorize URL and returned code
cross the boundary).