Authorization code with PKCE
A user signs in to a web application. The application sends the browser to the authorize endpoint with a PKCE challenge; the user signs in there; the server sends the browser back with a one-time code; and the application redeems the code, with the verifier only it knows, for an identity token and an access token. The client is the demo host's web: a public client (no secret), PKCE required.
Follows: RFC 6749 §4.1, RFC 7636 (PKCE), OpenID Connect Core 1.0 §3.1, RFC 9207 (iss).
Recorded from the demo host (src/Fdyay.IdentityServer.Host) by the AuthorizationCodeFlow test on every CI run. A value in braces, such as {access_token_1}, is one that differs on every run; the same placeholder is the same value wherever it appears. See how these pages are made.
sequenceDiagram
participant Browser
participant AuthorizationServer as Authorization Server
participant Client
Browser->>AuthorizationServer: GET /connect/authorize
AuthorizationServer-->>Browser: 302 Found to /account/login
Browser->>AuthorizationServer: GET /account/login
AuthorizationServer-->>Browser: 200 OK
Browser->>AuthorizationServer: POST /account/login
AuthorizationServer-->>Browser: 302 Found to /connect/authorize
Browser->>AuthorizationServer: GET /connect/authorize
AuthorizationServer-->>Browser: 302 Found to https://client.example.com/callback
Client->>AuthorizationServer: POST /connect/token
AuthorizationServer-->>Client: 200 OK
Client->>AuthorizationServer: GET /connect/userinfo
AuthorizationServer-->>Client: 200 OK
1. The client sends the browser to the authorize endpoint
code_challenge is the S256 hash of a verifier the client keeps. Nobody is signed in yet, so the server sends the browser to its sign-in page and carries the request along in returnUrl.
Browser → Authorization Server
GET /connect/authorize?client_id=web
&response_type=code
&scope=openid%20profile%20api
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4
&code_challenge_method=S256 HTTP/1.1
Authorization Server → Browser
HTTP/1.1 302 Found
Location: /account/login?returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dweb%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520api%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256
2. The user signs in
The sign-in page belongs to the host (here the Fdyay.IdentityServer.UI pages). Signing in sets the session cookies and sends the browser back to the authorize request.
Browser → Authorization Server
GET /account/login?returnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dweb%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520api%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256 HTTP/1.1
Authorization Server → Browser
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: no-cache, no-store
Pragma: no-cache
Set-Cookie: .AspNetCore.Antiforgery.{suffix}={antiforgery_cookie_1}; path=/; samesite=strict; httponly
(an HTML page, not shown)
Browser → Authorization Server
POST /account/login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Cookie: .AspNetCore.Antiforgery.{suffix}={antiforgery_cookie_1}
Input.Username=alice
&Input.Password=password
&Input.ReturnUrl=%2Fconnect%2Fauthorize%3Fclient_id%3Dweb%26response_type%3Dcode%26scope%3Dopenid%2520profile%2520api%26redirect_uri%3Dhttps%253A%252F%252Fclient.example.com%252Fcallback%26state%3Daf0ifjsldkj%26nonce%3Dn-0S6_WzA2Mj%26code_challenge%3D_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4%26code_challenge_method%3DS256
&__RequestVerificationToken={antiforgery_token_1}
Authorization Server → Browser
HTTP/1.1 302 Found
Location: /connect/authorize?client_id=web
&response_type=code
&scope=openid%20profile%20api
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4
&code_challenge_method=S256
Cache-Control: no-cache,no-store
Pragma: no-cache
Set-Cookie: idsrv={authentication_cookie_1}; path=/; samesite=none; httponly
3. The authorize endpoint issues a code
The code comes back on the redirect URI with the client's state, the session's session_state and the issuer (iss, RFC 9207), which tells the client which server the code is from.
Browser → Authorization Server
GET /connect/authorize?client_id=web
&response_type=code
&scope=openid%20profile%20api
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=_VOp_w53jMkBWlSwOlY2MntCuqr-wFZf-TdML17xmn4
&code_challenge_method=S256 HTTP/1.1
Cookie: .AspNetCore.Antiforgery.{suffix}={antiforgery_cookie_1}; idsrv={authentication_cookie_1}
Authorization Server → Browser
HTTP/1.1 302 Found
Location: https://client.example.com/callback?code={authorization_code_1}
&state=af0ifjsldkj
&session_state={session_state_1}
&iss=http%3A%2F%2Flocalhost
Cache-Control: no-cache,no-store
Pragma: no-cache
Set-Cookie: idsrv.session={session_id_1}; path=/; secure; samesite=none
Set-Cookie: idsrv={authentication_cookie_2}; path=/; samesite=none; httponly
4. The client redeems the code
With the verifier: a code intercepted on its way back through the browser is useless without it. The access token is a JWT for the api scope's resource; the identity token names the user and carries the client's nonce back.
Client → Authorization Server
POST /connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code={authorization_code_1}
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
&client_id=web
&code_verifier=documented-flow-code-verifier-0123456789-abcdefghij
Authorization Server → Client
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"access_token": "{access_token_1}",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "openid profile api",
"id_token": "{id_token_1}"
}
{access_token_1} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "at+jwt"
}
{
"aud": [
"api-resource"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 3600},
"jti": "{jti_1}",
"client_id": "web",
"scope": "api openid profile",
"sub": "alice",
"auth_time": {auth_time},
"idp": "local",
"amr": [
"pwd"
],
"sid": "{session_id_1}"
}
{id_token_1} decoded — the header, then the payload; the signature is not shown:
{
"alg": "RS256",
"kid": "{key_id_1}",
"typ": "JWT"
}
{
"aud": [
"web"
],
"iss": "http://localhost",
"iat": {iat},
"nbf": {iat},
"exp": {iat + 300},
"jti": "{jti_2}",
"azp": "web",
"sub": "alice",
"auth_time": {auth_time},
"idp": "local",
"amr": [
"pwd"
],
"name": "Alice Smith",
"nonce": "n-0S6_WzA2Mj",
"sid": "{session_id_1}",
"at_hash": "{at_hash_1}"
}
5. The client asks userinfo for the user's claims
With the access token as a bearer token.
Client → Authorization Server
GET /connect/userinfo HTTP/1.1
Authorization: Bearer {access_token_1}
Authorization Server → Client
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Cache-Control: no-store, no-cache, max-age=0
Pragma: no-cache
{
"sub": "alice",
"name": "Alice Smith"
}