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
LogtoOIDCSelf-hosted, tenant support
OryOIDCSelf-hosted, open source
Generic OIDCOIDCAny compliant provider

OAuth provider credentials are configured via environment variables. FraiseQL fetches the provider’s /.well-known/openid-configuration endpoint at startup to discover authorization and token endpoints.

Terminal window
export OIDC_DISCOVERY_URL="https://your-provider.example.com"
export OIDC_CLIENT_ID="your-client-id"
export OIDC_CLIENT_SECRET="your-client-secret"
export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"
Terminal window
export OIDC_DISCOVERY_URL="https://accounts.google.com"
export OIDC_CLIENT_ID="your-client-id.apps.googleusercontent.com"
export OIDC_CLIENT_SECRET="your-google-client-secret"
export OIDC_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

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.

FraiseQL is a compiled GraphQL engine. The Python SDK generates a schema at compile time — there are no runtime resolver functions accessing a Context object. Instead, JWT claims are injected into SQL parameters using the inject decorator argument:

import fraiseql
from fraiseql.scalars import ID
@fraiseql.type
class User:
id: ID
email: str
name: str
# The authenticated user's own profile — user_id is injected from jwt:sub
@fraiseql.query(
sql_source="v_user",
inject={"user_id": "jwt:sub"},
)
def me() -> User | None:
"""Get the currently authenticated user."""
pass
# Tenant-scoped queries — org_id injected from a custom JWT claim
@fraiseql.query(
sql_source="v_user",
inject={"org_id": "jwt:org_id"},
)
def users(limit: int = 20, offset: int = 0) -> list[User]:
"""List users in the caller's organization."""
pass

The inject={"param": "jwt:claim"} syntax tells the Rust engine to read the named JWT claim and pass it as a SQL parameter, keeping it out of the GraphQL API surface entirely. The SQL view or function receives it as a bound parameter.

Roles are read from a claim in the JWT returned by your provider. FraiseQL maps JWT roles to field-level authorization rules defined in fraiseql.toml:

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

Use requires_role on a type to restrict access to callers with that role:

@fraiseql.type(requires_role="admin")
class AdminDashboard:
total_users: int
revenue: float

Field-level access control uses fraiseql.field(requires_scope=...) — see Security.

Login endpoint accepts a provider parameter to select among configured providers:

/auth/login?provider=github
/auth/login?provider=google

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 } }"}'
HTTP StatusErrorCauseFix
400 Bad Requestinvalid_clientClient ID or secret is wrongVerify OIDC_CLIENT_ID and OIDC_CLIENT_SECRET env vars match the provider app settings
400 Bad Requestredirect_uri_mismatchCallback URL not registeredAdd server_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 scopeVerify the scopes requested in your provider app settings; 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 discovery_url and the issuer 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. OIDC_REDIRECT_URI environment variable
  2. Provider app settings
  3. Actual callback URL
  1. Check clock sync between servers
  2. Verify discovery_url is correct and reachable
  3. Check token hasn’t expired
  1. Verify scopes include required claims
  2. Check provider-specific claim names
  3. Review claim mapping configuration

OAuth/OIDC authentication protects all three FraiseQL transports equally. The Rust runtime validates the JWT before routing the request to its transport handler, regardless of whether the request arrived as GraphQL, REST, or gRPC.

  • GraphQL and REST: send Authorization: Bearer <token> as an HTTP header.
  • gRPC: send authorization: Bearer <token> as gRPC call metadata (lowercase key, same value format).

The OAuth flow itself (authorization code exchange, token issuance, refresh) always goes through the /auth/* HTTP endpoints — there is no gRPC-native OAuth flow.

Security

Security — RBAC and field-level authorization

Deployment

Deployment — Production OAuth setup