Skip to main content

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.
Neither is the default; both are configuration. A company with its own IdP routes its staff inward, and the same company may still expose “Sign in with Softcrum” on its storefront for its customers. Without the outbound half, every third party that wants to build on Softcrum asks the tenant for an API key with everything on it, and the tenant either over-grants or refuses. Both cost us the integration. DEC-D5 makes this cheaper than it usually is: OAuth scopes are bundles of the same permissions the platform already has. There is no second authorization vocabulary to design, document and keep in sync — the consent screen renders real permissions, and a token carries real role assignments. An invented scope language is where OAuth implementations become inconsistent with the product they guard. The distinction from an API key (FS-IDN-0006) is who grants what: a key is issued by the tenant to themselves; an OAuth client is granted by the tenant to somebody else, which is why it needs consent, revocation visible to the granter, and a refresh path.

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 and userinfo, 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/userinfo and 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)

  1. 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.
  2. Scopes are bundles of platform permissions, not a separate vocabulary. A scope requesting loyalty.redemptions.create grants exactly that permission and nothing adjacent.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. A tenant sees connected apps in a list and can revoke any of them. Revocation is immediate across access and refresh tokens.
  8. 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, user or member, at registration and can never reach the other. A member-targeting client authenticates members of the granting organization only, and the sub claim 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.
  9. 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.
  10. Every authorization and revocation writes to core.audit_log and is visible in the tenant’s own trail.
  11. 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)

  1. An authorization code flow without PKCE is rejected, for confidential clients too.
  2. A redirect URI differing by one character is rejected — no prefix matching anywhere.
  3. An authorization code is single-use and expires at 60 seconds.
  4. A reused refresh token revokes the entire authorization and notifies the tenant.
  5. A client requesting a permission outside its registration is rejected at the authorize step, with the reason shown to the user.
  6. A user cannot grant a permission they do not themselves hold.
  7. Revoking a connected app invalidates its access and refresh tokens immediately.
  8. 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 sub claims, and no response allows the two to be correlated.
  9. 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 a frontend/console route driven by this module’s data.

Open questions

Changelog

Delivery record

Not implemented yet.