supabase-rb-rb
Controllers

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
end

Source

# 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
end

Routes

HelperVerbURLAction
oauth_authorize_path(provider)GET/oauth/:provider/authorizeauthorize
oauth_callback_pathGET/oauth/callbackcallback

Mounted by:

get "/oauth/:provider/authorize", to: "oauth#authorize", as: :oauth_authorize
get "/oauth/callback",            to: "oauth#callback",  as: :oauth_callback

inside 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_url

The 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

ParamSourceNotes
providerURL pathRequired. One of "google", "github", "discord", etc. — any provider configured in the Supabase dashboard's Authentication → Providers page.
redirect_toQuery stringOptional. Defaults to oauth_callback_url. Validated against config.supabase.allowed_redirect_origins by RedirectValidator before the upstream call.

Outcome dispatch

BranchRedirect / renderStatusFlash
Successredirect_to result.value, allow_other_host: true302
Failure — invalid redirect_toredirect_to new_session_path302alert: I18n.t("supabase.rails.oauth.failed")
Failure — provider unknown / not configuredredirect_to new_session_path302alert: I18n.t("supabase.rails.oauth.failed")
Failure — 5xx upstreamredirect_to new_session_path302alert: 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

ParamSourceNotes
codeQuery stringRequired. OAuth authorization code returned by the provider.
stateQuery stringRequired. Used to find the matching sb-oauth-state-<state> signed cookie that holds the PKCE verifier.

Outcome dispatch

BranchRedirect / renderStatusFlash
Successredirect_to after_authentication_url302notice: I18n.t("supabase.rails.oauth.connected")
Failure — missing PKCE verifierredirect_to new_session_path302alert: result.error.message (PKCE_ERROR, 400)
Failure — invalid code / 4xxredirect_to new_session_path302alert: result.error.message
Failure — 5xxredirect_to new_session_path302alert: 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

HookDefaultWhere to override
after_authentication_urlstored_location_for_redirect || root_urlapp/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 emptyThe 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
end

Surfacing 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
end

Capturing 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
end

Replacing 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

On this page