Better Auth OAuth Provider
External OAuth provider plugin package for better_auth.
Upstream ships OAuth provider as @better-auth/oauth-provider, separate from core plugin exports. This gem mirrors that boundary for Ruby while keeping Ruby option names snake_case and upstream-compatible HTTP paths and JSON keys.
require "better_auth"
require "better_auth/oauth_provider"
auth = BetterAuth.auth(
secret: ENV.fetch("BETTER_AUTH_SECRET"),
base_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
)
]
)
Client Registration
Dynamic registration is disabled by default. Enable it explicitly and call it with an authenticated session unless unauthenticated registration is also enabled.
client = auth.api.register_o_auth_client(
headers: {"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"
}
)
Authorization Code Token Exchange
Authorization code clients use S256 PKCE by default.
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
}
)
When resource is present and valid, access tokens are JWTs. Without resource, access tokens are opaque and introspectable.
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 -> /oauth2/get-client, GET /oauth2/clients -> /oauth2/get-clients, PATCH /oauth2/client -> /oauth2/update-client, DELETE /oauth2/client -> /oauth2/delete-client, GET /oauth2/client -> /oauth2/public-client, and GET/PATCH/DELETE /oauth2/consent plus GET /oauth2/consents -> the get/update/delete/get-consents consent routes.
Admin client routes are server-only. They are available through auth.api.* and return 403 through the Rack handler.
Options
Common options accepted by BetterAuth::Plugins.oauth_provider:
login_pageconsent_pagescopesclaimsgrant_typesallow_dynamic_client_registrationallow_unauthenticated_client_registrationclient_registration_default_scopesclient_registration_allowed_scopesstore_client_secretprefixrefresh_token_expires_inadvertised_metadatavalid_audiencescustom_token_response_fieldscustom_access_token_claimscustom_user_info_claimspairwise_secretsignupselect_accountpost_loginclient_privilegesrate_limitjwks_uridisable_jwt_pluginstore
rate_limit accepts per-route overrides:
rate_limit: {
token: {window: 60, max: 20},
authorize: {window: 60, max: 30},
introspect: {window: 60, max: 100},
revoke: {window: 60, max: 30},
register: {window: 60, max: 5},
userinfo: false
}
Use false to disable a route-specific rule.
Schema Migration
oauthAccessToken now uses the upstream canonical columns token, expiresAt, scopes, clientId, sessionId, userId, referenceId, and refreshId. Legacy access_token, refresh_token, access_token_expires_at, and scope columns should be copied forward then dropped. oauthConsent#consent_given is also removed; a consent row means consent was granted.
For non-Rails SQL apps, run the equivalent of:
UPDATE better_auth_oauth_access_tokens
SET token = COALESCE(token, access_token),
expires_at = COALESCE(expires_at, access_token_expires_at),
scopes = COALESCE(scopes, scope)
WHERE token IS NULL OR scopes IS NULL;
DELETE FROM better_auth_oauth_consents WHERE consent_given = false;
Then drop the legacy access-token and consent columns. Rails apps using better_auth-rails generate migrations from the active plugin schema, so regenerating after this change emits only the canonical columns.
Ruby Adaptations
When the JWT plugin is registered, OIDC metadata advertises its configured jwks.key_pair_config.alg, defaulting to EdDSA like upstream. If disable_jwt_plugin: true is set, Ruby intentionally falls back to HS256 ID tokens signed with the Better Auth secret.
Route OpenAPI metadata blocks from upstream TypeScript are intentionally not ported into this package. Use the Ruby open_api plugin for generated OpenAPI output.
The upstream @better-auth/oauth-provider/client, React/Solid client plugins, dashboard UI, and browser helpers are not ported. Ruby apps call the JSON endpoints directly or wrap auth.api.*.
OIDC provider remains a core better_auth plugin because upstream still exposes it from better-auth/plugins. OAuth provider is the newer standalone provider package.