OauthController
Supabase::Rails::OauthController — OAuth 2.0 + PKCE provider sign-in.
Supabase::Rails::OauthController handles the two-leg OAuth 2.0 + PKCE flow: send the user to the upstream provider, then exchange the returned code for a Supabase session. The install generator writes a top-level OauthController that inherits from it.
Under the hood, authorize wraps the supabase-rb auth.sign_in_with_oauth call (via supabase_sign_in_with_oauth); callback wraps the same flow's auth.exchange_code_for_session (via supabase_exchange_code_for_session) to finish the PKCE round-trip.
# app/controllers/oauth_controller.rb (written by `bin/rails generate supabase:install`)
class OauthController < Supabase::Rails::OauthController
endSource
# app/controllers/supabase/rails/oauth_controller.rb (in the gem)
class OauthController < BaseController
allow_unauthenticated_access only: %i[authorize callback]
def authorize
result = supabase_sign_in_with_oauth(
provider: params[:provider],
redirect_to: params[:redirect_to] || oauth_callback_url,
)
if result.success?
redirect_to result.value, allow_other_host: true
else
redirect_to new_session_path,
alert: I18n.t("supabase.rails.oauth.failed")
end
end
def callback
result = supabase_exchange_code_for_session(
code: params[:code],
state: params[:state],
)
if result.success?
redirect_to after_authentication_url,
notice: I18n.t("supabase.rails.oauth.connected")
else
redirect_to new_session_path, alert: result.error.message
end
end
endRoutes
| Helper | Verb | URL | Action |
|---|---|---|---|
oauth_authorize_path(provider) | GET | /oauth/:provider/authorize | authorize |
oauth_callback_path | GET | /oauth/callback | callback |
Mounted by:
get "/oauth/:provider/authorize", to: "oauth#authorize", as: :oauth_authorize
get "/oauth/callback", to: "oauth#callback", as: :oauth_callbackinside supabase_authentication_routes. The URLs are GETs because OAuth providers redirect via HTTP 302 — the upstream cannot POST back to your callback.
The flow
┌──────┐ GET /oauth/google/authorize ┌──────────────────┐
│ User │ ────────────────────────────► │ host: authorize │
└──────┘ └────────┬─────────┘
▲ │ build PKCE state + verifier,
│ │ write sb-oauth-state-<state> cookie,
│ │ ask Supabase for authorize URL
│ │
│ 302 to provider URL ▼
│ ◄────────────────────────── result.value (provider URL)
│
│ user signs in at provider ────────────► provider
│
│ ◄────── 302 to /oauth/callback?code=...&state=...
│
│ GET /oauth/callback?code=...&state=... ┌──────────────────┐
└──────────────────────────────────────────► │ host: callback │
└────────┬─────────┘
│ read sb-oauth-state-<state> cookie,
│ exchange code for session,
│ start_new_session_for(session),
│ redirect_to after_authentication_urlThe PKCE verifier survives the round-trip via a signed cookie keyed on the state — see RequestScopedStorage. The provider cannot see the verifier; only the host's signed-cookie keystore has it. The cookie name is sb-oauth-state-<state> and it expires after 10 minutes.
Actions
authorize
Generates the PKCE state + verifier, asks Supabase Auth for the provider authorize URL, and 302s the user to it. Allowed without authentication.
Params
| Param | Source | Notes |
|---|---|---|
provider | URL path | Required. One of "google", "github", "discord", etc. — any provider configured in the Supabase dashboard's Authentication → Providers page. |
redirect_to | Query string | Optional. Defaults to oauth_callback_url. Validated against config.supabase.allowed_redirect_origins by RedirectValidator before the upstream call. |
Outcome dispatch
| Branch | Redirect / render | Status | Flash |
|---|---|---|---|
| Success | redirect_to result.value, allow_other_host: true | 302 | — |
Failure — invalid redirect_to | redirect_to new_session_path | 302 | alert: I18n.t("supabase.rails.oauth.failed") |
| Failure — provider unknown / not configured | redirect_to new_session_path | 302 | alert: I18n.t("supabase.rails.oauth.failed") |
| Failure — 5xx upstream | redirect_to new_session_path | 302 | alert: I18n.t("supabase.rails.oauth.failed") |
The allow_other_host: true flag is mandatory — the upstream provider's authorize URL is cross-origin, and Rails refuses cross-origin redirects unless explicitly told to allow them. The failure flash is generic ("We couldn't start that sign-in") regardless of whether the failure was a validation error, an unknown provider, or an upstream outage.
The default `redirect_to` is your callback, not your dashboard
The default body's params[:redirect_to] || oauth_callback_url is what the provider should POST back to once the user has consented. Override this only if you have a custom callback URL — passing params[:redirect_to] as e.g. dashboard_path will send the user to the dashboard before any session is established and the upstream code exchange happens. The post-sign-in destination comes from after_authentication_url, not from redirect_to.
callback
Exchanges the code returned by the provider for a Supabase session. Allowed without authentication (the cookie session has not been written yet at this point).
Params
| Param | Source | Notes |
|---|---|---|
code | Query string | Required. OAuth authorization code returned by the provider. |
state | Query string | Required. Used to find the matching sb-oauth-state-<state> signed cookie that holds the PKCE verifier. |
Outcome dispatch
| Branch | Redirect / render | Status | Flash |
|---|---|---|---|
| Success | redirect_to after_authentication_url | 302 | notice: I18n.t("supabase.rails.oauth.connected") |
| Failure — missing PKCE verifier | redirect_to new_session_path | 302 | alert: result.error.message (PKCE_ERROR, 400) |
| Failure — invalid code / 4xx | redirect_to new_session_path | 302 | alert: result.error.message |
| Failure — 5xx | redirect_to new_session_path | 302 | alert: result.error.message |
supabase_exchange_code_for_session fast-fails with PKCE_ERROR when the sb-oauth-state-<state> cookie is missing — that is the "user clicked an expired link" / "different browser" case. The upstream is never called in that branch. On success the helper internally calls start_new_session_for so the encrypted cookie is written and Current.user / Current.session are populated before the redirect runs.
allow_unauthenticated_access
allow_unauthenticated_access only: %i[authorize callback]Both actions are exempt. The session is not established until callback succeeds.
Hookable callbacks
| Hook | Default | Where to override |
|---|---|---|
after_authentication_url | stored_location_for_redirect || root_url | app/controllers/concerns/authentication.rb (preferred) or the host's OauthController |
supabase_allowed_redirect_origins (private) | config.supabase.allowed_redirect_origins, or [request.host] fallback if empty | The host's OauthController (private method; override to compute the allowlist dynamically per-request) |
Linking to the start URL
The shipped oauth/_buttons partial iterates config.supabase.oauth_providers and renders one link per provider. Configure the providers in your initializer:
# config/initializers/supabase.rb
Rails.application.config.supabase.oauth_providers = %i[google github]The partial then renders:
<%= link_to "Continue with Google", oauth_authorize_path(provider: "google"), data: { turbo_method: :post } %>
<%= link_to "Continue with GitHub", oauth_authorize_path(provider: "github"), data: { turbo_method: :post } %>…inside the sign-in form. With oauth_providers = [] (the default) no buttons render at all.
Override patterns
Provider-specific scopes. supabase_sign_in_with_oauth accepts a scopes: keyword forwarded to the provider verbatim (space-separated string per the OAuth 2.0 spec). Override authorize to pass scopes per-provider:
class OauthController < Supabase::Rails::OauthController
PROVIDER_SCOPES = {
"google" => "openid email profile https://www.googleapis.com/auth/calendar.readonly",
"github" => "user:email read:org",
}.freeze
def authorize
result = supabase_sign_in_with_oauth(
provider: params[:provider],
redirect_to: params[:redirect_to] || oauth_callback_url,
scopes: PROVIDER_SCOPES[params[:provider].to_s],
)
if result.success?
redirect_to result.value, allow_other_host: true
else
redirect_to new_session_path, alert: t(".failed")
end
end
endSurfacing the specific error. The default authorize body shows a generic "We couldn't start that sign-in" flash because the failure is most often opaque to the user (bad provider symbol, unconfigured provider, 5xx upstream). To distinguish the cases for debugging, branch on the mapper's stable codes:
class OauthController < Supabase::Rails::OauthController
def authorize
result = supabase_sign_in_with_oauth(
provider: params[:provider],
redirect_to: params[:redirect_to] || oauth_callback_url,
)
if result.success?
redirect_to result.value, allow_other_host: true
else
msg = case result.error.code
when Supabase::Rails::AuthError::INVALID_REDIRECT then t(".bad_redirect")
when Supabase::Rails::AuthError::AUTH_UPSTREAM_ERROR then t(".try_again")
else t(".failed")
end
redirect_to new_session_path, alert: msg
end
end
endCapturing the provider's profile data. Supabase Auth populates user_metadata from the provider's profile on first sign-in. Read it after super:
class OauthController < Supabase::Rails::OauthController
def callback
super
if authenticated?
Profile.upsert(
user_id: Current.user.id,
full_name: Current.user.user_metadata["full_name"],
avatar_url: Current.user.user_metadata["avatar_url"],
)
end
end
endReplacing with a from-scratch controller — point the host subclass at Supabase::Rails::BaseController directly. Both supabase_sign_in_with_oauth and supabase_exchange_code_for_session are still available from the included Authentication concern, including the PKCE round-trip behaviour and RedirectValidator checks.
See also
- Controllers overview — the full route table and the override pattern.
BaseController— the common parent class.Authenticationconcern —supabase_sign_in_with_oauth,supabase_exchange_code_for_session,start_new_session_for.RequestScopedStorage— the PKCE verifier round-trip via thesb-oauth-state-<state>signed cookie.RedirectValidator— howredirect_to:is screened against the allowlist.oauth_providersconfig — driving the shipped buttons partial.AuthErrorMapper—INVALID_REDIRECT,PKCE_ERROR,AUTH_UPSTREAM_ERROR.OtpController— passwordless sign-in (different two-leg flow).- supabase-rb:
auth.sign_in_with_oauth— the underlying Ruby method that starts the PKCE flow and exposesexchange_code_for_sessionfor the callback leg.