Python
Sign in with NIA — Django
Add “Sign in with NIA” to a Django app with Authlib and the standard OpenID Connect flow. Follow the steps top to bottom — tick each one off as you go.
01Overview
This guide adds “Sign in with NIA” to a Django application using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use Authlib — Django's most common OAuth/OIDC client — with no NIA-specific SDK.
Django runs as a confidential client: the browser is redirected to NIA to sign in, and your Django server exchanges the returned code for tokens using your client secret. Authlib handles PKCE, state, and ID-token signature validation for you.
- Flow: Authorization Code + PKCE (S256)
- Tokens: RS256 access & ID tokens, opaque refresh token
- You write: three views — login, callback, logout
02Requirements
- Python 3.10+ and Django 4.2+
- A NIA account with access to the client dashboard
- A registered OAuth client (
client_id+client_secret) - Your app's redirect URI registered on that client
- Django's session middleware enabled (default in
startproject)
03Installation
Install Authlib and an HTTP backend. Authlib fetches the discovery document and JWKS over HTTP.
pip install Authlib requests python-dotenv04Required OAuth library
Register NIA once with Authlib's Django integration. The server_metadata_url points at NIA's discovery document — every endpoint, the JWKS URL, and supported algorithms are configured automatically from it.
from authlib.integrations.django_client import OAuth
from django.conf import settings
oauth = OAuth()
oauth.register(
name="nia",
server_metadata_url=f"{settings.NIA_ISSUER}/.well-known/openid-configuration",
client_id=settings.NIA_CLIENT_ID,
client_secret=settings.NIA_CLIENT_SECRET,
client_kwargs={
"scope": "openid email profile",
# NIA requires PKCE — Authlib generates the verifier/challenge for you.
"code_challenge_method": "S256",
},
)offline_access to the scope only if you need refresh tokens (see step 14).05Environment Variables
Never hard-code secrets. Keep them in a .env file (git-ignored) and read them in settings.py.
NIA_ISSUER=https://dev-api.niaauth.com
NIA_CLIENT_ID=your-client-id
NIA_CLIENT_SECRET=your-client-secret
DJANGO_SECRET_KEY=replace-with-a-long-random-value06Register OAuth Client
In the NIA dashboard, create the client your Django app will authenticate as:
- Open Dashboard → Clients → New client
- Client type: Web (confidential)
- Allowed scopes:
openidemailprofile(addoffline_accessfor refresh) - Add your redirect URI (next step)
- Copy the
client_idandclient_secret— the secret is shown once
07Redirect URI
NIA matches redirect URIs exactly — scheme, host, port, and path must be identical, with no trailing-slash difference. Register one URI per environment.
# Development
http://localhost:8000/auth/callback
# Production
https://app.example.com/auth/callbackhttp://localhost:8000/auth/callback/ (trailing slash) will be rejected.08Issuer
The issuer is the single URL every OIDC library needs. Its discovery document advertises all endpoints below.
Issuer: https://dev-api.niaauth.com
Discovery: https://dev-api.niaauth.com/.well-known/openid-configuration
Authorization: https://dev-api.niaauth.com/oauth/authorize
Token: https://dev-api.niaauth.com/oauth/token
UserInfo: https://dev-api.niaauth.com/oauth/userinfo
JWKS: https://dev-api.niaauth.com/oauth/jwks.json09Authorization Flow
Add a login view that redirects to NIA, and wire the URLs. Authlib stores the PKCE verifier and state in the Django session, so session middleware must be active.
from django.shortcuts import redirect
from .oauth import oauth
def login(request):
redirect_uri = request.build_absolute_uri("/auth/callback")
return oauth.nia.authorize_redirect(request, redirect_uri)10Token Exchange
On callback, exchange the authorization code for tokens. authorize_access_token verifies state, sends the PKCE verifier, exchanges the code, and validates the ID token — all in one call.
def callback(request):
token = oauth.nia.authorize_access_token(request)
userinfo = token["userinfo"] # validated ID-token claims
request.session["user"] = {
"sub": userinfo["sub"],
"email": userinfo.get("email"),
"name": userinfo.get("name"),
}
request.session["nia_id_token"] = token["id_token"]
request.session["nia_refresh"] = token.get("refresh_token")
return redirect("/")11JWKS Validation
NIA signs ID tokens with RS256. Authlib fetches the public keys from the JWKS endpoint (discovered automatically), caches them, and verifies the signature, iss, aud, exp, and nonce during authorize_access_token. You do not verify manually.
To confirm what was validated, inspect the parsed claims:
token = oauth.nia.authorize_access_token(request)
claims = token["userinfo"]
assert claims["iss"] == settings.NIA_ISSUER # issuer
assert claims["aud"] == settings.NIA_CLIENT_ID # audience = your client
# signature (RS256, via JWKS), exp and nonce are already verified by Authlib.kid, rotation needs no code change on your side.12Session Management
Once request.session["user"] is set, use Django's session framework as normal. Protect views with a small decorator:
from functools import wraps
from django.shortcuts import redirect
def require_user(view):
@wraps(view)
def wrapper(request, *args, **kwargs):
if not request.session.get("user"):
return redirect("/auth/login")
return view(request, *args, **kwargs)
return wrapper13Refresh Tokens
If you requested offline_access, NIA returns a refresh token. Exchange it for a fresh access token when the current one expires. NIA rotates refresh tokens, so always store the new one.
from .oauth import oauth
def refresh_session(request):
refresh_token = request.session.get("nia_refresh")
if not refresh_token:
return None
new_token = oauth.nia.fetch_access_token(
grant_type="refresh_token",
refresh_token=refresh_token,
)
# NIA rotates the refresh token — persist the replacement.
request.session["nia_refresh"] = new_token.get("refresh_token", refresh_token)
return new_token14Logout
Clear the Django session and send the user to NIA's end-session endpoint so they are signed out of NIA too. Register your post_logout_redirect_uri on the client.
from urllib.parse import urlencode
from django.conf import settings
from django.shortcuts import redirect
def logout(request):
id_token = request.session.get("nia_id_token")
request.session.flush()
params = urlencode({
"id_token_hint": id_token or "",
"post_logout_redirect_uri": request.build_absolute_uri("/"),
})
return redirect(f"{settings.NIA_ISSUER}/oauth/logout?{params}")15Deployment
Before going live:
- Register the production redirect URI (HTTPS) on the client
- Set all env vars in your host — never commit
.env - Set
ALLOWED_HOSTSandCSRF_TRUSTED_ORIGINS - Enable secure cookies:
SESSION_COOKIE_SECURE=True,CSRF_COOKIE_SECURE=True - Use a persistent session backend (DB/cache) behind multiple workers
ALLOWED_HOSTS = ["app.example.com"]
CSRF_TRUSTED_ORIGINS = ["https://app.example.com"]
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = "Lax" # required for the OIDC redirect round-trip16Troubleshooting
- redirect_uri_mismatch — the URI must match the registered one exactly (no trailing slash, correct scheme/port).
- invalid_client — wrong
client_secret, or the client's token-auth method differs from what Authlib sends. - invalid_grant / PKCE failed — the session was lost between login and callback. Ensure session middleware is on and
SESSION_COOKIE_SAMESITE="Lax". - mismatching_state — same root cause: the session cookie isn't surviving the redirect. Check cookie domain/secure settings.
- invalid_scope — the scope isn't in the client's
allowed_scopes; add it in the dashboard. - ID-token signature — usually server clock skew; sync time via NTP.