feat(auth): add "Continue with GitHub" sign-in via smbCloud

Adds GitHub social login to the web auth flow. smbCloud Auth brokers the GitHub OAuth dance and redirects back with an access_token, which we exchange for a local session through the existing SmbcloudAuthService.me upsert path. - SmbcloudAuthService.github_authorize_url builds the smbCloud authorize URL - Oauth::GithubController#start redirects to it; #callback signs the user in - "Continue with GitHub" button on the sign-in and sign-up pages Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Seto Elkahfi committed Jun 30, 2026 at 19:54 UTC 7b0c788b82ba60799cdc24c136c3f67d38ed1c84
6 files changed +95
app/controllers/oauth/github_controller.rb
+58
new file mode 100644 index 0000000..11f60c0 --- /dev/null +++ b/app/controllers/oauth/github_controller.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Oauth + # "Continue with GitHub" — delegates to smbCloud Auth, which brokers the + # GitHub OAuth flow (smbCloud is our Auth0-style auth-as-a-service). smbCloud + # signs the user in / creates the account from their GitHub profile, then + # redirects back to #callback with an `access_token` we exchange for a local + # session — the same upsert path as SessionsController#create. + class GithubController < ApplicationController + before_action :redirect_if_signed_in + + # GET /auth/github + def start + redirect_to SmbcloudAuthService.github_authorize_url(redirect_uri: github_auth_callback_url), + allow_other_host: true + end + + # GET /auth/github/callback + def callback + if params[:error].present? + Rails.logger.warn("GitHub sign-in error: #{params[:error]}") + return redirect_to signin_path, alert: "GitHub sign-in was cancelled or failed. Please try again." + end + + access_token = params[:access_token].to_s + if access_token.blank? + return redirect_to signin_path, alert: "GitHub sign-in failed. Please try again." + end + + profile = SmbcloudAuthService.me(access_token: access_token) + user = User.find_or_create_from_smbcloud(profile.merge(access_token: access_token)) + + reset_session + session[:user_id] = user.id + + return_to = session.delete(:return_to) + redirect_to(return_to || root_path, notice: "Welcome, #{user.display_name_or_username}!") + + rescue SmbcloudAuthService::AuthenticationError => e + Rails.logger.warn("GitHub sign-in auth error: #{e.message}") + redirect_to signin_path, alert: "GitHub sign-in failed. Please try again." + + rescue KeyError => e + Rails.logger.error("smbCloud configuration error during GitHub sign-in: #{e.message}") + redirect_to signin_path, alert: "Authentication service is not configured. Please contact support." + + rescue => e + Rails.logger.error("Unexpected GitHub sign-in error: #{e.message}\n#{e.backtrace.first(5).join("\n")}") + redirect_to signin_path, alert: "An unexpected error occurred. Please try again." + end + + private + + def redirect_if_signed_in + redirect_to root_path, notice: "You are already signed in." if signed_in? + end + end +end
app/services/smbcloud_auth_service.rb
+16
index 8907039..a5205a8 100644 --- a/app/services/smbcloud_auth_service.rb +++ b/app/services/smbcloud_auth_service.rb @@ -80,6 +80,22 @@ class SmbcloudAuthService raise AuthenticationError.new(e.message, error_code: e.error_code) end + # Builds the smbCloud "Sign in with GitHub" authorize URL. The browser is + # redirected here; smbCloud brokers the GitHub OAuth dance and bounces back to + # `redirect_uri` with an `access_token` (or an `error`) query param. + # + # Mirrors the platform's existing client convention of passing the app's + # client_id/client_secret as query params (see #post_client). + def self.github_authorize_url(redirect_uri:) + uri = URI.parse("#{smbcloud_base_url}/v1/client/oauth/github/authorize") + uri.query = URI.encode_www_form( + client_id: smbcloud_app_id, + client_secret: smbcloud_app_secret, + redirect_uri: redirect_uri + ) + uri.to_s + end + # Fetches the current user's profile from the smbCloud Auth API. # # Returns a symbolized hash:
app/views/registrations/new.html.erb
+2
index 95bdf0f..f49260b 100644 --- a/app/views/registrations/new.html.erb +++ b/app/views/registrations/new.html.erb @@ -58,6 +58,8 @@ </button> <% end %> + + <%= render "sessions/github_button" %> </div> <p class="mt-6 text-center text-xs text-gray-500">
app/views/sessions/_github_button.html.erb
+13
new file mode 100644 index 0000000..4fa561c --- /dev/null +++ b/app/views/sessions/_github_button.html.erb @@ -0,0 +1,13 @@ +<div class="my-5 flex items-center gap-3"> + <span class="h-px flex-1 bg-surface-500"></span> + <span class="text-xs uppercase tracking-wide text-gray-500">or</span> + <span class="h-px flex-1 bg-surface-500"></span> +</div> + +<%= link_to github_auth_path, + class: "btn-secondary w-full justify-center gap-2.5 py-2.5 inline-flex items-center" do %> + <svg class="w-5 h-5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"> + <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/> + </svg> + <span>Continue with GitHub</span> +<% end %>
app/views/sessions/new.html.erb
+2
index 655c946..c07ed47 100644 --- a/app/views/sessions/new.html.erb +++ b/app/views/sessions/new.html.erb @@ -48,6 +48,8 @@ </button> <% end %> + + <%= render "sessions/github_button" %> </div> <p class="mt-6 text-center text-xs text-gray-500">
config/routes.rb
+4
index 8c828ce..f00acd2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -25,6 +25,10 @@ Rails.application.routes.draw do post "/auth", to: "sessions#create", as: :auth_create delete "/auth/signout", to: "sessions#destroy", as: :signout + # Auth — "Continue with GitHub" (brokered by smbCloud Auth) + get "/auth/github", to: "oauth/github#start", as: :github_auth + get "/auth/github/callback", to: "oauth/github#callback", as: :github_auth_callback + # Signup get "/auth/signup", to: "registrations#new", as: :signup post "/auth/signup", to: "registrations#create", as: :signup_create