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