IdRa

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:

  1. Under Connect your app, copy the Issuer and Client ID. The .env snippet there has the same values, named OIDC_*; this quickstart names them IDRA_*.
  2. 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

.env
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.0

Code

app.py
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("<", "&lt;")
    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("<", "&lt;"), 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 8000

Verify sign-in and sign-out

  1. Open http://localhost:8000 and select Sign in with IdRa. You land on IdRa's sign-in page.
  2. Sign in, with MFA if your policies ask for it. You come back to a Signed in page showing your claims.
  3. 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.

ClaimID tokenUserInfoScope
subYesYesopenid
preferred_username (the email), updated_atYesYesprofile
name, given_name, family_nameNoYesprofile
email, email_verifiedNoYesemail
groups (group names, always including Everyone)YesYesgroups

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 seeCauseFix
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 uriThe 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_scopeThe 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 clientThe authorize request has no PKCE challenge.Send code_challenge with code_challenge_method=S256. IdRa accepts only S256.
invalid_client: invalid client_secretWrong secret, or it was rotated.Use Generate new secret on Sign On.
invalid_client: client registered for client_secret_basic cannot use client_secret_postThe library sent the secret in the body.Send it as HTTP Basic, or change Client authentication on Sign On.
invalid_grant: redirect_uri mismatchThe token request's redirect URI differs from the authorize request's.Use the same value in both.
invalid_grant: code verification failedThe PKCE verifier doesn't match the challenge.Keep the verifier from the authorize step, per sign-in.
404 on discoveryThe 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 pageNo 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.

On this page