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.