Commit 69a6a3b
Changed files (10)
app
controllers
models
spec
requests
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