Authorization Code Grant (w/ PKCE)
A temporary code that the client will exchange for an access token. The user authorizes the application, they are redirected back to the application with a temporary code in the URL. The application exchanges that code for the access token.
Enable this grant
authorizationServer.enableGrantType({
grant: "authorization_code",
userRepository,
authCodeRepository,
});Flow
Part One
The client redirects the user to the /authorize with the following query parameters:
- response_type must be set to
code - client_id is the client identifier you received when you first created the application
- redirect_uri indicates the URL to return the user to after authorization is complete, such as org.example.app://redirect
- state is a random string generated by your application, which you'll verify later
- code_challenge must match the The code challenge as generated below,
- code_challenge_method – Either
plainorS256, depending on whether the challenge is the plain verifier string or the SHA256 hash of the string. If this parameter is omitted, the server will assume plain.
INFO
The client secret should never be used during the Part One of the authorization_code flow.
View sample authorization_code (part 1) request
GET /authorize HTTP/1.1
Host: example.com
response_type=code
&client_id=xxxxxxx
&redirect_uri=http://localhost
&scope="contacts.read contacts.write"
&state=abcdefghijklmnopqrstuvwxyz123456789
&code_challenge=92d3b56942866d1edf02c33339b7c3dc37c6201282bb238cb47f0d3289f28a93f1bdd8af6ca9913aed0c4c
&code_challenge_method=S256The user will be asked to login to the authorization server and approve the client and requested scopes.
If the user approves the client, they will be redirected from the authorization server to the provided redirect_uri with the following fields in the query string:
- code is the authorization code that will soon be exchanged for a token
- state is the random string provided and should be compared against the initially provided state
View sample authorization_code (part 1) response
HTTP/1.1 302 Found
Location: http://localhost&code=eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJhdXRoY29kZWNsaWVudCIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJhdXRoX2NvZGVfaWQiOiJteS1zdXBlci1zZWNyZXQtYXV0aC1jb2RlIiwic2NvcGVzIjpbXSwiZXhwaXJlX3RpbWUiOjE2MDE3NTM3MzMsImNvZGVfY2hhbGxlbmdlIjoiT0RRd1pHTTRZelpsTnpNeU1qUXlaREF4WWpFNU1XWmtZMlJrTmpKbU1UbGxNbUkwTnpJMFpEbGtNR0psWWpGbE1tTXhPV1kyWkRJMVpEZGpNak13WWciLCJjb2RlX2NoYWxsZW5nZV9tZXRob2QiOiJTMjU2In0.OIEtZN5BHNaB4Mz0plUpGAP93EHyoil2smJiG3S_2BM&state=abcdefghijklmnopqrstuvwxyz123456789Part Two
The client sends a POST to the /token endpoint with the following body:
- grant_type must be set to
authorization_code - client_id is the client identifier you received when you first created the application
- client_secret (optional) is the client secret and should only be provided if the client is confidential
- redirect_uri
- code_verifier
- code is the authorization code from the query string
Private Key Leak Potential
Clients such as Browser Based Apps and Native Mobile Apps should NEVER have or use a client_secret. That means the client_secret should be omitted both when initially creating the OAuthClient entity, and when making requests.
View sample authorization_code (part 2) request
POST /token HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=xxxxxxxxxx
&client_secret=xxxxxxxxxx
&redirect_uri=http://localhost
&code_verifier=OTJkM2I1Njk0Mjg2NmQxZWRmMDJjMzMzMzliN2MzZGMzN2M2MjAxMjgyYmIyMzhjYjQ3ZjBkMzI4OWYyOGE5M2YxYmRkOGFmNmNhOTkxM2FlZDBjNGM
&code=eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJhdXRoY29kZWNsaWVudCIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJhdXRoX2NvZGVfaWQiOiJteS1zdXBlci1zZWNyZXQtYXV0aC1jb2RlIiwic2NvcGVzIjpbXSwiZXhwaXJlX3RpbWUiOjE2MDE3NTM3MzMsImNvZGVfY2hhbGxlbmdlIjoiT0RRd1pHTTRZelpsTnpNeU1qUXlaREF4WWpFNU1XWmtZMlJrTmpKbU1UbGxNbUkwTnpJMFpEbGtNR0psWWpGbE1tTXhPV1kyWkRJMVpEZGpNak13WWciLCJjb2RlX2NoYWxsZW5nZV9tZXRob2QiOiJTMjU2In0.OIEtZN5BHNaB4Mz0plUpGAP93EHyoil2smJiG3S_2BMThe authorization server will respond with the following response
- token_type will always be
Bearer - expires_in is the time the token will live in seconds
- access_token is a JWT signed token and is used to authenticate into the resource server
- refresh_token is a JWT signed token and can be used in with the refresh grant
- scope is a space delimited list of scopes the token has access to
View sample authorization_code (part 2) response
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
token_type: 'Bearer',
expires_in: 3600,
access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDE3NTUxMDQsIm5iZiI6MTYwMTc1MTUwNCwiaWF0IjoxNjAxNzUxNTA0LCJqdGkiOiJuZXcgdG9rZW4iLCJjaWQiOiJ0ZXN0IGF1dGggY29kZSBjbGllbnQiLCJzY29wZSI6IiJ9.-V9x03iz-3ISRMdj9m1-FCKjmtfjvv6wqnBj6VZdW28',
refresh_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiJhdXRoY29kZWNsaWVudCIsImFjY2Vzc190b2tlbl9pZCI6Im5ldyB0b2tlbiIsInJlZnJlc2hfdG9rZW5faWQiOiJ0aGlzLWlzLW15LXN1cGVyLXNlY3JldC1yZWZyZXNoLXRva2VuIiwic2NvcGUiOiIiLCJleHBpcmVfdGltZSI6MTYwMTc1NTEwNCwiaWF0IjoxNjAxNzUxNTAzfQ.J_RUFD5-158atTmI98R95vowZWi4mUEXYCO7iNwzpK4',
scope: 'contacts.read contacts.write'
}PKCE
PKCE (RFC 7636) is an extension to the Authorization Code flow to prevent several attacks and to be able to securely perform the OAuth exchange from public clients.
By default, PKCE is enabled and encouraged for all users. If you need to support a legacy client system without PKCE, you can disable PKCE with the authorization server using the requiresPKCE configuration option.
Code Verifier
The code_verifier is part of the extended "PKCE" and helps mitigate the threat of having authorization codes intercepted.
Before initializing Part One of the authorization code flow, the client first creats a code_verifier. This is a cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters -._~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long.
We can do this in Node using the native crypto package and a base64urlencode function:
import { randomBytes } from "crypto";
const code_verifier = randomBytes(43).toString("hex");@see https://www.oauth.com/oauth2-servers/pkce/authorization-request/
Code Challenge
Now we need to create a code_challenge from our code_verifier.
For devices that can perform a SHA256 hash, the code challenge is a BASE64-URL-encoded string of the SHA256 hash of the code verifier.
const code_challenge = base64urlencode(crypto.createHash("sha256").update(code_verifier).digest());Clients that do not have the ability to perform a SHA256 hash are permitted to use the plain code_verifier string as the code_challenge.
const code_challenge = code_verifier;Need a base64urlencode function?
function base64urlencode(str: string) {
return Buffer.from(str)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}Revocation
Authorization codes are only valid for a single use. In addition, they can be explicitly revoked on a server that supports RFC7009 "OAuth 2.0 Token Revocation".
An authorization code revocation request will include the following parameters:
- token is the authorization code previously issued to the client
- token_type_hint (optional) should be set to
authorization_code
View sample revoke authorization_code request
POST /token HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
token_type_hint=authorization_code
&refresh_token=xxxxxxxxxThe authorization server will respond with the following response
View sample revoke authorization_code response
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cacheOpenID Connect ID Tokens
When OIDC is enabled (an oidc config block is set on the authorization server) and the authorization code flow is run with the openid scope granted, the token response carries a signed id_token alongside the access_token:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "<RS256 JWT, JOSE header typ:at+jwt>",
"refresh_token": "...",
"scope": "openid",
"id_token": "<RS256 JWT, JOSE header typ:JWT>"
}Claim set
The default ID token is lean — it carries Protocol Claims only. Scope-derived user attributes (name, email, …) are never placed in the ID token; relying parties read those from the UserInfo endpoint.
| Claim | Value |
|---|---|
iss | The configured issuer. |
sub | The canonical subject — String(user.id), byte-identical to the UserInfo sub. |
aud | The client id (deliberately different from the access token's resource aud). |
exp | Reuses the access-token TTL. |
iat | Issued-at, in epoch seconds. |
nonce | Present only when the authorization request carried a nonce. |
auth_time | Present only when a consumer-supplied authTime was set. |
at_hash | Base64url left-half of SHA-256(access_token) (OIDC Core §3.1.3.6). |
azp is omitted in v1 (valid only while aud is the single client id).
Behaviour notes
- Scope gating: an authorization code flow without the
openidscope returns noid_token. - Auto-recognized scopes: when OIDC is enabled, the authorization code grant accepts
openid,profile,email,address, andphonewithout registering them in your scope repository. Other grants (client_credentials,password, …) do not auto-recognize these, since only the authorization code flow issues ID tokens.offline_accessis not auto-recognized in v1. - Token typing: OIDC access tokens are signed with the JOSE header
typ: "at+jwt"(RFC 9068) so an ID token can never be accepted as a bearer access token. ID tokens keeptyp: "JWT". - Single use: authorization codes remain single-use; replaying a redeemed code is rejected and mints no second ID token.