Python
Sign in with NIA — Flask
Add “Sign in with NIA” to a Flask 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 Flask application using the standard OpenID Connect flow. NIA is a conformant OIDC provider, so you use Authlib — Flask's most common OAuth/OIDC client — with no NIA-specific SDK.
Flask runs as a confidential client: the browser is redirected to NIA to sign in, and your Flask 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 routes — login, callback, logout
02Requirements
- Python 3.10+ and Flask 2.3+
- 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
- A configured
app.secret_key(required for Flask's session)
03Installation
Install Authlib, Flask, and an HTTP backend. Authlib fetches the discovery document and JWKS over HTTP.
pip install Authlib Flask requests python-dotenv04Required OAuth library
Register NIA once with Authlib's Flask 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.flask_client import OAuth
oauth = OAuth(app)
oauth.register(
name="nia",
server_metadata_url=f"{app.config['NIA_ISSUER']}/.well-known/openid-configuration",
client_id=app.config["NIA_CLIENT_ID"],
client_secret=app.config["NIA_CLIENT_SECRET"],
client_kwargs={
"scope": "openid email profile",
# NIA requires PKCE — Authlib generates the verifier/challenge for you.
"code_challenge_method": "S256",
},
)app.secret_key must be set before you register the client — Authlib stores the PKCE verifier and state in Flask's signed session cookie.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 into app.config.
NIA_ISSUER=https://dev-api.niaauth.com
NIA_CLIENT_ID=your-client-id
NIA_CLIENT_SECRET=your-client-secret
FLASK_SECRET_KEY=replace-with-a-long-random-value06Register OAuth Client
In the NIA dashboard, create the client your Flask 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:5000/auth/callback
# Production
https://app.example.com/auth/callbackhttp://localhost:5000/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 route that redirects to NIA. Authlib stores the PKCE verifier and state in Flask's session, so app.secret_key must be set.
from flask import url_for
from nia_auth import oauth
@app.route("/auth/login")
def login():
redirect_uri = url_for("callback", _external=True)
return oauth.nia.authorize_redirect(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.
from flask import redirect, session
@app.route("/auth/callback")
def callback():
token = oauth.nia.authorize_access_token()
userinfo = token["userinfo"] # validated ID-token claims
session["user"] = {
"sub": userinfo["sub"],
"email": userinfo.get("email"),
"name": userinfo.get("name"),
}
session["nia_id_token"] = token["id_token"]
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()
claims = token["userinfo"]
assert claims["iss"] == app.config["NIA_ISSUER"] # issuer
assert claims["aud"] == app.config["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 session["user"] is set, use Flask's signed session cookie as normal. Protect routes with a small decorator:
from functools import wraps
from flask import redirect, session
def login_required(view):
@wraps(view)
def wrapper(*args, **kwargs):
if not session.get("user"):
return redirect("/auth/login")
return view(*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 flask import session
from nia_auth import oauth
def refresh_session():
refresh_token = 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.
session["nia_refresh"] = new_token.get("refresh_token", refresh_token)
return new_token14Logout
Clear the Flask 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 flask import redirect, session, url_for
@app.route("/auth/logout")
def logout():
id_token = session.get("nia_id_token")
session.clear()
params = urlencode({
"id_token_hint": id_token or "",
"post_logout_redirect_uri": url_for("index", _external=True),
})
return redirect(f"{app.config['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 a strong, stable
FLASK_SECRET_KEY(rotating it invalidates every session) - Enable secure cookies:
SESSION_COOKIE_SECURE=True - Run behind a production WSGI server (gunicorn/uWSGI), not
flask run
app.config.update(
SESSION_COOKIE_SECURE=True,
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax", # required for the OIDC redirect round-trip
)16Troubleshooting
- 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. Verify
app.secret_keyis set and doesn't change between requests (e.g. across reloads or multiple workers). - mismatching_state — same root cause: the session cookie isn't surviving the redirect. Check
SESSION_COOKIE_SAMESITEand 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.