Skip to content

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

ts
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 plain or S256, 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
http
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=S256

The 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
HTTP/1.1 302 Found
Location: http://localhost&code=eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJhdXRoY29kZWNsaWVudCIsInJlZGlyZWN0X3VyaSI6Imh0dHA6Ly9sb2NhbGhvc3QiLCJhdXRoX2NvZGVfaWQiOiJteS1zdXBlci1zZWNyZXQtYXV0aC1jb2RlIiwic2NvcGVzIjpbXSwiZXhwaXJlX3RpbWUiOjE2MDE3NTM3MzMsImNvZGVfY2hhbGxlbmdlIjoiT0RRd1pHTTRZelpsTnpNeU1qUXlaREF4WWpFNU1XWmtZMlJrTmpKbU1UbGxNbUkwTnpJMFpEbGtNR0psWWpGbE1tTXhPV1kyWkRJMVpEZGpNak13WWciLCJjb2RlX2NoYWxsZW5nZV9tZXRob2QiOiJTMjU2In0.OIEtZN5BHNaB4Mz0plUpGAP93EHyoil2smJiG3S_2BM&state=abcdefghijklmnopqrstuvwxyz123456789

Part 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
http
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_2BM

The 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
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:

ts
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.

ts
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.

ts
const code_challenge = code_verifier;
Need a base64urlencode function?
ts
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
http
POST /token HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded

token_type_hint=authorization_code
&refresh_token=xxxxxxxxx

The authorization server will respond with the following response

View sample revoke authorization_code response
http
HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache

OpenID 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:

json
{
  "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.

ClaimValue
issThe configured issuer.
subThe canonical subject — String(user.id), byte-identical to the UserInfo sub.
audThe client id (deliberately different from the access token's resource aud).
expReuses the access-token TTL.
iatIssued-at, in epoch seconds.
noncePresent only when the authorization request carried a nonce.
auth_timePresent only when a consumer-supplied authTime was set.
at_hashBase64url 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 openid scope returns no id_token.
  • Auto-recognized scopes: when OIDC is enabled, the authorization code grant accepts openid, profile, email, address, and phone without 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_access is 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 keep typ: "JWT".
  • Single use: authorization codes remain single-use; replaying a redeemed code is rejected and mints no second ID token.