Skip to content

Commit 78a792b

Browse files
committed
FIX: Improved 'discovery' error handling, with tests
1 parent 0008d9b commit 78a792b

File tree

6 files changed

+136
-18
lines changed

6 files changed

+136
-18
lines changed

.travis.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# We want to use the KVM-based system, so require sudo
2+
sudo: required
3+
services:
4+
- docker
5+
6+
before_install:
7+
- git clone https://github.com/discourse/discourse-plugin-ci
8+
9+
install: true # Prevent travis doing bundle install
10+
11+
script:
12+
- discourse-plugin-ci/script.sh

config/locales/server.en.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,4 @@ en:
55
openid_connect_client_secret: "OpenID Connect client secret"
66
openid_connect_authorize_scope: "The scopes sent to the authorize endpoint. This must include 'openid'."
77
openid_connect_token_scope: "The scopes sent when requesting the token endpoint. The official specification does not require this."
8-
openid_connect_use_userinfo: "Contact the userinfo endpoint for user metadata. If left blank, the 'id_token' will be used instead."
98
openid_connect_error_redirects: "If the callback error_reason contains the first parameter, the user will be redirected to the URL in the second parameter"

config/settings.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ plugins:
99
default: "openid"
1010
openid_connect_token_scope:
1111
default: ""
12-
openid_connect_use_userinfo:
13-
default: true
1412
openid_connect_error_redirects:
1513
default: ''
1614
type: list

lib/omniauth_open_id_connect.rb

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,47 @@
11
require 'omniauth-oauth2'
22

33
module ::OmniAuth
4+
module OpenIDConnect
5+
class DiscoveryError < Error; end
6+
end
7+
48
module Strategies
59
class OpenIDConnect < OmniAuth::Strategies::OAuth2
610
option :scope, "openid"
711
option :discovery, true
12+
option :use_userinfo, true
813
option :cache, lambda { |key, &blk| blk.call } # Default no-op cache
914
option :error_handler, lambda { |error, message| nil } # Default no-op handler
10-
option :authorize_options, [:p]
11-
option :token_options, [:p]
15+
option :passthrough_authorize_options, [:p]
16+
option :passthrough_token_options, [:p]
1217

1318
option :client_options,
14-
site: 'https://op.com/',
15-
authorize_url: 'authorize',
16-
token_url: 'token',
17-
userinfo_endpoint: 'userinfo',
19+
discovery_document: nil,
20+
site: nil,
21+
authorize_url: nil,
22+
token_url: nil,
23+
userinfo_endpoint: nil,
1824
auth_scheme: :basic_auth
1925

2026
def discover!
2127
discovery_document = options.cache.call("openid_discovery_#{options[:client_options][:discovery_document]}") do
2228
client.request(:get, options[:client_options][:discovery_document], parse: :json).parsed
2329
end
24-
options[:client_options][:authorize_url] = discovery_document["authorization_endpoint"].to_s
25-
options[:client_options][:token_url] = discovery_document["token_endpoint"].to_s
26-
options[:client_options][:userinfo_endpoint] = discovery_document["userinfo_endpoint"].to_s
27-
options[:client_options][:site] = discovery_document["issuer"].to_s
30+
31+
{
32+
authorize_url: "authorization_endpoint",
33+
token_url: "token_endpoint",
34+
site: "issuer"
35+
}.each do |internal_key, external_key|
36+
val = discovery_document[external_key].to_s
37+
raise ::OmniAuth::OpenIDConnect::DiscoveryError.new("missing discovery parameter #{external_key}") if val.nil? || val.empty?
38+
options[:client_options][internal_key] = val
39+
end
40+
41+
userinfo_endpoint = options[:client_options][:userinfo_endpoint] = discovery_document["userinfo_endpoint"].to_s
42+
if userinfo_endpoint.nil? || userinfo_endpoint.empty?
43+
options.use_userinfo = false
44+
end
2845
end
2946

3047
def request_phase
@@ -34,14 +51,14 @@ def request_phase
3451

3552
def authorize_params
3653
super.tap do |params|
37-
options[:authorize_options].each do |k|
54+
options[:passthrough_authorize_options].each do |k|
3855
params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s])
3956
end
4057

4158
params[:scope] = options[:scope]
4259
session['omniauth.nonce'] = params[:nonce] = SecureRandom.hex(32)
4360

44-
options[:token_options].each do |k|
61+
options[:passthrough_token_options].each do |k|
4562
session["omniauth.param.#{k}"] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s])
4663
end
4764
end
@@ -95,8 +112,15 @@ def callback_phase
95112
if request.params["error"] && request.params["error_description"] && response = options.error_handler.call(request.params["error"], request.params["error_description"])
96113
return redirect(response)
97114
end
98-
discover! if options[:discovery]
115+
116+
begin
117+
discover! if options[:discovery]
118+
rescue ::OmniAuth::OpenIDConnect::DiscoveryError => e
119+
fail!(:openid_connect_discovery_error, e)
120+
end
121+
99122
oauth2_callback_phase = super
123+
100124
return oauth2_callback_phase if env['omniauth.error']
101125

102126
if id_token_info["nonce"].empty? || id_token_info["nonce"] != session.delete("omniauth.nonce")
@@ -113,7 +137,7 @@ def callback_url
113137

114138
def token_params
115139
params = {}
116-
options[:token_options].each do |k|
140+
options[:passthrough_token_options].each do |k|
117141
val = session.delete("omniauth.param.#{k}")
118142
params[k] = val unless [nil, ''].include?(val)
119143
end

plugin.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def register_middleware(omniauth)
8383
setup: lambda { |env|
8484
opts = env['omniauth.strategy'].options
8585
opts.deep_merge!(
86-
use_userinfo: SiteSetting.openid_connect_use_userinfo,
8786
client_id: SiteSetting.openid_connect_client_id,
8887
client_secret: SiteSetting.openid_connect_client_secret,
8988
client_options: {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../../lib/omniauth_open_id_connect'
4+
5+
require 'webmock/rspec'
6+
WebMock.disable_net_connect!
7+
8+
describe OmniAuth::Strategies::OpenIDConnect do
9+
# let(:request) { double('Request', params: {}, cookies: {}, env: {}) }
10+
let(:app) do
11+
lambda do
12+
[200, {}, ['Hello.']]
13+
end
14+
end
15+
16+
before do
17+
stub_request(:get, "https://id.example.com/.well-known/openid-configuration").
18+
to_return(status: 200, body: {
19+
"issuer": "https://id.example.com/",
20+
"authorization_endpoint": "https://id.example.com/authorize",
21+
"token_endpoint": "https://id.example.com/token",
22+
"userinfo_endpoint": "https://id.example.com/userinfo",
23+
}.to_json)
24+
end
25+
26+
subject do
27+
OmniAuth::Strategies::OpenIDConnect.new(app, 'appid', 'secret',
28+
client_options: {
29+
discovery_document: "https://id.example.com/.well-known/openid-configuration"
30+
}
31+
32+
).tap do |strategy|
33+
# allow(strategy).to receive(:request) do
34+
# request
35+
# end
36+
end
37+
end
38+
39+
before { OmniAuth.config.test_mode = true }
40+
41+
after { OmniAuth.config.test_mode = false }
42+
43+
it "throws error for on invalid discovery document" do
44+
stub_request(:get, "https://id.example.com/.well-known/openid-configuration").
45+
to_return(status: 200, body: {
46+
"issuer": "https://id.example.com/",
47+
"token_endpoint": "https://id.example.com/token",
48+
"userinfo_endpoint": "https://id.example.com/userinfo",
49+
}.to_json)
50+
51+
expect { subject.discover! }.to raise_error(::OmniAuth::OpenIDConnect::DiscoveryError)
52+
end
53+
54+
it "disables userinfo if not included in discovery document" do
55+
stub_request(:get, "https://id.example.com/.well-known/openid-configuration").
56+
to_return(status: 200, body: {
57+
"issuer": "https://id.example.com/",
58+
"authorization_endpoint": "https://id.example.com/authorize",
59+
"token_endpoint": "https://id.example.com/token",
60+
}.to_json)
61+
62+
subject.discover!
63+
expect(subject.options.use_userinfo).to eq(false)
64+
end
65+
66+
context 'with valid document' do
67+
before do
68+
stub_request(:get, "https://id.example.com/.well-known/openid-configuration").
69+
to_return(status: 200, body: {
70+
"issuer": "https://id.example.com/",
71+
"authorization_endpoint": "https://id.example.com/authorize",
72+
"token_endpoint": "https://id.example.com/token",
73+
"userinfo_endpoint": "https://id.example.com/userinfo",
74+
}.to_json)
75+
end
76+
77+
it "discovers correctly" do
78+
subject.discover!
79+
expect(subject.options.client_options.site).to eq("https://id.example.com/")
80+
expect(subject.options.client_options.authorize_url).to eq("https://id.example.com/authorize")
81+
expect(subject.options.client_options.token_url).to eq("https://id.example.com/token")
82+
expect(subject.options.client_options.userinfo_endpoint).to eq("https://id.example.com/userinfo")
83+
end
84+
end
85+
86+
end

0 commit comments

Comments
 (0)