RFDs/0015
OpenID Connect sign-in
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
oidcproviders (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) orgithub(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:
-
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.
-
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.
-
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), orclosed, which removes/users/registerand creates users only through provider sign-up or an administrator. -
local_sign_in:enabled(passwords and magic links), ordisabled, 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.
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.