Skip to content

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_id and the right Authentik endpoints
  • Implements the post-login hook that maps the Authentik id_token to a local res.users record
  • 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

FileRole
system-extensions/plana_auth/__manifest__.pyModule manifest
system-extensions/plana_auth/models/auth_oauth_provider.pyOIDC 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.xmlProvider seed data — overridden on cluster by ConfigMap when needed
system-extensions/plana_auth/controllers/main.pyOAuth callback controller

The OIDC provider in Authentik

PropertyValue
Authentik provider PK10
Provider name"PLANA Odoo Platform"
Client ID (shared across all tenants)pNQrL29SXWXiLgNHdPWvFiRR85cmyxrGdTwoXgas
Client SecretIn 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$
Scopesopenid, email, profile
Subject identifiersub (Authentik UUID)
Token formatJWT, 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:

VersionSignature
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

  1. Confirm the user exists in Authentik (admin → Users)
  2. Confirm the user exists in the tenant Odoo (admin → Users)
  3. Confirm the emails match exactly (case-insensitive)
  4. Check the tenant Odoo log for auth_oauth errors: kubectl -n plana-odoo-18 logs deploy/worker-odoo | grep auth_oauth | tail -50
  5. Common cause: oauth_uid is unset on the tenant user record but the email matches. Resolve by triggering one successful login — the fallback linker will populate oauth_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:

bash
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.

  1. Authentik admin → Providers → "PLANA Odoo Platform" → Regenerate Secret

  2. Update SOPS plana.oauth_client_secret

  3. Run the platform-wide secret-bump Job which iterates every tenant DB and updates ir.config_parameter with the new value:

    bash
    kubectl -n plana-odoo-18 create job --from=cronjob/oauth-secret-rotate \
      oauth-rotate-$(date +%s)
  4. 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

© PLANA Digital Ltd.