Refresh and Logout
Rotate refresh tokens, switch organizations, and revoke sessions.
Refresh uses the old refresh token. SqlOS returns a new pair. Idle timeout extends.
RefreshAsync and the JSON POST /sqlos/auth/token/refresh route rotate public clients' refresh tokens (token_endpoint_auth_method=none). A refresh token issued to a confidential client also needs that client's secret. Exchange it at the OAuth token endpoint, described below. RefreshAsync throws SqlOSClientAuthenticationException, and the JSON route returns 401 invalid_client, before rotation.
var tokens = await authService.RefreshAsync(
new SqlOSRefreshRequest(refreshToken, OrganizationId: null), ct);OrganizationId: null means "keep the session's stored organization," not "skip organization checks." SqlOS requires the stored organization and membership to remain active before rotating the token. It also requires the user to remain active. The OAuth token endpoint reports lifecycle rejection as a generic invalid_grant; the detailed reason is available only in the audit log.
Browser backend-for-frontend:
var refreshToken = await tokenVault.GetRefreshTokenAsync(sessionId, ct);
var tokens = await authService.RefreshAsync(
new SqlOSRefreshRequest(refreshToken, OrganizationId: null), ct);
await tokenVault.ReplaceTokensAsync(sessionId, tokens, ct);Keep refresh tokens out of browser JavaScript. Store them in a server-side session or, for native and desktop apps, the platform credential vault. The browser should receive only your application's encrypted Secure, HttpOnly, SameSite session cookie.
/token refresh#POST /sqlos/auth/token with grant_type=refresh_token is the canonical refresh flow for every client. For public clients, grant_type=refresh_token plus refresh_token is enough. The token is bound to a client and session, so SqlOS does not treat client_id as client authentication for this grant. Sending client_id is still allowed and must match the token's client.
Confidential clients are different. Their refresh exchange must authenticate with the client's registered method, client_secret_basic or client_secret_post, and the token endpoint is the only route that accepts it. A missing, wrong, expired, revoked, or wrong-client secret returns invalid_client or invalid_grant and does not consume the refresh token. See Clients and the HTTP Protocol API.
The refresh response's scope field echoes the scope originally granted when the session was established (RFC 6749 §5.1). Sessions created before SqlOS recorded granted scope omit the field instead of claiming an empty grant.
Switch organizations without re-authenticating by passing a different organizationId:
var tokens = await authService.RefreshAsync(
new SqlOSRefreshRequest(refreshToken, organizationId: "org_newOrgId"), ct);Refresh tokens are single use. By default, SqlOS allows a 30-second retry grace period for the immediately previous token so a network retry does not destroy a healthy session. A retry inside that window receives the cached access token plus a fresh sibling refresh token.
Reuse outside the grace window revokes the entire token family and session. This protects against token theft: if an attacker captures and consumes a refresh token, a later replay shuts down the family instead of minting another usable branch. Set the initial default with options.AuthServer.RefreshTokenGraceWindowSeconds = 0 if your threat model values strict replay rejection over network-retry tolerance. A persisted dashboard or SqlOSSettingsService value overrides that startup default; see Security Settings.
Concurrent refreshes inside the configured grace window can receive the cached token produced by the winning rotation. Before returning that cached result, SqlOS rechecks session deadlines, client access, the user lifecycle, and the exact organization represented by the cached token.
A browser login creates two sessions. They do not share a lifetime.
| Session | Where it lives | What it unlocks |
|---|---|---|
| OAuth access and refresh tokens | Your app (localStorage, a BFF cookie, or a vault) | API calls |
| Issuer session | sqlos_auth_page cookie on the identity host | First-party /authorize without showing AuthPage; shared by hosted, headless, and device-approval flows |
Clearing the app tokens is not sign-out. The issuer session cookie can last days. The next first-party Sign in or Sign up then mints a code immediately, and the user never sees AuthPage.
End the issuer session with a browser navigation so the cookie can be cleared:
GET /sqlos/auth/logout?returnTo=/returnTo (or post_logout_redirect_uri) is resolved before SqlOS emits Location. A local destination is accepted only when it begins with exactly one /, contains no scheme or authority (including protocol-relative //host and backslash or encoded-authority variants), and still resolves under the configured application origin after decoding and normalization. Query and fragment are kept when that resolved destination stays local. Absolute http/https URIs are accepted only when their origin is the application origin or an origin already allowed by a registered redirect URI. Anything else, including javascript: and other non-http schemes, falls back to the hosted logged-out page.
GET /sqlos/auth/logout revokes the logical issuer session, not only the cookie the browser currently presents. Silent renewal and later authorization completions mint a replacement sqlos_auth_page credential on the same session family. Logging out any credential in that family invalidates the current cookie and every superseded predecessor. A retained copy of an earlier cookie cannot obtain a new authorization code or access token after that logout.
This is the issuer session used by website, mobile, desktop, and CLI sign-in when those clients go through the authorization server's browser or device-approval flow. Separately issued app access and refresh tokens keep their own logout lifecycle; clearing them does not end the issuer session, and issuer-session logout does not revoke those OAuth tokens.
Upgrade note: applying the issuer session-family schema consumes already-issued, unlinked issuer session cookies. Users sign in once after the upgrade. Independent browser or device logins remain separate sessions: logging out one does not sign out the others unless you call logout-all, reset the password, or revoke at user/organization scope.
After that, an explicit Sign in / Sign up should send prompt=login so a leftover cookie cannot silent-SSO. view=signup alone does not skip an existing issuer session.
LogoutAsync and POST /sqlos/auth/logout revoke the OAuth session and refresh-token family. They do not clear the issuer session cookie. Call both when the user clicks Sign out: revoke the refresh token, then send the browser to GET /sqlos/auth/logout.
Revoke a session by refresh token:
await authService.LogoutAsync(refreshToken: "rt_...", sessionId: null, ct);By session ID:
await authService.LogoutAsync(refreshToken: null, sessionId: "ses_...", ct);Revoke all sessions for a user:
await authService.LogoutAllAsync(userId, ct);Logout-all invalidates issuer sessions as well as OAuth sessions and refresh-token families. Password reset has the same all-session invalidation behavior. Organization deactivation and the SSO organization-session revocation action invalidate the corresponding organization-bound issuer sessions.
Browser backend-for-frontend:
app.MapPost("/logout", async (
HttpContext httpContext,
SqlOSAuthService authService,
IApplicationTokenVault tokenVault,
CancellationToken ct) =>
{
var sessionId = httpContext.User.FindFirst("app_session_id")?.Value;
var refreshToken = sessionId is null
? null
: await tokenVault.GetRefreshTokenAsync(sessionId, ct);
if (refreshToken is not null)
await authService.LogoutAsync(refreshToken, sessionId: null, ct);
if (sessionId is not null)
await tokenVault.DeleteAsync(sessionId, ct);
await httpContext.SignOutAsync();
return Results.NoContent();
}).RequireAuthorization();Protect a cookie-authenticated logout endpoint against CSRF using an antiforgery token or a strict same-origin policy. Clear the local application session even if remote revocation times out; the ASP.NET Core example demonstrates that finally-style cleanup. After the BFF session is gone, still send the browser to GET /sqlos/auth/logout if the user should have to complete AuthPage again.