Context
OAuth in this platform runs in both directions, and the tenant picks which they want:- Outbound (this spec). Softcrum is the provider. A third-party app acts on a tenant’s behalf with scoped consent, and a tenant’s own site can offer “Sign in with Softcrum” so a member reuses the identity they already have with that tenant.
- Inbound (FS-IDN-0008). Softcrum is the client. A tenant’s staff sign in through the organization’s own identity provider, or through Google, Microsoft or GitHub.
Scope (normative)
identity.oauth_clients: registered third-party integrations.- Authorization code with PKCE for apps acting on a user’s behalf.
- Client credentials for server-to-server integrations acting as themselves.
- A consent screen rendering the actual permissions requested.
- OIDC on top of OAuth2:
id_token,.well-known/openid-configuration, a JWKS endpoint anduserinfo, so “Sign in with Softcrum” works with any standard client library. - Both realms are addressable. A client may authenticate an organizational user (tenant staff signing into a partner tool) or a member (a tenant’s customer signing into that tenant’s own site). The realms stay separate; a client declares which one it targets and can never reach the other.
- Access and refresh tokens with rotation.
- Revocation by the tenant, per client, visible in a list of connected apps.
GET /oauth/userinfoand token introspection.
Non-scope (normative)
- Being a general-purpose login for unrelated products. “Sign in with Softcrum” is offered so a tenant’s own surfaces — their storefront, their portal, a partner tool they authorised — can reuse an identity that already belongs to that tenant. It is not a public identity network, and a client is always scoped to the organization that authorised it.
- Implicit and password grants. Both are deprecated and neither will be offered — supporting a grant we know is unsafe to please one integrator is how a provider ages badly.
- A public app marketplace. That is a product surface of its own; this is its precondition.
- Dynamic client registration in F1b. Clients are registered by us, reviewed, until volume justifies automating it.
Behaviour (normative)
- Authorization code with PKCE is mandatory for user-facing flows, for every client including confidential ones. Implicit and password grants are not implemented at all.
- Scopes are bundles of platform permissions, not a separate vocabulary. A scope requesting
loyalty.redemptions.creategrants exactly that permission and nothing adjacent. - The consent screen shows the permissions in plain language, names the client, and states what the client can do — not an opaque scope string the tenant has to decode.
- A granted authorization creates role assignments for the client as a subject (FS-IDN-0002), so the check at request time is identical to a user’s.
- Authorization codes are single-use and expire in 60 seconds. Redirect URIs match exactly — no prefix or wildcard matching, which is where redirect-based attacks live.
- Access tokens are short-lived (1 hour); refresh tokens rotate on every use, and a reused refresh token revokes the whole authorization and notifies the tenant. Reuse means the token was stolen.
- A tenant sees connected apps in a list and can revoke any of them. Revocation is immediate across access and refresh tokens.
- A client belongs to a developer and is granted per organization. One client authorized by
three tenants holds three independent authorizations, revocable independently.
8b. A client declares the realm it targets,
userormember, at registration and can never reach the other. A member-targeting client authenticates members of the granting organization only, and thesubclaim it receives is scoped to that organization — consistent with member identity being per tenant (standards/data.md§2b). Two tenants using the same client for the same person’s email receive two different subjects, and correlating them is impossible by design. - A client may never request a permission its own registration does not declare, and never a permission the granting user does not hold. A user cannot grant more than they have.
- Every authorization and revocation writes to
core.audit_logand is visible in the tenant’s own trail. - FORBIDDEN: a token in a log, a URL fragment persisted anywhere, or a client secret in a browser-delivered bundle.
Data (normative)
API (normative)
OAuth endpoints live at the root, outside
/v1/{module}, because they follow a standard that
clients and libraries expect at conventional paths. It is the one documented exception to DEC-D1.
Events (normative)
identity.oauth.authorized and identity.oauth.revoked, carrying client and organization.
Acceptance criteria (normative)
- An authorization code flow without PKCE is rejected, for confidential clients too.
- A redirect URI differing by one character is rejected — no prefix matching anywhere.
- An authorization code is single-use and expires at 60 seconds.
- A reused refresh token revokes the entire authorization and notifies the tenant.
- A client requesting a permission outside its registration is rejected at the authorize step, with the reason shown to the user.
- A user cannot grant a permission they do not themselves hold.
- Revoking a connected app invalidates its access and refresh tokens immediately.
- The same client authorized by two organizations holds two independent authorizations; revoking
one leaves the other working.
8b. A member-targeting client cannot authenticate an organizational user, and vice versa.
8c. The same email as a member of two organizations yields two different
subclaims, and no response allows the two to be correlated. - Negative: implicit and password grants return
unsupported_grant_type, and no token appears in any log.
Execution
Single slice, synchronous command. F1b. Better Auth’s OAuth provider capability where it fits; any gap is implemented against the specification rather than by loosening it. The consent screen is afrontend/console route driven by this module’s data.