diff --git a/.reek.yml b/.reek.yml index 8214cbb..c008b76 100644 --- a/.reek.yml +++ b/.reek.yml @@ -67,3 +67,7 @@ detectors: exclude: - AccessToken#valid_exp? - API::BaseController#user_from_token + UtilityFunction: + exclude: + - OAuthSessionExchangeable#valid_resource? + - OAuthSessionExchangeable#valid_subject_token_type? diff --git a/app/controllers/oauth/sessions_controller.rb b/app/controllers/oauth/sessions_controller.rb index 582772e..8c69dda 100644 --- a/app/controllers/oauth/sessions_controller.rb +++ b/app/controllers/oauth/sessions_controller.rb @@ -43,7 +43,25 @@ def refresh end def exchange - head :ok + oauth_session = oauth_session_from_subject_token + resource = params[:resource] + subject_token_type = params[:subject_token_type] + oauth_session.validate_params_for_exchange!(resource:, subject_token_type:) + + render json: { resource:, subject_token_type: } + rescue OAuth::InvalidResourceError, OAuth::InvalidSubjectTokenTypeError + render_token_request_error(error: 'invalid_request') + end + + def oauth_session_from_subject_token + access_token = AccessToken.new(JsonWebToken.decode(params[:subject_token])) + raise OAuth::UnauthorizedAccessTokenError unless access_token.valid? + + OAuthSession.find_by!(access_token_jti: access_token.jti) + rescue JWT::DecodeError + raise OAuth::InvalidAccessTokenError + rescue ActiveRecord::RecordNotFound + raise OAuth::OAuthSessionNotFound end def unsupported_grant_type @@ -79,6 +97,10 @@ def revoke_refresh_token private + def token_params + params.permit(:grant_type, :code, :code_verifier, :resource, :subject_token, :subject_token_type, :client_id) + end + def render_token_request_error(error:, status: :bad_request) render json: { error: }, status: end diff --git a/app/models/concerns/oauth_session_exchangeable.rb b/app/models/concerns/oauth_session_exchangeable.rb new file mode 100644 index 0000000..26ecc0d --- /dev/null +++ b/app/models/concerns/oauth_session_exchangeable.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +## +# Provides support for validating token claims +module OAuthSessionExchangeable + extend ActiveSupport::Concern + + VALID_URIS = ['/api/v1/users/current'].freeze + VALID_TOKEN_TYPES = ['urn:ietf:params:oauth:token-type:access_token'].freeze + + def validate_params_for_exchange!(resource:, subject_token_type:) + raise OAuth::InvalidResourceError unless valid_resource?(resource) + + return if valid_subject_token_type?(subject_token_type) + + raise OAuth::InvalidSubjectTokenTypeError + end + + private + + def valid_resource?(resource) + resource.present? && VALID_URIS.include?(resource) + end + + def valid_subject_token_type?(subject_token_type) + subject_token_type.present? && VALID_TOKEN_TYPES.include?(subject_token_type) + end +end diff --git a/app/models/oauth_session.rb b/app/models/oauth_session.rb index 49cc910..09a8f8b 100644 --- a/app/models/oauth_session.rb +++ b/app/models/oauth_session.rb @@ -4,6 +4,7 @@ # Models an oauth session with minimal data from session. class OAuthSession < ApplicationRecord include OAuthSessionCreatable + include OAuthSessionExchangeable STATUS_ENUM_VALUES = { created: 'created', diff --git a/lib/oauth.rb b/lib/oauth.rb index 54aebd0..c6c1ce7 100644 --- a/lib/oauth.rb +++ b/lib/oauth.rb @@ -23,10 +23,22 @@ class AccessDenied < StandardError; end # Error for when client provides an unsupported grant type param. class UnsupportedGrantTypeError < StandardError; end + ## + # Error for when OAuth Session is not found. + class OAuthSessionNotFound < StandardError; end + ## # Error for when client provides a code that does not map to an valid authorization grant. class InvalidGrantError < StandardError; end + ## + # Error for when the resource probided fails validation. + class InvalidResourceError < StandardError; end + + ## + # Error for when the subject token type fails validation. + class InvalidSubjectTokenTypeError < StandardError; end + ## # Error for when client provides a code verifier that fails validation. class InvalidCodeVerifierError < StandardError; end diff --git a/spec/models/authorization_grant_spec.rb b/spec/models/authorization_grant_spec.rb index b42250b..3ccb552 100644 --- a/spec/models/authorization_grant_spec.rb +++ b/spec/models/authorization_grant_spec.rb @@ -84,6 +84,8 @@ subject(:method_call) { authorization_grant.redeem(code_verifier:) } let(:code_verifier) { 'code_verifier' } + let(:resource) { 'resource' } + let(:subject_token_type) { 'subject_token_type' } context 'when the authorization grant has not been redeemed' do let_it_be(:authorization_grant) { create(:authorization_grant) } diff --git a/spec/requests/oauth/sessions_controller_spec.rb b/spec/requests/oauth/sessions_controller_spec.rb index 454895e..3428592 100644 --- a/spec/requests/oauth/sessions_controller_spec.rb +++ b/spec/requests/oauth/sessions_controller_spec.rb @@ -217,16 +217,70 @@ describe 'POST /token (grant_type="urn:ietf:params:oauth:grant-type:token-exchange")' do let_it_be(:user) { create(:user) } let_it_be(:authorization_grant) { create(:authorization_grant, user:) } - let(:params) { { grant_type: } } + let(:params) { { grant_type:, subject_token:, resource:, subject_token_type:, client_id: } } + let!(:oauth_session) { create(:oauth_session, authorization_grant:) } + let(:subject_token) { JsonWebToken.encode(attributes_for(:access_token, oauth_session:)) } let(:grant_type) { 'urn:ietf:params:oauth:grant-type:token-exchange' } + let(:resource) { '/api/v1/users/current' } + let(:subject_token_type) { 'urn:ietf:params:oauth:token-type:access_token' } + let(:client_id) { 'democlient' } include_context 'with an authenticated client', :post, :oauth_create_session_path it_behaves_like 'an endpoint that requires client authentication' - it 'creates an OAuth session and serializes the token data' do - call_endpoint - expect(response).to have_http_status(:ok) + ### need to create expected endpoint tests + + context 'when an invalid subject token type is provided' do + let(:subject_token_type) { 'foobar' } + + it 'responds with HTTP status bad request and error invalid_subject_token_type as JSON' do + call_endpoint + aggregate_failures do + expect(response).to have_http_status(:bad_request) + expect(response.parsed_body).to eq({ 'error' => 'invalid_request' }) + end + end + end + + context 'when an invalid resource is provided' do + let(:resource) { 'foobar' } + + it 'responds with HTTP status bad request and error invalid_resource as JSON' do + call_endpoint + aggregate_failures do + expect(response).to have_http_status(:bad_request) + expect(response.parsed_body).to eq({ 'error' => 'invalid_request' }) + end + end + end + + context 'when the resource is not valid' do + let(:resource) { '' } + + it 'raises an OAuth::InvalidResourceError' do + expect { raise OAuth::InvalidResourceError }.to raise_error(OAuth::InvalidResourceError) + end + end + + context 'when the subject token type is not valid' do + let(:subject_token_type) { '' } + + it 'raises an OAuth::InvalidSubjectTokenTypeError' do + expect { raise OAuth::InvalidSubjectTokenTypeError }.to raise_error(OAuth::InvalidSubjectTokenTypeError) + end + end + + context 'when valid params are provided' do + it 'returns resource and subject_token_type' do + call_endpoint + aggregate_failures do + expect(response).to have_http_status(:ok) + expect(response.parsed_body).to eq( + { 'resource' => resource, 'subject_token_type' => subject_token_type } + ) + end + end end end