Authorization Code Grant (w/ PKCE)
The server issues a temporary code, and the Client exchanges it for an Access Token. First the user authorizes the application. Then the server redirects the user to the application, and puts the code in the URL. Last, the application exchanges the code for the Access Token.
Enable this grant
authorizationServer.enableGrantType({
grant: "authorization_code",
userRepository,
authCodeRepository,
});Flow
Part One
The Client redirects the user to /authorize with these query parameters:
- response_type: Set it to
code. - client_id: The identifier that you gave to the Client at registration.
- redirect_uri: The destination of the redirect after the authorization, for example
org.example.app://redirect. It must match a Registered Redirect URI exactly. - state: A random string from your application. Compare it with the returned value later.
- code_challenge: The code challenge. The next section shows you how to make it.
- code_challenge_method: Set it to
S256for the SHA256 hash of the verifier. If you omit this parameter, the server usesplain, which the defaultrequiresS256configuration option rejects. Sendplainfor the verifier itself only when you disablerequiresS256.
INFO
Do not send the client secret in 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 log in to the authorization server and approve the Client and the requested scopes.
If the user approves the Client, the authorization server redirects the user to the redirect_uri. The query string contains these fields:
- code: The authorization code. The Client exchanges it for a token in part two.
- state: The random string from the request. Compare it with the value that your application sent.
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 request to the /token endpoint with this body:
- grant_type: Set it to
authorization_code. - client_id: The identifier that you gave to the Client at registration.
- client_secret (optional): The secret. Send it only for a Confidential Client.
- redirect_uri: The same URI that part one used.
- code_verifier: The code verifier for the code challenge from part one.
- code: The authorization code from the query string.
A browser or mobile Client can leak a secret
A browser application and a native mobile application must never hold a client_secret or send one. Omit the secret when you make the OAuthClient entity, and also omit it from each request.
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 returns this response:
- token_type: Always
Bearer. - expires_in: The life of the Access Token, in seconds.
- access_token: A signed JWT. The Client sends it to the resource server.
- refresh_token: A signed JWT for the refresh token grant.
- scope: The scopes of the token, separated by spaces.
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. It prevents several attacks, and it makes the OAuth exchange safe for a Public Client.
The library enables PKCE by default, and you must use it. To support an old Client that cannot use PKCE, disable the requiresPKCE configuration option.
Code Verifier
The code_verifier prevents an attack that intercepts the authorization code.
Before part one of the flow, the Client makes a code_verifier. This is a random string of 43 to 128 characters. Use the characters A-Z, a-z, 0-9, and the four punctuation characters -, ., _, and ~.
In Node.js, use the native crypto package:
import { randomBytes } from "crypto";
const code_verifier = randomBytes(43).toString("hex");@see https://www.oauth.com/oauth2-servers/pkce/authorization-request/
Code Challenge
Next, make a code_challenge from the code_verifier.
If your device can make a SHA256 hash, use the S256 method. The code challenge is then the SHA256 hash of the code verifier, in base64url format.
const code_challenge = base64urlencode(crypto.createHash("sha256").update(code_verifier).digest());If your device cannot make a SHA256 hash, disable the requiresS256 configuration option and use the plain method. The code challenge is then the code_verifier itself.
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
A Client can use each authorization code one time only. You can also revoke an authorization code with the /token/revoke endpoint, from RFC 7009 "OAuth 2.0 Token Revocation".
A revocation request contains these parameters:
- token: The authorization code that the server issued to the Client.
- token_type_hint: Set it to
auth_code. The server revokes an authorization code only when the request contains this hint.
View sample revoke authorization_code request
POST /token/revoke HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
token=xxxxxxxxx
&token_type_hint=auth_codeThe authorization server returns this response:
View sample revoke authorization_code response
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cacheOpenID Connect ID Tokens
Set a top-level issuer and an oidc block on the authorization server to enable OIDC. Then, when the server grants the openid scope, the token response contains a signed ID Token with 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 contains Protocol Claims only. The library never puts a Scope-Derived Claim, such as name or email, in the ID Token. Each Client reads those claims from the UserInfo endpoint.
| Claim | Value |
|---|---|
iss | The configured issuer. |
sub | The canonical subject, String(user.id). It is identical to the UserInfo sub. |
aud | The Client identifier. This is not the resource audience of the Access Token. |
exp | The expiry. It uses the life of the Access Token. |
iat | The time of issue, in epoch seconds. |
nonce | Present only when the authorization request contains a nonce. |
auth_time | Present only when you set authTime. |
at_hash | The base64url left half of SHA-256(access_token) (OIDC Core §3.1.3.6). |
The library does not write azp in v1, because azp is correct only while aud holds one Client identifier.
Behaviour Notes
- Scope gate: The server returns no ID Token if it does not grant the
openidscope. - Automatic scopes: When you enable OIDC, the authorization code grant accepts the
openid,profile,email,address, andphonescopes. You do not register them in your scope repository. The other grants, such asclient_credentialsandpassword, do not accept them, because only the authorization code flow issues an ID Token. The server does not acceptoffline_accessautomatically in v1. - Token type: The library signs each OIDC Access Token with the JOSE header
typ: "at+jwt"(RFC 9068), and keepstyp: "JWT"on each ID Token. Thus no server accepts an ID Token as an Access Token. - One use: The server rejects a second request with the same authorization code, and issues no second ID Token.