Compare commits

...

1 Commits

  1. 2
      Gemfile
  2. 4
      Gemfile.lock
  3. 20
      app/controllers/oauth_clients_controller.rb
  4. 37
      app/services/oauth_clients/redirect_uri_from_state_service.rb

@ -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'

@ -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)

@ -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

@ -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

Loading…
Cancel
Save