Commit 69a6a3b

mo <mo.khan@gmail.com>
2018-09-18 20:22:33
extract tokens controller
1 parent 2c29a23
app/controllers/oauths_controller.rb
@@ -1,9 +1,6 @@
 # frozen_string_literal: true
 
 class OauthsController < ApplicationController
-  skip_before_action :authenticate!, only: [:token]
-  before_action :http_basic_authenticate!, only: [:token]
-
   def show
     return render_error(:not_found) unless params[:response_type] == 'code'
     @client = Client.find_by!(uuid: params[:client_id])
@@ -17,48 +14,4 @@ class OauthsController < ApplicationController
       state: params[:state]
     )
   end
-
-  def token
-    response.headers['Cache-Control'] = 'no-store'
-    response.headers['Pragma'] = 'no-cache'
-
-    if params[:grant_type] == 'authorization_code'
-      authorization = current_client.authorizations.active.find_by!(code: params[:code])
-      @access_token, @refresh_token = authorization.issue_tokens_to(current_client)
-    elsif params[:grant_type] == 'refresh_token'
-      refresh_token = params[:refresh_token]
-      jti = Token.claims_for(refresh_token, token_type: :refresh)[:jti]
-      @access_token, @refresh_token = Token.find_by!(uuid: jti).issue_tokens_to(current_client)
-    elsif params[:grant_type] == 'client_credentials'
-      @access_token = current_client.access_token
-    elsif params[:grant_type] == 'password'
-      user = User.login(params[:username], params[:password])
-      return render "bad_request", formats: :json, status: :bad_request unless user
-      @access_token, @refresh_token = user.issue_tokens_to(current_client)
-    elsif params[:grant_type] == 'urn:ietf:params:oauth:grant-type:saml2-bearer'
-      assertion = Saml::Kit::Assertion.new(Base64.urlsafe_decode64(params[:assertion]))
-      return render "bad_request", formats: :json, status: :bad_request if assertion.invalid?
-      user = assertion.name_id_format == Saml::Kit::Namespaces::PERSISTENT ?
-        User.find_by!(uuid: assertion.name_id) :
-        User.find_by!(email: assertion.name_id)
-      @access_token, @refresh_token = user.issue_tokens_to(current_client)
-    else
-      return render "bad_request", formats: :json, status: :bad_request
-    end
-    render formats: :json
-  rescue StandardError => error
-    Rails.logger.error(error)
-    render "bad_request", formats: :json, status: :bad_request
-  end
-
-  private
-
-  attr_reader :current_client
-
-  def http_basic_authenticate!
-    @current_client = authenticate_with_http_basic do |client_id, client_secret|
-      Client.find_by(uuid: client_id)&.authenticate(client_secret)
-    end
-    render "invalid_client", formats: :json, status: :unauthorized unless current_client
-  end
 end
app/controllers/tokens_controller.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+class TokensController < ApplicationController
+  def create
+    response.headers['Cache-Control'] = 'no-store'
+    response.headers['Pragma'] = 'no-cache'
+
+    if params[:grant_type] == 'authorization_code'
+      authorization = current_client.authorizations.active.find_by!(code: params[:code])
+      @access_token, @refresh_token = authorization.issue_tokens_to(current_client)
+    elsif params[:grant_type] == 'refresh_token'
+      refresh_token = params[:refresh_token]
+      jti = Token.claims_for(refresh_token, token_type: :refresh)[:jti]
+      @access_token, @refresh_token = Token.find_by!(uuid: jti).issue_tokens_to(current_client)
+    elsif params[:grant_type] == 'client_credentials'
+      @access_token = current_client.access_token
+    elsif params[:grant_type] == 'password'
+      user = User.login(params[:username], params[:password])
+      return render "bad_request", formats: :json, status: :bad_request unless user
+      @access_token, @refresh_token = user.issue_tokens_to(current_client)
+    elsif params[:grant_type] == 'urn:ietf:params:oauth:grant-type:saml2-bearer'
+      assertion = Saml::Kit::Assertion.new(Base64.urlsafe_decode64(params[:assertion]))
+      return render "bad_request", formats: :json, status: :bad_request if assertion.invalid?
+      user = assertion.name_id_format == Saml::Kit::Namespaces::PERSISTENT ?
+        User.find_by!(uuid: assertion.name_id) :
+        User.find_by!(email: assertion.name_id)
+      @access_token, @refresh_token = user.issue_tokens_to(current_client)
+    else
+      return render "bad_request", formats: :json, status: :bad_request
+    end
+    render formats: :json
+  rescue StandardError => error
+    Rails.logger.error(error)
+    render "bad_request", formats: :json, status: :bad_request
+  end
+
+  private
+
+  attr_reader :current_client
+
+  def authenticate!
+    @current_client = authenticate_with_http_basic do |client_id, client_secret|
+      Client.find_by(uuid: client_id)&.authenticate(client_secret)
+    end
+    render "invalid_client", formats: :json, status: :unauthorized unless current_client
+  end
+end
app/models/client.rb
@@ -10,7 +10,7 @@ class Client < ApplicationRecord
   end
 
   def authenticate(provided_secret)
-    return self if self.secret == provided_secret
+    return self if secret == provided_secret
   end
 
   def access_token
app/views/oauths/bad_request.json.jbuilder โ†’ app/views/tokens/bad_request.json.jbuilder
File renamed without changes
app/views/oauths/token.json.jbuilder โ†’ app/views/tokens/create.json.jbuilder
File renamed without changes
app/views/oauths/invalid_client.json.jbuilder โ†’ app/views/tokens/invalid_client.json.jbuilder
File renamed without changes
config/locales/en.yml
@@ -26,8 +26,6 @@ en:
         cancel: Cancel
         enable: Enable
   oauths:
-    bad_request:
-      invalid_request: invalid_request
     show:
       authorize: Authorize
       authorize_prompt_html: Do you authorize <strong>%{name}</strong> to access your data?
@@ -39,3 +37,6 @@ en:
   sessions:
     new:
       login: Login
+  tokens:
+    bad_request:
+      invalid_request: invalid_request
config/routes.rb
@@ -2,15 +2,15 @@ Rails.application.routes.draw do
   # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
   post "/session/logout" => "sessions#destroy", as: :logout
   post "/session/new" => "sessions#new"
+  post '/oauth/token', to: 'tokens#create'
   resource :metadata, only: [:show]
   resource :mfa, only: [:new, :create]
   resource :response, only: [:show]
   resource :session, only: [:new, :create, :destroy]
   resource :oauth, only: [:show, :create] do
     get :authorize, to: "oauths#show"
-    post :token, to: "oauths#token"
   end
-
+  resource :tokens, only: [:create]
   resources :registrations, only: [:new, :create]
 
   namespace :my do
spec/requests/oauth_spec.rb
@@ -51,229 +51,4 @@ RSpec.describe '/oauth' do
       end
     end
   end
-
-  describe "POST /oauth/token" do
-    let(:client) { create(:client) }
-    let(:credentials) { ActionController::HttpAuthentication::Basic.encode_credentials(client.uuid, client.secret) }
-    let(:headers) { { 'Authorization' => credentials } }
-
-    context "when using the authorization_code grant" do
-      context "when the code is still valid" do
-        let(:authorization) { create(:authorization, client: client) }
-
-        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:access_token]).to be_present }
-        specify { expect(json[:token_type]).to eql('Bearer') }
-        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
-        specify { expect(json[:refresh_token]).to be_present }
-        specify { expect(authorization.reload).to be_revoked }
-      end
-
-      context "when the code is expired" do
-        let(:authorization) { create(:authorization, client: client, expired_at: 1.second.ago) }
-
-        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
-
-        specify { expect(response).to have_http_status(:bad_request) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:error]).to eql('invalid_request') }
-      end
-
-      context "when the code is not known" do
-        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
-
-        specify { expect(response).to have_http_status(:bad_request) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:error]).to eql('invalid_request') }
-      end
-    end
-
-    context "when requesting a token using the client_credentials grant" do
-      context "when the client credentials are valid" do
-        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:access_token]).to be_present }
-        specify { expect(json[:token_type]).to eql('Bearer') }
-        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
-        specify { expect(json[:refresh_token]).to be_nil }
-      end
-
-      context "when the credentials are unknown" do
-        let(:headers) { { 'Authorization' => 'invalid' } }
-        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
-
-        specify { expect(response).to have_http_status(:unauthorized) }
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:error]).to eql('invalid_client') }
-      end
-    end
-
-    context "when requesting tokens using the resource owner password credentials grant" do
-      context "when the credentials are valid" do
-        let(:user) { create(:user) }
-        before { post '/oauth/token', params: { grant_type: 'password', username: user.email, password: user.password }, headers: headers }
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:access_token]).to be_present }
-        specify { expect(json[:token_type]).to eql('Bearer') }
-        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
-        specify { expect(json[:refresh_token]).to be_present }
-      end
-
-      context "when the credentials are invalid" do
-        before { post '/oauth/token', params: { grant_type: 'password', username: generate(:email), password: generate(:password) }, headers: headers }
-
-        specify { expect(response).to have_http_status(:bad_request) }
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:error]).to eql('invalid_request') }
-      end
-    end
-
-    context "when exchanging a refresh token for a new access token" do
-      context "when the refresh token is still active" do
-        let(:refresh_token) { create(:refresh_token) }
-
-        before { post '/oauth/token', params: { grant_type: 'refresh_token', refresh_token: refresh_token.to_jwt }, headers: headers }
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:access_token]).to be_present }
-        specify { expect(json[:token_type]).to eql('Bearer') }
-        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
-        specify { expect(json[:refresh_token]).to be_present }
-        specify { expect(refresh_token.reload).to be_revoked }
-      end
-    end
-
-    context "when exchanging a SAML 2.0 assertion grant for tokens" do
-      context "when the assertion contains a valid email address" do
-        let(:user) { create(:user) }
-        let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: true) }
-        let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) }
-        let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
-
-        before :each do
-          allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-          post '/oauth/token', params: {
-            grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
-            assertion: Base64.urlsafe_encode64(saml),
-          }, headers: headers
-        end
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:access_token]).to be_present }
-        specify { expect(json[:token_type]).to eql('Bearer') }
-        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
-        specify { expect(json[:refresh_token]).to be_present }
-      end
-
-      context "when the assertion contains a valid uuid" do
-        let(:user) { create(:user) }
-        let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: true, name_id_format: Saml::Kit::Namespaces::PERSISTENT) }
-        let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) }
-        let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
-
-        before :each do
-          allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-          post '/oauth/token', params: {
-            grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
-            assertion: Base64.urlsafe_encode64(saml),
-          }, headers: headers
-        end
-
-        specify { expect(response).to have_http_status(:ok) }
-        specify { expect(response.headers['Content-Type']).to include('application/json') }
-        specify { expect(response.headers['Cache-Control']).to include('no-store') }
-        specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-        let(:json) { JSON.parse(response.body, symbolize_names: true) }
-        specify { expect(json[:access_token]).to be_present }
-        specify { expect(json[:token_type]).to eql('Bearer') }
-        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
-        specify { expect(json[:refresh_token]).to be_present }
-      end
-    end
-
-    context "when the assertion is not a valid document" do
-      let(:user) { create(:user) }
-      let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id) }
-      let(:saml) { 'invalid' }
-      let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
-
-      before :each do
-        allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-        post '/oauth/token', params: {
-          grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
-          assertion: Base64.urlsafe_encode64(saml),
-        }, headers: headers
-      end
-
-      specify { expect(response).to have_http_status(:bad_request) }
-      specify { expect(response.headers['Content-Type']).to include('application/json') }
-      specify { expect(response.headers['Cache-Control']).to include('no-store') }
-      specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-      let(:json) { JSON.parse(response.body, symbolize_names: true) }
-      specify { expect(json[:error]).to eql('invalid_request') }
-    end
-
-    context "when the assertion has an invalid signature" do
-      let(:user) { create(:user) }
-      let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: false) }
-      let(:key_pair) { Xml::Kit::KeyPair.generate(use: :signing) }
-      let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) { |x| x.sign_with(key_pair) } }
-      let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
-
-      before :each do
-        allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
-        post '/oauth/token', params: {
-          grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
-          assertion: Base64.urlsafe_encode64(saml),
-        }, headers: headers
-      end
-
-      specify { expect(response).to have_http_status(:bad_request) }
-      specify { expect(response.headers['Content-Type']).to include('application/json') }
-      specify { expect(response.headers['Cache-Control']).to include('no-store') }
-      specify { expect(response.headers['Pragma']).to eql('no-cache') }
-
-      let(:json) { JSON.parse(response.body, symbolize_names: true) }
-      specify { expect(json[:error]).to eql('invalid_request') }
-    end
-  end
 end
spec/requests/tokens_spec.rb
@@ -0,0 +1,228 @@
+require 'rails_helper'
+
+RSpec.describe '/tokens' do
+  describe "POST /oauth/token" do
+    let(:client) { create(:client) }
+    let(:credentials) { ActionController::HttpAuthentication::Basic.encode_credentials(client.uuid, client.secret) }
+    let(:headers) { { 'Authorization' => credentials } }
+
+    context "when using the authorization_code grant" do
+      context "when the code is still valid" do
+        let(:authorization) { create(:authorization, client: client) }
+
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_present }
+        specify { expect(authorization.reload).to be_revoked }
+      end
+
+      context "when the code is expired" do
+        let(:authorization) { create(:authorization, client: client, expired_at: 1.second.ago) }
+
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: authorization.code }, headers: headers }
+
+        specify { expect(response).to have_http_status(:bad_request) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:error]).to eql('invalid_request') }
+      end
+
+      context "when the code is not known" do
+        before { post '/oauth/token', params: { grant_type: 'authorization_code', code: SecureRandom.hex(20) }, headers: headers }
+
+        specify { expect(response).to have_http_status(:bad_request) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:error]).to eql('invalid_request') }
+      end
+    end
+
+    context "when requesting a token using the client_credentials grant" do
+      context "when the client credentials are valid" do
+        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_nil }
+      end
+
+      context "when the credentials are unknown" do
+        let(:headers) { { 'Authorization' => 'invalid' } }
+        before { post '/oauth/token', params: { grant_type: 'client_credentials' }, headers: headers }
+
+        specify { expect(response).to have_http_status(:unauthorized) }
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:error]).to eql('invalid_client') }
+      end
+    end
+
+    context "when requesting tokens using the resource owner password credentials grant" do
+      context "when the credentials are valid" do
+        let(:user) { create(:user) }
+        before { post '/oauth/token', params: { grant_type: 'password', username: user.email, password: user.password }, headers: headers }
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_present }
+      end
+
+      context "when the credentials are invalid" do
+        before { post '/oauth/token', params: { grant_type: 'password', username: generate(:email), password: generate(:password) }, headers: headers }
+
+        specify { expect(response).to have_http_status(:bad_request) }
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:error]).to eql('invalid_request') }
+      end
+    end
+
+    context "when exchanging a refresh token for a new access token" do
+      context "when the refresh token is still active" do
+        let(:refresh_token) { create(:refresh_token) }
+
+        before { post '/oauth/token', params: { grant_type: 'refresh_token', refresh_token: refresh_token.to_jwt }, headers: headers }
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_present }
+        specify { expect(refresh_token.reload).to be_revoked }
+      end
+    end
+
+    context "when exchanging a SAML 2.0 assertion grant for tokens" do
+      context "when the assertion contains a valid email address" do
+        let(:user) { create(:user) }
+        let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: true) }
+        let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) }
+        let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
+
+        before :each do
+          allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
+          post '/oauth/token', params: {
+            grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
+            assertion: Base64.urlsafe_encode64(saml),
+          }, headers: headers
+        end
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_present }
+      end
+
+      context "when the assertion contains a valid uuid" do
+        let(:user) { create(:user) }
+        let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: true, name_id_format: Saml::Kit::Namespaces::PERSISTENT) }
+        let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) }
+        let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
+
+        before :each do
+          allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
+          post '/oauth/token', params: {
+            grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
+            assertion: Base64.urlsafe_encode64(saml),
+          }, headers: headers
+        end
+
+        specify { expect(response).to have_http_status(:ok) }
+        specify { expect(response.headers['Content-Type']).to include('application/json') }
+        specify { expect(response.headers['Cache-Control']).to include('no-store') }
+        specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+        let(:json) { JSON.parse(response.body, symbolize_names: true) }
+        specify { expect(json[:access_token]).to be_present }
+        specify { expect(json[:token_type]).to eql('Bearer') }
+        specify { expect(json[:expires_in]).to eql(1.hour.to_i) }
+        specify { expect(json[:refresh_token]).to be_present }
+      end
+    end
+
+    context "when the assertion is not a valid document" do
+      let(:user) { create(:user) }
+      let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id) }
+      let(:saml) { 'invalid' }
+      let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
+
+      before :each do
+        allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
+        post '/oauth/token', params: {
+          grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
+          assertion: Base64.urlsafe_encode64(saml),
+        }, headers: headers
+      end
+
+      specify { expect(response).to have_http_status(:bad_request) }
+      specify { expect(response.headers['Content-Type']).to include('application/json') }
+      specify { expect(response.headers['Cache-Control']).to include('no-store') }
+      specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+      let(:json) { JSON.parse(response.body, symbolize_names: true) }
+      specify { expect(json[:error]).to eql('invalid_request') }
+    end
+
+    context "when the assertion has an invalid signature" do
+      let(:user) { create(:user) }
+      let(:saml_request) { double(id: Xml::Kit::Id.generate, issuer: Saml::Kit.configuration.entity_id, trusted?: false) }
+      let(:key_pair) { Xml::Kit::KeyPair.generate(use: :signing) }
+      let(:saml) { Saml::Kit::Assertion.build_xml(user, saml_request) { |x| x.sign_with(key_pair) } }
+      let(:metadata) { Saml::Kit::Metadata.build(&:build_identity_provider) }
+
+      before :each do
+        allow(Saml::Kit.configuration.registry).to receive(:metadata_for).and_return(metadata)
+        post '/oauth/token', params: {
+          grant_type: 'urn:ietf:params:oauth:grant-type:saml2-bearer',
+          assertion: Base64.urlsafe_encode64(saml),
+        }, headers: headers
+      end
+
+      specify { expect(response).to have_http_status(:bad_request) }
+      specify { expect(response.headers['Content-Type']).to include('application/json') }
+      specify { expect(response.headers['Cache-Control']).to include('no-store') }
+      specify { expect(response.headers['Pragma']).to eql('no-cache') }
+
+      let(:json) { JSON.parse(response.body, symbolize_names: true) }
+      specify { expect(json[:error]).to eql('invalid_request') }
+    end
+  end
+end