A confidential client — your Django app holds a real client_secret (from registering your
Application, see Organizations & Applications).
Redirects to this platform's real hosted login page — see the integration guides overview — which shows every currently-real first-factor option; you never build a login form.
Two different hosts are used below: accounts.onehux.com for the hosted login/logout pages a browser is redirected to, and api-accounts.onehux.com for the actual API
calls your backend makes server-to-server — mixing these up is the single most common
integration mistake, since the wrong host doesn't error loudly, it just 404s.
Prefer the SDK — it already implements everything below (PKCE, the redirect, callback handling, token exchange, automatic refresh-token rotation, `/userinfo`, RP-initiated logout, OIDC Back-Channel Logout, and the public application launcher), real and maintained, rather than hand-rolling the raw API calls this page walks through manually:
pip install onehux-sso pypi.org/project/onehux-sso — the manual walkthrough below is still worth reading to understand what the SDK is doing on your behalf, and remains the reference if you're integrating in a language without an official SDK.
# yourapp/views.py
import base64
import hashlib
import secrets
from urllib.parse import urlencode
from django.conf import settings
from django.http import HttpResponseRedirect
ONEHUX_LOGIN_BASE_URL = "https://your-org.onehux.com" # see "Two different hosts" above
ONEHUX_API_BASE_URL = "https://api.your-domain.com" # see "Two different hosts" above
REDIRECT_URI = "https://yourapp.example.com/auth/callback"
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def login(request):
code_verifier = _b64url(secrets.token_bytes(48))
code_challenge = _b64url(hashlib.sha256(code_verifier.encode()).digest())
state = _b64url(secrets.token_bytes(16))
# PKCE verifier + state live server-side in the session — never in a cookie the
# browser can read, never round-tripped through the client.
request.session["onehux_pkce_verifier"] = code_verifier
request.session["onehux_oauth_state"] = state
params = {
"client_id": settings.ONEHUX_CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
"scope": "openid profile email",
"state": state,
}
return HttpResponseRedirect(f"{ONEHUX_LOGIN_BASE_URL}/login?{urlencode(params)}")# yourapp/views.py (continued)
import requests
from django.http import HttpResponseBadRequest, HttpResponseRedirect
def callback(request):
code = request.GET.get("code")
state = request.GET.get("state")
expected_state = request.session.pop("onehux_oauth_state", None)
code_verifier = request.session.pop("onehux_pkce_verifier", None)
if not code or not state or state != expected_state:
return HttpResponseBadRequest("Invalid or missing OAuth state.")
token_res = requests.post(
f"{ONEHUX_API_BASE_URL}/api/v1/oauth/token/",
json={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": settings.ONEHUX_CLIENT_ID,
"client_secret": settings.ONEHUX_CLIENT_SECRET,
"code_verifier": code_verifier,
},
timeout=10,
)
if not token_res.ok:
return HttpResponseBadRequest("Sign-in failed.")
tokens = token_res.json() # {access_token, id_token, refresh_token, token_type, expires_in, scope}
# Session establishment — the access token lives server-side only (Django's own
# session store), the same BFF discipline this platform's own dashboard follows on
# itself. The browser only ever gets Django's session cookie, never the raw token.
request.session["onehux_access_token"] = tokens["access_token"]
request.session["onehux_refresh_token"] = tokens["refresh_token"]
return HttpResponseRedirect("/dashboard")A confidential client's refresh token rotates on every use and is itself long-lived
(30-day idle timeout, 30-day absolute cap — see Sessions),
so a 401 from /userinfo almost always means
just the 15-minute access token expired, not that the user needs to sign in again:
def _refresh_tokens(request) -> str | None:
"""Rotates the stored refresh token for a new access/refresh pair. Returns the new
access token, or None if there's nothing to refresh with or the refresh itself fails
(expired, already used, or the whole family was revoked — see Sessions)."""
refresh_token = request.session.get("onehux_refresh_token")
if not refresh_token:
return None
res = requests.post(
f"{ONEHUX_API_BASE_URL}/api/v1/oauth/token/",
json={
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"client_id": settings.ONEHUX_CLIENT_ID,
"client_secret": settings.ONEHUX_CLIENT_SECRET,
},
timeout=10,
)
if not res.ok:
return None
tokens = res.json() # {access_token, id_token, refresh_token, token_type, expires_in, scope}
request.session["onehux_access_token"] = tokens["access_token"]
request.session["onehux_refresh_token"] = tokens["refresh_token"]
return tokens["access_token"]
def me(request):
access_token = request.session.get("onehux_access_token")
if not access_token:
return HttpResponseRedirect("/auth/login")
res = requests.get(
f"{ONEHUX_API_BASE_URL}/api/v1/oauth/userinfo/",
headers={"Authorization": f"Bearer {access_token}"},
timeout=10,
)
if res.status_code == 401:
access_token = _refresh_tokens(request)
if access_token is None:
request.session.pop("onehux_access_token", None)
request.session.pop("onehux_refresh_token", None)
return HttpResponseRedirect("/auth/login")
res = requests.get(
f"{ONEHUX_API_BASE_URL}/api/v1/oauth/userinfo/",
headers={"Authorization": f"Bearer {access_token}"},
timeout=10,
)
if not res.ok:
return HttpResponseRedirect("/auth/login")
claims = res.json() # sub, name, email, picture, roles, permissions, ...
return JsonResponse(claims)Redirect to end the platform-wide session, not just clear your own local one — every other Application relying on that same session is signed out too:
def logout(request):
request.session.pop("onehux_access_token", None)
request.session.pop("onehux_refresh_token", None)
params = urlencode({
"client_id": settings.ONEHUX_CLIENT_ID,
"post_logout_redirect_uri": "https://yourapp.example.com/logged-out",
})
return HttpResponseRedirect(f"{ONEHUX_LOGIN_BASE_URL}/end-session?{params}") Register this URL too: post_logout_redirect_uri is validated against
the exact same registered redirect_uris list as
your login callback — if https://yourapp.example.com/logged-out isn't in
that list too, /end-session rejects the request
with a real 400, even though your login callback
works fine.
The redirect above only notifies the app the user actually
clicked "log out" in. If they instead log out of a different app,
or directly at accounts.onehux.com, the
shared session is still revoked immediately and correctly — and if this Application
has registered a Back-Channel Logout endpoint, this platform pushes it a real,
spec-compliant logout_token (server-to-server,
HS256-signed with a dedicated backchannel signing secret — deliberately not your OAuth client_secret) as soon as the revocation
happens, rather than waiting for your app to notice on its own:
PATCH /api/v1/applications/{id}/backchannel-logout/
{ "backchannel_logout_uri": "https://yourapp.example.com/auth/backchannel-logout" }
-> { "backchannel_logout_uri": "...", "backchannel_logout_secret": "..." } (shown once) Receiving and verifying that push is its own integration surface — not covered by
this walkthrough. Without registering it, your app's own local session keeps
showing "signed in" until its next real /userinfo call fails, bounded by the access token's 15-minute lifetime. Don't treat a
locally-held session as a live signal of the IdP's true logout state unless you've
wired up the push.
Everything above assumes this Django app is the OAuth
client. If instead a separate frontend already holds the tokens — a
SvelteKit/Next.js app, a mobile app, another service — and this Django app is a pure
API sitting behind it, none of the above applies: no session, no redirect, no client_secret. This app only needs to verify
the Bearer token it was handed, against this platform's own published JWKS.
pip install "onehux-sso[resource-server]" # settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"onehux_sso.resource_server.OneHuxResourceServerAuthentication",
],
}
ONEHUX_RESOURCE_SERVER = {
"ISSUER": "https://api-accounts.onehux.com",
"TRUSTED_CLIENT_IDS": ["<your frontend BFFs client_id>"],
} # yourapp/views.py
from rest_framework.response import Response
from rest_framework.views import APIView
from onehux_sso.resource_server import HasOneHuxScope
class InvoicesView(APIView):
required_scope = "invoices:read"
permission_classes = [HasOneHuxScope]
def get(self, request):
# request.user is a OneHuxRemoteUser — sub, email, org_id, sid, scope, no local
# Users table row involved.
return Response({"sub": request.user.sub}) OneHuxResourceServerAuthentication fetches and
caches the platform's real, live JWKS
(/.well-known/jwks.json), matched by kid, and automatically re-fetches on a kid it hasn't seen — key rotation on the
platform side needs zero action here. TRUSTED_CLIENT_IDS is optional but recommended: it restricts acceptance to token(s) issued to specific
Application(s), real tenant isolation on a shared identity platform, not just "is this
token valid at all." Installing the [resource-server] extra is what pulls in djangorestframework —
the base pip install onehux-sso above has no
DRF dependency at all, since a Django-as-BFF integrator doesn't need one.