main
1# frozen_string_literal: true
2
3require 'rails_helper'
4
5RSpec.describe '/oauth/tokens' do
6 let(:client) { create(:client) }
7 let(:credentials) { ActionController::HttpAuthentication::Basic.encode_credentials(client.to_param, client.password) }
8 let(:headers) { { 'Authorization' => credentials } }
9
10 describe "POST /oauth/tokens" do
11 context "when using the authorization_code grant" do
12 context "when the code is still valid" do
13 let(:authorization) { create(:authorization, client: client) }
14 let(:json) { JSON.parse(response.body, symbolize_names: true) }
15
16 before { post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
17
18 specify { expect(response).to have_http_status(:ok) }
19 specify { expect(response.headers['Content-Type']).to include('application/json') }
20 specify { expect(response.headers['Cache-Control']).to include('no-store') }
21 specify { expect(response.headers['Pragma']).to eql('no-cache') }
22 specify { expect(json[:access_token]).to be_present }
23 specify { expect(json[:token_type]).to eql('Bearer') }
24 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
25 specify { expect(json[:refresh_token]).to be_present }
26 specify { expect(authorization.reload).to be_revoked }
27 end
28
29 context "when the code is expired" do
30 let(:authorization) { create(:authorization, client: client, expired_at: 1.second.ago) }
31 let(:json) { JSON.parse(response.body, symbolize_names: true) }
32
33 before { post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
34
35 specify { expect(response).to have_http_status(:bad_request) }
36 specify { expect(response.headers['Content-Type']).to include('application/json') }
37 specify { expect(response.headers['Cache-Control']).to include('no-store') }
38 specify { expect(response.headers['Pragma']).to eql('no-cache') }
39 specify { expect(json[:error]).to eql('invalid_request') }
40 end
41
42 context "when the code is not known" do
43 before { post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
44
45 let(:json) { JSON.parse(response.body, symbolize_names: true) }
46
47 specify { expect(response).to have_http_status(:bad_request) }
48 specify { expect(response.headers['Content-Type']).to include('application/json') }
49 specify { expect(response.headers['Cache-Control']).to include('no-store') }
50 specify { expect(response.headers['Pragma']).to eql('no-cache') }
51
52 specify { expect(json[:error]).to eql('invalid_request') }
53 end
54
55 context "when the authorization was created with the code_challenge_method of SHA256" do
56 let(:code_verifier) { SecureRandom.hex(128) }
57 let(:authorization) { create(:authorization, client: client, challenge: Base64.urlsafe_encode64(Digest::SHA256.hexdigest(code_verifier)), challenge_method: :sha256) }
58 let(:json) { JSON.parse(response.body, symbolize_names: true) }
59
60 before do
61 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, headers: headers
62 end
63
64 specify { expect(response).to have_http_status(:ok) }
65 specify { expect(response.headers['Content-Type']).to include('application/json') }
66 specify { expect(response.headers['Cache-Control']).to include('no-store') }
67 specify { expect(response.headers['Pragma']).to eql('no-cache') }
68 specify { expect(json[:access_token]).to be_present }
69 specify { expect(json[:token_type]).to eql('Bearer') }
70 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
71 specify { expect(json[:refresh_token]).to be_present }
72 specify { expect(authorization.reload).to be_revoked }
73 end
74
75 context "when the authorization was created with the code_challenge_method of plain" do
76 let(:code_verifier) { SecureRandom.hex(128) }
77 let(:authorization) { create(:authorization, client: client, challenge: code_verifier, challenge_method: :plain) }
78 let(:json) { JSON.parse(response.body, symbolize_names: true) }
79
80 before do
81 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
82 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: code_verifier }, headers: headers
83 end
84
85 specify { expect(response).to have_http_status(:ok) }
86 specify { expect(response.headers['Content-Type']).to include('application/json') }
87 specify { expect(response.headers['Cache-Control']).to include('no-store') }
88 specify { expect(response.headers['Pragma']).to eql('no-cache') }
89 specify { expect(json[:access_token]).to be_present }
90 specify { expect(json[:token_type]).to eql('Bearer') }
91 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
92 specify { expect(json[:refresh_token]).to be_present }
93 specify { expect(authorization.reload).to be_revoked }
94 end
95
96 context "when the SHA256 challenge is invalid" do
97 let(:code_verifier) { SecureRandom.hex(128) }
98 let(:authorization) { create(:authorization, client: client, challenge: Base64.urlsafe_encode64(Digest::SHA256.hexdigest(code_verifier)), challenge_method: :sha256) }
99 let(:json) { JSON.parse(response.body, symbolize_names: true) }
100
101 before do
102 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
103 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers
104 end
105
106 specify { expect(response).to have_http_status(:bad_request) }
107 specify { expect(response.headers['Content-Type']).to include('application/json') }
108 specify { expect(response.headers['Cache-Control']).to include('no-store') }
109 specify { expect(response.headers['Pragma']).to eql('no-cache') }
110
111 specify { expect(json[:error]).to eql('invalid_request') }
112 end
113
114 context "when the plain challenge is invalid" do
115 let(:code_verifier) { SecureRandom.hex(128) }
116 let(:authorization) { create(:authorization, client: client, challenge: code_verifier, challenge_method: :plain) }
117 let(:json) { JSON.parse(response.body, symbolize_names: true) }
118
119 before do
120 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers
121 post '/oauth/tokens', params: { grant_type: 'authorization_code', code: authorization.code, code_verifier: 'invalid' }, headers: headers
122 end
123
124 specify { expect(response).to have_http_status(:bad_request) }
125 specify { expect(response.headers['Content-Type']).to include('application/json') }
126 specify { expect(response.headers['Cache-Control']).to include('no-store') }
127 specify { expect(response.headers['Pragma']).to eql('no-cache') }
128
129 specify { expect(json[:error]).to eql('invalid_request') }
130 end
131 end
132
133 context "when requesting a token using the client_credentials grant" do
134 context "when the client credentials are valid" do
135 let(:json) { JSON.parse(response.body, symbolize_names: true) }
136
137 before { post '/oauth/tokens', params: { grant_type: 'client_credentials' }, headers: headers }
138
139 specify { expect(response).to have_http_status(:ok) }
140 specify { expect(response.headers['Content-Type']).to include('application/json') }
141 specify { expect(response.headers['Cache-Control']).to include('no-store') }
142 specify { expect(response.headers['Pragma']).to eql('no-cache') }
143 specify { expect(json[:access_token]).to be_present }
144 specify { expect(json[:token_type]).to eql('Bearer') }
145 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
146 specify { expect(json[:refresh_token]).to be_nil }
147 end
148
149 context "when the credentials are unknown" do
150 let(:headers) { { 'Authorization' => 'invalid' } }
151 let(:json) { JSON.parse(response.body, symbolize_names: true) }
152
153 before { post '/oauth/tokens', params: { grant_type: 'client_credentials' }, headers: headers }
154
155 specify { expect(response).to have_http_status(:unauthorized) }
156 specify { expect(json[:error]).to eql('invalid_client') }
157 end
158 end
159
160 context "when requesting tokens using the resource owner password credentials grant" do
161 context "when the credentials are valid" do
162 let(:user) { create(:user) }
163 let(:json) { JSON.parse(response.body, symbolize_names: true) }
164
165 before { post '/oauth/tokens', params: { grant_type: 'password', username: user.email, password: user.password }, headers: headers }
166
167 specify { expect(response).to have_http_status(:ok) }
168 specify { expect(response.headers['Content-Type']).to include('application/json') }
169 specify { expect(response.headers['Cache-Control']).to include('no-store') }
170 specify { expect(response.headers['Pragma']).to eql('no-cache') }
171 specify { expect(json[:access_token]).to be_present }
172 specify { expect(json[:token_type]).to eql('Bearer') }
173 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
174 specify { expect(json[:refresh_token]).to be_present }
175 end
176
177 context "when the credentials are invalid" do
178 let(:json) { JSON.parse(response.body, symbolize_names: true) }
179
180 before { post '/oauth/tokens', params: { grant_type: 'password', username: generate(:email), password: generate(:password) }, headers: headers }
181
182 specify { expect(response).to have_http_status(:bad_request) }
183 specify { expect(json[:error]).to eql('invalid_request') }
184 end
185 end
186
187 context "when exchanging a refresh token for a new access token" do
188 context "when the refresh token is still active" do
189 let(:refresh_token) { create(:refresh_token) }
190 let(:json) { JSON.parse(response.body, symbolize_names: true) }
191
192 before { post '/oauth/tokens', params: { grant_type: 'refresh_token', refresh_token: refresh_token.to_jwt }, headers: headers }
193
194 specify { expect(response).to have_http_status(:ok) }
195 specify { expect(response.headers['Content-Type']).to include('application/json') }
196 specify { expect(response.headers['Cache-Control']).to include('no-store') }
197 specify { expect(response.headers['Pragma']).to eql('no-cache') }
198 specify { expect(json[:access_token]).to be_present }
199 specify { expect(json[:token_type]).to eql('Bearer') }
200 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
201 specify { expect(json[:refresh_token]).to be_present }
202 specify { expect(refresh_token.reload).to be_revoked }
203 end
204 end
205
206 context "when exchanging a SAML 2.0 assertion grant for tokens" do
207 context "when the assertion contains a valid email address" do
208 let(:user) { create(:user) }
209 let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: true) }
210 let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) }
211 let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
212 let(:json) { JSON.parse(response.body, symbolize_names: true) }
213
214 before do
215 allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
216 post '/oauth/tokens', params: {
217 grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
218 assertion: Base64.urlsafe_encode64(saml),
219 }, headers: headers
220 end
221
222 specify { expect(response).to have_http_status(:ok) }
223 specify { expect(response.headers['Content-Type']).to include('application/json') }
224 specify { expect(response.headers['Cache-Control']).to include('no-store') }
225 specify { expect(response.headers['Pragma']).to eql('no-cache') }
226 specify { expect(json[:access_token]).to be_present }
227 specify { expect(json[:token_type]).to eql('Bearer') }
228 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
229 specify { expect(json[:refresh_token]).to be_present }
230 end
231
232 context "when the assertion contains a valid uuid" do
233 let(:user) { create(:user) }
234 let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: true, name_id_format: Saml::Kit::Namespaces::PERSISTENT) }
235 let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) }
236 let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
237 let(:json) { JSON.parse(response.body, symbolize_names: true) }
238
239 before do
240 allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
241 post '/oauth/tokens', params: {
242 grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
243 assertion: Base64.urlsafe_encode64(saml),
244 }, headers: headers
245 end
246
247 specify { expect(response).to have_http_status(:ok) }
248 specify { expect(response.headers['Content-Type']).to include('application/json') }
249 specify { expect(response.headers['Cache-Control']).to include('no-store') }
250 specify { expect(response.headers['Pragma']).to eql('no-cache') }
251 specify { expect(json[:access_token]).to be_present }
252 specify { expect(json[:token_type]).to eql('Bearer') }
253 specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
254 specify { expect(json[:refresh_token]).to be_present }
255 end
256 end
257
258 context "when the assertion is not a valid document" do
259 let(:user) { create(:user) }
260 let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id) }
261 let(:saml) { 'invalid' }
262 let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
263 let(:json) { JSON.parse(response.body, symbolize_names: true) }
264
265 before do
266 allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
267 post '/oauth/tokens', params: {
268 grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
269 assertion: Base64.urlsafe_encode64(saml),
270 }, headers: headers
271 end
272
273 specify { expect(response).to have_http_status(:bad_request) }
274 specify { expect(response.headers['Content-Type']).to include('application/json') }
275 specify { expect(response.headers['Cache-Control']).to include('no-store') }
276 specify { expect(response.headers['Pragma']).to eql('no-cache') }
277 specify { expect(json[:error]).to eql('invalid_request') }
278 end
279
280 context "when the assertion has an invalid signature" do
281 let(:user) { create(:user) }
282 let(:saml_request) { instance_double(Saml::Kit::AuthenticationRequest, id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: false) }
283 let(:key_pair) { Xml::Kit::KeyPair.generate(use: :signing) }
284 let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) { |x| x.sign_with(key_pair) } }
285 let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
286 let(:json) { JSON.parse(response.body, symbolize_names: true) }
287
288 before do
289 allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
290 post '/oauth/tokens', params: {
291 grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
292 assertion: Base64.urlsafe_encode64(saml),
293 }, headers: headers
294 end
295
296 specify { expect(response).to have_http_status(:bad_request) }
297 specify { expect(response.headers['Content-Type']).to include('application/json') }
298 specify { expect(response.headers['Cache-Control']).to include('no-store') }
299 specify { expect(response.headers['Pragma']).to eql('no-cache') }
300
301 specify { expect(json[:error]).to eql('invalid_request') }
302 end
303 end
304
305 describe "POST /oauth/tokens/introspect" do
306 context "when the access_token is valid" do
307 let(:token) { create(:access_token) }
308 let(:json) { JSON.parse(response.body, symbolize_names: true) }
309
310 before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
311
312 specify { expect(response).to have_http_status(:ok) }
313 specify { expect(response['Content-Type']).to include('application/json') }
314 specify { expect(response.headers['Set-Cookie']).to be_nil }
315 specify { expect(json[:active]).to be(true) }
316 specify { expect(json[:sub]).to eql(token.claims[:sub]) }
317 specify { expect(json[:aud]).to eql(token.claims[:aud]) }
318 specify { expect(json[:iss]).to eql(token.claims[:iss]) }
319 specify { expect(json[:exp]).to eql(token.claims[:exp]) }
320 specify { expect(json[:iat]).to eql(token.claims[:iat]) }
321 end
322
323 context "when the refresh_token is valid" do
324 let(:token) { create(:refresh_token) }
325 let(:json) { JSON.parse(response.body, symbolize_names: true) }
326
327 before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
328
329 specify { expect(response).to have_http_status(:ok) }
330 specify { expect(response['Content-Type']).to include('application/json') }
331 specify { expect(json[:active]).to be(true) }
332 specify { expect(json[:sub]).to eql(token.claims[:sub]) }
333 specify { expect(json[:aud]).to eql(token.claims[:aud]) }
334 specify { expect(json[:iss]).to eql(token.claims[:iss]) }
335 specify { expect(json[:exp]).to eql(token.claims[:exp]) }
336 specify { expect(json[:iat]).to eql(token.claims[:iat]) }
337 end
338
339 context "when the token is revoked" do
340 let(:token) { create(:access_token, :revoked) }
341 let(:json) { JSON.parse(response.body, symbolize_names: true) }
342
343 before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
344
345 specify { expect(response).to have_http_status(:ok) }
346 specify { expect(response['Content-Type']).to include('application/json') }
347 specify { expect(json[:active]).to be(false) }
348 end
349
350 context "when the token is expired" do
351 let(:token) { create(:access_token, :expired) }
352 let(:json) { JSON.parse(response.body, symbolize_names: true) }
353
354 before { post '/oauth/tokens/introspect', params: { token: token.to_jwt }, headers: headers }
355
356 specify { expect(response).to have_http_status(:ok) }
357 specify { expect(response['Content-Type']).to include('application/json') }
358 specify { expect(json[:active]).to be(false) }
359 end
360 end
361
362 describe "POST /oauth/tokens/revoke" do
363 context "when the client credentials are valid" do
364 context "when the access token is active and known" do
365 let(:token) { create(:access_token, audience: client) }
366
367 before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :access_token }, headers: headers }
368
369 specify { expect(response).to have_http_status(:ok) }
370 specify { expect(response.body).to be_empty }
371 specify { expect(token.reload).to be_revoked }
372 end
373
374 context "when the token was not issued to this client" do
375 let(:token) { create(:access_token, audience: other_client) }
376 let(:other_client) { create(:client) }
377
378 before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :access_token }, headers: headers }
379
380 specify { expect(response).to have_http_status(:ok) }
381 specify { expect(token.reload).not_to be_revoked }
382 end
383
384 context "when the refresh token is active and known" do
385 let(:token) { create(:refresh_token, audience: client) }
386
387 before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :refresh_token }, headers: headers }
388
389 specify { expect(response).to have_http_status(:ok) }
390 specify { expect(response.body).to be_empty }
391 specify { expect(token.reload).to be_revoked }
392 end
393
394 context "when the access token is expired" do
395 let(:token) { create(:access_token, :expired, audience: client) }
396
397 before { post '/oauth/tokens/revoke', params: { token: token.to_jwt, token_type_hint: :refresh_token }, headers: headers }
398
399 specify { expect(response).to have_http_status(:ok) }
400 end
401 end
402 end
403end