From 6111686233769a8044b1e8a3ba44b2a3af43a687 Mon Sep 17 00:00:00 2001 From: Emily Nguyen Date: Tue, 9 Jul 2024 17:25:23 -0500 Subject: [PATCH] Add validations to exchange action in sessions controller --- .reek.yml | 4 ++ app/controllers/oauth/sessions_controller.rb | 20 ++++++- .../concerns/oauth_session_exchangeable.rb | 28 ++++++++++ app/models/oauth_session.rb | 1 + lib/oauth.rb | 12 +++++ .../oauth/sessions_controller_spec.rb | 54 +++++++++++++++++-- 6 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 app/models/concerns/oauth_session_exchangeable.rb 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..87e62fb 100644 --- a/app/controllers/oauth/sessions_controller.rb +++ b/app/controllers/oauth/sessions_controller.rb @@ -43,7 +43,14 @@ 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 unsupported_grant_type @@ -86,5 +93,16 @@ def render_token_request_error(error:, status: :bad_request) def render_unsupported_token_type_error render json: { error: 'unsupported_token_type' }, status: :bad_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 end 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/requests/oauth/sessions_controller_spec.rb b/spec/requests/oauth/sessions_controller_spec.rb index 454895e..c9c9ed1 100644 --- a/spec/requests/oauth/sessions_controller_spec.rb +++ b/spec/requests/oauth/sessions_controller_spec.rb @@ -217,16 +217,62 @@ 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) + shared_examples 'returns invalid request response' do + 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 an empty resource is provided' do + let(:resource) { '' } + + include_examples 'returns invalid request response' + end + + context 'when an invalid resource is provided' do + let(:resource) { 'foobar' } + + include_examples 'returns invalid request response' + end + + context 'when an empty subject token type is provided' do + let(:subject_token_type) { '' } + + include_examples 'returns invalid request response' + end + + context 'when the subject token type is not valid' do + let(:subject_token_type) { 'foobar' } + + include_examples 'returns invalid request response' + 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