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
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
| Method | Path | Ruby API method |
|---|---|---|
GET | /.well-known/oauth-authorization-server | auth.api.get_o_auth_server_config |
GET | /.well-known/openid-configuration | auth.api.get_open_id_config |
POST | /oauth2/register | auth.api.register_o_auth_client |
POST | /oauth2/create-client | auth.api.create_o_auth_client |
POST | /admin/oauth2/create-client | auth.api.admin_create_o_auth_client |
PATCH | /admin/oauth2/update-client | auth.api.admin_update_o_auth_client |
GET | /oauth2/get-client?client_id=... | auth.api.get_o_auth_client |
GET | /oauth2/get-clients | auth.api.get_o_auth_clients |
POST | /oauth2/update-client | auth.api.update_o_auth_client |
POST | /oauth2/delete-client | auth.api.delete_o_auth_client |
GET | /oauth2/public-client?client_id=... | auth.api.get_o_auth_client_public |
POST | /oauth2/public-client-prelogin | auth.api.get_o_auth_client_public_prelogin |
POST | /oauth2/client/rotate-secret | auth.api.rotate_o_auth_client_secret |
GET | /oauth2/authorize | auth.api.o_auth2_authorize |
POST | /oauth2/continue | auth.api.o_auth2_continue |
POST | /oauth2/consent | auth.api.o_auth2_consent |
GET | /oauth2/get-consent?id=... | auth.api.get_o_auth_consent |
GET | /oauth2/get-consents | auth.api.get_o_auth_consents |
POST | /oauth2/update-consent | auth.api.update_o_auth_consent |
POST | /oauth2/delete-consent | auth.api.delete_o_auth_consent |
POST | /oauth2/token | auth.api.o_auth2_token |
POST | /oauth2/introspect | auth.api.o_auth2_introspect |
POST | /oauth2/revoke | auth.api.o_auth2_revoke |
GET | /oauth2/userinfo | auth.api.o_auth2_user_info |
GET, POST | /oauth2/end-session | auth.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
| Option | Notes |
|---|---|
login_page | Page used when an authorization request needs login. |
consent_page | Page used when an authorization request needs consent. |
scopes | Provider-supported scopes. |
claims | OIDC claims advertised in metadata. |
grant_types | Supported grant types. |
allow_dynamic_client_registration | Enables /oauth2/register. |
allow_unauthenticated_client_registration | Allows unauthenticated DCR and coerces clients to public. |
client_registration_default_scopes | Default scopes for DCR requests without scope. |
client_registration_allowed_scopes | Scope allow-list for DCR. |
client_credential_grant_default_scopes | Default scopes for client_credentials when the stored client has none. |
store_client_secret | plain, hashed, encrypted, or custom hash/encrypt callbacks. |
prefix | Prefixes for opaque access tokens, refresh tokens, and client secrets. |
code_expires_in | Authorization code lifetime in seconds. |
id_token_expires_in | ID token lifetime in seconds. |
refresh_token_expires_in | Refresh token lifetime in seconds. |
access_token_expires_in | User access token lifetime in seconds. |
m2m_access_token_expires_in | Client-credentials access token lifetime in seconds. |
scope_expirations | Per-scope shorter access token lifetimes. |
advertised_metadata | Overrides metadata fields such as scopes_supported, claims_supported, and jwks_uri. |
valid_audiences | Accepted resource values for JWT access tokens. |
allow_public_client_prelogin | Allows signed /oauth2/public-client-prelogin requests before login. |
custom_token_response_fields | Adds fields to token responses. |
custom_access_token_claims | Adds custom JWT access token claims before pinned OAuth claims. |
custom_user_info_claims | Adds claims to /oauth2/userinfo. |
pairwise_secret | Enables pairwise subject identifiers. |
signup, select_account, post_login | Page and callback controls used by /oauth2/continue. |
client_privileges | Callback for client management authorization. |
rate_limit | Per-endpoint rate limit overrides or false to disable a rule. |
jwks_uri | Advertises a configured JWKS URL. |
disable_jwt_plugin | Keeps 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.