OAuth Client Authentication#
Overview#
The oauth-provider plugin authenticates clients at the token endpoint (POST /oauth2/token) using one of two methods: HTTP Basic authentication (client_secret_basic) or body parameter authentication (client_secret_post). Both methods are advertised in the server metadata and supported by all three grant types. Public clients (no secret) are optionally supported and must use PKCE instead.
The token endpoint only accepts application/x-www-form-urlencoded requests, per RFC 6749 conventions.
Advertised Auth Methods#
The server's /.well-known/oauth-authorization-server (and OpenID Configuration) document advertises supported methods via authServerMetadata:
| Endpoint | Supported Methods |
|---|---|
token_endpoint | none (if public clients enabled), client_secret_basic, client_secret_post |
introspection_endpoint | client_secret_basic, client_secret_post |
revocation_endpoint | client_secret_basic, client_secret_post |
none appears in token_endpoint_auth_methods_supported only when allowUnauthenticatedClientRegistration: true is set .
Credential Extraction#
In every grant handler, credentials are resolved from the Authorization header first, then fall back to body parameters. The helper basicToClientCredentials handles the HTTP Basic case: it Base64-decodes the header, splits on the first : separator, and returns { client_id, client_secret }. If either value is missing or the format is invalid, it throws invalid_client.
All three grant handlers follow the same pattern :
if (Authorization header starts with "Basic ") → basicToClientCredentials()
else → use client_id / client_secret from body
Core Validation: validateClientCredentials#
All grant handlers delegate to validateClientCredentials after credential extraction. It runs these checks in order:
- Client lookup — fetches from DB (or in-memory trusted-client cache via
getClient); throwsinvalid_clientif not found. - Disabled check — throws
invalid_clientifclient.disabledis set . - Confidential client requires secret — if
!client.public && !clientSecret, throwsinvalid_client. - Public client rejects secret — if a secret is sent to a public client (no
clientSecretstored), throwsinvalid_client. - Secret verification — calls
verifyStoredClientSecretusing constant-time comparison . - Scope check — validates requested scopes against the client's registered scopes .
- Grant type check — calls
clientAllowsGrantto verify the client is registered for the requested grant .
Secret Storage & Verification#
Client secrets are stored according to the storeClientSecret option :
storeClientSecret value | Storage | Verification |
|---|---|---|
"hashed" (default when JWT plugin enabled) | SHA-256 / base64url | Hash then constant-time compare |
"encrypted" (default when JWT plugin disabled) | Symmetric AES (via secretConfig) | Decrypt then constant-time compare |
{ hash, verify?, encrypt?, decrypt? } | Custom | Custom |
The default hasher uses SHA-256 with base64url encoding (no padding). All comparisons use constantTimeEqual to prevent timing attacks.
An optional prefix (opts.prefix.clientSecret) is stripped before hashing/comparison; a secret that doesn't start with the configured prefix throws invalid_client immediately .
PKCE vs. Secret: Authorization Code Grant#
The authorization_code grant requires either a client_secret or a code_verifier (PKCE) — never neither . isPKCERequired determines whether PKCE is mandatory:
- Always required for public clients (
tokenEndpointAuthMethod === "none",type === "native"or"user-agent-based", orpublic === true) . - Always required when
offline_accessscope is requested . - Required by default for confidential clients unless
requirePKCE: falseis explicitly set .
Only S256 is supported as a PKCE challenge method .
When PKCE is used, the token endpoint verifies that PKCE was also used at the authorization endpoint (and vice versa), preventing downgrade attacks .
Per-Grant Behavior#
authorization_code#
- Accepts Basic or body credentials .
- If confidential: requires
client_secretORcode_verifier. - PKCE challenge method must be
S256.
client_credentials#
- Requires both
client_idandclient_secret. - OIDC scopes (
openid,profile,email,offline_access) are explicitly rejected .
refresh_token#
client_idis always required;client_secretis optional for public clients but required for confidential ones — enforced byvalidateClientCredentials.clientIdon the refresh token row must match the requestingclient_id, orinvalid_clientis returned .
Key Source Files#
| File | Role |
|---|---|
packages/oauth-provider/src/utils/index.ts | validateClientCredentials, basicToClientCredentials, verifyStoredClientSecret, isPKCERequired, clientAllowsGrant |
packages/oauth-provider/src/token.ts | Grant handlers (handleAuthorizationCodeGrant, handleClientCredentialsGrant, handleRefreshTokenGrant) |
packages/oauth-provider/src/metadata.ts | Advertised token_endpoint_auth_methods_supported in server metadata |