Skip to content

Commit 49de681

Browse files
ClearlyClairenykma
authored andcommitted
(feat) OAuth: add third-party OAuth2 login procedure
1 parent eeb6805 commit 49de681

17 files changed

Lines changed: 288 additions & 6 deletions

File tree

.devcontainer/compose.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ services:
2424
LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
2525
LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000}
2626
VITE_DEV_SERVER_PUBLIC: ${VITE_DEV_SERVER_PUBLIC:-localhost:3036}
27+
OAUTH_GOOGLE_ENABLED: 'true'
28+
OAUTH_GOOGLE_CLIENT_ID: '100000000000000000000'
29+
OAUTH_GOOGLE_CLIENT_SECRET: '100000000000000000000'
2730
# Overrides default command so things don't shut down after the process ends.
2831
command: sleep infinity
2932
ports:

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,14 @@ gem 'net-ldap', '~> 0.18'
3939

4040
gem 'omniauth', '~> 2.0'
4141
gem 'omniauth-cas', '~> 3.0.0.beta.1'
42+
gem 'omniauth-line', '~> 0.1.0'
4243
gem 'omniauth_openid_connect', '~> 0.8.0'
4344
gem 'omniauth-rails_csrf_protection', '~> 2.0'
4445
gem 'omniauth-saml', '~> 2.0'
46+
gem 'omniauth-twitter', github: 'arunagw/omniauth-twitter'
47+
48+
# OAuth2 login
49+
gem 'omniauth-google-oauth2'
4550

4651
gem 'color_diff', '~> 0.1'
4752
gem 'csv', '~> 3.2'

Gemfile.lock

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
GIT
2+
remote: https://github.com/arunagw/omniauth-twitter.git
3+
revision: 79ef26df596037fbb06967c4b14b9fde92ed99cb
4+
specs:
5+
omniauth-twitter (1.5.0)
6+
cgi
7+
omniauth-oauth (~> 1.2)
8+
rack
9+
110
GIT
211
remote: https://github.com/mastodon/webpush.git
312
revision: 9631ac63045cfabddacc69fc06e919b4c13eb913
@@ -96,8 +105,12 @@ GEM
96105
annotaterb (4.22.0)
97106
activerecord (>= 6.0.0)
98107
activesupport (>= 6.0.0)
108+
anonymous_loader (0.1.2)
109+
version_gem (~> 1.1, >= 1.1.13)
99110
ast (2.4.3)
100111
attr_required (1.0.2)
112+
auth-sanitizer (0.2.2)
113+
version_gem (~> 1.1, >= 1.1.10)
101114
aws-eventstream (1.4.0)
102115
aws-partitions (1.1259.0)
103116
aws-sdk-core (3.252.0)
@@ -453,6 +466,8 @@ GEM
453466
prism (~> 1.5)
454467
msgpack (1.8.3)
455468
multi_json (1.21.1)
469+
multi_xml (0.9.1)
470+
bigdecimal (>= 3.1, < 5)
456471
mutex_m (0.3.0)
457472
net-http (0.9.1)
458473
uri (>= 0.11.1)
@@ -472,6 +487,26 @@ GEM
472487
nokogiri (1.19.4)
473488
mini_portile2 (~> 2.8.2)
474489
racc (~> 1.4)
490+
oauth (1.1.7)
491+
auth-sanitizer (~> 0.2, >= 0.2.1)
492+
base64 (~> 0.1)
493+
oauth-tty (~> 1.0, >= 1.0.10)
494+
snaky_hash (~> 2.0, >= 2.0.5)
495+
version_gem (~> 1.1, >= 1.1.12)
496+
oauth-tty (1.0.12)
497+
anonymous_loader (~> 0.1, >= 0.1.1)
498+
auth-sanitizer (~> 0.2, >= 0.2.2)
499+
version_gem (~> 1.1, >= 1.1.12)
500+
oauth2 (2.0.24)
501+
anonymous_loader (~> 0.1, >= 0.1.1)
502+
auth-sanitizer (~> 0.2, >= 0.2.2)
503+
faraday (>= 0.17.3, < 4.0)
504+
jwt (>= 1.0, < 4.0)
505+
logger (~> 1.2)
506+
multi_xml (~> 0.5)
507+
rack (>= 1.2, < 4)
508+
snaky_hash (~> 2.0, >= 2.0.6)
509+
version_gem (~> 1.1, >= 1.1.12)
475510
omniauth (2.1.4)
476511
hashie (>= 3.4.6)
477512
logger
@@ -481,6 +516,21 @@ GEM
481516
addressable (~> 2.8)
482517
nokogiri (~> 1.12)
483518
omniauth (~> 2.1)
519+
omniauth-google-oauth2 (1.2.2)
520+
jwt (>= 2.9.2)
521+
oauth2 (~> 2.0)
522+
omniauth (~> 2.0)
523+
omniauth-oauth2 (~> 1.8)
524+
omniauth-line (0.1.0)
525+
json (>= 2.3.0)
526+
omniauth-oauth2 (~> 1.3)
527+
omniauth-oauth (1.2.1)
528+
oauth
529+
omniauth (>= 1.0, < 3)
530+
rack (>= 1.6.2, < 4)
531+
omniauth-oauth2 (1.9.0)
532+
oauth2 (>= 2.0.2, < 3)
533+
omniauth (~> 2.0)
484534
omniauth-rails_csrf_protection (2.0.1)
485535
actionpack (>= 4.2)
486536
omniauth (~> 2.0)
@@ -844,6 +894,9 @@ GEM
844894
simplecov-html (0.13.2)
845895
simplecov-lcov (0.9.0)
846896
simplecov_json_formatter (0.1.4)
897+
snaky_hash (2.0.6)
898+
hashie (>= 0.1.0, < 6)
899+
version_gem (>= 1.1.8, < 3)
847900
ssrf_filter (1.5.0)
848901
stackprof (0.2.28)
849902
starry (0.2.0)
@@ -903,6 +956,7 @@ GEM
903956
validate_url (1.0.15)
904957
activemodel (>= 3.0.0)
905958
public_suffix
959+
version_gem (1.1.13)
906960
vite_rails (3.11.0)
907961
railties (>= 5.1, < 9)
908962
vite_ruby (~> 3.0, >= 3.2.2)
@@ -1019,8 +1073,11 @@ DEPENDENCIES
10191073
nokogiri (~> 1.15)
10201074
omniauth (~> 2.0)
10211075
omniauth-cas (~> 3.0.0.beta.1)
1076+
omniauth-google-oauth2
1077+
omniauth-line (~> 0.1.0)
10221078
omniauth-rails_csrf_protection (~> 2.0)
10231079
omniauth-saml (~> 2.0)
1080+
omniauth-twitter!
10241081
omniauth_openid_connect (~> 0.8.0)
10251082
opentelemetry-api (~> 1.10.0)
10261083
opentelemetry-exporter-otlp (~> 0.34.0)

app/controllers/auth/omniauth_callbacks_controller.rb

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,39 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
77
def self.provides_callback_for(provider)
88
define_method provider do
99
@provider = provider
10-
@user = User.find_for_omniauth(request.env['omniauth.auth'], current_user)
10+
# | =identity= | =current_user= | Action |
11+
# |------------+-----------------+---------------------------------|
12+
# | Exist | Exist | Report error |
13+
# | Exist | Absence | Login |
14+
# | Absence | Exist | Bind |
15+
# | Absence | Absence | Register or login (depend on ENV) |
16+
auth = request.env['omniauth.auth']
17+
if ENV['OAUTH_DISABLE_AUTO_REGISTER'] == 'true'
18+
uid = auth.uid
19+
uid = uid[0][:uid] || uid[0][:user] if uid.is_a? Hashie::Array
20+
identity = Identity.find_or_create_by(provider: provider, uid: uid)
21+
@user = identity&.user
22+
if @user.blank? # Identity is fresh: just created, no user binded yet
23+
if current_user.present?
24+
@user = current_user
25+
identity.user = @user
26+
identity.save!
27+
else
28+
identity.destroy!
29+
flash[:alert] = I18n.t('settings.identities.cannot_login_without_register') if is_navigational_format?
30+
return redirect_to new_user_session_url
31+
end
32+
end
33+
else
34+
@user = User.find_for_omniauth(auth, current_user)
35+
end
1136

1237
if @user.persisted?
1338
record_login_activity
1439
sign_in_and_redirect @user, event: :authentication
1540
set_flash_message(:notice, :success, kind: label_for_provider) if is_navigational_format?
1641
else
17-
session["devise.#{provider}_data"] = request.env['omniauth.auth']
42+
session["devise.#{provider}_data"] = auth
1843
redirect_to new_user_registration_url
1944
end
2045
rescue ActiveRecord::RecordInvalid
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
class Settings::IdentitiesController < Settings::BaseController
4+
layout 'admin'
5+
before_action :authenticate_user!
6+
before_action :set_identity, only: [:destroy]
7+
8+
content_security_policy do |p|
9+
p.form_action(false)
10+
end
11+
12+
def index
13+
@identities = current_user.identities
14+
@all_providers = {}
15+
Devise.omniauth_configs.each_key do |platform|
16+
@all_providers[platform] = @identities.find { |i| i.provider == platform.to_s }
17+
end
18+
end
19+
20+
def destroy
21+
if @identity&.destroy
22+
redirect_to({ action: :index }, success: t('settings.identities.oauth_binding_removed'))
23+
else
24+
redirect_to({ action: :index }, alert: t('settings.identities.oauth_binding_remove_failed'))
25+
end
26+
end
27+
28+
private
29+
30+
def set_identity
31+
@identity = current_user.identities.find(params[:id])
32+
end
33+
end

app/models/user.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class User < ApplicationRecord
8282
has_many :markers, inverse_of: :user, dependent: :destroy
8383
has_many :webauthn_credentials, dependent: :destroy
8484
has_many :ips, class_name: 'UserIp', inverse_of: :user, dependent: nil
85+
has_many :identities, dependent: :destroy
8586

8687
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
8788
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
- content_for :page_title do
2+
= t('settings.identities.title')
3+
4+
%table.table
5+
%thead
6+
%tr
7+
%th= t('settings.identities.provider')
8+
%th= t('settings.identities.uid')
9+
%th= t('settings.identities.action')
10+
%tbody
11+
- @all_providers.each do |provider, identity|
12+
%tr
13+
%td= t("settings.identities.platforms.#{provider}")
14+
15+
- if identity.present?
16+
%td= identity.uid
17+
- else
18+
%td= t('settings.identities.not_binded')
19+
20+
- if identity.blank?
21+
%td= link_to t('settings.identities.link'), omniauth_authorize_path(:user, provider), method: :post
22+
- else
23+
%td= link_to t('settings.identities.unlink'), settings_identity_path(identity), method: :delete, data: { confirm: t('settings.identities.confirm_unlink') }

config/initializers/3_omniauth.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,25 @@
105105
oidc_options[:security][:assume_email_is_verified] = ENV['OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED'] == 'true' # OPTIONAL
106106
config.omniauth :openid_connect, oidc_options
107107
end
108+
109+
if ENV['OAUTH_GOOGLE_ENABLED'] == 'true'
110+
options = {
111+
scope: 'profile',
112+
prompt: 'select_account',
113+
image_aspect_ratio: 'square',
114+
image_size: 200,
115+
}
116+
config.omniauth :google_oauth2, ENV.fetch('OAUTH_GOOGLE_CLIENT_ID', nil), ENV.fetch('OAUTH_GOOGLE_CLIENT_SECRET', nil), options
117+
end
118+
119+
if ENV['OAUTH_TWITTER_ENABLED'] == 'true'
120+
options = {
121+
secure_image_url: true,
122+
x_auth_access_type: 'read',
123+
use_authorize: true,
124+
}
125+
config.omniauth :twitter, ENV.fetch('OAUTH_TWITTER_API_KEY', nil), ENV.fetch('OAUTH_TWITTER_API_SECRET', nil), options
126+
end
127+
128+
config.omniauth :line, ENV.fetch('OAUTH_LINE_CHANNEL_ID', nil), ENV.fetch('OAUTH_LINE_CHANNEL_SECRET', nil) if ENV['OAUTH_LINE_ENABLED'] == 'true'
108129
end

config/locales/en.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,6 +2071,21 @@ en:
20712071
edit_profile: Edit profile
20722072
export: Export
20732073
featured_tags: Featured hashtags
2074+
identities:
2075+
action: Action
2076+
cannot_login_without_register: You cannot login with this account because you have not registered yet.
2077+
confirm_unlink: Are you sure you want to unlink this OAuth binding?
2078+
link: Link
2079+
menu_title: OAuth Login
2080+
not_binded: "(Not binded)"
2081+
oauth_binding_remove_failed: Failed to remove OAuth binding
2082+
oauth_binding_removed: OAuth binding removed
2083+
platforms:
2084+
google_oauth2: Google
2085+
provider: Provider
2086+
title: OAuth Binding Management
2087+
uid: UID
2088+
unlink: Unlink
20742089
import: Import
20752090
import_and_export: Import and export
20762091
migrate: Account migration

0 commit comments

Comments
 (0)