RFDs/0015

OpenID Connect sign-in

State
discussion
Authors
Darwin D Wu
Discussion
renga-dcim/renga#165
Labels
rengaaccountsauthenticationsecuritysoftware
Implemented
0%Checklist

Goal

Let people sign in to Renga with an identity provider their organization already runs, such as Google Workspace, Microsoft Entra ID, Okta, or Keycloak, and with GitHub. Today Renga only offers email with a password or a magic link, so every deployment keeps its own credentials and has to offboard people by hand.

Renga is a relying party only. It never issues tokens for other applications, and no provider is special to it: every provider, including whichever one an operator chooses as the only way in, is configuration. That configuration lives in Renga’s database and takes effect without a restart.

Providers

Providers exist at two levels:

  • Deployment providers appear on the sign-in page for everyone. An operator manages them.

  • Organization providers belong to one organization, such as that organization’s own Okta or Microsoft Entra ID tenant. Its owners manage them in organization settings, and they are always oidc providers (see Organization providers).

Each provider has:

  • a stable id, used in callback URLs and stored on linked identities. The ID is globally unique and permanently names that provider’s kind, OpenID Connect issuer, deployment-or-organization scope, and owning organization. An ID is not reused after deletion. Changing any of those properties requires a new ID or an explicit migration that cannot silently broaden existing identities, sessions, or grants;

  • a display name and icon for the sign-in page;

  • a kind: oidc (any OpenID Connect issuer, discovered from its issuer URL) or github (OAuth 2.0 with GitHub’s user and email APIs, because GitHub is not an OpenID Connect provider);

  • a client ID and client secret;

  • optional extra scopes, and an optional allowlist of email domains;

  • whether it is enabled;

  • for a deployment provider, whether it is hidden from the sign-in page, in which case it is reached only at /users/log-in?provider=<id>, and whether it is the deployment’s support source (RFD 22). A support source must be hidden, have an email-domain allowlist, and carry RFD 19's enforces MFA mark; a deployment has at most one.

Storage and changes

Providers are stored in the database. Client secrets are encrypted at rest with AES-256-GCM under a key-encryption key that Renga reads from the environment at boot. Each key has an ID stored beside every ciphertext, so an operator rotates by adding a new key, letting Renga re-encrypt, and then retiring the old one. A saved secret is never shown again; the settings page only says that one is set and when it changed.

Changes take effect without a restart. Each stored provider has an authoritative configuration revision in the database. Before a node initiates or completes a sign-in or renews a provider session, it verifies that its cached revision is still current; if it cannot establish freshness, it fails closed. PubSub invalidation evicts caches promptly but is only an acceleration, so a missed or delayed message cannot keep old configuration usable. Discovery documents and signing keys stay cached per provider and refresh as today.

Creating or changing authentication-affecting settings produces a pending revision while the previous active revision remains in use. The person making the change runs a test sign-in against that exact pending revision. Its callback is bound to the actor, test purpose, provider, and revision and cannot create an ordinary session, link an identity, or satisfy the account re-check. A successful test and activation are one atomic transition; another edit invalidates the result.

Renga serializes activation, disable, and removal with the guard that preserves at least one usable deployment sign-in method under the effective local policy, including environment overrides. Every pending change, activation, enable, disable, and removal is recorded in Activity with the actor and the fields that changed, never the secret.

Operators

Operators manage deployment providers with release commands, renga providers list | add | update | test | enable | disable | recover-local, which work even when nobody can sign in. test creates a short-lived, single-use URL for the exact pending revision. The browser callback is available in recovery mode and can only validate and atomically activate that revision; it never creates a user session or other authentication evidence. This is how an operator bootstraps the first provider without an existing sign-in method.

recover-local creates a time-limited database override that enables existing local credentials even when the environment policy disables them. Only the release command can create or revoke the override; it does not open registration, it expires automatically, and every use and policy change is recorded in Activity. An operator can establish the override and disable a broken stored provider in one serialized operation, repair and test the provider, and then revoke the override.

Operators who prefer declarative configuration can still define deployment providers in the environment, read in config/runtime.exs. Those providers are loaded at boot, shown read-only in the UI, and override a stored provider with the same ID only when it is also a deployment provider with the same immutable properties. Renga refuses to boot on an ID collision with an organization provider or any other immutable-property mismatch rather than shadowing it. A release command cannot change an environment-managed provider; repairing one requires changing the deployment environment and restarting, or using the temporary local recovery override while doing so.

Organization providers

An organization owner can add an OpenID Connect provider for their organization. Members reach it from the organization’s sign-in link, /users/log-in?organization=<slug>, or from the main sign-in page by entering an email address whose domain the organization has verified with a DNS TXT record. A domain can be verified by one organization at a time, and an unverified domain never routes anyone to an organization’s provider.

Because an organization owner does not control the deployment network, every server-side request for an organization provider follows an operator-controlled destination policy. It applies to discovery, token and UserInfo endpoints, signing keys, every redirect, and any future back-channel endpoint. By default Renga rejects loopback, link-local and metadata addresses, private deployment networks, and any other non-public destination. It resolves and checks the destination again for each connection and redirect so a DNS change cannot bypass the policy. An operator may explicitly allow the destinations of an internal enterprise provider; an organization owner cannot add an exception.

An organization provider is trusted only for its own organization, because its owners control the identities it asserts:

  • A session that signed in through an organization provider can open only that organization. Reaching another organization needs a different sign-in method.

  • Its claim mappings and SCIM connections (RFD 16) can manage only that organization.

  • Sign-up through it creates a user only when the organization allows it. The asserted email remains an organization-scoped profile attribute: it neither populates nor confirms the account’s global login email, reserves that email against another account, nor accepts another organization’s invitation. A global login email must be proved through the independent initialization flow below.

  • Its authentication evidence has organization scope. It can establish access and satisfy policy only within its own organization, but cannot change a global password or recovery email, link or unlink a global sign-in identity, or perform another account-wide credential operation. Those operations use RFD 19's account re-check with a local credential or deployment provider.

These rules keep an organization’s identity provider from reaching a person’s access in other organizations, even when that person links the provider to an account they use elsewhere.

Turning a GitHub organization into an organization provider is out of scope; GitHub stays a deployment provider.

SAML 2.0 is out of scope. Every major workforce identity provider also offers OpenID Connect, and SAML would add XML signature validation, a frequent source of authentication bypasses, to Renga’s sign-in path. A deployment whose provider only speaks SAML can put an OpenID Connect broker such as Keycloak, Authentik, or Dex in front of it and configure the broker as an oidc provider.

Sign-in flow

Renga uses a pinned, reviewed version of oidcc for oidc providers. The library supplies discovery, token verification, signing-key caching and rotation, and protocol request primitives. Renga still owns correct use of those primitives: it generates and binds state, nonce, and the PKCE verifier to each attempt, selects required validations, and separately enforces claims such as auth_time that the library does not age-check. The github kind is a small adapter on Req for the code exchange and GitHub’s user and email APIs, which no OpenID Connect library covers.

Renga uses the authorization code flow with PKCE, a per-attempt state, and for OpenID Connect a nonce. It validates the ID token’s signature against the issuer’s published keys, and its issuer, audience, expiry, and nonce, before trusting any claim. The callback runs in the browser pipeline and ends in the existing session creation, so a provider sign-in produces the same session, current_scope, and organization selection as a password sign-in.

Local session issuance, provider validation, and the account re-check (RFD 19) are three separate records of evidence. An ordinary provider callback, silent authorization, token refresh, or local session rotation never creates or refreshes re-check evidence merely because it issued a new Renga session.

Identities and accounts

A new user_identities table links a user to a provider subject: (provider_id, subject) is unique, and a user may have several identities. An OpenID Connect identity uses the token’s sub; a GitHub identity uses the stable numeric user ID, never the mutable login name.

Renga matches a returning sign-in by (provider_id, subject) only, never by email, because email addresses are reassigned and some providers let users change them freely.

An identity from the support source is always its own account, created on first sign-in and marked as a support identity. It is never linked to an existing account, no other provider can be linked to it, and it can hold only the built-in support role (RFD 17), granted by Renga while an organization allows support access (RFD 19); invitations, claim mappings, and SCIM never apply to it.

The first sign-in through a provider is resolved in this order:

  1. If a user with the same email exists, Renga refuses to link automatically, even when both sides verified the address. Email verification does not prove the two accounts have the same owner because addresses can be reassigned. The person must sign in to the existing Renga account and explicitly link the provider, which requires the account re-check.

  2. If no user exists and the provider allows sign-up, a deployment provider creates a confirmed user from the verified email. An organization provider creates an account with no global login email; its asserted email and authority remain scoped as described above. Provider sign-up creates no organization membership on its own; the person lands where a new password user does today.

  3. Otherwise sign-in is refused.

An organization-created account that still has no global login email may start RFD 19's first-password or first-factor bootstrap by entering an address and proving mailbox control. Renga binds the challenge to that account, purpose, browser, and session and expires it after five minutes; the provider’s email claim neither chooses the address nor satisfies the challenge. Success may initialize only an empty global email and grants only RFD 19's limited bootstrap authorization, never a general account re-check. Renga refuses an address that belongs to another account and never uses this path to overwrite an existing global email or automatically link accounts.

Signed-in users can link and unlink providers from their settings, which requires the account re-check. The linking callback is bound to the initiating user and refuses a provider subject already linked to another user. Renga refuses to unlink a user’s last usable sign-in method under the current local sign-in and provider policy, or the last method capable of passing the account re-check.

GitHub cannot provide the fresh authentication evidence the re-check requires. The first release therefore does not allow GitHub to create a new user: a GitHub identity must be linked, after a re-check, to an existing account that already has a passkey, an authenticator app, a password, or a suitable OpenID Connect identity. A password remains usable for reauthentication even when local password sign-in is disabled.

Organization membership from claims

A provider claim (for example groups, or a custom claim) can be mapped to organization memberships and roles. An operator sets the mapping for a deployment provider; an organization’s owners set it for their own organization’s provider. Renga records each mapped grant with the provider identity and mapping that issued it and a valid_until time; it does not flatten provider authority into an indefinite user membership. Authorization treats a mapped grant as a membership only while that source is valid.

On each successful validation with the issuing provider, Renga grants, updates, and renews the mapped grants present in the fresh claims and revokes ones that are no longer present. Signing in through a password, GitHub, or another OpenID Connect provider never extends them. Once a grant expires it provides no access until its own provider is successfully validated again. Unlinking its identity or removing its provider or mapping immediately invalidates its grants.

The first release permits at most one provider mapping to manage an organization, avoiding conflicting external authorities. A membership granted by hand takes precedence over a mapped grant for the same user and organization and is never changed or revoked by a mapping. Without a mapping, provider sign-in never touches memberships.

The mapping names organizations by slug and roles by Renga’s role names (owner, admin, member, viewer). A claim value that names an unknown organization or role is ignored and logged, not created. An organization provider’s mapping can name only its own organization.

Provider session lifetime

A session created through a provider carries a lifetime, 12 hours by default and configurable per provider. An operator may set a longest allowed lifetime that organization providers cannot exceed. Renga’s remember-me cookie would otherwise keep a person signed in for weeks after their identity provider removed them.

When the lifetime has passed, Renga confirms the person with the provider again and recomputes claim-mapped memberships only from claims that provider defines as current. Renewal is provider-specific.

For OpenID Connect, provider configuration identifies the authoritative source for every mapped claim: a freshly validated ID token or UserInfo. Renga may use a refresh token in the background only when the resulting ID token or UserInfo response satisfies that provider’s freshness contract and contains every required claim. A successful token refresh alone is not revalidation and does not extend either the provider session or its mapped grants. Otherwise Renga starts authorization with prompt=none; if the provider does not support a silent result or requires interaction, Renga asks the person to sign in again.

For GitHub, Renga revalidates the stable numeric user ID through the user API with a usable access token, refreshing an expiring token where the GitHub OAuth application supports it. If that fails, it repeats the ordinary GitHub OAuth flow; it does not send the OpenID Connect-only prompt=none behavior.

Missing required claims, identity changes, expired or rejected credentials, and provider or network failures never silently extend expired authorization. Renga suspends the session and its mapped access until revalidation succeeds; a definitive refusal ends the session and shows the sign-in page.

Expiry is enforced on connected LiveViews as well as HTTP requests and navigations. Authenticated LiveViews schedule the provider deadline and recheck it before protected events and data delivery. Once it passes, the socket stops accepting mutations and receiving organization data until provider validation succeeds; keeping a page open never extends authorization.

The browser preserves its rendered form and unsaved input through reauthentication where feasible, but preserving a draft is a user-experience goal, not an exception to the authorization boundary. Membership changes take effect as soon as renewal completes; losing access to the current organization returns the person to organization selection rather than resuming the old socket scope.

Renga does not sweep memberships on a schedule. OpenID Connect UserInfo can refresh claims for a known subject while its access token is usable, but OpenID Connect has no standard provisioning feed that reports every offboarding or membership change. Request-driven revalidation provides the bounded lifetime; SCIM or back-channel events are future work. GitHub identities carry no group claims, so their renewal only confirms that the same account is still valid.

Local sign-in policy

Two independent settings control local credentials:

  • registration: open (today’s behavior), or closed, which removes /users/register and creates users only through provider sign-up or an administrator.

  • local_sign_in: enabled (passwords and magic links), or disabled, which leaves provider sign-in as the only way in.

Both are deployment settings, read from the environment at boot. The only runtime exception is the time-limited, operator-only local recovery override described above; it does not change registration.

If local sign-in is disabled and no deployment provider is active, a web node starts in fail-closed recovery mode instead of serving normal sign-in or protected routes. Health checks, release commands, and the short-lived provider test callback remain available so an operator can bootstrap or repair a provider. Leaving recovery mode requires atomically activating a tested deployment provider or establishing the temporary local recovery override.

Re-authentication and step-up

The account re-check (RFD 19) and RFD 13 step-up are separate security levels. For the re-check, a user without a passkey, authenticator app, or password may reauthenticate through a linked deployment OpenID Connect provider. An organization-owned provider never authorizes an account-wide credential or identity change; an organization-created account with no other method first uses the email initialization and limited RFD 19 bootstrap above. Renga sends prompt=login and max_age, and binds the callback to the initiating local session, the re-check’s purpose, and the expected provider ID and subject; switching accounts fails the attempt. Renga accepts the result only after its normal ID-token checks and its own validation that auth_time is present, well formed, not in the future, and within the re-check window. Ordinary SSO, silent renewal, and GitHub OAuth never count as a re-check.

Where an organization requires RFD 13 step-up, it is a passkey or authenticator code when enrolled, otherwise a fresh password entry within five minutes. A recent auth_time alone says when authentication happened, not which factor or assurance level it used, so provider reauthentication does not satisfy terminal step-up. Supporting federated terminal step-up would require a later amendment that defines accepted acr and amr evidence per provider without weakening the enrolled-factor rule.

Sign-out

Signing out ends the Renga session. When the provider publishes an end_session_endpoint, an operator may enable RP-initiated logout so Renga also redirects there. Back-channel logout is out of scope for the first release.

Machine access

Provider sign-in is for people in browsers. Agents keep RFD 3 intake keys and RFD 13 installation credentials; nothing in this RFD changes /api authentication.

Future work

  • SCIM provisioning, for removing people and memberships the moment an identity provider does, without waiting for a renewal (RFD 16).

  • OpenID Connect back-channel logout.