Auth0 OAuth 2.0 / OIDC Setup Guide¶
This guide walks you through setting up Auth0 authentication with FraiseQL. Auth0 is a
first-class, built-in provider in FraiseQL v1: you configure it in Python (via
Auth0Config / Auth0Provider) and authentication runs inside your FastAPI app.
Why Auth0?¶
- Managed service: No infrastructure to maintain
- Enterprise-grade: Proven by thousands of companies
- Fast setup: Minutes to configure
- Rich features: MFA, social login, passwordless auth
- Scalability: Handles millions of authentications
How it works in FraiseQL¶
FraiseQL validates the access token (a JWT) that Auth0 issues for your API. On every
GraphQL request, the Auth0Provider:
- Fetches and caches Auth0's JWKS (public signing keys).
- Verifies the token's RS256 signature,
audience(your API identifier), andissuer(https://your-domain.auth0.com/). - Builds a
UserContext(user id, email, roles, permissions) and places it atinfo.context["user"].
Your resolvers then enforce access with the @requires_auth / @requires_permission /
@requires_role decorators. FraiseQL does not mint or store tokens itself — Auth0 (or
your frontend SDK) handles the login redirect, callback, and token exchange; FraiseQL only
verifies the bearer token on incoming requests.
Prerequisites¶
Required Knowledge:
- OAuth 2.0 and OIDC fundamentals (authorization code flow, ID tokens, access tokens)
- JWT token structure and RS256 signature verification
- HTTP/REST APIs and bearer-token authorization
- Auth0 tenant management and application concepts
- Basic Python / FastAPI
Required Software:
- FraiseQL v1
- Python 3.13+
- curl or Postman (for API testing)
- A code editor
Required Infrastructure:
- Auth0 account (free tier available at https://auth0.com/signup)
- Auth0 tenant/domain (created automatically with account)
- A FraiseQL FastAPI app (local or deployed)
- PostgreSQL database
- An Auth0 Application (for the frontend login flow — created in Step 1)
- An Auth0 API definition (defines the audience your tokens target — created in Step 3)
Optional but Recommended:
- A frontend Auth0 SDK (auth0-react, auth0-spa-js, etc.) to drive the login flow
- Auth0 Actions for custom claims (roles/permissions in the token)
- Auth0 Logs page for debugging authentication issues
Time Estimate: 20-40 minutes for complete setup and first authenticated request
Step 1: Create Auth0 Application¶
This application represents the client (your frontend) that logs users in.
- Go to Auth0 Dashboard
- Click "Applications" (left sidebar)
- Click "Create Application"
- Enter a name, e.g. "MyApp Frontend"
- Choose the application type that matches your frontend (Single Page Application for a SPA, Regular Web Application for server-rendered)
- Click "Create"
Step 2: Configure Application Settings¶
- In the application settings, go to the "Settings" tab
- Note these values (the frontend SDK needs them):
- Domain:
your-domain.auth0.com - Client ID: (copy this)
-
Client Secret: (only for Regular Web Applications)
-
Under "Allowed Callback URLs" add the URLs your frontend redirects back to after login:
http://localhost:3000/callback
https://yourdomain.com/callback
- Under "Allowed Logout URLs" add:
http://localhost:3000
https://yourdomain.com
- Under "Allowed Web Origins" add:
http://localhost:3000
https://yourdomain.com
- Click "Save Changes"
The login redirect and callback are handled by your frontend and Auth0. Your FraiseQL backend only receives the resulting access token as a
Bearerheader — it has no callback route of its own.
Step 3: Create API (defines the token audience)¶
The API identifier becomes the audience of the access tokens FraiseQL validates.
- Go to "Applications" → "APIs" (left sidebar)
- Click "Create API"
- Enter a name, e.g. "MyApp API"
- Identifier (audience):
https://api.myapp.com(any stable URI — it does not need to resolve) - Signing algorithm: RS256 (default)
- Click "Create"
Keep this identifier handy — it maps to FraiseQL's api_identifier / auth0_api_identifier.
Step 4: Configure FraiseQL¶
Set your settings via environment variables (prefixed with FRAISEQL_). For example, in a
.env file:
# Database
FRAISEQL_DATABASE_URL=postgresql://user:password@localhost/myapp
# Auth0 (audience = the API Identifier from Step 3)
FRAISEQL_AUTH_PROVIDER=auth0
FRAISEQL_AUTH0_DOMAIN=your-domain.auth0.com
FRAISEQL_AUTH0_API_IDENTIFIER=https://api.myapp.com
# FRAISEQL_AUTH0_ALGORITHMS defaults to ["RS256"]
These map onto the FraiseQLConfig fields auth_provider="auth0", auth0_domain,
auth0_api_identifier, and auth0_algorithms (default ["RS256"]).
Step 5: Wire up the FraiseQL app¶
Pass an Auth0Config (or an Auth0Provider) to create_fraiseql_app via the auth
argument. FraiseQL builds the provider and validates tokens in-process.
import os
from fraiseql.auth import Auth0Config
from fraiseql.fastapi import create_fraiseql_app
from myapp.schema import User, Post, users, user, create_user
auth = Auth0Config(
domain=os.environ["FRAISEQL_AUTH0_DOMAIN"], # "your-domain.auth0.com"
api_identifier=os.environ["FRAISEQL_AUTH0_API_IDENTIFIER"], # "https://api.myapp.com"
algorithms=["RS256"],
)
app = create_fraiseql_app(
database_url=os.environ["FRAISEQL_DATABASE_URL"],
types=[User, Post],
queries=[users, user],
mutations=[create_user],
auth=auth,
production=True, # False enables the GraphQL playground
)
Run it with any ASGI server:
uvicorn app:app --host 0.0.0.0 --port 8000
Alternative wiring. You can construct the provider directly and pass it instead:
from fraiseql.auth import Auth0Provider auth = Auth0Provider( domain="your-domain.auth0.com", api_identifier="https://api.myapp.com", algorithms=["RS256"], cache_jwks=True, ) app = create_fraiseql_app(database_url=..., types=[...], auth=auth)Or skip the
auth=argument entirely and rely on theFRAISEQL_AUTH0_*environment variables from Step 4 (withFRAISEQL_AUTH_PROVIDER=auth0).
Step 6: Protect resolvers¶
Authorization is enforced per resolver in Python. The decorators read the authenticated
UserContext from info.context["user"].
import fraiseql
from fraiseql.auth import requires_auth, requires_permission, requires_role
@fraiseql.query
@requires_auth
async def me(info) -> User:
user = info.context["user"] # a UserContext (guaranteed authenticated)
db = info.context["db"]
return await db.find_one("v_user", id=user.user_id)
@fraiseql.mutation
@requires_permission("users:write")
async def create_user(info, input: CreateUserInput) -> CreateUserSuccess | CreateUserError:
db = info.context["db"]
result = await db.execute_function(
"fn_create_user", {"name": input.name, "email": input.email}
)
if not result.get("success"):
return CreateUserError(message=result.get("message", "failed"))
return CreateUserSuccess(user=User(**result["user"]))
@fraiseql.mutation
@requires_role("admin")
async def delete_user(info, input: DeleteUserInput) -> DeleteUserSuccess | DeleteUserError:
db = info.context["db"]
result = await db.execute_function("fn_delete_user", {"id": str(input.id)})
if not result.get("success"):
return DeleteUserError(message=result.get("message", "failed"))
return DeleteUserSuccess(id=input.id)
UserContext exposes .user_id, .email, .name, .roles, .permissions, .metadata,
plus helpers .has_role(...), .has_permission(...), .has_any_role(...), and
.has_any_permission(...) for ad-hoc checks inside a resolver.
When access is denied, FraiseQL returns a GraphQL error with extensions.code set to
"UNAUTHENTICATED" (no/invalid token) or "FORBIDDEN" (missing role/permission) — not an
HTTP 4xx redirect.
Testing¶
1. Obtain an access token¶
In development you can request a token for your API directly from Auth0 using the client-credentials grant (enable a Machine-to-Machine application authorized for your API, or use the "Test" tab on the API page in the dashboard):
curl -X POST https://your-domain.auth0.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_M2M_CLIENT_ID",
"client_secret": "YOUR_M2M_CLIENT_SECRET",
"audience": "https://api.myapp.com",
"grant_type": "client_credentials"
}'
In production, your frontend obtains the access token via the Auth0 login flow and sends it to the GraphQL endpoint.
2. Call the GraphQL endpoint with the token¶
curl -X POST http://localhost:8000/graphql \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"query": "{ me { id email } }"}'
A request without a valid token to a protected resolver returns a GraphQL error with
extensions.code = "UNAUTHENTICATED".
Advanced: Roles and permissions in tokens¶
For @requires_role / @requires_permission to work, the user's roles and permissions must
be present in the access token. Configure this in Auth0:
- Go to "User Management" → "Roles", create roles (e.g.
admin) and attach permissions (e.g.users:read,users:write,users:delete) defined on your API. - Assign roles to users under "User Management" → "Users".
- Enable "Add Permissions in the Access Token" (and optionally RBAC) on your API's settings
so
permissionsappears in the token. FraiseQL reads the token'spermissionsclaim directly intoUserContext.permissions.
For roles, add a custom claim via an Auth0 Action (Post-Login trigger). FraiseQL reads roles from a namespaced claim derived from your API identifier:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://api.myapp.com/';
const roles = event.authorization?.roles ?? [];
api.accessToken.setCustomClaim(`${namespace}roles`, roles);
};
Then check roles in any resolver:
@fraiseql.query
@requires_auth
async def admin_dashboard(info) -> Dashboard:
user = info.context["user"]
if not user.has_role("admin"):
raise PermissionError("admin role required")
...
Advanced: Social login¶
Auth0 supports social login (Google, GitHub, etc.). To enable:
- Go to "Authentication" → "Social"
- Enable the desired providers and supply their credentials
- Auth0 handles the OAuth flow automatically
No FraiseQL change is needed — tokens still arrive as a Bearer access token validated the
same way.
Advanced: Multi-Factor Authentication (MFA)¶
To require MFA:
- Go to "Security" → "Multi-factor Authentication"
- Enable the desired factors (SMS, Email, Authenticator app)
- Configure enrollment
Auth0 prompts for MFA during login; FraiseQL is unaffected.
Troubleshooting¶
Error: "Invalid token" / INVALID_TOKEN¶
Cause: The token signature, audience, or issuer doesn't match.
Solution:
- Verify
auth0_domainmatches your tenant exactly (no scheme, no trailing slash). - Verify
auth0_api_identifierequals the token'saudclaim (the API Identifier from Step 3). - Confirm the token was issued for this API, not just an ID token from the application.
Error: "Token has expired" / TOKEN_EXPIRED¶
Cause: The access token's exp has passed.
Solution: Have the frontend refresh the token (Auth0 SDK handles this) and retry.
Error: FORBIDDEN (permission/role required)¶
Cause: The user is authenticated but lacks the required permission or role.
Solution:
- Confirm the role/permission is assigned in Auth0.
- Confirm "Add Permissions in the Access Token" is enabled (for
permissions) and your Post-Login Action sets the namespacedrolesclaim (for roles).
Tokens validate but roles are empty¶
Cause: Roles aren't in the access token.
Solution: Add the namespaced roles claim via an Auth0 Action (see "Roles and
permissions in tokens" above). The namespace FraiseQL reads is
https://<api_identifier>/roles.
Connectivity check¶
# Confirm the tenant's OIDC metadata and JWKS are reachable
curl https://your-domain.auth0.com/.well-known/openid-configuration
curl https://your-domain.auth0.com/.well-known/jwks.json
Production Deployment¶
Environment configuration¶
# .env.prod
FRAISEQL_DATABASE_URL=postgresql://user:pass@prod-db/myapp
FRAISEQL_AUTH_PROVIDER=auth0
FRAISEQL_AUTH0_DOMAIN=your-domain.auth0.com
FRAISEQL_AUTH0_API_IDENTIFIER=https://api.myapp.com
Run with a production ASGI setup (for example multiple Uvicorn workers behind a proxy):
uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
Set production=True on create_fraiseql_app (or FRAISEQL_ENVIRONMENT=production) to
disable the playground and enable production hardening.
Auth0 tenant configuration¶
- Go to "Tenant Settings"
- Set a friendly name for production
- Configure session timeout
- Set a custom domain (optional but recommended): use
auth.example.cominstead ofyour-domain.auth0.com— this also keeps the issuer stable if you migrate tenants
Monitoring¶
Auth0 provides logs under "Monitoring" → "Logs": authentication events, failures, and anomaly detection. FraiseQL additionally emits security audit events (auth success/failure, token expired/invalid) through its audit logger.
Cost¶
Auth0 pricing:
- Free tier: Up to 7,000 active users
- Pro: Pay-as-you-go, starts around $13/month
- Enterprise: Custom pricing
Most applications fit in the free tier initially.
See Also¶
Next Step: See API Reference for the full authentication API.