diff --git a/Gemfile b/Gemfile index ccbbb3d60e..f453260b2e 100644 --- a/Gemfile +++ b/Gemfile @@ -207,6 +207,8 @@ gem "sentry-ruby", '~> 5.3.0' # Appsignal integration gem "appsignal", "~> 3.0", require: false +gem 'dry-monads', '~> 1.4' + group :test do gem 'launchy', '~> 2.5.0' gem 'rack-test', '~> 2.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index d35fb1a810..de2a6b5f7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -439,6 +439,9 @@ GEM dry-logic (1.2.0) concurrent-ruby (~> 1.0) dry-core (~> 0.5, >= 0.5) + dry-monads (1.4.0) + concurrent-ruby (~> 1.0) + dry-core (~> 0.7) dry-types (1.5.1) concurrent-ruby (~> 1.0) dry-container (~> 0.3) @@ -1052,6 +1055,7 @@ DEPENDENCIES disposable (~> 0.6.2) doorkeeper (~> 5.5.0) dotenv-rails + dry-monads (~> 1.4) email_validator (~> 2.2.3) equivalent-xml (~> 0.6) escape_utils (~> 1.3) diff --git a/app/controllers/oauth_clients_controller.rb b/app/controllers/oauth_clients_controller.rb index 77755db894..781e734403 100644 --- a/app/controllers/oauth_clients_controller.rb +++ b/app/controllers/oauth_clients_controller.rb @@ -29,20 +29,20 @@ # This controller handles OAuth2 Authorization Code Grant redirects from a Authorization Server to # "callback" endpoint. class OAuthClientsController < ApplicationController + before_action :set_oauth_state + before_action :find_oauth_client before_action :set_redirect_uri before_action :set_code before_action :set_connection_manager + after_action :clear_oauth_state_cookie + # Provide the OAuth2 "callback" endpoint. # The Authorization Server redirects # here after successful authentication and authorization. # This endpoint gets a "code" parameter that cryptographically # contains a grant. - # We get here by a URL like this: - # http://localhost:4200/oauth_clients/asdf12341234qsdfasdfasdf/callback? - # state=http%3A%2F%2Flocalhost%3A4200%2Fprojects%2Fdemo-project%2Foauth2_example& - # code=MQoOnUTJGFdAo5jBGD1SqnDH0PV6yioG7NoYM2zZZlK3g6LuKrGUmOxjIS1bIy7fHEfZy2WrgYcx def callback # Exchange the code with a token using a HTTP call to the Authorization Server service_result = @connection_manager.code_to_token(@code) @@ -65,6 +65,14 @@ class OAuthClientsController < ApplicationController private + def set_oauth_state + @oauth_state = params[:state] + end + + def clear_oauth_state_cookie + cookies.delete("oauth_state_#{@oauth_state}") unless @oauth_state.nil? + end + def set_oauth_errors(service_result) flash[:error] = ["#{t(:'oauth_client.errors.oauth_authorization_code_grant_had_errors')}:"] service_result.errors.each do |error| @@ -94,7 +102,7 @@ class OAuthClientsController < ApplicationController # redirect_uri is used by OpenProject to redirect to # after receiving an OAuth2 access token. So it should not be blank. service_result = ::OAuthClients::RedirectUriFromStateService - .new(state: params[:state], cookies:) + .new(state: @oauth_state, cookies:) .call if service_result.success? @@ -153,7 +161,7 @@ class OAuthClientsController < ApplicationController def get_redirect_uri ::OAuthClients::RedirectUriFromStateService - .new(state: params[:state], cookies:) + .new(state: @oauth_state, cookies:) .call .result end diff --git a/app/services/oauth_clients/redirect_uri_from_state_service.rb b/app/services/oauth_clients/redirect_uri_from_state_service.rb index 5a20266c66..eb20b01a55 100644 --- a/app/services/oauth_clients/redirect_uri_from_state_service.rb +++ b/app/services/oauth_clients/redirect_uri_from_state_service.rb @@ -28,30 +28,45 @@ require "rack/oauth2" require "uri/http" +require 'dry/monads' +require 'dry/monads/do' module OAuthClients class RedirectUriFromStateService + include Dry::Monads[:maybe] + include Dry::Monads::Do.for(:process) + def initialize(state:, cookies:) - @state = state + @state = Maybe(state) @cookies = cookies end def call - redirect_uri = oauth_state_cookie - - if redirect_uri.present? && ::API::V3::Utilities::PathHelper::ApiV3Path::same_origin?(redirect_uri) - ServiceResult.success(result: redirect_uri) - else - ServiceResult.failure - end + process(@cookies, @state) + .fmap { |uri| ServiceResult.success(result: uri) } + .value_or(ServiceResult.failure) end private - def oauth_state_cookie - return nil if @state.blank? + def process(cookies, state) + state_key = yield state + uri = yield callback_uri_from_cookies(cookies, "oauth_state_#{state_key}") + callback_uri = yield validate_callback_uri(uri) + + Some(callback_uri) + end + + def callback_uri_from_cookies(cookies, name) + Maybe(cookies[name]) + end - @cookies["oauth_state_#{@state}"] + def validate_callback_uri(uri) + if ::API::V3::Utilities::PathHelper::ApiV3Path::same_origin?(uri) + Some(uri) + else + None() + end end end end