Passport
Authorization Code Flow
Step-by-step walkthrough of the OIDC authorization code flow with PKCE — from browser redirect to access token, with code examples for each stage.
Complete Flow Diagram
Browser/App Your Backend Passport
│ │ │
│ User clicks Sign In │ │
├──── generate PKCE ──────>│ │
│ │ build authorization URL │
│<──── redirect ──────────┤ │
│ │ │
│──── GET /passport/authorize ──────────────────────>│
│ ?client_id=... │ │
│ &code_challenge=... │ │
│ &state=... │ │
│ │ │
│ [Passport login UI] │
│ [User authenticates] │
│ │ │
│<────────────────────── redirect to callback ──────┤
│ ?code=... │ │
│ &state=... │ │
│ │ │
├──── POST /callback ─────>│ │
│ │ verify state │
│ ├── POST /passport/token ─>│
│ │ grant_type=authorization_code
│ │ code=... │
│ │ code_verifier=... │
│ │<── access_token ────────┤
│ │ id_token │
│ │ refresh_token │
│<──── session + tokens ──┤ │
Step 1 — Generate PKCE Parameters
// TypeScript/JavaScript (SPA)
function generatePkce(): { verifier: string; challenge: string } {
const verifier = btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(64))))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
const challengeBuffer = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(verifier)
);
const challenge = btoa(String.fromCharCode(...new Uint8Array(challengeBuffer)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
return { verifier, challenge };
}
// C# (server-side)
using System.Security.Cryptography;
string GenerateCodeVerifier() {
var bytes = RandomNumberGenerator.GetBytes(64);
return Convert.ToBase64String(bytes)
.Replace('+', '-').Replace('/', '_').TrimEnd('=');
}
string GenerateCodeChallenge(string verifier) {
var hash = SHA256.HashData(Encoding.ASCII.GetBytes(verifier));
return Convert.ToBase64String(hash)
.Replace('+', '-').Replace('/', '_').TrimEnd('=');
}
Step 2 — Build the Authorization URL
// Parameters for the authorization request
var state = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
// Store in session for CSRF validation
Session["oauth_state"] = state;
Session["pkce_verifier"] = codeVerifier;
Session["return_url"] = Request.Query["returnUrl"];
var authUrl = new UriBuilder("https://passport.bizfirst.ai/passport/authorize");
authUrl.Query = QueryString.Create([
("response_type", "code"),
("client_id", "workflow-api-prod"),
("redirect_uri", "https://myapp.example.com/auth/callback"),
("scope", "openid profile email roles tenant"),
("state", state),
("code_challenge", codeChallenge),
("code_challenge_method", "S256"),
("nonce", Guid.NewGuid().ToString("N")) // for id_token validation
]).ToString();
return Redirect(authUrl.ToString());
Step 3 — Handle the Callback
[HttpGet("/auth/callback")]
public async Task<IActionResult> Callback(
string code,
string state,
string? error = null,
string? errorDescription = null)
{
// Handle errors from Passport (e.g., user denied access)
if (error != null)
return BadRequest($"SSO error: {error} — {errorDescription}");
// Validate state (CSRF protection)
var expectedState = Session["oauth_state"] as string;
if (state != expectedState)
return BadRequest("Invalid state — possible CSRF attack");
var codeVerifier = Session["pkce_verifier"] as string;
// Exchange code for tokens
var tokens = await _passportClient.ExchangeCodeAsync(new CodeExchangeRequest
{
Code = code,
CodeVerifier = codeVerifier,
RedirectUri = "https://myapp.example.com/auth/callback"
});
// Establish application session
await _sessionService.CreateSessionAsync(HttpContext, tokens);
var returnUrl = Session["return_url"] as string ?? "/";
return Redirect(returnUrl);
}
Step 4 — Token Exchange
// ISsoOrchestrationService internally calls Passport token endpoint
// Direct HTTP example:
POST /passport/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64("workflow-api-prod:client-secret")
grant_type=authorization_code
&code=short-lived-authorization-code
&redirect_uri=https://myapp.example.com/auth/callback
&code_verifier=original-code-verifier-not-hashed
// Successful Response (200 OK)
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"refresh_token": "opaque-refresh-token-value",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile email roles tenant"
}
// Error Response (400 Bad Request)
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid or expired."
}
State Parameter Security
The ISsoStateManager in Passport signs the state parameter using HMAC-SHA256 to prevent tampering. Your application should:
- Generate a cryptographically random state (at least 32 bytes) for each authorization request
- Store the state in the user's session (not in a cookie or URL)
- Verify the returned state exactly matches before processing the callback
- Reject and log any callback where state validation fails
Authorization Code Is Single-Use
The authorization code is valid for 60 seconds and can only be exchanged once. If you receive an invalid_grant error during exchange, the code has already been used (replay attack) or has expired. Restart the authorization flow.