OAuth client
For apps that complete the OAuth redirect flow server-side (confidential or public/PKCE clients), register the OAuth client:
builder.Services.AddArrowLabsOAuthClient(options =>{ options.BaseUrl = "https://api.arrowlabs.co.uk"; options.ClientId = "your-client-id"; options.ClientSecret = "your-client-secret"; // omit for public (PKCE-only) clients});Inject IArrowLabsOAuthClient where you need it.
1. Start the flow (PKCE + redirect)
Section titled “1. Start the flow (PKCE + redirect)”app.MapGet("/login", (IArrowLabsOAuthClient oauth, HttpContext ctx) =>{ var pkce = Pkce.Generate(); var state = Guid.NewGuid().ToString("N");
// Stash the verifier + state in the user's session for the callback. ctx.Session.SetString("pkce_verifier", pkce.Verifier); ctx.Session.SetString("oauth_state", state);
var url = oauth.BuildAuthorizationUrl( codeChallenge: pkce.Challenge, redirectUri: "https://app.example.com/callback", state: state);
return Results.Redirect(url);});2. Handle the callback (exchange the code)
Section titled “2. Handle the callback (exchange the code)”app.MapGet("/callback", async (string code, string state, IArrowLabsOAuthClient oauth, HttpContext ctx) =>{ if (state != ctx.Session.GetString("oauth_state")) return Results.BadRequest("state mismatch");
var result = await oauth.ExchangeCodeAsync( code, redirectUri: "https://app.example.com/callback", codeVerifier: ctx.Session.GetString("pkce_verifier"));
return result switch { TokenResult.Success s => Results.Ok(new { s.AccessToken, s.ExpiresIn, s.RefreshToken }), TokenResult.Failure f => Results.BadRequest(new { f.Error, f.ErrorDescription }), _ => Results.StatusCode(500), };});3. Refresh & revoke
Section titled “3. Refresh & revoke”// Rotate tokens (the refresh token rotates on every use):var refreshed = await oauth.RefreshTokensAsync(currentRefreshToken);
// Revoke a refresh token (e.g. on sign-out). Idempotent:var revoked = await oauth.RevokeTokenAsync(currentRefreshToken);Both token operations return the same TokenResult union (Success / Failure); transport failures surface as Failure("network_error", …), so there’s a single error path — no try/catch for expected outcomes.
Validating the resulting access tokens in your resource API is a separate concern — see Getting started for
AddArrowLabsAuth.