Python (FastAPI)
Add IdRa sign-in to a FastAPI app with Authlib: register, configure, run, sign out.
A server-side FastAPI app using the authorization code flow with PKCE and Authlib. Tested with Python 3.12, FastAPI 0.162 and Authlib 1.9.
Register the app
Use one app for local development and production. Open your organization's admin console at https://<your-org>.idra.team/admin (or /admin on your own IdRa). Go to Applications → Create App Integration, choose OIDC - OpenID Connect, and set:
- Application type: Web Application
- App integration name: any name
- Sign-in redirect URIs:
http://localhost:8000/callback
Leave Assign this app to me so I can test it on, then select Create. Copy the client secret, shown once, and select I've saved it.
The app's Sign On tab opens:
- Under Connect your app, copy the Issuer and Client ID. The
.envsnippet there has the same values, namedOIDC_*; this quickstart names themIDRA_*. - Add
http://localhost:8000/to Sign-out redirect URIs and select Save.
To let other people sign in, assign them or their groups on Assignments.
Local redirects and production
http redirects work on localhost, 127.0.0.1 or [::1], with a port, and match exactly. PKCE stays required while the app has one. For production, add your https URIs (such as https://app.example.com/callback and https://app.example.com/) to the same app, then remove the local ones.
Configure
IDRA_ISSUER=https://<your-org>.idra.team/api/auth
IDRA_CLIENT_ID=<Client ID>
IDRA_CLIENT_SECRET=<client secret>
APP_URL=http://localhost:8000
SESSION_SECRET=<output of: openssl rand -hex 32>Install
python3 -m venv .venv && source .venv/bin/activate
pip install fastapi==0.162.0 uvicorn==0.54.0 authlib==1.9.1 httpx==0.28.1 itsdangerous==2.2.0Code
import json
import os
from urllib.parse import urlencode
from authlib.integrations.starlette_client import OAuth
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
ISSUER = os.environ["IDRA_ISSUER"]
APP_URL = os.environ["APP_URL"]
app = FastAPI()
app.add_middleware(
SessionMiddleware,
secret_key=os.environ["SESSION_SECRET"],
same_site="lax",
https_only=APP_URL.startswith("https://"),
)
oauth = OAuth()
idra = oauth.register(
"idra",
client_id=os.environ["IDRA_CLIENT_ID"],
client_secret=os.environ["IDRA_CLIENT_SECRET"],
server_metadata_url=f"{ISSUER}/.well-known/openid-configuration",
client_kwargs={
"scope": "openid profile email groups",
"code_challenge_method": "S256",
"token_endpoint_auth_method": "client_secret_basic",
},
)
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
user = request.session.get("user")
if not user:
return '<h1>Signed out</h1><a href="/login">Sign in with IdRa</a>'
body = json.dumps(user, indent=2).replace("<", "<")
return f'<h1>Signed in</h1><pre>{body}</pre><a href="/logout">Sign out</a>'
@app.get("/login")
async def login(request: Request):
return await idra.authorize_redirect(request, f"{APP_URL}/callback")
@app.get("/callback")
async def callback(request: Request):
try:
token = await idra.authorize_access_token(request)
# The ID token has no email or name: they come from UserInfo.
request.session["user"] = dict(await idra.userinfo(token=token))
request.session["id_token"] = token["id_token"]
except Exception as e:
print(f"Sign-in failed: {e!r}")
return HTMLResponse(f"Sign-in failed: {e}".replace("<", "<"), status_code=400)
return RedirectResponse("/", status_code=302)
@app.get("/logout")
async def logout(request: Request):
id_token = request.session.get("id_token")
request.session.clear()
if not id_token:
return RedirectResponse("/", status_code=302)
metadata = await idra.load_server_metadata()
query = urlencode({"id_token_hint": id_token, "post_logout_redirect_uri": f"{APP_URL}/"})
return RedirectResponse(f"{metadata['end_session_endpoint']}?{query}", status_code=302)The session is a signed cookie: readable by the browser, though it can't be changed. This one holds the claims and ID token, so use a server-side session if those are sensitive for you.
Run
set -a; source .env; set +a
uvicorn app:app --port 8000Verify sign-in and sign-out
- Open
http://localhost:8000and select Sign in with IdRa. You land on IdRa's sign-in page. - Sign in, with MFA if your policies ask for it. You come back to a Signed in page showing your claims.
- Select Sign out. You return to Signed out. This also ends the IdRa session, so the next sign-in asks for your credentials again.
Reports → System Log in the admin console shows the sign-in.
What the app receives
The ID token doesn't carry email or name. Read them from UserInfo, as this sample does.
| Claim | ID token | UserInfo | Scope |
|---|---|---|---|
sub | Yes | Yes | openid |
preferred_username (the email), updated_at | Yes | Yes | profile |
name, given_name, family_name | No | Yes | profile |
email, email_verified | No | Yes | email |
groups (group names, always including Everyone) | Yes | Yes | groups |
Add offline_access to the scopes to receive a refresh token. Access tokens last an hour. See OpenID Connect for the rest.
Troubleshooting
| What you see | Cause | Fix |
|---|---|---|
Use https on a public host, or for local development http on localhost, 127.0.0.1 or [::1] with a port (…) | The redirect URI is http on another host or without a port, or https on a loopback host. | Use http://localhost:8000/callback locally and https on a public host in production. |
Sign-in error: invalid redirect uri | The app sent a redirect URI that isn't registered. Matching is exact, including port and path. | Make it match Sign-in redirect URIs on the Sign On tab. |
Access to … denied: You are not assigned to this application. | The person isn't assigned. | Assign them on Assignments. |
Access to … denied: This application is currently inactive. | The app is deactivated. | Activate it on the app's page. |
Access denied: This application is not registered with your organization. | Wrong client ID, or a client ID from another organization. | Copy the Client ID from Sign On. |
Access denied: Your organization's sign-on policy does not allow this access., URL has code=invalid_scope | The app asked for a scope that doesn't exist. | Use openid, profile, email, groups, offline_access or a scope defined under Security → API. |
pkce is required for this client | The authorize request has no PKCE challenge. | Send code_challenge with code_challenge_method=S256. IdRa accepts only S256. |
invalid_client: invalid client_secret | Wrong secret, or it was rotated. | Use Generate new secret on Sign On. |
invalid_client: client registered for client_secret_basic cannot use client_secret_post | The library sent the secret in the body. | Send it as HTTP Basic, or change Client authentication on Sign On. |
invalid_grant: redirect_uri mismatch | The token request's redirect URI differs from the authorize request's. | Use the same value in both. |
invalid_grant: code verification failed | The PKCE verifier doesn't match the challenge. | Keep the verifier from the authorize step, per sign-in. |
404 on discovery | The issuer is missing /api/auth. | Use the Issuer from Sign On: https://<your-org>.idra.team/api/auth. |
Page says Logged out. The requested post-logout redirect was not registered. | The person was signed out, but the return URL isn't registered. | Add it to Sign-out redirect URIs. |
| Confirm logout page | No id_token_hint, or it's for a different session than the browser's. | Send the ID token as the hint. Without one, the page is the expected safeguard. |
Reports → System Log records refused access as app.access with the reason.