Skip to content

Commit 0b714e0

Browse files
Copilotnbulaj
andauthored
Add nonce validation for implicit OIDC flow
Per OpenID Connect Core 1.0 Section 3.2.2.1, nonce is REQUIRED for implicit flow (id_token and id_token token response types). Add server- side validation to enforce this requirement. - Add validate_nonce method to PreAuthorization module - Register validation using Doorkeeper's validation DSL - Update existing tests to include nonce for implicit flow requests - Add new tests for nonce validation behavior Agent-Logs-Url: https://github.com/doorkeeper-gem/doorkeeper-openid_connect/sessions/7792b5af-9397-4ed2-9520-787df9698a07 Co-authored-by: nbulaj <1443426+nbulaj@users.noreply.github.com>
1 parent 1f68bf8 commit 0b714e0

5 files changed

Lines changed: 160 additions & 8 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/.bundle
2+
/gemfiles/.bundle
23
/vendor/bundle
34
/Gemfile.lock
45
/spec/dummy/db/*.sqlite3*

gemfiles/.bundle/config

Lines changed: 0 additions & 2 deletions
This file was deleted.

lib/doorkeeper/openid_connect/oauth/pre_authorization.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module Doorkeeper
44
module OpenidConnect
55
module OAuth
66
module PreAuthorization
7+
def self.prepended(base)
8+
base.validate :nonce, error: Doorkeeper::Errors::InvalidRequest
9+
end
10+
711
attr_reader :nonce
812

913
def initialize(server, attrs = {}, resource_owner = nil)
@@ -22,6 +26,26 @@ def response_on_fragment?
2226

2327
grant_flow&.default_response_mode == 'fragment'
2428
end
29+
30+
private
31+
32+
# Per OpenID Connect Core 1.0 Section 3.2.2.1, nonce is REQUIRED for the
33+
# implicit flow (id_token and id_token token response types).
34+
def validate_nonce
35+
return true unless openid_implicit_flow?
36+
37+
if nonce.blank?
38+
@missing_param = :nonce
39+
return false
40+
end
41+
42+
true
43+
end
44+
45+
def openid_implicit_flow?
46+
scopes.include?('openid') &&
47+
response_type.to_s.split(' ').include?('id_token')
48+
end
2549
end
2650
end
2751
end

spec/controllers/doorkeeper/authorizations_controller_spec.rb

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,15 @@ def expect_successful_callback!
139139
it 'redirect as the fragment style uri when response_type is implicit flow request' do
140140
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
141141

142-
authorize! request_param.merge(response_type: 'id_token token')
142+
authorize! request_param.merge(response_type: 'id_token token', nonce: 'abc123')
143143

144144
expect(response).to redirect_to build_redirect_uri(error_params, type: 'fragment')
145145
end
146146

147147
it 'set @authorize_response variable and render form_post template and when the form_post response_mode is specified' do
148148
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
149149

150-
authorize! request_param.merge(response_type: 'id_token token', response_mode: 'form_post')
150+
authorize! request_param.merge(response_type: 'id_token token', response_mode: 'form_post', nonce: 'abc123')
151151

152152
authorize_response = controller.instance_variable_get :@authorize_response
153153
expect(authorize_response.body.to_json).to eq(error_params.to_json)
@@ -174,15 +174,15 @@ def expect_successful_callback!
174174
it 'redirect as the fragment style uri when response_type is implicit flow request' do
175175
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
176176

177-
authorize! request_param.merge(response_type: 'id_token token', prompt: 'none', state: 'somestate')
177+
authorize! request_param.merge(response_type: 'id_token token', prompt: 'none', state: 'somestate', nonce: 'abc123')
178178

179179
expect(response).to redirect_to build_redirect_uri(error_params, type: 'fragment')
180180
end
181181

182182
it 'set @authorize_response variable and render form_post template and when the form_post response_mode is specified' do
183183
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
184184

185-
authorize! request_param.merge(response_type: 'id_token token', response_mode: 'form_post', prompt: 'none', state: 'somestate')
185+
authorize! request_param.merge(response_type: 'id_token token', response_mode: 'form_post', prompt: 'none', state: 'somestate', nonce: 'abc123')
186186

187187
authorize_response = controller.instance_variable_get :@authorize_response
188188
expect(authorize_response.body.to_json).to eq(error_params.to_json)
@@ -218,15 +218,15 @@ def expect_successful_callback!
218218
it 'uses the fragment style uris when redirecting an error for implicit flow request' do
219219
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
220220

221-
authorize! request_param.merge(response_type: 'id_token token', prompt: 'none', state: 'somestate')
221+
authorize! request_param.merge(response_type: 'id_token token', prompt: 'none', state: 'somestate', nonce: 'abc123')
222222

223223
expect(response).to redirect_to build_redirect_uri(error_params, type: 'fragment')
224224
end
225225

226226
it 'set @authorize_response variable and render form_post template and when the form_post response_mode is specified' do
227227
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
228228

229-
authorize! request_param.merge(response_type: 'id_token token', response_mode: 'form_post', prompt: 'none', state: 'somestate')
229+
authorize! request_param.merge(response_type: 'id_token token', response_mode: 'form_post', prompt: 'none', state: 'somestate', nonce: 'abc123')
230230

231231
authorize_response = controller.instance_variable_get :@authorize_response
232232
expect(authorize_response.body.to_json).to eq(error_params.to_json)
@@ -520,4 +520,38 @@ def select_account!
520520
expect(assigns(:pre_auth).nonce).to eq '123456'
521521
end
522522
end
523+
524+
describe 'nonce validation' do
525+
context 'with implicit flow (id_token response type)' do
526+
before do
527+
allow(Doorkeeper.configuration).to receive(:grant_flows).and_return(['implicit_oidc'])
528+
end
529+
530+
it 'returns an error when nonce is missing' do
531+
get :new, params: {
532+
response_type: 'id_token',
533+
current_user: user.id,
534+
client_id: application.uid,
535+
scope: 'openid',
536+
redirect_uri: application.redirect_uri,
537+
}
538+
539+
expect(response).to have_http_status(:bad_request)
540+
expect(response).to render_template('doorkeeper/authorizations/error')
541+
end
542+
543+
it 'succeeds when nonce is present' do
544+
get :new, params: {
545+
response_type: 'id_token',
546+
current_user: user.id,
547+
client_id: application.uid,
548+
scope: 'openid',
549+
redirect_uri: application.redirect_uri,
550+
nonce: 'abc123',
551+
}
552+
553+
expect(response).to render_template('doorkeeper/authorizations/new')
554+
end
555+
end
556+
end
523557
end

spec/lib/oauth/pre_authorization_spec.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
subject { Doorkeeper::OAuth::PreAuthorization.new server, attrs }
77

88
let(:server) { Doorkeeper.configuration }
9+
let(:application) { create :application, scopes: 'openid public' }
910
let(:attrs) {}
1011

1112
describe '#initialize' do
@@ -18,6 +19,100 @@
1819
end
1920
end
2021

22+
describe '#authorizable?' do
23+
let(:client) { Doorkeeper::OAuth::Client.new(application) }
24+
25+
context 'with response_type = id_token (implicit flow)' do
26+
let(:base_attrs) do
27+
{
28+
client_id: client.uid,
29+
response_type: 'id_token',
30+
redirect_uri: 'https://app.com/callback',
31+
scope: 'openid',
32+
}
33+
end
34+
35+
before do
36+
allow(server).to receive(:grant_flows).and_return(['implicit_oidc'])
37+
end
38+
39+
it 'is not authorizable without a nonce' do
40+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs)
41+
pre_auth.authorizable?
42+
43+
expect(pre_auth).not_to be_authorizable
44+
end
45+
46+
it 'is authorizable with a nonce' do
47+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs.merge(nonce: 'abc123'))
48+
pre_auth.authorizable?
49+
50+
expect(pre_auth).to be_authorizable
51+
end
52+
53+
it 'sets missing_param to :nonce when nonce is absent' do
54+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs)
55+
pre_auth.authorizable?
56+
57+
expect(pre_auth.missing_param).to eq :nonce
58+
end
59+
end
60+
61+
context 'with response_type = id_token token (implicit flow)' do
62+
let(:base_attrs) do
63+
{
64+
client_id: client.uid,
65+
response_type: 'id_token token',
66+
redirect_uri: 'https://app.com/callback',
67+
scope: 'openid',
68+
}
69+
end
70+
71+
before do
72+
allow(server).to receive(:grant_flows).and_return(['implicit_oidc'])
73+
end
74+
75+
it 'is not authorizable without a nonce' do
76+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs)
77+
pre_auth.authorizable?
78+
79+
expect(pre_auth).not_to be_authorizable
80+
end
81+
82+
it 'is authorizable with a nonce' do
83+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs.merge(nonce: 'abc123'))
84+
pre_auth.authorizable?
85+
86+
expect(pre_auth).to be_authorizable
87+
end
88+
end
89+
90+
context 'with response_type = code (authorization code flow)' do
91+
let(:base_attrs) do
92+
{
93+
client_id: client.uid,
94+
response_type: 'code',
95+
redirect_uri: 'https://app.com/callback',
96+
scope: 'openid',
97+
}
98+
end
99+
100+
it 'is authorizable without a nonce' do
101+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs)
102+
pre_auth.authorizable?
103+
104+
expect(pre_auth).to be_authorizable
105+
end
106+
107+
it 'is authorizable with a nonce' do
108+
pre_auth = Doorkeeper::OAuth::PreAuthorization.new(server, base_attrs.merge(nonce: 'abc123'))
109+
pre_auth.authorizable?
110+
111+
expect(pre_auth).to be_authorizable
112+
end
113+
end
114+
end
115+
21116
describe '#error_response' do
22117
context 'with response_type = code' do
23118
let(:attrs) { { response_type: 'code', redirect_uri: 'client.com/callback' } }

0 commit comments

Comments
 (0)