Tenant auth (plana_auth)
Audience
PLANA staff and integration partners. End-users sign in by clicking the PLANA SSO button — they don't need to read this. See PLANA Business Cloud → First login for the user-facing flow.
Every customer tenant uses the same authentication mechanism: PLANA's plana_auth Odoo module, which talks to Authentik over OIDC. The result is a single login experience across every tenant, with passwordless options (Google federation, TOTP) layered in.
The plana_auth module
plana_auth is one of the PLANA system-extensions installed on every tenant at provisioning time. It sits between Odoo's authentication API and Authentik:
- Adds the "Sign in with PLANA" button to the Odoo login page
- Configures the OIDC provider on the tenant DB with the right
client_idand the right Authentik endpoints - Implements the post-login hook that maps the Authentik
id_tokento a localres.usersrecord - Implements the email-fallback linking (since v18.0.1.1.0) — a user authenticating via Authentik whose email matches an existing tenant user is auto-linked
Where it lives
| File | Role |
|---|---|
system-extensions/plana_auth/__manifest__.py | Module manifest |
system-extensions/plana_auth/models/auth_oauth_provider.py | OIDC provider with regex redirect_uri |
system-extensions/plana_auth/models/res_users.py | _login override, email-fallback linking |
system-extensions/plana_auth/data/auth_oauth_provider.xml | Provider seed data — overridden on cluster by ConfigMap when needed |
system-extensions/plana_auth/controllers/main.py | OAuth callback controller |
The OIDC provider in Authentik
| Property | Value |
|---|---|
| Authentik provider PK | 10 |
| Provider name | "PLANA Odoo Platform" |
| Client ID (shared across all tenants) | pNQrL29SXWXiLgNHdPWvFiRR85cmyxrGdTwoXgas |
| Client Secret | In SOPS at plana.oauth_client_secret |
| Redirect URI pattern (regex) | https://([a-z0-9-]+)\.planapulse\.(app|online|dev)/auth_oauth/signin$ or https://erp\.planapulse\.ai/auth_oauth/signin$ |
| Scopes | openid, email, profile |
| Subject identifier | sub (Authentik UUID) |
| Token format | JWT, RS256 |
The shared client ID means every tenant authenticates against the same Authentik provider, which keeps Authentik configuration simple. Per-tenant isolation is at the redirect URI level — the URI literally encodes the tenant subdomain.
The flow
User opens: https://acme.planapulse.app/web/login
1. Click "Sign in with PLANA"
└─→ Odoo redirects to:
https://auth.planapulse.com/application/o/authorize/?
client_id=pNQrL29...&
redirect_uri=https://acme.planapulse.app/auth_oauth/signin&
response_type=token&
scope=openid+email+profile
2. Authentik presents login (password + TOTP, or Google)
3. After login, Authentik redirects to:
https://acme.planapulse.app/auth_oauth/signin#access_token=...&id_token=...
4. plana_auth's controller receives the callback:
- Verifies the id_token signature against Authentik's JWKS
- Extracts the email claim
- Looks up a matching res.users record:
a. By oauth_uid (subject identifier) → if found, log in
b. By email (case-insensitive fallback) → if found, link and log in
c. If neither → reject (no auto-provisioning)
5. User is logged into the tenant. Session is established.Why no auto-provisioning
A user authenticating against Authentik does not automatically get a res.users record on the tenant. The administrator must create the user in the tenant first, then on first login plana_auth matches and links.
This is intentional:
- Tenant admins control who has access to their business data, including what role / which groups they belong to
- Authentik staff users should not automatically appear in customer tenants
- It prevents an Authentik misconfiguration from silently granting access across tenants
The cost: when onboarding a new tenant user, the admin must (a) create the res.users record in Odoo, (b) ensure the email matches the Authentik user's email. Both steps are described in PLANA Business Cloud → User roles.
Why JSON-RPC is blocked
Odoo exposes two RPC mechanisms: XML-RPC (/xmlrpc/2/common, /xmlrpc/2/object) and JSON-RPC (/web/dataset/call_kw). The auth_session_timeout OCA module disables JSON-RPC for non-authenticated sessions, which has the side effect of blocking unauthenticated JSON-RPC calls completely.
This affects PLANA's services in one specific way: pulse-account-api talks to tenant Odoos via XML-RPC only. The Odoo JSON-RPC is not available to outside callers.
The AI agents talk to Odoo via JSON-RPC through an internal route (ODOO_URL env var) using session cookies established at startup.
The _login signature change in v18
Between Odoo v17 and v18, the signature of res.users._login changed:
| Version | Signature |
|---|---|
| v17 | _login(cls, db, login, password, user_agent_env) |
| v18+ | _login(cls, db, credential, user_agent_env=None) |
plana_auth._login was carrying the v17 signature when we cut over to v18 in May 2026 — first-login attempts threw TypeError: _login() missing 1 required positional argument: 'password'. The fix on the 18.0 branch updates the override to use the new credential-dict signature. The 17.0 branch keeps the old signature.
When porting plana_auth to a new major, check the upstream res.users.authenticate signature first. The override must match.
Common operational tasks
A user can't sign in despite a correct password
- Confirm the user exists in Authentik (admin → Users)
- Confirm the user exists in the tenant Odoo (admin → Users)
- Confirm the emails match exactly (case-insensitive)
- Check the tenant Odoo log for
auth_oautherrors:kubectl -n plana-odoo-18 logs deploy/worker-odoo | grep auth_oauth | tail -50 - Common cause:
oauth_uidis unset on the tenant user record but the email matches. Resolve by triggering one successful login — the fallback linker will populateoauth_uid.
"Access Denied" / oauth_error=2
Indicates the OIDC callback was received but the post-login hook rejected the user. Most common cause is the v17→v18 signature mismatch described above. Confirm the worker pod is running the latest image and the plana_auth module has been updated:
kubectl -n plana-odoo-18 exec deploy/worker-odoo -- \
psql -U plana -d <tenant.planapulse.app> -c \
"SELECT name, latest_version FROM ir_module_module WHERE name='plana_auth'"Updating the shared client secret
Rotation involves coordinating Authentik and every tenant DB, because the secret is stored both in Authentik and in ir.config_parameter on each tenant DB.
Authentik admin → Providers → "PLANA Odoo Platform" → Regenerate Secret
Update SOPS
plana.oauth_client_secretRun the platform-wide secret-bump Job which iterates every tenant DB and updates
ir.config_parameterwith the new value:bashkubectl -n plana-odoo-18 create job --from=cronjob/oauth-secret-rotate \ oauth-rotate-$(date +%s)Restart the worker pods to clear the in-memory cache
We have not yet done a routine rotation; the procedure is documented but will be exercised the next time we rotate.
Where to read more
- Authentik SSO — the IdP staff also use
- Google federation — passwordless option for tenant users
- Two-factor (TOTP)
- PLANA Business Cloud → Single sign-on — the customer-facing description
- Source:
odoo-modules/system-extensions/plana_auth/on each version branch