Skip to content

Commit c000494

Browse files
committed
(feat) OAuth: add third-party OAuth2 login procedure
(feat) OAuth: disable login without binding Add Twitter and Line OAuth login (fix) wrong layout in OAuth Login management page (fix) oauth: shrink scope requested in Google
1 parent ff7266c commit c000494

17 files changed

Lines changed: 278 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', '~> 1.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: 48 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: e898df6857bd2e31b7bd1efe0e7a09c97b0041f0
4+
specs:
5+
omniauth-twitter (1.4.0)
6+
cgi
7+
omniauth-oauth (~> 1.1)
8+
rack
9+
110
GIT
211
remote: https://github.com/mastodon/webpush.git
312
revision: 9631ac63045cfabddacc69fc06e919b4c13eb913
@@ -449,6 +458,8 @@ GEM
449458
minitest (5.26.0)
450459
msgpack (1.8.0)
451460
multi_json (1.17.0)
461+
multi_xml (0.9.1)
462+
bigdecimal (>= 3.1, < 5)
452463
mutex_m (0.3.0)
453464
net-http (0.6.0)
454465
uri
@@ -468,6 +479,21 @@ GEM
468479
nokogiri (1.19.2)
469480
mini_portile2 (~> 2.8.2)
470481
racc (~> 1.4)
482+
oauth (1.1.3)
483+
base64 (~> 0.1)
484+
oauth-tty (~> 1.0, >= 1.0.6)
485+
snaky_hash (~> 2.0)
486+
version_gem (~> 1.1, >= 1.1.9)
487+
oauth-tty (1.0.6)
488+
version_gem (~> 1.1, >= 1.1.9)
489+
oauth2 (2.0.18)
490+
faraday (>= 0.17.3, < 4.0)
491+
jwt (>= 1.0, < 4.0)
492+
logger (~> 1.2)
493+
multi_xml (~> 0.5)
494+
rack (>= 1.2, < 4)
495+
snaky_hash (~> 2.0, >= 2.0.3)
496+
version_gem (~> 1.1, >= 1.1.9)
471497
oj (3.16.11)
472498
bigdecimal (>= 3.0)
473499
ostruct (>= 0.2)
@@ -480,6 +506,21 @@ GEM
480506
addressable (~> 2.8)
481507
nokogiri (~> 1.12)
482508
omniauth (~> 2.1)
509+
omniauth-google-oauth2 (1.2.2)
510+
jwt (>= 2.9.2)
511+
oauth2 (~> 2.0)
512+
omniauth (~> 2.0)
513+
omniauth-oauth2 (~> 1.8)
514+
omniauth-line (0.1.0)
515+
json (>= 2.3.0)
516+
omniauth-oauth2 (~> 1.3)
517+
omniauth-oauth (1.2.1)
518+
oauth
519+
omniauth (>= 1.0, < 3)
520+
rack (>= 1.6.2, < 4)
521+
omniauth-oauth2 (1.9.0)
522+
oauth2 (>= 2.0.2, < 3)
523+
omniauth (~> 2.0)
483524
omniauth-rails_csrf_protection (1.0.2)
484525
actionpack (>= 4.2)
485526
omniauth (~> 2.0)
@@ -835,6 +876,9 @@ GEM
835876
simplecov-html (0.13.2)
836877
simplecov-lcov (0.9.0)
837878
simplecov_json_formatter (0.1.4)
879+
snaky_hash (2.0.3)
880+
hashie (>= 0.1.0, < 6)
881+
version_gem (>= 1.1.8, < 3)
838882
stackprof (0.2.27)
839883
starry (0.2.0)
840884
base64
@@ -891,6 +935,7 @@ GEM
891935
validate_url (1.0.15)
892936
activemodel (>= 3.0.0)
893937
public_suffix
938+
version_gem (1.1.9)
894939
vite_rails (3.0.19)
895940
railties (>= 5.1, < 9)
896941
vite_ruby (~> 3.0, >= 3.2.2)
@@ -1008,8 +1053,11 @@ DEPENDENCIES
10081053
oj (~> 3.14)
10091054
omniauth (~> 2.0)
10101055
omniauth-cas (~> 3.0.0.beta.1)
1056+
omniauth-google-oauth2
1057+
omniauth-line (~> 0.1.0)
10111058
omniauth-rails_csrf_protection (~> 1.0)
10121059
omniauth-saml (~> 2.0)
1060+
omniauth-twitter!
10131061
omniauth_openid_connect (~> 0.8.0)
10141062
opentelemetry-api (~> 1.7.0)
10151063
opentelemetry-exporter-otlp (~> 0.31.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
@@ -87,6 +87,7 @@ class User < ApplicationRecord
8787
has_many :markers, inverse_of: :user, dependent: :destroy
8888
has_many :webauthn_credentials, dependent: :destroy
8989
has_many :ips, class_name: 'UserIp', inverse_of: :user, dependent: nil
90+
has_many :identities, dependent: :destroy
9091

9192
has_one :invite_request, class_name: 'UserInviteRequest', inverse_of: :user, dependent: :destroy
9293
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
@@ -1882,6 +1882,21 @@ en:
18821882
edit_profile: Edit profile
18831883
export: Export
18841884
featured_tags: Featured hashtags
1885+
identities:
1886+
action: Action
1887+
cannot_login_without_register: You cannot login with this account because you have not registered yet.
1888+
confirm_unlink: Are you sure you want to unlink this OAuth binding?
1889+
link: Link
1890+
menu_title: OAuth Login
1891+
not_binded: "(Not binded)"
1892+
oauth_binding_remove_failed: Failed to remove OAuth binding
1893+
oauth_binding_removed: OAuth binding removed
1894+
platforms:
1895+
google_oauth2: Google
1896+
provider: Provider
1897+
title: OAuth Binding Management
1898+
uid: UID
1899+
unlink: Unlink
18851900
import: Import
18861901
import_and_export: Import and export
18871902
migrate: Account migration

0 commit comments

Comments
 (0)