IdRa

Python (Django)

Add IdRa sign-in to a Django app with Authlib: register, configure, run, sign out.

A server-side Django app using the authorization code flow with PKCE and Authlib. Tested with Python 3.14, Django 6.1 and Authlib 1.9, through a real browser sign-in against an IdRa running locally over http. It hasn't been run over https or against idra.team.

Sample repository (being opened soon): IdRaHQ/idra-sample-django. It has this quickstart's code plus a protected page.

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

Django 6.1 needs Python 3.12 or later.

python3 -m venv .venv && source .venv/bin/activate
pip install django==6.1.2 authlib==1.9.1 requests==2.34.2
django-admin startproject mysite .

Code

Add these lines to the end of the generated settings:

mysite/settings.py
import os

SECRET_KEY = os.environ["SESSION_SECRET"]
SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
SESSION_COOKIE_SECURE = os.environ["APP_URL"].startswith("https://")
mysite/views.py
import json
import os
from urllib.parse import urlencode

from authlib.integrations.django_client import OAuth
from django.http import HttpResponse
from django.shortcuts import redirect
from django.utils.html import format_html

ISSUER = os.environ["IDRA_ISSUER"]
APP_URL = os.environ["APP_URL"]

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",
    },
)


def home(request):
    user = request.session.get("user")
    if not user:
        return HttpResponse('<h1>Signed out</h1><a href="/login">Sign in with IdRa</a>')
    return HttpResponse(
        format_html(
            '<h1>Signed in</h1><pre>{}</pre><a href="/logout">Sign out</a>',
            json.dumps(user, indent=2),
        )
    )


def login(request):
    return idra.authorize_redirect(request, f"{APP_URL}/callback")


def callback(request):
    try:
        token = idra.authorize_access_token(request)
        # The ID token has no email or name: they come from UserInfo.
        request.session["user"] = dict(idra.userinfo(token=token))
        request.session["id_token"] = token["id_token"]
    except Exception as e:
        print(f"Sign-in failed: {e!r}")
        return HttpResponse(format_html("Sign-in failed: {}", e), status=400)
    return redirect("/")


def logout(request):
    id_token = request.session.get("id_token")
    request.session.flush()
    if not id_token:
        return redirect("/")
    metadata = idra.load_server_metadata()
    query = urlencode({"id_token_hint": id_token, "post_logout_redirect_uri": f"{APP_URL}/"})
    return redirect(f"{metadata['end_session_endpoint']}?{query}")
mysite/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path("", views.home),
    path("login", views.login),
    path("callback", views.callback),
    path("logout", views.logout),
]

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. It also means this app needs no database, so ignore the unapplied migrations warning from runserver.

startproject leaves DEBUG = True. Turn it off and set ALLOWED_HOSTS before you deploy.

Run

set -a; source .env; set +a
python manage.py runserver 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.
Sign-in error: client not found, or once signed in 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