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