Native OAuth / OpenID Connect sign-in, without a plugin #6

Closed
opened 2026-09-05 11:55:57 +00:00 by tiagoagueda · 0 comments
Owner

Observation

i want native oauth / sso on the next release, preferable native and not via plugin

What exists today

Sign-in is by email and password only. INSTALLED_APPS in src/postulo/config/settings/base.py enables allauth and allauth.account; nothing else.

But the capability is already installed. django-allauth 65.19.2 — a dependency since M1 — ships allauth.socialaccount with a generic OpenID Connect provider (allauth.socialaccount.providers.openid_connect), which covers Authentik, Keycloak, Pocket ID, Zitadel, Kanidm, Google, and anything else that speaks OIDC, plus dedicated GitHub and GitLab providers. So "native" here means enabling and wiring what Postulo already depends on, not adding a plugin. One correction (2026-09-05): it does need allauth's socialaccount extra — requests, oauthlib and pyjwt[crypto] — which is not installed today; django-allauth[socialaccount] in pyproject.toml is the whole of it. Good: this is the one place where the modularity principle should not apply — signing in is part of the core, and an instance must be able to do it with nothing extra installed.

Shape

1. Enable allauth.socialaccount with the OpenID Connect provider, configured from the environment rather than the database, which is what allauth's SOCIALACCOUNT_PROVIDERS setting allows:

POSTULO_OIDC_NAME=Authentik            # what the button says
POSTULO_OIDC_SERVER_URL=https://auth.example.org/application/o/postulo/
POSTULO_OIDC_CLIENT_ID=…
POSTULO_OIDC_CLIENT_SECRET=…

Unset means no button, and nothing changes for anyone who does not want it. Room for a second, dedicated provider (GitHub, GitLab) later, but one generic OIDC entry covers every self-hosted identity provider that matters.

2. Account linking. An existing account with the same address as the identity provider reports should be linked, not duplicated. allauth's SOCIALACCOUNT_EMAIL_AUTHENTICATION and SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT do this, and they are safe only because the provider verified the address — which is exactly the case with OIDC. Postulo's own "Your details" page gets a Connected accounts section (allauth's connections view, in our shell like the email page).

3. Provisioning versus invite-only. Postulo is invite-only by default, and SOCIALACCOUNT_AUTO_SIGNUP would let anyone the identity provider knows walk straight in. That is a decision, not an accident. The options, all defensible:

  • SSO can sign in existing accounts only; new people still need an invitation (safest default);
  • SSO may create accounts, because the identity provider is the invitation — right for a household or a small team where the IdP already gates everything;
  • SSO may create accounts for members of a named group claim.

Proposal: the first as the default, the second behind POSTULO_OIDC_AUTO_SIGNUP=true, the third later if anyone asks.

4. Forward-auth, the other SSO. For a self-hoster, "SSO" often means the reverse proxy authenticates and passes a header — Remote-User, X-Forwarded-User — which is how oauth2-proxy and Authentik's outposts work, and what the oauth2-* containers on ragnar are doing. Django supports this natively with RemoteUserMiddleware and RemoteUserBackend. Worth offering as a second, explicitly opt-in mode (POSTULO_TRUSTED_PROXY_HEADER), with the loud caveat that it is only safe when the proxy strips that header from incoming requests — a misconfiguration makes it a way to become anyone by sending one header. If offered, the docs have to say that in bold.

5. SSO-only. Some operators will want to switch the password form off once SSO works. Cheap to add as POSTULO_PASSWORD_LOGIN=false once the rest exists; not required for this issue.

Interactions with the other 0.2.0 account work

  • #1 (username): OIDC provides preferred_username; map it on first sign-in, and fall back to the email local part when the claim is absent.
  • #2 (full name): OIDC provides name, given_name, family_name; populate on first sign-in so an SSO-created account is not born without a name.
  • #3 (verified email): the identity provider has already verified the address, and allauth can record it as such (SOCIALACCOUNT_EMAIL_VERIFICATION). An SSO sign-in must not then be asked to click a verification link for the address it just proved.
  • Invitations: see provisioning above. An invitation link followed and then completed by SSO should spend the invitation the same way a password signup does (accounts/adapter.py, accounts/signals.py).

Two gotchas, found before they cost an afternoon

  • CSP. src/postulo/config/settings/prod.py sets form-action 'self'. allauth starts a provider login with a POST form that then redirects to the identity provider, and Chrome applies form-action to that redirect — so the sign-in button silently does nothing. Either add the provider's origin to form-action, or use SOCIALACCOUNT_LOGIN_ON_GET = True so the button is a plain link (allauth defaults to POST to guard against login CSRF; a GET is acceptable when the provider round-trip carries its own state).
  • Redirect URIs. The callback the identity provider is told about must match exactly what the browser reaches: scheme, host, port. allauth builds it from the request (or from django.contrib.sites where installed — it is optional in this version). On the ragnar test instance, that is http://100.66.30.95:8000/…, and the IdP must be configured to accept a plain-HTTP redirect for it. ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS need the same host.

Classification

An enhancement. Not a breaking change: password sign-in stays; SSO is off until configured; no migration touches existing data (allauth's socialaccount tables are new). If forward-auth is included, its opt-in flag is the only operator-facing surface, and it is the one that most needs a warning label.

Open questions

  1. Is forward-auth (item 4) in scope for 0.2.0 alongside OIDC, or is OIDC alone "native SSO" for this release? The homelab runs oauth2-proxy, which argues for including it.
  2. Default provisioning policy: existing accounts only, or let the IdP be the invitation?
  3. Should the OIDC connection be configurable from the admin as well as the environment? Environment-only is simpler and matches every other setting; the admin is friendlier.
  4. Store provider tokens (SOCIALACCOUNT_STORE_TOKENS)? Postulo has no use for them today; storing credentials with no purpose is a liability. Proposal: no.
## Observation > i want native oauth / sso on the next release, preferable native and not via plugin ## What exists today Sign-in is by email and password only. `INSTALLED_APPS` in `src/postulo/config/settings/base.py` enables `allauth` and `allauth.account`; nothing else. But the capability is already installed. django-allauth 65.19.2 — a dependency since M1 — ships `allauth.socialaccount` with a **generic OpenID Connect provider** (`allauth.socialaccount.providers.openid_connect`), which covers Authentik, Keycloak, Pocket ID, Zitadel, Kanidm, Google, and anything else that speaks OIDC, plus dedicated GitHub and GitLab providers. So "native" here means enabling and wiring what Postulo already depends on, not adding a plugin. One correction (2026-09-05): it does need allauth's `socialaccount` extra — `requests`, `oauthlib` and `pyjwt[crypto]` — which is not installed today; `django-allauth[socialaccount]` in `pyproject.toml` is the whole of it. Good: this is the one place where the modularity principle should *not* apply — signing in is part of the core, and an instance must be able to do it with nothing extra installed. ## Shape **1. Enable `allauth.socialaccount`** with the OpenID Connect provider, configured from the environment rather than the database, which is what allauth's `SOCIALACCOUNT_PROVIDERS` setting allows: ``` POSTULO_OIDC_NAME=Authentik # what the button says POSTULO_OIDC_SERVER_URL=https://auth.example.org/application/o/postulo/ POSTULO_OIDC_CLIENT_ID=… POSTULO_OIDC_CLIENT_SECRET=… ``` Unset means no button, and nothing changes for anyone who does not want it. Room for a second, dedicated provider (GitHub, GitLab) later, but one generic OIDC entry covers every self-hosted identity provider that matters. **2. Account linking.** An existing account with the same address as the identity provider reports should be *linked*, not duplicated. allauth's `SOCIALACCOUNT_EMAIL_AUTHENTICATION` and `SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT` do this, and they are safe only because the provider verified the address — which is exactly the case with OIDC. Postulo's own "Your details" page gets a *Connected accounts* section (allauth's connections view, in our shell like the email page). **3. Provisioning versus invite-only.** Postulo is invite-only by default, and `SOCIALACCOUNT_AUTO_SIGNUP` would let anyone the identity provider knows walk straight in. That is a decision, not an accident. The options, all defensible: - SSO can sign in *existing* accounts only; new people still need an invitation (safest default); - SSO may create accounts, because the identity provider *is* the invitation — right for a household or a small team where the IdP already gates everything; - SSO may create accounts for members of a named group claim. Proposal: the first as the default, the second behind `POSTULO_OIDC_AUTO_SIGNUP=true`, the third later if anyone asks. **4. Forward-auth, the other SSO.** For a self-hoster, "SSO" often means the reverse proxy authenticates and passes a header — `Remote-User`, `X-Forwarded-User` — which is how oauth2-proxy and Authentik's outposts work, and what the `oauth2-*` containers on ragnar are doing. Django supports this natively with `RemoteUserMiddleware` and `RemoteUserBackend`. Worth offering as a second, explicitly opt-in mode (`POSTULO_TRUSTED_PROXY_HEADER`), with the loud caveat that it is only safe when the proxy strips that header from incoming requests — a misconfiguration makes it a way to become anyone by sending one header. If offered, the docs have to say that in bold. **5. SSO-only.** Some operators will want to switch the password form off once SSO works. Cheap to add as `POSTULO_PASSWORD_LOGIN=false` once the rest exists; not required for this issue. ## Interactions with the other 0.2.0 account work - **#1 (username):** OIDC provides `preferred_username`; map it on first sign-in, and fall back to the email local part when the claim is absent. - **#2 (full name):** OIDC provides `name`, `given_name`, `family_name`; populate on first sign-in so an SSO-created account is not born without a name. - **#3 (verified email):** the identity provider has already verified the address, and allauth can record it as such (`SOCIALACCOUNT_EMAIL_VERIFICATION`). An SSO sign-in must not then be asked to click a verification link for the address it just proved. - **Invitations:** see provisioning above. An invitation link followed and then completed by SSO should spend the invitation the same way a password signup does (`accounts/adapter.py`, `accounts/signals.py`). ## Two gotchas, found before they cost an afternoon - **CSP.** `src/postulo/config/settings/prod.py` sets `form-action 'self'`. allauth starts a provider login with a POST form that then redirects to the identity provider, and Chrome applies `form-action` to that redirect — so the sign-in button silently does nothing. Either add the provider's origin to `form-action`, or use `SOCIALACCOUNT_LOGIN_ON_GET = True` so the button is a plain link (allauth defaults to POST to guard against login CSRF; a GET is acceptable when the provider round-trip carries its own `state`). - **Redirect URIs.** The callback the identity provider is told about must match exactly what the browser reaches: scheme, host, port. allauth builds it from the request (or from `django.contrib.sites` where installed — it is optional in this version). On the ragnar test instance, that is `http://100.66.30.95:8000/…`, and the IdP must be configured to accept a plain-HTTP redirect for it. `ALLOWED_HOSTS` and `CSRF_TRUSTED_ORIGINS` need the same host. ## Classification An enhancement. Not a breaking change: password sign-in stays; SSO is off until configured; no migration touches existing data (allauth's `socialaccount` tables are new). If forward-auth is included, its opt-in flag is the only operator-facing surface, and it is the one that most needs a warning label. ## Open questions 1. Is forward-auth (item 4) in scope for 0.2.0 alongside OIDC, or is OIDC alone "native SSO" for this release? The homelab runs oauth2-proxy, which argues for including it. 2. Default provisioning policy: existing accounts only, or let the IdP be the invitation? 3. Should the OIDC connection be configurable from the admin as well as the environment? Environment-only is simpler and matches every other setting; the admin is friendlier. 4. Store provider tokens (`SOCIALACCOUNT_STORE_TOKENS`)? Postulo has no use for them today; storing credentials with no purpose is a liability. Proposal: no.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 11:55:57 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#6
No description provided.