← insidertrades.directory / built with rails

Sign in with google (no devise/omniauth)

Jason Kotchoff committed on 2025-09-29 14:48:05 +1000 · commit 394aa758f3c6e718747fae2d3df1d6d62d13d9d0

@@ -17,6 +17,8 @@ gem "stimulus-rails"
1717 # Build JSON APIs with ease [https://github.com/rails/jbuilder]
1818 gem "jbuilder"
1919
20+gem "jwt" # for google signin
21+
2022 # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword]
2123 # gem "bcrypt", "~> 3.1.7"
2224
@@ -122,6 +122,8 @@ GEM
122122 actionview (>= 7.0.0)
123123 activesupport (>= 7.0.0)
124124 json (2.13.2)
125+ jwt (3.1.2)
126+ base64
125127 kamal (2.7.0)
126128 activesupport (>= 7.0)
127129 base64 (~> 0.2)
@@ -373,6 +375,7 @@ DEPENDENCIES
373375 debug
374376 importmap-rails
375377 jbuilder
378+ jwt
376379 kamal
377380 letter_opener
378381 propshaft
@@ -1,6 +1,7 @@
11 class SessionsController < ApplicationController
22 require_authentication only: :destroy
33 rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
4+ skip_before_action :verify_authenticity_token, only: [:create_for_google]
45
56 def new
67 # Show signin form with email subscription
@@ -80,4 +81,33 @@ class SessionsController < ApplicationController
8081 terminate_session
8182 redirect_to root_path, notice: "You have been signed out."
8283 end
84+
85+ def create_for_google
86+ google_signin = GoogleSignin.new(web_credential: params[:credential])
87+
88+ if google_signin.valid?
89+ # Find or create user by email
90+ user = User.find_or_initialize_by(email_address: google_signin.email)
91+
92+ # Update Google user ID if it's a new association
93+ user.google_user_id = google_signin.user_id if user.google_user_id.blank?
94+ user.first_name = google_signin.first_name if user.first_name.blank?
95+ user.last_name = google_signin.last_name if user.last_name.blank?
96+ user.photo_url = google_signin.photo_url if user.photo_url.blank?
97+ user.save!
98+
99+ # Sign them in
100+ start_new_session_for(user)
101+
102+ flash[:notice] = "Successfully signed in with Google!"
103+ redirect_to root_path
104+ else
105+ flash[:alert] = "Could not authenticate you from Google"
106+ redirect_to signin_path
107+ end
108+ rescue => e
109+ Rails.logger.error "Google signin error: #{e.message}"
110+ flash[:alert] = "Could not authenticate you from Google"
111+ redirect_to signin_path
112+ end
83113 end
@@ -0,0 +1,50 @@
1+require 'net/http'
2+require 'json'
3+require 'jwt'
4+
5+class GoogleIdTokenValidator
6+ GOOGLE_JWKS_URI = 'https://www.googleapis.com/oauth2/v3/certs'
7+ GOOGLE_ISSUER = 'https://accounts.google.com'
8+
9+ def self.validate(token:, client_id:)
10+ # Fetch the JWT header without verification
11+ header = JWT.decode(token, nil, false)[1]
12+ kid = header['kid']
13+
14+ # Fetch Google's public keys
15+ uri = URI(GOOGLE_JWKS_URI)
16+ response = Net::HTTP.get_response(uri)
17+ jwks = JSON.parse(response.body)
18+
19+ # Find the key that matches our token's kid
20+ public_key = nil
21+ jwks['keys'].each do |key|
22+ if key['kid'] == kid
23+ public_key = JWT::JWK.import(key).public_key
24+ break
25+ end
26+ end
27+
28+ raise 'No matching key found' unless public_key
29+
30+ # Verify the token
31+ decoded_token = JWT.decode(
32+ token,
33+ public_key,
34+ true,
35+ {
36+ algorithm: 'RS256',
37+ verify_iss: true,
38+ iss: GOOGLE_ISSUER,
39+ verify_aud: true,
40+ aud: client_id,
41+ verify_iat: true
42+ }
43+ )
44+
45+ # Return the payload
46+ decoded_token.first
47+ rescue JWT::DecodeError, OpenSSL::PKey::RSAError => e
48+ raise "Token validation failed: #{e.message}"
49+ end
50+end
\ No newline at end of file
@@ -0,0 +1,31 @@
1+class GoogleSignin
2+ attr_reader :email, :first_name, :last_name, :photo_url, :user_id
3+
4+ # Your provided client ID
5+ WEB_CLIENT_ID = "98725179367-0nt90fh00mfb6vmpm3as1hb8453r0ki0.apps.googleusercontent.com"
6+
7+ def initialize(web_credential:)
8+ @web_credential = web_credential
9+ @client_id = WEB_CLIENT_ID
10+ end
11+
12+ def valid?
13+ return false if @web_credential.blank?
14+
15+ validated_token = GoogleIdTokenValidator.validate(
16+ token: @web_credential,
17+ client_id: @client_id,
18+ ).stringify_keys
19+
20+ @email = validated_token['email']
21+ @first_name = validated_token['given_name']
22+ @last_name = validated_token['family_name']
23+ @photo_url = validated_token['picture']
24+ @user_id = validated_token['sub']
25+
26+ @email.present?
27+ rescue => e
28+ Rails.logger.error "Google signin validation failed: #{e.message}"
29+ false
30+ end
31+end
\ No newline at end of file
@@ -3,6 +3,38 @@
33 <div class="signin-form" style="flex: 1; padding: 2rem; background: #f8f9fa; border-radius: 8px;">
44 <h2>Join our mailing list</h2>
55
6+ <!-- Google Sign-In Button -->
7+ <div style="margin-bottom: 2rem;">
8+ <div id="g_id_onload"
9+ data-client_id="<%= GoogleSignin::WEB_CLIENT_ID %>"
10+ data-context="signin"
11+ data-ux_mode="redirect"
12+ data-login_uri="<%= google_signin_url %>"
13+ data-auto_prompt="false"
14+ data-itp_support="true">
15+ </div>
16+
17+ <div class="g_id_signin"
18+ data-type="standard"
19+ data-shape="rectangular"
20+ data-theme="outline"
21+ data-text="continue_with"
22+ data-size="large"
23+ data-logo_alignment="left">
24+ </div>
25+
26+ <% content_for(:head) do %>
27+ <script src="https://accounts.google.com/gsi/client" async defer></script>
28+ <% end %>
29+
30+ </div>
31+
32+ <!-- Divider -->
33+ <div style="text-align: center; margin: 2rem 0; position: relative;">
34+ <hr style="border: none; border-top: 1px solid #ddd; margin: 0;">
35+ <span style="background: #f8f9fa; padding: 0 1rem; color: #666; position: absolute; top: -0.5rem; left: 50%; transform: translateX(-50%);">or</span>
36+ </div>
37+
638 <%= form_with url: signin_path, local: true, class: "subscription-form" do |form| %>
739 <div style="margin-bottom: 1.5rem;">
840 <%= form.email_field :email_address,
@@ -10,6 +10,7 @@ Rails.application.routes.draw do
1010 post "signin", to: "sessions#create"
1111 get "verify", to: "sessions#verify"
1212 post "verify", to: "sessions#verify_code"
13+ post "google-signin", to: "sessions#create_for_google", as: :google_signin
1314 # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
1415
1516 # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
@@ -0,0 +1,8 @@
1+class AddGoogleUserId < ActiveRecord::Migration[8.0]
2+ def change
3+ add_column :users, :google_user_id, :string
4+ add_column :users, :first_name, :string
5+ add_column :users, :last_name, :string
6+ add_column :users, :photo_url, :string
7+ end
8+end
@@ -10,7 +10,7 @@
1010 #
1111 # It's strongly recommended that you check this file into your version control system.
1212
13-ActiveRecord::Schema[8.0].define(version: 2025_09_29_031843) do
13+ActiveRecord::Schema[8.0].define(version: 2025_09_29_043752) do
1414 create_table "posts", force: :cascade do |t|
1515 t.string "title", null: false
1616 t.string "slug", null: false
@@ -42,6 +42,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_29_031843) do
4242 t.datetime "updated_at", null: false
4343 t.string "verification_code"
4444 t.datetime "verification_code_generated_at"
45+ t.string "google_user_id"
46+ t.string "first_name"
47+ t.string "last_name"
48+ t.string "photo_url"
4549 t.index ["email_address"], name: "index_users_on_email_address", unique: true
4650 end
4751