OAuth Provider

Expose this app as an OAuth 2.0 authorization server.

Ruby uses snake_case option names and auth.api method names. HTTP paths and JSON keys keep the upstream wire shape where implemented.

Configure

config/auth.rb
require "better_auth"
require "better_auth/oauth_provider"

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  base_url: ENV.fetch("BETTER_AUTH_URL", "https://auth.example.com/api/auth"),
  plugins: [
    BetterAuth::Plugins.oauth_provider(
      scopes: ["openid", "profile", "email", "offline_access"],
      consent_page: "/oauth2/consent",
      allow_dynamic_client_registration: true,
      valid_audiences: ["https://api.example.com"]
    )
  ]
)

Register A Client

Dynamic client registration is disabled by default. Enable allow_dynamic_client_registration and call registration with a session cookie unless allow_unauthenticated_client_registration is enabled.

client = auth.api.register_o_auth_client(
  headers: {"cookie" => session_cookie},
  body: {
    client_name: "Example Client",
    redirect_uris: ["https://client.example.com/callback"],
    token_endpoint_auth_method: "client_secret_post",
    grant_types: ["authorization_code", "refresh_token"],
    response_types: ["code"],
    scope: "openid profile offline_access"
  }
)

Public clients use token_endpoint_auth_method: "none" and must use S256 PKCE.

Authorization Code Flow

Call /oauth2/authorize with response_type=code, a registered redirect_uri, and S256 PKCE parameters. If consent is needed, the endpoint redirects to consent_page with a consent_code.

consent = auth.api.o_auth2_consent(
  headers: {"cookie" => session_cookie},
  body: {
    accept: true,
    consent_code: params[:consent_code],
    scope: "openid profile"
  }
)

The consent scope may be narrower than the requested scope.

tokens = auth.api.o_auth2_token(
  body: {
    grant_type: "authorization_code",
    code: params[:code],
    redirect_uri: "https://client.example.com/callback",
    client_id: client[:client_id],
    client_secret: client[:client_secret],
    code_verifier: verifier
  }
)

Opaque access tokens are returned by default. If a valid resource is supplied, the access token is a JWT and the resource becomes the JWT aud. The default valid audience is the provider issuer URL; when openid is granted, the UserInfo endpoint is also allowed.

Routes

MethodPathRuby API method
GET/.well-known/oauth-authorization-serverauth.api.get_o_auth_server_config
GET/.well-known/openid-configurationauth.api.get_open_id_config
POST/oauth2/registerauth.api.register_o_auth_client
POST/oauth2/create-clientauth.api.create_o_auth_client
POST/admin/oauth2/create-clientauth.api.admin_create_o_auth_client
PATCH/admin/oauth2/update-clientauth.api.admin_update_o_auth_client
GET/oauth2/get-client?client_id=...auth.api.get_o_auth_client
GET/oauth2/get-clientsauth.api.get_o_auth_clients
POST/oauth2/update-clientauth.api.update_o_auth_client
POST/oauth2/delete-clientauth.api.delete_o_auth_client
GET/oauth2/public-client?client_id=...auth.api.get_o_auth_client_public
POST/oauth2/public-client-preloginauth.api.get_o_auth_client_public_prelogin
POST/oauth2/client/rotate-secretauth.api.rotate_o_auth_client_secret
GET/oauth2/authorizeauth.api.o_auth2_authorize
POST/oauth2/continueauth.api.o_auth2_continue
POST/oauth2/consentauth.api.o_auth2_consent
GET/oauth2/get-consent?id=...auth.api.get_o_auth_consent
GET/oauth2/get-consentsauth.api.get_o_auth_consents
POST/oauth2/update-consentauth.api.update_o_auth_consent
POST/oauth2/delete-consentauth.api.delete_o_auth_consent
POST/oauth2/tokenauth.api.o_auth2_token
POST/oauth2/introspectauth.api.o_auth2_introspect
POST/oauth2/revokeauth.api.o_auth2_revoke
GET/oauth2/userinfoauth.api.o_auth2_user_info
GET, POST/oauth2/end-sessionauth.api.o_auth2_end_session

Deprecated aliases remain for one minor release: GET /oauth2/client/:id, GET /oauth2/client, GET /oauth2/clients, PATCH /oauth2/client, DELETE /oauth2/client, GET /oauth2/consents, and GET/PATCH/DELETE /oauth2/consent.

Options

OptionNotes
login_pagePage used when an authorization request needs login.
consent_pagePage used when an authorization request needs consent.
scopesProvider-supported scopes.
claimsOIDC claims advertised in metadata.
grant_typesSupported grant types.
allow_dynamic_client_registrationEnables /oauth2/register.
allow_unauthenticated_client_registrationAllows unauthenticated DCR and coerces clients to public.
client_registration_default_scopesDefault scopes for DCR requests without scope.
client_registration_allowed_scopesScope allow-list for DCR.
client_credential_grant_default_scopesDefault scopes for client_credentials when the stored client has none.
store_client_secretplain, hashed, encrypted, or custom hash/encrypt callbacks.
prefixPrefixes for opaque access tokens, refresh tokens, and client secrets.
code_expires_inAuthorization code lifetime in seconds.
id_token_expires_inID token lifetime in seconds.
refresh_token_expires_inRefresh token lifetime in seconds.
access_token_expires_inUser access token lifetime in seconds.
m2m_access_token_expires_inClient-credentials access token lifetime in seconds.
scope_expirationsPer-scope shorter access token lifetimes.
advertised_metadataOverrides metadata fields such as scopes_supported, claims_supported, and jwks_uri.
valid_audiencesAccepted resource values for JWT access tokens.
allow_public_client_preloginAllows signed /oauth2/public-client-prelogin requests before login.
custom_token_response_fieldsAdds fields to token responses.
custom_access_token_claimsAdds custom JWT access token claims before pinned OAuth claims.
custom_user_info_claimsAdds claims to /oauth2/userinfo.
pairwise_secretEnables pairwise subject identifiers.
signup, select_account, post_loginPage and callback controls used by /oauth2/continue.
client_privilegesCallback for client management authorization.
rate_limitPer-endpoint rate limit overrides or false to disable a rule.
jwks_uriAdvertises a configured JWKS URL.
disable_jwt_pluginKeeps access tokens opaque even when resource is supplied.

When the JWT plugin is registered, JWT access tokens and ID tokens use its configured signing algorithm. Without the JWT plugin, Ruby falls back to HS256 for compatibility.

Metadata

OpenID metadata is available only when openid is in configured scopes. jwks_uri is advertised only when explicitly configured through advertised_metadata[:jwks_uri], jwks_uri, or jwks[:remote_url].

Organization Integration

OAuth clients can be tied to an active organization or team through the Ruby organization plugin by providing client_reference and, when consent should be scoped to the selected organization, post_login[:consent_reference_id].

auth = BetterAuth.auth(
  secret: ENV.fetch("BETTER_AUTH_SECRET"),
  base_url: ENV.fetch("BETTER_AUTH_URL"),
  plugins: [
    BetterAuth::Plugins.organization(teams: {enabled: true}),
    BetterAuth::Plugins.oauth_provider(
      scopes: ["openid", "read:posts"],
      allow_dynamic_client_registration: true,
      client_reference: ->(info) { info[:session]["activeOrganizationId"] },
      post_login: {
        page: "/select-organization",
        should_redirect: ->(info) { info[:session]["activeOrganizationId"].to_s.empty? },
        consent_reference_id: ->(info) { info[:session]["activeOrganizationId"] }
      }
    )
  ]
)

When client_reference returns an organization id, dynamically registered clients store it as reference_id and do not store user_id. Client list/get/update/delete operations are then scoped through the current client_reference.

When post_login[:consent_reference_id] returns an organization id, consent records and authorization codes carry that reference. A user who already consented for one organization must consent again after selecting a different organization.

Support Boundary

This gem implements the OAuth 2.0/OIDC authorization server behavior from upstream @better-auth/oauth-provider: metadata, registration, authorization, consent, token, introspection, revocation, userinfo, logout, pairwise subjects, PKCE, organization references, and rate limits.

It does not currently expose an equivalent to upstream oauthProviderResourceClient. That upstream client is for protected resource servers: verifying bearer access tokens, generating OAuth protected resource metadata, and returning WWW-Authenticate challenges with resource_metadata.

MCP support in Ruby currently lives in the core MCP plugin (BetterAuth::Plugins.mcp), not in this OAuth provider gem. The upstream OAuth provider MCP tests combine the resource client, MCP SDK transports, and protected-resource metadata helpers. Porting those tests to this gem would require a new public Ruby resource-client API or a dedicated package for protected resource helpers.

On this page