Skip to content

Add Google OAuth login and GAR CI/CD pipeline#1

Open
dnplkndll wants to merge 1 commit intomasterfrom
feature/google-oauth-login
Open

Add Google OAuth login and GAR CI/CD pipeline#1
dnplkndll wants to merge 1 commit intomasterfrom
feature/google-oauth-login

Conversation

@dnplkndll
Copy link

@dnplkndll dnplkndll commented Feb 15, 2026

Summary

  • Enable Google OAuth2 sign-in for self-hosted DocuSeal (gated on GOOGLE_CLIENT_ID env var)
  • Add GitHub Actions workflow to build linux/amd64 images and push to Google Artifact Registry
  • GCP infrastructure (WIF pool, SA, AR repo) has been provisioned

Image

us-central1-docker.pkg.dev/kencove-prod/docuseal/docuseal

Tags: semver (1.2.3), short SHA (abc1234), latest (on default branch)

Google OAuth Changes

File Change
Gemfile Added omniauth-google-oauth2, omniauth-rails_csrf_protection
app/models/user.rb Conditional :omniauthable devise module
config/initializers/devise.rb google_oauth2 provider config with hd: kencove.com
config/routes.rb Added omniauth_callbacks to devise routes
app/controllers/omniauth_callbacks_controller.rb New — OAuth callback handler
lib/users.rb New — OAuth user lookup (find-only, no auto-create)
config/locales/i18n.yml Added error message i18n keys

GAR CI/CD Workflow

.github/workflows/docker-gar.yml — triggers on semver tags + feature/google-oauth-login branch.

GCP Resources Created

Resource Details
Artifact Registry us-central1-docker.pkg.dev/kencove-prod/docuseal (docker)
WIF Pool projects/103143301688/locations/global/workloadIdentityPools/github-actions
OIDC Provider github — issuer token.actions.githubusercontent.com, scoped to kencove org
Service Account github-actions-docuseal@kencove-prod.iam.gserviceaccount.com
IAM SA has artifactregistry.writer on docuseal repo; WIF binding scoped to kencove/docuseal
GitHub Secrets GCP_WORKLOAD_IDENTITY_PROVIDER, GCP_SERVICE_ACCOUNT set on repo

Environment Variables for Deployment

To enable Google OAuth in the cluster, set these on the DocuSeal deployment:

env:
  - name: GOOGLE_CLIENT_ID
    value: "<your-google-oauth-client-id>"
  - name: GOOGLE_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: docuseal-google-oauth
        key: client-secret

Google OAuth App Setup (Google Cloud Console)

  1. Go to APIs & Services → Credentials in the kencove-prod GCP project
  2. Create an OAuth 2.0 Client ID (Web application type)
  3. Set Authorized redirect URI to: https://<your-docuseal-domain>/users/auth/google_oauth2/callback
  4. Copy the Client ID and Client Secret into the K8s env/secret above

Behavior

  • With env vars set: Google "Sign in with Google" button appears on /sign_in, restricted to @kencove.com accounts
  • Without env vars: Standard email/password login only (no change)
  • Users must be pre-created by an admin — Google login only matches existing users, no self-registration

Test plan

  • GAR workflow triggers on push to feature/google-oauth-login
  • Image appears at us-central1-docker.pkg.dev/kencove-prod/docuseal/docuseal
  • App starts without errors with GOOGLE_CLIENT_ID set
  • App starts without errors without GOOGLE_CLIENT_ID (no regression)
  • Google sign-in button visible when env vars present
  • Sign-in redirects to Google, callback signs in existing user
  • Non-existent user gets "User not found" error

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Google OAuth2 authentication as a login option with organizational domain support.
    • Integrated authentication feedback messages for failed logins and user lookup scenarios.
  • Chores

    • Updated Docker build and deployment workflow for cloud artifact registry integration.
    • Added authentication framework dependencies to support OAuth login flows.

Enable Google OAuth2 login for self-hosted DocuSeal when GOOGLE_CLIENT_ID
env var is present. Adds GitHub Actions workflow to build amd64 images and
push to Google Artifact Registry via Workload Identity Federation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

This pull request adds Google OAuth2 authentication to the application using Devise and OmniAuth. It includes GitHub Actions workflow configuration, necessary dependencies, controller handling for OAuth callbacks, model updates for omniauthable support, Devise initialization, route configuration, and a utility module for user lookup from OAuth data.

Changes

Cohort / File(s) Summary
CI/CD Pipeline
.github/workflows/docker-gar.yml
Introduces GitHub Actions workflow for building and pushing Docker images to Google Artifact Registry on version tags and feature branch pushes, including authentication and multi-stage image tagging with caching.
Dependencies
Gemfile
Adds omniauth-google-oauth2 and omniauth-rails_csrf_protection gems to support Google OAuth2 integration.
Configuration
config/initializers/devise.rb, config/routes.rb, config/locales/i18n.yml
Configures Devise for Google OAuth2 with client ID/secret from environment variables and hosted domain restriction; extends devise routing to include omniauth_callbacks; adds translation keys for authentication error messages.
Application Logic
app/models/user.rb, app/controllers/omniauth_callbacks_controller.rb, lib/users.rb
Adds omniauthable devise module to User model (conditional on GOOGLE_CLIENT_ID presence); implements OmniauthCallbacksController to handle Google OAuth2 callbacks and failures; creates Users utility module with from_omniauth method for OAuth user lookup.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Browser
    participant Rails as Rails App
    participant Google as Google OAuth
    participant DB as Database

    User->>Rails: Click "Sign in with Google"
    Rails->>Google: Redirect to OAuth authorization
    Google->>User: Show consent screen
    User->>Google: Authorize
    Google->>Rails: Redirect with auth code
    Rails->>Google: Exchange code for access token
    Google->>Rails: Return access token & user info
    Rails->>Rails: OmniauthCallbacksController#google_oauth2
    Rails->>DB: Query user by email (from_omniauth)
    DB->>Rails: Return user record or nil
    alt User found and active
        Rails->>Rails: Sign in user
        Rails->>User: Redirect to dashboard
    else User not found or inactive
        Rails->>User: Redirect to login with alert
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hops excitedly

Google logins now bloom bright,
OAuth flows through day and night,
With Devise and OmniAuth's care,
Users sign in everywhere! 🔐✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes both major components of the changeset: Google OAuth login implementation and the GAR CI/CD pipeline workflow addition.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/google-oauth-login

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @.github/workflows/docker-gar.yml:
- Around line 25-31: The "Create .version file" run block uses ${{
github.ref_name }} directly, which can be an injection vector; instead, add an
environment variable (e.g., REF_NAME) in that job/step set to ${{
github.ref_name }} and reference the shell-safe variable (e.g., "$REF_NAME")
inside the run script (keep the existing github.ref_type check and fallback to
github.sha as an env var if desired) and ensure the variable is quoted when
writing to .version to avoid word-splitting or shell expansion.
- Around line 3-8: Remove the development branch trigger under on.push.branches
(delete the "feature/google-oauth-login" entry) and tighten the tag trigger
under on.push.tags from "*.*.*" to a semver-style pattern such as "v*.*.*" (or
use pattern={{version}} with docker/metadata-action) so only proper semver tags
trigger the workflow; ensure any downstream uses of the tag
(docker/metadata-action inputs) are updated to match the chosen convention.

In `@app/models/user.rb`:
- Line 72: The app boots fail because the User model conditionally includes
devise :omniauthable while routes unconditionally map omniauth_callbacks; make
the guard consistent by introducing a shared early-loaded flag (e.g.
GOOGLE_OAUTH_ENABLED) and use it in both the model line that currently reads
devise :omniauthable, omniauth_providers: [:google_oauth2] and in the routes
where omniauth_callbacks is added (or alternatively wrap the routes'
omniauth_callbacks mapping in the same ENV check); ensure the initializer that
sets GOOGLE_OAUTH_ENABLED is loaded before routes and models so both sides see
the same boolean.

In `@config/locales/i18n.yml`:
- Around line 176-177: The new i18n keys user_not_found and
authentication_failed were added only for English; update all supported locale
YAML files to include equivalent translations for these keys (or add them to the
locale fallback file) so missing-translation errors don't occur; locate the
locale files that mirror i18n.yml (e.g., the other locale YAMLs or the default
fallback locale) and add matching keys with appropriate translated messages, or
confirm/update the app's I18n.fallbacks configuration to ensure these keys
resolve to the English strings when a locale translation is absent.

In `@config/routes.rb`:
- Around line 17-19: The devise_for :users route registers omniauth_callbacks
unconditionally causing boot failure when User is not omniauthable; update the
routes so omniauth_callbacks is only added when ENV['GOOGLE_CLIENT_ID'] (the
same guard used in the User model) is present — conditionally include the
:omniauth_callbacks controller and the :omniauthable routes in the devise_for
:users call (or split into two devise_for calls) so that omniauth_callbacks is
not registered unless ENV['GOOGLE_CLIENT_ID'] is set.
🧹 Nitpick comments (2)
config/initializers/devise.rb (1)

337-342: Consider making the hosted domain configurable via an environment variable.

The hd: 'kencove.com' restriction is hardcoded. If this app is intended to be a reusable self-hosted deployment, extracting it to an env var (e.g., GOOGLE_HOSTED_DOMAIN) would make it more flexible without code changes.

Suggested change
   if ENV['GOOGLE_CLIENT_ID'].present?
     config.omniauth :google_oauth2,
                     ENV.fetch('GOOGLE_CLIENT_ID'),
                     ENV.fetch('GOOGLE_CLIENT_SECRET'),
-                    { hd: 'kencove.com' }
+                    { hd: ENV.fetch('GOOGLE_HOSTED_DOMAIN', 'kencove.com') }
   end
app/controllers/omniauth_callbacks_controller.rb (1)

6-14: Consider distinguishing "user not found" from "user archived/inactive".

Both a missing user (nil) and an archived/locked user (active_for_authentication? == false) produce the same user_not_found flash message. This can be confusing for an admin debugging login issues for a known but deactivated user.

A small improvement:

💡 Suggested change
   def google_oauth2
     user = Users.from_omniauth(request.env['omniauth.auth'])
 
     if user&.active_for_authentication?
       sign_in_and_redirect(user, event: :authentication)
+    elsif user
+      redirect_to new_user_session_path, alert: I18n.t('authentication_failed')
     else
       redirect_to new_user_session_path, alert: I18n.t('user_not_found')
     end
   end

Comment on lines +3 to +8
on:
push:
tags:
- "*.*.*"
branches:
- feature/google-oauth-login
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove the feature branch trigger and tighten the tag pattern before merging.

  1. The feature/google-oauth-login branch trigger is clearly for development/testing. Leaving it in will cause every push to this branch to build and push images to GAR, which is wasteful once merged.
  2. The tag glob *.*.* is very permissive — it matches any tag with two dots (e.g., foo.bar.baz). Since docker/metadata-action uses type=semver, consider restricting to a semver-like pattern (e.g., v*.*.*) and aligning the tag convention.
Suggested fix
 on:
   push:
     tags:
-      - "*.*.*"
-    branches:
-      - feature/google-oauth-login
+      - "v*.*.*"

And update the semver pattern accordingly:

         tags: |
-          type=semver,pattern={{version}}
+          type=semver,pattern=v{{version}}

Or keep pattern={{version}} if you want to strip the v prefix from the image tag.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
on:
push:
tags:
- "*.*.*"
branches:
- feature/google-oauth-login
on:
push:
tags:
- "v*.*.*"
🤖 Prompt for AI Agents
In @.github/workflows/docker-gar.yml around lines 3 - 8, Remove the development
branch trigger under on.push.branches (delete the "feature/google-oauth-login"
entry) and tighten the tag trigger under on.push.tags from "*.*.*" to a
semver-style pattern such as "v*.*.*" (or use pattern={{version}} with
docker/metadata-action) so only proper semver tags trigger the workflow; ensure
any downstream uses of the tag (docker/metadata-action inputs) are updated to
match the chosen convention.

Comment on lines +25 to +31
- name: Create .version file
run: |
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "${{ github.ref_name }}" > .version
else
echo "${{ github.sha }}" > .version
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential script injection via github.ref_name.

Using ${{ github.ref_name }} directly in a run: block is a known GitHub Actions injection vector. While git ref naming rules limit the practical risk, the safer approach is to pass it through an environment variable.

Suggested fix
     - name: Create .version file
+      env:
+        REF_TYPE: ${{ github.ref_type }}
+        REF_NAME: ${{ github.ref_name }}
+        COMMIT_SHA: ${{ github.sha }}
       run: |
-        if [[ "${{ github.ref_type }}" == "tag" ]]; then
-          echo "${{ github.ref_name }}" > .version
+        if [[ "${REF_TYPE}" == "tag" ]]; then
+          echo "${REF_NAME}" > .version
         else
-          echo "${{ github.sha }}" > .version
+          echo "${COMMIT_SHA}" > .version
         fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Create .version file
run: |
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "${{ github.ref_name }}" > .version
else
echo "${{ github.sha }}" > .version
fi
- name: Create .version file
env:
REF_TYPE: ${{ github.ref_type }}
REF_NAME: ${{ github.ref_name }}
COMMIT_SHA: ${{ github.sha }}
run: |
if [[ "${REF_TYPE}" == "tag" ]]; then
echo "${REF_NAME}" > .version
else
echo "${COMMIT_SHA}" > .version
fi
🤖 Prompt for AI Agents
In @.github/workflows/docker-gar.yml around lines 25 - 31, The "Create .version
file" run block uses ${{ github.ref_name }} directly, which can be an injection
vector; instead, add an environment variable (e.g., REF_NAME) in that job/step
set to ${{ github.ref_name }} and reference the shell-safe variable (e.g.,
"$REF_NAME") inside the run script (keep the existing github.ref_type check and
fallback to github.sha as an env var if desired) and ensure the variable is
quoted when writing to .version to avoid word-splitting or shell expansion.

has_many :email_messages, dependent: :destroy, foreign_key: :author_id, inverse_of: :author

devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable, :lockable
devise :omniauthable, omniauth_providers: [:google_oauth2] if ENV['GOOGLE_CLIENT_ID'].present?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Conditional :omniauthable causes the CI pipeline failure.

The route file unconditionally includes omniauth_callbacks in devise_for :users, but this line only adds :omniauthable when GOOGLE_CLIENT_ID is set. When the env var is absent (e.g. CI/test), Rails fails at boot with:

Mapping omniauth_callbacks on a resource that is not omniauthable.

Both the model and routes must use the same guard. The simplest fix is to make the route conditional too (see the config/routes.rb comment), but you could also gate both with a shared helper, e.g.:

# config/initializers/google_oauth.rb  (or anywhere loaded early)
GOOGLE_OAUTH_ENABLED = ENV['GOOGLE_CLIENT_ID'].present?

…then reference GOOGLE_OAUTH_ENABLED in both the model and routes.

🤖 Prompt for AI Agents
In `@app/models/user.rb` at line 72, The app boots fail because the User model
conditionally includes devise :omniauthable while routes unconditionally map
omniauth_callbacks; make the guard consistent by introducing a shared
early-loaded flag (e.g. GOOGLE_OAUTH_ENABLED) and use it in both the model line
that currently reads devise :omniauthable, omniauth_providers: [:google_oauth2]
and in the routes where omniauth_callbacks is added (or alternatively wrap the
routes' omniauth_callbacks mapping in the same ENV check); ensure the
initializer that sets GOOGLE_OAUTH_ENABLED is loaded before routes and models so
both sides see the same boolean.

Comment on lines +176 to +177
user_not_found: User not found. Please contact your administrator.
authentication_failed: Authentication failed. Please try again.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add translations for non‑English locales.

These new keys are only present in en. If other locales are active, they’ll render as missing translations. Please add equivalent keys for each supported locale or confirm a fallback strategy.

🤖 Prompt for AI Agents
In `@config/locales/i18n.yml` around lines 176 - 177, The new i18n keys
user_not_found and authentication_failed were added only for English; update all
supported locale YAML files to include equivalent translations for these keys
(or add them to the locale fallback file) so missing-translation errors don't
occur; locate the locale files that mirror i18n.yml (e.g., the other locale
YAMLs or the default fallback locale) and add matching keys with appropriate
translated messages, or confirm/update the app's I18n.fallbacks configuration to
ensure these keys resolve to the English strings when a locale translation is
absent.

Comment on lines +17 to +19
devise_for :users, path: '/', only: %i[sessions passwords omniauth_callbacks],
controllers: { sessions: 'sessions', passwords: 'passwords',
omniauth_callbacks: 'omniauth_callbacks' }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Routes must be conditional on the same guard as the model — this is the direct cause of the pipeline failure.

omniauth_callbacks is registered unconditionally here, but User is only :omniauthable when ENV['GOOGLE_CLIENT_ID'] is present. When the env var is missing the app fails to boot.

🐛 Proposed fix
-  devise_for :users, path: '/', only: %i[sessions passwords omniauth_callbacks],
-                     controllers: { sessions: 'sessions', passwords: 'passwords',
-                                    omniauth_callbacks: 'omniauth_callbacks' }
+  devise_actions = %i[sessions passwords]
+  devise_controllers = { sessions: 'sessions', passwords: 'passwords' }
+
+  if ENV['GOOGLE_CLIENT_ID'].present?
+    devise_actions << :omniauth_callbacks
+    devise_controllers[:omniauth_callbacks] = 'omniauth_callbacks'
+  end
+
+  devise_for :users, path: '/', only: devise_actions, controllers: devise_controllers
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
devise_for :users, path: '/', only: %i[sessions passwords omniauth_callbacks],
controllers: { sessions: 'sessions', passwords: 'passwords',
omniauth_callbacks: 'omniauth_callbacks' }
devise_actions = %i[sessions passwords]
devise_controllers = { sessions: 'sessions', passwords: 'passwords' }
if ENV['GOOGLE_CLIENT_ID'].present?
devise_actions << :omniauth_callbacks
devise_controllers[:omniauth_callbacks] = 'omniauth_callbacks'
end
devise_for :users, path: '/', only: devise_actions, controllers: devise_controllers
🤖 Prompt for AI Agents
In `@config/routes.rb` around lines 17 - 19, The devise_for :users route registers
omniauth_callbacks unconditionally causing boot failure when User is not
omniauthable; update the routes so omniauth_callbacks is only added when
ENV['GOOGLE_CLIENT_ID'] (the same guard used in the User model) is present —
conditionally include the :omniauth_callbacks controller and the :omniauthable
routes in the devise_for :users call (or split into two devise_for calls) so
that omniauth_callbacks is not registered unless ENV['GOOGLE_CLIENT_ID'] is set.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant