Portal Community

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:

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.