Skip to content

OAuth Providers

FraiseQL supports multiple OAuth2/OIDC providers for authentication, with built-in support for popular identity platforms.

ProviderProtocolFeatures
GoogleOIDCEmail verification, profile
GitHubOAuth2Org membership, teams
Microsoft Azure ADOIDCTenant isolation, groups
OktaOIDCCustom claims, MFA
Auth0OIDCRules, roles, permissions
KeycloakOIDCSelf-hosted, realm support
Generic OIDCOIDCAny compliant provider

OAuth provider credentials are configured via environment variables — there is no [auth] section in fraiseql.toml.

Terminal window
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
OAUTH_REDIRECT_URI=https://api.example.com/auth/callback
  1. Client sends login request to FraiseQL
  2. FraiseQL redirects to the OAuth provider
  3. Provider sends Auth Code + State back to client
  4. Client sends the code to FraiseQL callback
  5. FraiseQL exchanges the code with the provider for tokens
  6. FraiseQL issues a session token to the client
EndpointPurpose
/auth/loginInitiate OAuth flow
/auth/callbackOAuth callback handler
/auth/logoutEnd session
/auth/refreshRefresh access token
/auth/userinfoGet current user info

Sessions are stored in PostgreSQL or Redis. Configuration is via environment variables:

Terminal window
SESSION_STORAGE=postgres # postgres | redis | memory
SESSION_TTL_SECONDS=86400 # 24 hours
REFRESH_TTL_SECONDS=604800 # 7 days
REDIS_URL=redis://host:6379/0 # required when SESSION_STORAGE=redis

PKCE (Proof Key for Code Exchange) is configured via [security.pkce] in fraiseql.toml — see Security.

State blobs are encrypted via [security.state_encryption] in fraiseql.toml — see Security.

Roles are read from the roles claim in the JWT returned by your provider. JWT claim names are configured via environment variables:

Terminal window
JWT_ROLES_CLAIM=roles # JWT field containing roles (default: roles)
JWT_DEFAULT_ROLE=user # fallback if no roles claim present

Set the active provider via environment variable:

Terminal window
OAUTH_PROVIDER=google # active provider
OAUTH_DEFAULT_PROVIDER=google # fallback when ?provider= not specified

Login endpoint accepts provider parameter:

/auth/login?provider=github
/auth/login?provider=google
# In resolvers, access the authenticated user
@fraiseql.query(sql_source="v_user")
def me(context: Context) -> User:
user_id = context.user_id # From token
roles = context.roles # Mapped roles
email = context.email # From claims

Custom claim namespacing (used by Auth0, Okta, etc.) is handled automatically — FraiseQL extracts claims from the JWT and makes them available in context. Access in context:

tenant_id = context.claims.get("tenant_id")

After configuring a provider, verify the full authorization code exchange manually before wiring up a frontend.

  1. Start the login flow — open the login URL in a browser or follow the redirect:

    Terminal window
    curl -v http://localhost:8080/auth/login?provider=google
    # Follow the Location header to the Google authorization page
  2. Authorize in the browser — log in and approve the consent screen. Your browser is redirected back to the callback URL with a code parameter.

  3. Exchange the authorization code — FraiseQL handles this automatically via the callback endpoint. To test it directly:

    Terminal window
    curl -X POST http://localhost:8080/auth/callback \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "code=AUTHORIZATION_CODE&state=STATE_VALUE&provider=google"
  4. Inspect the token response — a successful exchange returns a session token:

    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "token_type": "Bearer",
    "expires_in": 86400,
    "refresh_token": "rt_01HV3KQBN7MXSZQZQR4F5P0G2Y",
    "user": {
    "id": "usr_01HV3KQBN7MXSZQZQR4F5P0G2Y",
    "email": "alice@example.com",
    "name": "Alice Smith",
    "roles": ["user"]
    }
    }
  5. Call a protected query using the returned token:

    Terminal window
    curl http://localhost:8080/graphql \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
    -H "Content-Type: application/json" \
    -d '{"query":"{ me { id email roles } }"}'
HTTP StatusErrorCauseFix
400 Bad Requestinvalid_clientClient ID or secret is wrongVerify client_id and client_secret match the provider app settings
400 Bad Requestredirect_uri_mismatchCallback URL not registeredAdd redirect_uri exactly as configured to the provider’s allowed redirect list
400 Bad Requestinvalid_grantAuthorization code already used or expiredCodes are single-use and expire quickly (usually 60 seconds); do not reuse or delay exchange
401 Unauthorizedinvalid_tokenAccess token expired or revokedUse /auth/refresh with the refresh token to obtain a new access token
403 Forbiddenaccess_deniedUser denied consent or lacks required scopeCheck required scopes in fraiseql.toml; ensure the user approves all requested permissions
403 Forbiddenorg_requiredUser is not a member of required_org (GitHub)Verify required_org value; user must be a member of that GitHub organization
500 Internal Server Errortoken_exchange_failedNetwork error reaching the providerCheck outbound connectivity; verify issuer URL and discovery_url are reachable
MetricDescription
fraiseql_auth_logins_totalLogin attempts
fraiseql_auth_login_success_totalSuccessful logins
fraiseql_auth_login_failure_totalFailed logins
fraiseql_auth_token_refresh_totalToken refreshes
fraiseql_auth_session_activeActive sessions

Ensure redirect URI matches exactly in:

  1. FraiseQL config
  2. Provider app settings
  3. Actual callback URL
  1. Check clock sync between servers
  2. Verify issuer URL is correct
  3. Check token hasn’t expired
  1. Verify scopes include required claims
  2. Check provider-specific claim names
  3. Review claim mapping configuration

FraiseQL maps JWT claims to user roles automatically. Given this token (decoded):

{
"sub": "usr_123",
"email": "alice@example.com",
"roles": ["admin", "editor"],
"exp": 1740787200,
"iss": "https://auth.example.com"
}

FraiseQL grants access to all resolvers decorated with @role("admin") or @role("editor"). The roles claim field is configured via the JWT_ROLES_CLAIM environment variable (default: roles).

Security

Security — RBAC and field-level authorization

Deployment

Deployment — Production OAuth setup