Python (Flask)
Add IdRa sign-in to a Flask app with Authlib: register, configure, run, sign out.
A server-side Flask app using the authorization code flow with PKCE and Authlib. Tested with Python 3.12.
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:5050/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:5050/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.
Port 5050 avoids macOS AirPlay, which often holds 5000.
Configure
IDRA_ISSUER=https://<your-org>.idra.team/api/auth
IDRA_CLIENT_ID=<Client ID>
IDRA_CLIENT_SECRET=<client secret>
APP_URL=http://localhost:5050
SESSION_SECRET=<output of: openssl rand -hex 32>Install
python3 -m venv .venv && source .venv/bin/activate
pip install flask authlib requestsCode
import json
import os
from urllib.parse import urlencode
from authlib.integrations.flask_client import OAuth
from flask import Flask, redirect, session
ISSUER = os.environ["IDRA_ISSUER"]
SECRET = os.environ.get("IDRA_CLIENT_SECRET")
APP_URL = os.environ["APP_URL"]
app = Flask(__name__)
app.secret_key = os.environ["SESSION_SECRET"]
app.config.update(
SESSION_COOKIE_SAMESITE="Lax",
SESSION_COOKIE_SECURE=APP_URL.startswith("https://"),
)
oauth = OAuth(app)
idra = oauth.register(
"idra",
client_id=os.environ["IDRA_CLIENT_ID"],
client_secret=SECRET,
server_metadata_url=f"{ISSUER}/.well-known/openid-configuration",
client_kwargs={
"scope": "openid profile email groups",
"code_challenge_method": "S256",
# With a secret: client_secret_basic. Without one: a public client.
"token_endpoint_auth_method": "client_secret_basic" if SECRET else "none",
},
)
@app.get("/")
def home():
user = 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")
def login():
return idra.authorize_redirect(f"{APP_URL}/callback")
@app.get("/callback")
def callback():
try:
token = idra.authorize_access_token()
# The ID token has no email or name: they come from UserInfo.
session["user"] = dict(idra.userinfo(token=token))
session["id_token"] = token["id_token"]
except Exception as e:
app.logger.exception("Sign-in failed")
return f"Sign-in failed: {e}", 400
return redirect("/")
@app.get("/logout")
def logout():
id_token = session.get("id_token")
session.clear()
if not id_token:
return redirect("/")
query = urlencode({"id_token_hint": id_token, "post_logout_redirect_uri": f"{APP_URL}/"})
return redirect(f"{idra.load_server_metadata()['end_session_endpoint']}?{query}")
if __name__ == "__main__":
app.run(port=int(os.environ.get("PORT", 5050)))Flask's default 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
python app.pyVerify sign-in and sign-out
- Open
http://localhost:5050and 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:5050/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 none | A Web Application is running without its secret. | Set IDRA_CLIENT_SECRET. |
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.