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 new
+58
@@ -0,0 +1,58 @@
1 +# frozen_string_literal: true
2 +
3 +module Oauth
4 + # "Continue with GitHub" — delegates to smbCloud Auth, which brokers the
5 + # GitHub OAuth flow (smbCloud is our Auth0-style auth-as-a-service). smbCloud
6 + # signs the user in / creates the account from their GitHub profile, then
7 + # redirects back to #callback with an `access_token` we exchange for a local
8 + # session — the same upsert path as SessionsController#create.
9 + class GithubController < ApplicationController
10 + before_action :redirect_if_signed_in
11 +
12 + # GET /auth/github
13 + def start
14 + redirect_to SmbcloudAuthService.github_authorize_url(redirect_uri: github_auth_callback_url),
15 + allow_other_host: true
16 + end
17 +
18 + # GET /auth/github/callback
19 + def callback
20 + if params[:error].present?
21 + Rails.logger.warn("GitHub sign-in error: #{params[:error]}")
22 + return redirect_to signin_path, alert: "GitHub sign-in was cancelled or failed. Please try again."
23 + end
24 +
25 + access_token = params[:access_token].to_s
26 + if access_token.blank?
27 + return redirect_to signin_path, alert: "GitHub sign-in failed. Please try again."
28 + end
29 +
30 + profile = SmbcloudAuthService.me(access_token: access_token)
31 + user = User.find_or_create_from_smbcloud(profile.merge(access_token: access_token))
32 +
33 + reset_session
34 + session[:user_id] = user.id
35 +
36 + return_to = session.delete(:return_to)
37 + redirect_to(return_to || root_path, notice: "Welcome, #{user.display_name_or_username}!")
38 +
39 + rescue SmbcloudAuthService::AuthenticationError => e
40 + Rails.logger.warn("GitHub sign-in auth error: #{e.message}")
41 + redirect_to signin_path, alert: "GitHub sign-in failed. Please try again."
42 +
43 + rescue KeyError => e
44 + Rails.logger.error("smbCloud configuration error during GitHub sign-in: #{e.message}")
45 + redirect_to signin_path, alert: "Authentication service is not configured. Please contact support."
46 +
47 + rescue => e
48 + Rails.logger.error("Unexpected GitHub sign-in error: #{e.message}\n#{e.backtrace.first(5).join("\n")}")
49 + redirect_to signin_path, alert: "An unexpected error occurred. Please try again."
50 + end
51 +
52 + private
53 +
54 + def redirect_if_signed_in
55 + redirect_to root_path, notice: "You are already signed in." if signed_in?
56 + end
57 + end
58 +end
app/services/smbcloud_auth_service.rb
+16
@@ -80,6 +80,22 @@ class SmbcloudAuthService
80 raise AuthenticationError.new(e.message, error_code: e.error_code)
81 end
82
83 + # Builds the smbCloud "Sign in with GitHub" authorize URL. The browser is
84 + # redirected here; smbCloud brokers the GitHub OAuth dance and bounces back to
85 + # `redirect_uri` with an `access_token` (or an `error`) query param.
86 + #
87 + # Mirrors the platform's existing client convention of passing the app's
88 + # client_id/client_secret as query params (see #post_client).
89 + def self.github_authorize_url(redirect_uri:)
90 + uri = URI.parse("#{smbcloud_base_url}/v1/client/oauth/github/authorize")
91 + uri.query = URI.encode_www_form(
92 + client_id: smbcloud_app_id,
93 + client_secret: smbcloud_app_secret,
94 + redirect_uri: redirect_uri
95 + )
96 + uri.to_s
97 + end
98 +
99 # Fetches the current user's profile from the smbCloud Auth API.
100 #
101 # Returns a symbolized hash:
app/views/registrations/new.html.erb
+2
@@ -58,6 +58,8 @@
58 </button>
59
60 <% end %>
61 +
62 + <%= render "sessions/github_button" %>
63 </div>
64
65 <p class="mt-6 text-center text-xs text-gray-500">
app/views/sessions/_github_button.html.erb new
+13
@@ -0,0 +1,13 @@
1 +<div class="my-5 flex items-center gap-3">
2 + <span class="h-px flex-1 bg-surface-500"></span>
3 + <span class="text-xs uppercase tracking-wide text-gray-500">or</span>
4 + <span class="h-px flex-1 bg-surface-500"></span>
5 +</div>
6 +
7 +<%= link_to github_auth_path,
8 + class: "btn-secondary w-full justify-center gap-2.5 py-2.5 inline-flex items-center" do %>
9 + <svg class="w-5 h-5" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
10 + <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"/>
11 + </svg>
12 + <span>Continue with GitHub</span>
13 +<% end %>
app/views/sessions/new.html.erb
+2
@@ -48,6 +48,8 @@
48 </button>
49
50 <% end %>
51 +
52 + <%= render "sessions/github_button" %>
53 </div>
54
55 <p class="mt-6 text-center text-xs text-gray-500">
config/routes.rb
+4
@@ -25,6 +25,10 @@ Rails.application.routes.draw do
25 post "/auth", to: "sessions#create", as: :auth_create
26 delete "/auth/signout", to: "sessions#destroy", as: :signout
27
28 + # Auth — "Continue with GitHub" (brokered by smbCloud Auth)
29 + get "/auth/github", to: "oauth/github#start", as: :github_auth
30 + get "/auth/github/callback", to: "oauth/github#callback", as: :github_auth_callback
31 +
32 # Signup
33 get "/auth/signup", to: "registrations#new", as: :signup
34 post "/auth/signup", to: "registrations#create", as: :signup_create