# Python (Django) (https://docs.idra.zone/docs/quickstarts/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](https://docs.authlib.org/). Tested with Python 3.12 and 3.14, Django 6.1 and Authlib 1.9, through a real browser sign-in against an IdRa running locally, over `http` and `https`. It hasn't been run against idra.team.

Sample repository (being opened soon): [IdRaHQ/idra-sample-django](https://github.com/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

```sh title=".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.

```sh
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:

```python title="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://")
```

```python title="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}")
```

```python title="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

```sh
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.

| 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](https://docs.idra.zone/docs/admin/oidc) 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. |
| **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_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.