Skip to content
This repository was archived by the owner on Jul 14, 2025. It is now read-only.

Commit 5ae9f35

Browse files
angusmcleoddavidtaylorhq
authored andcommitted
FEATURE: Migrate to ManagedAuthenticator (#21)
This brings the plugin in-line with recent core improvements. Advantages include - Account-linking logic and storage is shared between all authentication providers - Optionally, users can be allowed to disconnect/reconnect their accounts - The 'last used' date of an association is recorded - Association metadata is recorded in the database for use in data explorer and other plugins Data migration will be performed automatically, and all existing functionality is maintained.
1 parent 4c833e8 commit 5ae9f35

File tree

5 files changed

+84
-67
lines changed

5 files changed

+84
-67
lines changed

config/locales/server.en.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ en:
2828
oauth2_scope: "When authorizing request this scope"
2929
oauth2_button_title: "The text for the OAuth2 button"
3030
oauth2_full_screen_login: "Use main browser window instead of popup for login"
31+
oauth2_allow_association_change: Allow users to disconnect and reconnect their Discourse accounts from the OAuth2 provider
3132

3233
errors:
3334
oauth2_fetch_user_details: "oauth2_callback_user_id_path must be present to disable oauth2_fetch_user_details"

config/settings.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ login:
4646
oauth2_full_screen_login:
4747
default: false
4848
client: true
49+
oauth2_allow_association_change:
50+
default: false
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
class MoveToManagedAuthenticator < ActiveRecord::Migration[5.2]
2+
def up
3+
execute <<~SQL
4+
INSERT INTO user_associated_accounts (
5+
provider_name,
6+
provider_uid,
7+
user_id,
8+
created_at,
9+
updated_at
10+
) SELECT
11+
'oauth2_basic',
12+
replace(key, 'oauth2_basic_user_', ''),
13+
(value::json->>'user_id')::integer,
14+
CURRENT_TIMESTAMP,
15+
CURRENT_TIMESTAMP
16+
FROM plugin_store_rows
17+
WHERE plugin_name = 'oauth2_basic'
18+
SQL
19+
end
20+
end

plugin.rb

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,22 @@ def recurse(obj, keys)
4646
end
4747
end
4848

49-
class OAuth2BasicAuthenticator < ::Auth::OAuth2Authenticator
49+
class OAuth2BasicAuthenticator < Auth::ManagedAuthenticator
50+
def name
51+
'oauth2_basic'
52+
end
53+
54+
def can_revoke?
55+
SiteSetting.oauth2_allow_association_change
56+
end
57+
58+
def can_connect_existing_user?
59+
SiteSetting.oauth2_allow_association_change
60+
end
61+
5062
def register_middleware(omniauth)
5163
omniauth.provider :oauth2_basic,
52-
name: 'oauth2_basic',
64+
name: name,
5365
setup: lambda { |env|
5466
opts = env['omniauth.strategy'].options
5567
opts[:client_id] = SiteSetting.oauth2_client_id
@@ -110,7 +122,7 @@ def fetch_user_details(token, id)
110122
bearer_token = "Bearer #{token}"
111123
connection = Excon.new(
112124
user_json_url,
113-
:headers => { 'Authorization' => bearer_token, 'Accept' => 'application/json' }
125+
headers: { 'Authorization' => bearer_token, 'Accept' => 'application/json' }
114126
)
115127
user_json_response = connection.request(method: user_json_method)
116128

@@ -136,62 +148,35 @@ def fetch_user_details(token, id)
136148
end
137149
end
138150

139-
def after_authenticate(auth)
140-
log("after_authenticate response: \n\ncreds: #{auth['credentials'].to_hash}\nuid: #{auth['uid']}\ninfo: #{auth['info'].to_hash}\nextra: #{auth['extra'].to_hash}")
151+
def primary_email_verified?(auth)
152+
auth['info']['email_verified'] ||
153+
SiteSetting.oauth2_email_verified
154+
end
141155

142-
result = Auth::Result.new
143-
token = auth['credentials']['token']
156+
def always_update_user_email?
157+
SiteSetting.oauth2_overrides_email
158+
end
144159

145-
user_details = {}
146-
user_details[:user_id] = auth['uid'] if auth['uid']
147-
['name', 'username', 'email', 'email_verified', 'avatar'].each do |key|
148-
user_details[key.to_sym] = auth['info'][key] if auth['info'][key]
149-
end
160+
def after_authenticate(auth, existing_account: nil)
161+
log("after_authenticate response: \n\ncreds: #{auth['credentials'].to_hash}\nuid: #{auth['uid']}\ninfo: #{auth['info'].to_hash}\nextra: #{auth['extra'].to_hash}")
150162

151163
if SiteSetting.oauth2_fetch_user_details?
152-
if fetched_user_details = fetch_user_details(token, auth['uid'])
153-
user_details.merge!(fetched_user_details)
164+
if fetched_user_details = fetch_user_details(auth['credentials']['token'], auth['uid'])
165+
auth['uid'] = fetched_user_details[:user_id] if fetched_user_details[:user_id]
166+
auth['info']['nickname'] = fetched_user_details[:username] if fetched_user_details[:username]
167+
auth['info']['image'] = fetched_user_details[:avatar] if fetched_user_details[:avatar]
168+
['name', 'email', 'email_verified'].each do |property|
169+
auth['info'][property] = fetched_user_details[property.to_sym] if fetched_user_details[property.to_sym]
170+
end
154171
else
172+
result = Auth::Result.new
155173
result.failed = true
156174
result.failed_reason = I18n.t("login.authenticator_error_fetch_user_details")
157175
return result
158176
end
159177
end
160178

161-
result.name = user_details[:name]
162-
result.username = user_details[:username]
163-
result.email = user_details[:email]
164-
result.email_valid = result.email.present? && (user_details[:email_verified] || SiteSetting.oauth2_email_verified?)
165-
avatar_url = user_details[:avatar]
166-
167-
current_info = ::PluginStore.get("oauth2_basic", "oauth2_basic_user_#{user_details[:user_id]}")
168-
if current_info
169-
result.user = User.where(id: current_info[:user_id]).first
170-
result.user&.update!(email: result.email) if SiteSetting.oauth2_overrides_email && result.email
171-
elsif result.email_valid
172-
result.user = User.find_by_email(result.email)
173-
if result.user && user_details[:user_id]
174-
::PluginStore.set("oauth2_basic", "oauth2_basic_user_#{user_details[:user_id]}", user_id: result.user.id)
175-
end
176-
end
177-
178-
download_avatar(result.user, avatar_url)
179-
180-
result.extra_data = { oauth2_basic_user_id: user_details[:user_id], avatar_url: avatar_url }
181-
result
182-
end
183-
184-
def after_create_account(user, auth)
185-
::PluginStore.set("oauth2_basic", "oauth2_basic_user_#{auth[:extra_data][:oauth2_basic_user_id]}", user_id: user.id)
186-
download_avatar(user, auth[:extra_data][:avatar_url])
187-
end
188-
189-
def download_avatar(user, avatar_url)
190-
Jobs.enqueue(:download_avatar_from_url,
191-
url: avatar_url,
192-
user_id: user.id,
193-
override_gravatar: SiteSetting.sso_overrides_avatar
194-
) if user && avatar_url.present?
179+
super(auth, existing_account: existing_account)
195180
end
196181

197182
def enabled?
@@ -200,7 +185,7 @@ def enabled?
200185
end
201186

202187
auth_provider title_setting: "oauth2_button_title",
203-
authenticator: OAuth2BasicAuthenticator.new('oauth2_basic'),
188+
authenticator: OAuth2BasicAuthenticator.new,
204189
message: "OAuth2",
205190
full_screen_login_setting: "oauth2_full_screen_login"
206191

spec/plugin_spec.rb

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ def register_css(arg)
3030
describe OAuth2BasicAuthenticator do
3131
context 'after_authenticate' do
3232
let(:user) { Fabricate(:user) }
33-
let(:authenticator) { OAuth2BasicAuthenticator.new('oauth2_basic') }
33+
let(:authenticator) { OAuth2BasicAuthenticator.new }
3434

3535
let(:auth) do
36-
{ 'credentials' => { 'token': 'token' },
36+
OmniAuth::AuthHash.new({ 'provider' => 'oauth2_basic',
37+
'credentials' => { 'token': 'token' },
38+
'uid' => '123456789',
3739
'info' => { id: 'id' },
38-
'extra' => {} }
40+
'extra' => {} })
3941
end
4042

4143
before(:each) do
@@ -73,17 +75,19 @@ def register_css(arg)
7375

7476
it 'validates user email if provider has verified' do
7577
SiteSetting.oauth2_email_verified = false
76-
77-
# Check it's working
7878
authenticator.stubs(:fetch_user_details).returns(email: user.email, email_verified: true)
7979
result = authenticator.after_authenticate(auth)
8080
expect(result.email_valid).to eq(true)
81-
81+
end
82+
83+
it 'doesnt validate user email if provider hasnt verified' do
84+
SiteSetting.oauth2_email_verified = false
8285
authenticator.stubs(:fetch_user_details).returns(email: user.email, email_verified: nil)
8386
result = authenticator.after_authenticate(auth)
8487
expect(result.email_valid).to eq(false)
85-
86-
# Check it doesn't interfere with the site setting
88+
end
89+
90+
it 'doesnt affect the site setting' do
8791
SiteSetting.oauth2_email_verified = true
8892
authenticator.stubs(:fetch_user_details).returns(email: user.email, email_verified: false)
8993
result = authenticator.after_authenticate(auth)
@@ -135,7 +139,11 @@ def register_css(arg)
135139
end
136140

137141
context 'avatar downloading' do
138-
before { SiteSetting.queue_jobs = true }
142+
before do
143+
SiteSetting.queue_jobs = true
144+
SiteSetting.oauth2_fetch_user_details = true
145+
SiteSetting.oauth2_email_verified = true
146+
end
139147

140148
let(:job_klass) { Jobs::DownloadAvatarFromUrl }
141149

@@ -186,7 +194,7 @@ def register_css(arg)
186194
end
187195

188196
it 'can walk json' do
189-
authenticator = OAuth2BasicAuthenticator.new('oauth2_basic')
197+
authenticator = OAuth2BasicAuthenticator.new
190198
json_string = '{"user":{"id":1234,"email":{"address":"[email protected]"}}}'
191199
SiteSetting.oauth2_json_email_path = 'user.email.address'
192200
result = authenticator.json_walk({}, JSON.parse(json_string), :email)
@@ -195,7 +203,7 @@ def register_css(arg)
195203
end
196204

197205
it 'can walk json that contains an array' do
198-
authenticator = OAuth2BasicAuthenticator.new('oauth2_basic')
206+
authenticator = OAuth2BasicAuthenticator.new
199207
json_string = '{"email":"[email protected]","identities":[{"user_id":"123456789","provider":"auth0","isSocial":false}]}'
200208
SiteSetting.oauth2_json_user_id_path = 'identities.[].user_id'
201209
result = authenticator.json_walk({}, JSON.parse(json_string), :user_id)
@@ -204,7 +212,7 @@ def register_css(arg)
204212
end
205213

206214
it 'can walk json and handle an empty array' do
207-
authenticator = OAuth2BasicAuthenticator.new('oauth2_basic')
215+
authenticator = OAuth2BasicAuthenticator.new
208216
json_string = '{"email":"[email protected]","identities":[]}'
209217
SiteSetting.oauth2_json_user_id_path = 'identities.[].user_id'
210218
result = authenticator.json_walk({}, JSON.parse(json_string), :user_id)
@@ -213,7 +221,7 @@ def register_css(arg)
213221
end
214222

215223
it 'can walk json and download avatar' do
216-
authenticator = OAuth2BasicAuthenticator.new('oauth2_basic')
224+
authenticator = OAuth2BasicAuthenticator.new
217225
json_string = '{"user":{"avatar":"http://example.com/1.png"}}'
218226
SiteSetting.oauth2_json_avatar_path = 'user.avatar'
219227
result = authenticator.json_walk({}, JSON.parse(json_string), :avatar)
@@ -224,10 +232,11 @@ def register_css(arg)
224232
context 'token_callback' do
225233
let(:user) { Fabricate(:user) }
226234
let(:strategy) { OmniAuth::Strategies::Oauth2Basic.new({}) }
227-
let(:authenticator) { OAuth2BasicAuthenticator.new('oauth2_basic') }
235+
let(:authenticator) { OAuth2BasicAuthenticator.new }
228236

229237
let(:auth) do
230-
{
238+
OmniAuth::AuthHash.new({
239+
'provider' => 'oauth2_basic',
231240
'credentials' => {
232241
'token' => 'token'
233242
},
@@ -237,7 +246,7 @@ def register_css(arg)
237246
"email" => '[email protected]'
238247
},
239248
'extra' => {}
240-
}
249+
})
241250
end
242251

243252
let(:access_token) do
@@ -273,7 +282,7 @@ def register_css(arg)
273282
authenticator.stubs(:fetch_user_details).returns(email: '[email protected]')
274283
result = authenticator.after_authenticate(auth)
275284

276-
expect(result.extra_data[:oauth2_basic_user_id]).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
285+
expect(result.extra_data[:uid]).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
277286
expect(result.name).to eq 'Sammy the Shark'
278287
expect(result.email).to eq '[email protected]'
279288
end
@@ -282,7 +291,7 @@ def register_css(arg)
282291
SiteSetting.oauth2_fetch_user_details = false
283292
result = authenticator.after_authenticate(auth)
284293

285-
expect(result.extra_data[:oauth2_basic_user_id]).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
294+
expect(result.extra_data[:uid]).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
286295
expect(result.name).to eq 'Sammy the Shark'
287296
expect(result.email).to eq '[email protected]'
288297
end

0 commit comments

Comments
 (0)