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