Use token callback user details (#18)
* Add way to use user details returned in token response * Add spec * Apply suggestions from code review Co-Authored-By: Robin Ward <robin.ward@gmail.com>
This commit is contained in:
parent
ef5b3ee1ff
commit
a634ff896d
|
@ -6,6 +6,9 @@ en:
|
||||||
oauth2_authorize_url: 'Authorization URL for OAuth2'
|
oauth2_authorize_url: 'Authorization URL for OAuth2'
|
||||||
oauth2_token_url: 'Token URL for OAuth2'
|
oauth2_token_url: 'Token URL for OAuth2'
|
||||||
oauth2_token_url_method: 'Method used to fetch the Token URL'
|
oauth2_token_url_method: 'Method used to fetch the Token URL'
|
||||||
|
oauth2_callback_user_id_path: 'Path in the token response to the user id. eg: params.info.uuid'
|
||||||
|
oauth2_callback_user_info_paths: 'Paths in the token response to other user properties. Supported properties are name, username, email, email_verfied and avatar. Format is property:path, eg: name:params.info.name'
|
||||||
|
oauth2_fetch_user_details: "Fetch user JSON for OAuth2"
|
||||||
oauth2_user_json_url: 'URL to fetch user JSON for OAuth2 (note we replace :id with the id returned by OAuth call and :token with the token id)'
|
oauth2_user_json_url: 'URL to fetch user JSON for OAuth2 (note we replace :id with the id returned by OAuth call and :token with the token id)'
|
||||||
oauth2_user_json_url_method: 'Method used to fetch the user JSON URL'
|
oauth2_user_json_url_method: 'Method used to fetch the user JSON URL'
|
||||||
oauth2_json_user_id_path: 'Path in the OAuth2 User JSON to the user id. eg: user.id'
|
oauth2_json_user_id_path: 'Path in the OAuth2 User JSON to the user id. eg: user.id'
|
||||||
|
@ -22,3 +25,6 @@ en:
|
||||||
oauth2_scope: "When authorizing request this scope"
|
oauth2_scope: "When authorizing request this scope"
|
||||||
oauth2_button_title: "The text for the OAuth2 button"
|
oauth2_button_title: "The text for the OAuth2 button"
|
||||||
oauth2_full_screen_login: "Use main browser window instead of popup for login"
|
oauth2_full_screen_login: "Use main browser window instead of popup for login"
|
||||||
|
|
||||||
|
errors:
|
||||||
|
oauth2_fetch_user_details: "oauth2_callback_user_id_path must be present to disable oauth2_fetch_user_details"
|
||||||
|
|
|
@ -6,13 +6,20 @@ login:
|
||||||
oauth2_client_secret: ''
|
oauth2_client_secret: ''
|
||||||
oauth2_authorize_url: ''
|
oauth2_authorize_url: ''
|
||||||
oauth2_token_url: ''
|
oauth2_token_url: ''
|
||||||
oauth2_user_json_url: ''
|
|
||||||
oauth2_token_url_method:
|
oauth2_token_url_method:
|
||||||
default: 'POST'
|
default: 'POST'
|
||||||
type: enum
|
type: enum
|
||||||
choices:
|
choices:
|
||||||
- GET
|
- GET
|
||||||
- POST
|
- POST
|
||||||
|
oauth2_callback_user_id_path: ''
|
||||||
|
oauth2_callback_user_info_paths:
|
||||||
|
type: list
|
||||||
|
default: 'id'
|
||||||
|
oauth2_fetch_user_details:
|
||||||
|
default: true
|
||||||
|
validator: "Oauth2FetchUserDetailsValidator"
|
||||||
|
oauth2_user_json_url: ''
|
||||||
oauth2_user_json_url_method:
|
oauth2_user_json_url_method:
|
||||||
default: 'GET'
|
default: 'GET'
|
||||||
type: enum
|
type: enum
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Oauth2FetchUserDetailsValidator
|
||||||
|
def initialize(opts = {})
|
||||||
|
@opts = opts
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_value?(val)
|
||||||
|
return true if val == "t"
|
||||||
|
SiteSetting.oauth2_callback_user_id_path.length > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_message
|
||||||
|
I18n.t("site_settings.errors.oauth2_fetch_user_details")
|
||||||
|
end
|
||||||
|
end
|
45
plugin.rb
45
plugin.rb
|
@ -12,15 +12,38 @@ enabled_site_setting :oauth2_enabled
|
||||||
|
|
||||||
class ::OmniAuth::Strategies::Oauth2Basic < ::OmniAuth::Strategies::OAuth2
|
class ::OmniAuth::Strategies::Oauth2Basic < ::OmniAuth::Strategies::OAuth2
|
||||||
option :name, "oauth2_basic"
|
option :name, "oauth2_basic"
|
||||||
|
|
||||||
|
uid do
|
||||||
|
if path = SiteSetting.oauth2_callback_user_id_path.split('.')
|
||||||
|
recurse(access_token, [*path]) if path.present?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
info do
|
info do
|
||||||
{
|
if paths = SiteSetting.oauth2_callback_user_info_paths.split('|')
|
||||||
id: access_token['id']
|
result = Hash.new
|
||||||
}
|
paths.each do |p|
|
||||||
|
segments = p.split(':')
|
||||||
|
if segments.length == 2
|
||||||
|
key = segments.first
|
||||||
|
path = [*segments.last.split('.')]
|
||||||
|
result[key] = recurse(access_token, path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback_url
|
def callback_url
|
||||||
Discourse.base_url_no_prefix + script_name + callback_path
|
Discourse.base_url_no_prefix + script_name + callback_path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def recurse(obj, keys)
|
||||||
|
return nil if !obj
|
||||||
|
k = keys.shift
|
||||||
|
result = obj.respond_to?(k) ? obj.send(k) : obj[k]
|
||||||
|
keys.empty? ? result : recurse(result, keys)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class OAuth2BasicAuthenticator < ::Auth::OAuth2Authenticator
|
class OAuth2BasicAuthenticator < ::Auth::OAuth2Authenticator
|
||||||
|
@ -112,11 +135,21 @@ class OAuth2BasicAuthenticator < ::Auth::OAuth2Authenticator
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_authenticate(auth)
|
def after_authenticate(auth)
|
||||||
log("after_authenticate response: \n\ncreds: #{auth['credentials'].to_hash}\ninfo: #{auth['info'].to_hash}\nextra: #{auth['extra'].to_hash}")
|
log("after_authenticate response: \n\ncreds: #{auth['credentials'].to_hash}\nuid: #{auth['uid']}\ninfo: #{auth['info'].to_hash}\nextra: #{auth['extra'].to_hash}")
|
||||||
|
|
||||||
result = Auth::Result.new
|
result = Auth::Result.new
|
||||||
token = auth['credentials']['token']
|
token = auth['credentials']['token']
|
||||||
user_details = fetch_user_details(token, auth['info'][:id])
|
|
||||||
|
user_details = {}
|
||||||
|
user_details[:user_id] = auth['uid'] if auth['uid']
|
||||||
|
['name', 'username', 'email', 'email_verified', 'avatar'].each do |key|
|
||||||
|
user_details[key.to_sym] = auth['info'][key] if auth['info'][key]
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.oauth2_fetch_user_details?
|
||||||
|
fetched_user_details = fetch_user_details(token, auth['uid'])
|
||||||
|
user_details.merge!(fetched_user_details)
|
||||||
|
end
|
||||||
|
|
||||||
result.name = user_details[:name]
|
result.name = user_details[:name]
|
||||||
result.username = user_details[:username]
|
result.username = user_details[:username]
|
||||||
|
@ -171,3 +204,5 @@ register_css <<CSS
|
||||||
}
|
}
|
||||||
|
|
||||||
CSS
|
CSS
|
||||||
|
|
||||||
|
load File.expand_path("../lib/validators/oauth2_basic/oauth2_fetch_user_details_validator.rb", __FILE__)
|
||||||
|
|
|
@ -176,4 +176,71 @@ describe OAuth2BasicAuthenticator do
|
||||||
|
|
||||||
expect(result).to eq 'http://example.com/1.png'
|
expect(result).to eq 'http://example.com/1.png'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'token_callback' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:strategy) { OmniAuth::Strategies::Oauth2Basic.new({}) }
|
||||||
|
let(:authenticator) { OAuth2BasicAuthenticator.new('oauth2_basic') }
|
||||||
|
|
||||||
|
let(:auth) do
|
||||||
|
{
|
||||||
|
'credentials' => {
|
||||||
|
'token' => 'token'
|
||||||
|
},
|
||||||
|
'uid' => 'e028b1b918853eca7fba208a9d7e9d29a6e93c57',
|
||||||
|
'info' => {
|
||||||
|
"name" => 'Sammy the Shark',
|
||||||
|
"email" => 'sammy@digitalocean.com'
|
||||||
|
},
|
||||||
|
'extra' => {}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:access_token) do
|
||||||
|
{ "params" =>
|
||||||
|
{ "info" =>
|
||||||
|
{
|
||||||
|
"name" => "Sammy the Shark",
|
||||||
|
"email" => "sammy@digitalocean.com",
|
||||||
|
"uuid" => "e028b1b918853eca7fba208a9d7e9d29a6e93c57"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
SiteSetting.oauth2_callback_user_id_path = 'params.info.uuid'
|
||||||
|
SiteSetting.oauth2_callback_user_info_paths = 'name:params.info.name|email:params.info.email'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can retrieve user id from access token callback' do
|
||||||
|
strategy.stubs(:access_token).returns(access_token)
|
||||||
|
expect(strategy.uid).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can retrive user properties from access token callback' do
|
||||||
|
strategy.stubs(:access_token).returns(access_token)
|
||||||
|
expect(strategy.info['name']).to eq 'Sammy the Shark'
|
||||||
|
expect(strategy.info['email']).to eq 'sammy@digitalocean.com'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does apply user properties from access token callback in after_authenticate' do
|
||||||
|
SiteSetting.oauth2_fetch_user_details = true
|
||||||
|
authenticator.stubs(:fetch_user_details).returns(email: 'sammy@digitalocean.com')
|
||||||
|
result = authenticator.after_authenticate(auth)
|
||||||
|
|
||||||
|
expect(result.extra_data[:oauth2_basic_user_id]).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
|
||||||
|
expect(result.name).to eq 'Sammy the Shark'
|
||||||
|
expect(result.email).to eq 'sammy@digitalocean.com'
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does work if user details are not fetched' do
|
||||||
|
SiteSetting.oauth2_fetch_user_details = false
|
||||||
|
result = authenticator.after_authenticate(auth)
|
||||||
|
|
||||||
|
expect(result.extra_data[:oauth2_basic_user_id]).to eq 'e028b1b918853eca7fba208a9d7e9d29a6e93c57'
|
||||||
|
expect(result.name).to eq 'Sammy the Shark'
|
||||||
|
expect(result.email).to eq 'sammy@digitalocean.com'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue