Conversation
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>
📝 WalkthroughWalkthroughThis 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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') } endapp/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 sameuser_not_foundflash 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
| on: | ||
| push: | ||
| tags: | ||
| - "*.*.*" | ||
| branches: | ||
| - feature/google-oauth-login |
There was a problem hiding this comment.
Remove the feature branch trigger and tighten the tag pattern before merging.
- The
feature/google-oauth-loginbranch 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. - The tag glob
*.*.*is very permissive — it matches any tag with two dots (e.g.,foo.bar.baz). Sincedocker/metadata-actionusestype=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.
| 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.
| - name: Create .version file | ||
| run: | | ||
| if [[ "${{ github.ref_type }}" == "tag" ]]; then | ||
| echo "${{ github.ref_name }}" > .version | ||
| else | ||
| echo "${{ github.sha }}" > .version | ||
| fi |
There was a problem hiding this comment.
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.
| - 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? |
There was a problem hiding this comment.
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.
| user_not_found: User not found. Please contact your administrator. | ||
| authentication_failed: Authentication failed. Please try again. |
There was a problem hiding this comment.
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.
| devise_for :users, path: '/', only: %i[sessions passwords omniauth_callbacks], | ||
| controllers: { sessions: 'sessions', passwords: 'passwords', | ||
| omniauth_callbacks: 'omniauth_callbacks' } |
There was a problem hiding this comment.
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.
| 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.
Summary
GOOGLE_CLIENT_IDenv var)linux/amd64images and push to Google Artifact RegistryImage
Tags: semver (
1.2.3), short SHA (abc1234),latest(on default branch)Google OAuth Changes
Gemfileomniauth-google-oauth2,omniauth-rails_csrf_protectionapp/models/user.rb:omniauthabledevise moduleconfig/initializers/devise.rbgoogle_oauth2provider config withhd: kencove.comconfig/routes.rbomniauth_callbacksto devise routesapp/controllers/omniauth_callbacks_controller.rblib/users.rbconfig/locales/i18n.ymlGAR CI/CD Workflow
.github/workflows/docker-gar.yml— triggers on semver tags +feature/google-oauth-loginbranch.GCP Resources Created
us-central1-docker.pkg.dev/kencove-prod/docuseal(docker)projects/103143301688/locations/global/workloadIdentityPools/github-actionsgithub— issuertoken.actions.githubusercontent.com, scoped tokencoveorggithub-actions-docuseal@kencove-prod.iam.gserviceaccount.comartifactregistry.writeron docuseal repo; WIF binding scoped tokencove/docusealGCP_WORKLOAD_IDENTITY_PROVIDER,GCP_SERVICE_ACCOUNTset on repoEnvironment Variables for Deployment
To enable Google OAuth in the cluster, set these on the DocuSeal deployment:
Google OAuth App Setup (Google Cloud Console)
kencove-prodGCP projecthttps://<your-docuseal-domain>/users/auth/google_oauth2/callbackBehavior
/sign_in, restricted to@kencove.comaccountsTest plan
feature/google-oauth-loginus-central1-docker.pkg.dev/kencove-prod/docuseal/docusealGOOGLE_CLIENT_IDsetGOOGLE_CLIENT_ID(no regression)🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
New Features
Chores