Security
Security — RBAC and field-level authorization
FraiseQL supports multiple OAuth2/OIDC providers for authentication, with built-in support for popular identity platforms.
| Provider | Protocol | Features |
|---|---|---|
| OIDC | Email verification, profile | |
| GitHub | OAuth2 | Org membership, teams |
| Microsoft Azure AD | OIDC | Tenant isolation, groups |
| Okta | OIDC | Custom claims, MFA |
| Auth0 | OIDC | Rules, roles, permissions |
| Keycloak | OIDC | Self-hosted, realm support |
| Logto | OIDC | Self-hosted, tenant support |
| Ory | OIDC | Self-hosted, open source |
| Generic OIDC | OIDC | Any 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.
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"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"export OIDC_DISCOVERY_URL="https://token.actions.githubusercontent.com"export OIDC_CLIENT_ID="your-github-app-client-id"export OIDC_CLIENT_SECRET="your-github-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"export OIDC_DISCOVERY_URL="https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0"export OIDC_CLIENT_ID="your-azure-client-id"export OIDC_CLIENT_SECRET="your-azure-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"export OIDC_DISCOVERY_URL="https://your-domain.okta.com"export OIDC_CLIENT_ID="your-okta-client-id"export OIDC_CLIENT_SECRET="your-okta-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"export OIDC_DISCOVERY_URL="https://your-tenant.auth0.com"export OIDC_CLIENT_ID="your-auth0-client-id"export OIDC_CLIENT_SECRET="your-auth0-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"export OIDC_DISCOVERY_URL="https://keycloak.example.com/realms/my-realm"export OIDC_CLIENT_ID="your-keycloak-client-id"export OIDC_CLIENT_SECRET="your-keycloak-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"export OIDC_DISCOVERY_URL="https://your-logto-endpoint/oidc"export OIDC_CLIENT_ID="your-logto-client-id"export OIDC_CLIENT_SECRET="your-logto-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"export OIDC_DISCOVERY_URL="https://your-ory-issuer-url"export OIDC_CLIENT_ID="your-ory-client-id"export OIDC_CLIENT_SECRET="your-ory-client-secret"export OIDC_REDIRECT_URI="https://api.example.com/auth/callback"# Any OIDC-compliant providerexport OIDC_DISCOVERY_URL="https://identity.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"# FraiseQL auto-discovers endpoints from /.well-known/openid-configuration| Endpoint | Purpose |
|---|---|
/auth/login | Initiate OAuth flow |
/auth/callback | OAuth callback handler |
/auth/logout | End session |
/auth/refresh | Refresh access token |
/auth/userinfo | Get 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 fraiseqlfrom fraiseql.scalars import ID
@fraiseql.typeclass 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.""" passThe 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: floatField-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=googleAfter configuring a provider, verify the full authorization code exchange manually before wiring up a frontend.
Start the login flow — open the login URL in a browser or follow the redirect:
curl -v http://localhost:8080/auth/login?provider=google# Follow the Location header to the Google authorization pageAuthorize in the browser — log in and approve the consent screen. Your browser is redirected back to the callback URL with a code parameter.
Exchange the authorization code — FraiseQL handles this automatically via the callback endpoint. To test it directly:
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"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"] }}Call a protected query using the returned token:
curl http://localhost:8080/graphql \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -H "Content-Type: application/json" \ -d '{"query":"{ me { id email } }"}'| HTTP Status | Error | Cause | Fix |
|---|---|---|---|
400 Bad Request | invalid_client | Client ID or secret is wrong | Verify OIDC_CLIENT_ID and OIDC_CLIENT_SECRET env vars match the provider app settings |
400 Bad Request | redirect_uri_mismatch | Callback URL not registered | Add server_redirect_uri exactly as configured to the provider’s allowed redirect list |
400 Bad Request | invalid_grant | Authorization code already used or expired | Codes are single-use and expire quickly (usually 60 seconds); do not reuse or delay exchange |
401 Unauthorized | invalid_token | Access token expired or revoked | Use /auth/refresh with the refresh token to obtain a new access token |
403 Forbidden | access_denied | User denied consent or lacks required scope | Verify the scopes requested in your provider app settings; ensure the user approves all requested permissions |
403 Forbidden | org_required | User is not a member of required_org (GitHub) | Verify required_org value; user must be a member of that GitHub organization |
500 Internal Server Error | token_exchange_failed | Network error reaching the provider | Check outbound connectivity; verify discovery_url and the issuer are reachable |
| Metric | Description |
|---|---|
fraiseql_auth_logins_total | Login attempts |
fraiseql_auth_login_success_total | Successful logins |
fraiseql_auth_login_failure_total | Failed logins |
fraiseql_auth_token_refresh_total | Token refreshes |
fraiseql_auth_session_active | Active sessions |
Ensure redirect URI matches exactly in:
OIDC_REDIRECT_URI environment variablediscovery_url is correct and reachableOAuth/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.
Authorization: Bearer <token> as an HTTP header.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
Rate Limiting
Rate Limiting — Protect auth endpoints
Deployment
Deployment — Production OAuth setup