From f611ba3bcb6ef82e4f265cdd8e933778923888b8 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Thu, 18 Jul 2024 09:07:23 +0200 Subject: [PATCH 1/5] [#55643] Extend API authentication to accept JWT issued by OpenID provider to other client. https://community.openproject.org/work_packages/55643 - Add new warden authentication stategy to handle jwt issued by configured OIDC. - Modify exisiting doorkeeper_oauth stategy to ignore jwts. - Fill in WWW-Autheticate header with auth failure information. - Make keycloak docker dev setup use postgres as a database. --- config/initializers/jwt.rb | 29 +++ config/initializers/warden.rb | 13 +- docker/dev/keycloak/docker-compose.yml | 6 +- lib/api/root.rb | 11 +- lib_static/open_project/authentication.rb | 17 +- .../strategies/warden/doorkeeper_oauth.rb | 65 +++--- .../strategies/warden/fail_with_header.rb | 20 ++ .../strategies/warden/jwt_oidc.rb | 82 +++++++ .../lib/open_project/openid_connect/engine.rb | 12 +- spec/requests/api/v3/authentication_spec.rb | 203 ++++++++++++++++-- spec/support/wait_for.rb | 7 +- 11 files changed, 385 insertions(+), 80 deletions(-) create mode 100644 config/initializers/jwt.rb create mode 100644 lib_static/open_project/authentication/strategies/warden/fail_with_header.rb create mode 100644 lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb diff --git a/config/initializers/jwt.rb b/config/initializers/jwt.rb new file mode 100644 index 000000000000..4442e70229df --- /dev/null +++ b/config/initializers/jwt.rb @@ -0,0 +1,29 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +JWT.configuration.strict_base64_decoding = true diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index c94f72f62d5c..b2a43c58277c 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -5,16 +5,23 @@ [:basic_auth_failure, namespace::BasicAuthFailure, "Basic"], [:global_basic_auth, namespace::GlobalBasicAuth, "Basic"], [:user_basic_auth, namespace::UserBasicAuth, "Basic"], - [:oauth, namespace::DoorkeeperOAuth, "OAuth"], + [:oauth, namespace::DoorkeeperOAuth, "Bearer"], [:anonymous_fallback, namespace::AnonymousFallback, "Basic"], + [:jwt_oidc, namespace::JwtOidc, "Bearer"], [:session, namespace::Session, "Session"] ] strategies.each do |name, clazz, auth_scheme| - OpenProject::Authentication.add_strategy name, clazz, auth_scheme + OpenProject::Authentication.add_strategy(name, clazz, auth_scheme) end OpenProject::Authentication.update_strategies(OpenProject::Authentication::Scope::API_V3, { store: false }) do |_| - %i[global_basic_auth user_basic_auth basic_auth_failure oauth session anonymous_fallback] + %i[global_basic_auth + user_basic_auth + basic_auth_failure + oauth + jwt_oidc + session + anonymous_fallback] end end diff --git a/docker/dev/keycloak/docker-compose.yml b/docker/dev/keycloak/docker-compose.yml index 77b0aae0b217..aac66478bc40 100644 --- a/docker/dev/keycloak/docker-compose.yml +++ b/docker/dev/keycloak/docker-compose.yml @@ -20,14 +20,16 @@ services: extra_hosts: - "openproject.local:host-gateway" environment: - - KC_DB_URL_HOST=db + - KC_DB=postgres - KC_DB_USERNAME=keycloak - KC_DB_PASSWORD=keycloak - - KC_DB_URL_DATABASE=jdbc:postgresql://db:5432/keycloak + - KC_DB_URL=jdbc:postgresql://db-keycloak:5432/keycloak - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=admin - KC_DB_SCHEMA=public - KC_HOSTNAME=keycloak.local + - KC_FEATURES=token-exchange + - KC_TRANSACTION_XA_ENABLED=false volumes: - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro - keycloak-data:/opt/keycloak/data/ diff --git a/lib/api/root.rb b/lib/api/root.rb index b13d1697767d..a218fcf5ec74 100644 --- a/lib/api/root.rb +++ b/lib/api/root.rb @@ -39,13 +39,14 @@ class Root < ::API::RootAPI error_representer ::API::V3::Errors::ErrorRepresenter, "application/hal+json; charset=utf-8" authentication_scope OpenProject::Authentication::Scope::API_V3 - OpenProject::Authentication.handle_failure(scope: API_V3) do |warden, _opts| - e = grape_error_for warden.env, self + OpenProject::Authentication.handle_failure(scope: API_V3) do |warden, opts| + e = grape_error_for(warden.env, self) error_message = I18n.t("api_v3.errors.code_401_wrong_credentials") - api_error = ::API::Errors::Unauthenticated.new error_message - representer = ::API::V3::Errors::ErrorRepresenter.new api_error + api_error = ::API::Errors::Unauthenticated.new(error_message) + representer = ::API::V3::Errors::ErrorRepresenter.new(api_error) + status = opts[:message] == "insufficient_scope" ? 403 : 401 - e.error! representer.to_json, 401, warden.headers + e.error!(representer.to_json, status, warden.headers) end version "v3", using: :path do diff --git a/lib_static/open_project/authentication.rb b/lib_static/open_project/authentication.rb index 65f2fbb40e09..3dc9d2e19d32 100644 --- a/lib_static/open_project/authentication.rb +++ b/lib_static/open_project/authentication.rb @@ -261,18 +261,25 @@ def scope_realm(scope = nil) def response_header( default_auth_scheme: self.default_auth_scheme, scope: nil, - request_headers: {} + request_headers: {}, + error: nil, + error_description: nil ) - scheme = pick_auth_scheme auth_schemes(scope), default_auth_scheme, request_headers - - "#{scheme} realm=\"#{scope_realm(scope)}\"" + scheme = pick_auth_scheme(auth_schemes(scope), + default_auth_scheme, + request_headers) + + header = %{#{scheme} realm="#{scope_realm(scope)}"} + header << %{ error="#{error}"} if error + header << %{ error_description="#{error_description}"} if error && error_description + header end def auth_schemes(scope) strategies = Array(Manager.scope_config(scope).strategies) Manager.auth_schemes - .select { |_, info| scope.nil? or not (info.strategies & strategies).empty? } + .select { |_, info| scope.nil? or not !info.strategies.intersect?(strategies) } .keys end end diff --git a/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb index 7e7fcbf06d60..11cd7c23eccb 100644 --- a/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb +++ b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb @@ -4,65 +4,52 @@ module OpenProject module Authentication module Strategies module Warden - ## - # Allows testing authentication via doorkeeper OAuth2 token - # class DoorkeeperOAuth < ::Warden::Strategies::Base + include FailWithHeader + def valid? - ::Doorkeeper::OAuth::Token - .from_request(decorated_request, *Doorkeeper.configuration.access_token_methods) - .present? + access_token = ::Doorkeeper::OAuth::Token + .from_request(decorated_request, *Doorkeeper.configuration.access_token_methods) + access_token.present? && + begin + JWT.decode(access_token, nil, false) + false + rescue JWT::DecodeError + true + end end def authenticate! - token = ::Doorkeeper::OAuth::Token.authenticate(decorated_request, - *Doorkeeper.configuration.access_token_methods) - return fail!("invalid token") unless token&.acceptable?(scope) - - if token.resource_owner_id.nil? - authenticate_client_credentials(token) + access_token = ::Doorkeeper::OAuth::Token.authenticate(decorated_request, + *Doorkeeper.configuration.access_token_methods) + return fail_with_header!(error: "invalid_token") if access_token.blank? + return fail_with_header!(error: "invalid_token") if access_token.expired? || access_token.revoked? + return fail_with_header!(error: "insufficient_scope") if !access_token.includes_scope?(scope) + + if access_token.resource_owner_id.nil? + user_id = ::Doorkeeper::Application + .where(id: access_token.application_id) + .where.not(client_credentials_user_id: nil) + .pick(:client_credentials_user_id) + authenticate_user(user_id) if user_id else - authenticate_user(token.resource_owner_id) + authenticate_user(access_token.resource_owner_id) end end private - ## - # We allow applications to designate a user to be used for client credentials. - # When using client credentials flow, find this user and try to authenticate - def authenticate_client_credentials(token) - if client_credential_user = find_credential_app_user(token.application_id) - authenticate_user client_credential_user - else - success! User.anonymous - end - end - - ## - # Find a credentials-enabled application with the given ID - # and return its allowed application user, if there is one. - # Avoid going through token.application.client_credentials_user_id for performance - # (this is going to be called on every request with CC flows!) - def find_credential_app_user(app_id) - ::Doorkeeper::Application - .where(id: app_id) - .where.not(client_credentials_user_id: nil) - .pluck(:client_credentials_user_id) - .first - end - def authenticate_user(id) user = User.find_by(id:) if user success!(user) else - fail!("No such user") + fail_with_header!(error: "invalid_token") end end def decorated_request - ::Doorkeeper::Grape::AuthorizationDecorator.new(request) + @decorated_request ||= ::Doorkeeper::Grape::AuthorizationDecorator.new(request) end end end diff --git a/lib_static/open_project/authentication/strategies/warden/fail_with_header.rb b/lib_static/open_project/authentication/strategies/warden/fail_with_header.rb new file mode 100644 index 000000000000..8f8427ada1cb --- /dev/null +++ b/lib_static/open_project/authentication/strategies/warden/fail_with_header.rb @@ -0,0 +1,20 @@ +module OpenProject + module Authentication + module Strategies + module Warden + module FailWithHeader + def fail_with_header!(error:, error_description: nil) + headers( + "WWW-Authenticate" => OpenProject::Authentication::WWWAuthenticate.response_header( + default_auth_scheme: "Bearer", + error:, + error_description: + ) + ) + fail!(error) + end + end + end + end + end +end diff --git a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb new file mode 100644 index 000000000000..19a5a1a75952 --- /dev/null +++ b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb @@ -0,0 +1,82 @@ +module OpenProject + module Authentication + module Strategies + module Warden + class JwtOidc < ::Warden::Strategies::Base + include FailWithHeader + + SUPPORTED_ALG = %w[ + RS256 + RS384 + RS512 + ].freeze + + def valid? + @access_token = ::Doorkeeper::OAuth::Token.from_bearer_authorization( + ::Doorkeeper::Grape::AuthorizationDecorator.new(request) + ) + @access_token.present? && + begin + @unverified_payload, @unverified_header = JWT.decode(@access_token, nil, false) + @unverified_header.present? && @unverified_payload.present? + rescue JWT::DecodeError + false + end + end + + def authenticate! + issuer = @unverified_payload["iss"] + provider = OpenProject::OpenIDConnect.providers.find { |p| p.configuration[:issuer] == issuer } if issuer.present? + if provider.present? + client_id = provider.configuration.fetch(:identifier) + alg = @unverified_header.fetch("alg") + key = + if SUPPORTED_ALG.include?(alg) + kid = @unverified_header.fetch("kid") + jwks_uri = provider.configuration[:jwks_uri] + JSON::JWK::Set::Fetcher.fetch(jwks_uri, kid:).to_key + else + fail_with_header!(error: "invalid_token", error_description: "Token signature algorithm is not supported") + return + end + begin + verified_payload, = JWT.decode( + @access_token, + key, + true, + { + algorithm: alg, + verify_iss: true, + verify_aud: true, + iss: issuer, + aud: client_id, + required_claims: ["sub", "iss", "aud"] + } + ) + rescue JWT::ExpiredSignature + fail_with_header!(error: "invalid_token", error_description: "The access token expired") + return + rescue JWT::ImmatureSignature + # happens when nbf time is less than current + fail_with_header!(error: "invalid_token", error_description: "The access token is used too early") + + return + rescue JWT::InvalidIssuerError + fail_with_header!(error: "invalid_token", error_description: "The access token issuer is wrong") + return + rescue JWT::InvalidAudError + fail_with_header!(error: "invalid_token", error_description: "The access token audience claim is wrong") + return + end + + user = User.find_by(identity_url: "#{provider.name}:#{verified_payload['sub']}") + success!(user) if user + else + fail_with_header!(error: "invalid_token", error_description: "The access token issuer is unknown") + end + end + end + end + end + end +end diff --git a/modules/openid_connect/lib/open_project/openid_connect/engine.rb b/modules/openid_connect/lib/open_project/openid_connect/engine.rb index 4591f5ed9708..68bdfe4174b9 100644 --- a/modules/openid_connect/lib/open_project/openid_connect/engine.rb +++ b/modules/openid_connect/lib/open_project/openid_connect/engine.rb @@ -29,9 +29,15 @@ class Engine < ::Rails::Engine register_auth_providers do OmniAuth::OpenIDConnect::Providers.configure custom_options: %i[ - display_name? icon? sso? issuer? - check_session_iframe? end_session_endpoint? - limit_self_registration? use_graph_api? + display_name? + icon? + sso? + issuer? + check_session_iframe? + end_session_endpoint? + jwks_uri? + limit_self_registration? + use_graph_api? ] strategy :openid_connect do diff --git a/spec/requests/api/v3/authentication_spec.rb b/spec/requests/api/v3/authentication_spec.rb index ca642ee1d4c5..6702e9253c8b 100644 --- a/spec/requests/api/v3/authentication_spec.rb +++ b/spec/requests/api/v3/authentication_spec.rb @@ -28,15 +28,23 @@ require "spec_helper" -RSpec.describe API::V3 do +RSpec.describe "API V3 Authentication" do let(:resource) { "/api/v3/projects" } let(:user) { create(:user) } + let(:error_response_body) do + { + "_type" => "Error", + "errorIdentifier" => "urn:openproject-org:api:v3:errors:Unauthenticated", + "message" => expected_message + } + end describe "oauth" do let(:oauth_access_token) { "" } + let(:expected_message) { "You did not provide the correct credentials." } before do - login_as user + user header "Authorization", "Bearer #{oauth_access_token}" @@ -57,28 +65,68 @@ it "returns unauthorized" do expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="invalid_token"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) end end - context "with an expired access token" do + context "with a revoked access token" do let(:token) { create(:oauth_access_token, resource_owner: user, revoked_at: DateTime.now) } let(:oauth_access_token) { token.plaintext_token } it "returns unauthorized" do expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="invalid_token"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) end end - end - describe "basic auth" do - let(:response_401) do - { - "_type" => "Error", - "errorIdentifier" => "urn:openproject-org:api:v3:errors:Unauthenticated", - "message" => expected_message - } + context "with an expired access token" do + let(:token) { create(:oauth_access_token, resource_owner: user) } + let(:oauth_access_token) { token.plaintext_token } + + around do |ex| + Timecop.freeze(Time.current + (token.expires_in + 5).seconds) do + ex.run + end + end + + it "returns unauthorized" do + expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="invalid_token"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end + end + + context "with wrong scope" do + let(:token) { create(:oauth_access_token, resource_owner: user, scopes: "unknown_scope") } + let(:oauth_access_token) { token.plaintext_token } + + it "returns forbidden" do + expect(last_response).to have_http_status :forbidden + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="insufficient_scope"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end end + context "with not found user" do + let(:token) { create(:oauth_access_token, resource_owner: user) } + let(:oauth_access_token) { token.plaintext_token } + + around do |ex| + user.destroy + ex.run + end + + it "returns unauthorized" do + expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="invalid_token"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end + end + end + + describe "basic auth" do let(:expected_message) { "You need to be authenticated to access this resource." } strategies = OpenProject::Authentication::Strategies::Warden @@ -113,12 +161,11 @@ def set_basic_auth_header(user, password) end it "returns the correct JSON response" do - expect(JSON.parse(last_response.body)).to eq response_401 + expect(JSON.parse(last_response.body)).to eq error_response_body end it "returns the WWW-Authenticate header" do - expect(last_response.header["WWW-Authenticate"]) - .to include 'Basic realm="OpenProject API"' + expect(last_response.header["WWW-Authenticate"]).to include 'Basic realm="OpenProject API"' end end @@ -135,7 +182,7 @@ def set_basic_auth_header(user, password) end it "returns the correct JSON response" do - expect(JSON.parse(last_response.body)).to eq response_401 + expect(JSON.parse(last_response.body)).to eq error_response_body end it "returns the correct content type header" do @@ -160,7 +207,7 @@ def set_basic_auth_header(user, password) end it "returns the correct JSON response" do - expect(JSON.parse(last_response.body)).to eq response_401 + expect(JSON.parse(last_response.body)).to eq error_response_body end it "returns the correct content type header" do @@ -187,7 +234,7 @@ def set_basic_auth_header(user, password) end it "returns the correct JSON response" do - expect(JSON.parse(last_response.body)).to eq response_401 + expect(JSON.parse(last_response.body)).to eq error_response_body end it "returns the correct content type header" do @@ -215,8 +262,7 @@ def set_basic_auth_header(user, password) context "with login required" do before do - allow(Setting).to receive(:login_required).and_return(true) - allow(Setting).to receive(:login_required?).and_return(true) + allow(Setting).to receive_messages(login_required: true, login_required?: true) end context "with global basic auth configured" do @@ -254,8 +300,7 @@ def set_basic_auth_header(user, password) context "when enabled", with_config: { apiv3_enable_basic_auth: true } do context "without login required" do before do - allow(Setting).to receive(:login_required).and_return(false) - allow(Setting).to receive(:login_required?).and_return(false) + allow(Setting).to receive_messages(login_required: false, login_required?: false) end context "with global and user basic auth enabled" do @@ -320,4 +365,120 @@ def set_basic_auth_header(user, password) end end end + + describe( + "OIDC", + :webmock, + with_settings: { + plugin_openproject_openid_connect: { + "providers" => { + "keycloak" => { + "display_name" => "Keycloak", + "identifier" => "https://openproject.local", + "secret" => "9AWjVC3A4U1HLrZuSP4xiwHfw6zmgECn", + "host" => "keycloak.local", + "issuer" => "https://keycloak.local/realms/master", + "authorization_endpoint" => "/realms/master/protocol/openid-connect/auth", + "token_endpoint" => "/realms/master/protocol/openid-connect/token", + "userinfo_endpoint" => "/realms/master/protocol/openid-connect/userinfo", + "end_session_endpoint" => "https://keycloak.local/realms/master/protocol/openid-connect/logout", + "jwks_uri" => "https://keycloak.local/realms/master/protocol/openid-connect/certs" + } + } + } + } + ) do + let(:rsa_signed_access_token_without_aud) do + "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N0FteXZvUzhCRkZSZm01ODVHUGdBMTZHMUgyVjIyRWR4eHVBWVV1b0trIn0.eyJleHAiOjE3MjEyODM0MzAsImlhdCI6MTcyMTI4MzM3MCwianRpIjoiYzUyNmI0MzUtOTkxZi00NzRhLWFkMWItYzM3MTQ1NmQxZmQwIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5sb2NhbC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbIm1hc3Rlci1yZWFsbSIsImFjY291bnQiXSwic3ViIjoiYjcwZTJmYmYtZWE2OC00MjBjLWE3YTUtMGEyODdjYjY4OWM2IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiaHR0cHM6Ly9vcGVucHJvamVjdC5sb2NhbCIsInNlc3Npb25fc3RhdGUiOiJlYjIzNTI0MC0wYjQ3LTQ4ZmEtOGIzZS1mM2IzMTBkMzUyZTMiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHBzOi8vb3BlbnByb2plY3QubG9jYWwiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImNyZWF0ZS1yZWFsbSIsImRlZmF1bHQtcm9sZXMtbWFzdGVyIiwib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsibWFzdGVyLXJlYWxtIjp7InJvbGVzIjpbInZpZXctcmVhbG0iLCJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsIm1hbmFnZS1pZGVudGl0eS1wcm92aWRlcnMiLCJpbXBlcnNvbmF0aW9uIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6ImVtYWlsIHByb2ZpbGUiLCJzaWQiOiJlYjIzNTI0MC0wYjQ3LTQ4ZmEtOGIzZS1mM2IzMTBkMzUyZTMiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInByZWZlcnJlZF91c2VybmFtZSI6ImFkbWluIn0.cLgbN9kygRwthUx0R0FazPfIUeEUVnw4HnDgN-Hsnm9oXVr6MqmfTRKEI-6n62dlnVKsdadF_tWf3jp26d6neLj1zlR-vojwaHm8A08S9m6IeMr9e0CGiYVHjrJtEeTgq6P9cJJfe7uuhSSvlG3ltFPDxaAe14Dz3BjhLO3iaCRkWfAZjKmnW-IMzzzHfGH-7of7qCAlF5ObEax38mf1Q0OmsPA4_5po-FFtw7H7FfDjsr6EXgtdwloDePkk2XIHs2XsIo0YugVHC9GqCWgBA8MBvCirFivqM53paZMnjhpQH-xgTpYGWlw3WNbG2Rny2GoEwIxdYOUO2amDQ_zkrQ" + end + let(:rsa_signed_access_token_with_aud) do + "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5N0FteXZvUzhCRkZSZm01ODVHUGdBMTZHMUgyVjIyRWR4eHVBWVV1b0trIn0.eyJleHAiOjE3MjEyODQ3NjksImlhdCI6MTcyMTI4NDcwOSwianRpIjoiNjhiYzNmZTMtNDFhZi00MGUwLTg4NGEtNDgxNTM1MTU3NjIyIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay5sb2NhbC9yZWFsbXMvbWFzdGVyIiwiYXVkIjpbImh0dHBzOi8vb3BlbnByb2plY3QubG9jYWwiLCJtYXN0ZXItcmVhbG0iLCJhY2NvdW50Il0sInN1YiI6ImI3MGUyZmJmLWVhNjgtNDIwYy1hN2E1LTBhMjg3Y2I2ODljNiIsInR5cCI6IkJlYXJlciIsImF6cCI6Imh0dHBzOi8vb3BlbnByb2plY3QubG9jYWwiLCJzZXNzaW9uX3N0YXRlIjoiNWI5OWM3M2EtY2QwNS00N2MwLTgwZTctODRjYTNiYTI0MDQ1IiwiYWNyIjoiMSIsImFsbG93ZWQtb3JpZ2lucyI6WyJodHRwczovL29wZW5wcm9qZWN0LmxvY2FsIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjcmVhdGUtcmVhbG0iLCJkZWZhdWx0LXJvbGVzLW1hc3RlciIsIm9mZmxpbmVfYWNjZXNzIiwiYWRtaW4iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7Im1hc3Rlci1yZWFsbSI6eyJyb2xlcyI6WyJ2aWV3LXJlYWxtIiwidmlldy1pZGVudGl0eS1wcm92aWRlcnMiLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsImNyZWF0ZS1jbGllbnQiLCJtYW5hZ2UtdXNlcnMiLCJxdWVyeS1yZWFsbXMiLCJ2aWV3LWF1dGhvcml6YXRpb24iLCJxdWVyeS1jbGllbnRzIiwicXVlcnktdXNlcnMiLCJtYW5hZ2UtZXZlbnRzIiwibWFuYWdlLXJlYWxtIiwidmlldy1ldmVudHMiLCJ2aWV3LXVzZXJzIiwidmlldy1jbGllbnRzIiwibWFuYWdlLWF1dGhvcml6YXRpb24iLCJtYW5hZ2UtY2xpZW50cyIsInF1ZXJ5LWdyb3VwcyJdfSwiYWNjb3VudCI6eyJyb2xlcyI6WyJtYW5hZ2UtYWNjb3VudCIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwidmlldy1wcm9maWxlIl19fSwic2NvcGUiOiJlbWFpbCBwcm9maWxlIiwic2lkIjoiNWI5OWM3M2EtY2QwNS00N2MwLTgwZTctODRjYTNiYTI0MDQ1IiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhZG1pbiJ9.WS2TWDHFU2Amglj6j4LYhsUY5oyw3J7PhllGf0MH3Kz_ETT7GZCR6MvtvY1EuOb11t_YKrQ6M8LBHhh5j9mrFNrg-vTXMaYmXwXCQxfKtHvTVbo4coEPpnW_8NEVBG8dvduLRVK_o7BbNhZH9FCe5sb_7EbA18E7evHNLWi9co4nLsSBQSeBoHRSJqD28Yr2Xj1u618bVz_grAlm0DiwhJhGzkv-JJtUGa1xQyIkNeogPWalnLpzspa2Q2i5LeLB02aoPDlQ_PkUF6Tn6IGY2for8HQQlYkjBvhxL_wMBDoNRKlFycqkCBSedsPx2m6NdmBK8ppLgaMfKe0uVGvaTg" + end + let(:token_exp) { Time.zone.at(JWT.decode(token, nil, false)[0]["exp"]) } + let(:token_sub) { JWT.decode(token, nil, false)[0]["sub"] } + let(:expected_message) { "You did not provide the correct credentials." } + + before do + create(:user, identity_url: "keycloak:#{token_sub}") + stub_request(:get, "https://keycloak.local/realms/master/protocol/openid-connect/certs") + .with( + headers: { + "Accept" => "*/*", + "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3", + "User-Agent" => "JSON::JWK::Set::Fetcher 2.7.2" + } + ) + .to_return(status: 200, body: '{"keys":[{"kid":"CANAG6lJUPKqKDoWxxXL5wAHf2U18BAzm_LJm7RPTGk","kty":"RSA","alg":"RSA-OAEP","use":"enc","n":"nqJexS6n-SxKSDUxXp_dsNwDW6cZ4Rtgqq9ut_lp1CNSph5wTnLG3aQQsTEvx5o3-SZ-pHjJ0gtEpg7clAz-w-YQyZoAXkFtQqmZJxsmdS4K0yILxO3WUNdJQlutjmq-Ri50Senn5IV7yEYWLo8St1qzUqWZhp0HKudyty24triC9UJTK03W3_Tr5c1X8vKL8duAjvLB7p_sYUOrnLq5pD5lqwxVSAiN8qS5zVNZMrhGV5aN1vN_vue_tw8c2SVOCLLTrUh3441rYaeo-UwQZF7ZTm30xflqAIfe8qMoB20wtWYAXR0D5iqkkdEH4XanCYVm5vdUFIPPvXZhRDWoNQ","e":"AQAB","x5c":["MIICmzCCAYMCBgGQupeGPzANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzE2MDgwODMwWhcNMzQwNzE2MDgxMDEwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCeol7FLqf5LEpINTFen92w3ANbpxnhG2Cqr263+WnUI1KmHnBOcsbdpBCxMS/Hmjf5Jn6keMnSC0SmDtyUDP7D5hDJmgBeQW1CqZknGyZ1LgrTIgvE7dZQ10lCW62Oar5GLnRJ6efkhXvIRhYujxK3WrNSpZmGnQcq53K3Lbi2uIL1QlMrTdbf9OvlzVfy8ovx24CO8sHun+xhQ6ucurmkPmWrDFVICI3ypLnNU1kyuEZXlo3W83++57+3DxzZJU4IstOtSHfjjWthp6j5TBBkXtlObfTF+WoAh97yoygHbTC1ZgBdHQPmKqSR0QfhdqcJhWbm91QUg8+9dmFENag1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAB/AGvP0gviPoJszj/oQgBsMpPGRHLpnTmrXnTaa7Xk2sgExAb4zUAwxGjtR347t697cpiKQYBkR2ndswnt93Sx/Ot+yn5BdYcNvZuEh5jb5bkH2V4h6/LrYljTymby+XPBEf+XLhBOjoI3SKtNJk4pEqVNwLuKKbObqJcE3G3VBVSdzRUcIrjZr7yAQeLnhczS3hJ0Ct6Y7S5Q6DK+/PU1+AvlW+7GfzpRMqVfLcqhNpRwdCVGlJYKaUJfIe1vav10D94xA0U1sKex3iA1S+1HlS2BCWx/0rXwgcquMpUZlOAKiT0K6SIFxBFFnM9eQbF97Dz7Bzw+jyqStGUcH9YA="],"x5t":"TuBfrOL00KXDrOWTv3jw7Uxx3hA","x5t#S256":"7su5lOXF5qcMuvp44ynsoyk3B0l9Sr_bOVlg768shpY"},{"kid":"97AmyvoS8BFFRfm585GPgA16G1H2V22EdxxuAYUuoKk","kty":"RSA","alg":"RS256","use":"sig","n":"jMB2r7BG4QJzLnA2_fgG1mxlh2RX_MSx0lc2lrPIVFGYBuAu8irwRLSexX5aQdD_AtnxLD4g9jiG6VEDwmWopEe0fr-QMl0IiES5tJuQMrjhajOkzr8xTYu6zl-knL0tu99iRbmKNYzEcv0TAgY_95n4gD5tPhYvY4gXuHrFKqYkJQPsSgoThlH7hAtfzsDt6yp3P2lQUESGg3pzc_J_NKnQkkggcNB06Hlz4DmcHxhWXK51P1V9cE7qh4PrhsJ-SOH5grcN9PtOZi6f2VlWdFdyisT-YehNklfVqBtdCLm7Ocghhl0HSgLuV-9dHCdwBLUpABsdsd0L3LRCUgRfjQ","e":"AQAB","x5c":["MIICmzCCAYMCBgGQupeFFTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjQwNzE2MDgwODMwWhcNMzQwNzE2MDgxMDEwWjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCMwHavsEbhAnMucDb9+AbWbGWHZFf8xLHSVzaWs8hUUZgG4C7yKvBEtJ7FflpB0P8C2fEsPiD2OIbpUQPCZaikR7R+v5AyXQiIRLm0m5AyuOFqM6TOvzFNi7rOX6ScvS2732JFuYo1jMRy/RMCBj/3mfiAPm0+Fi9jiBe4esUqpiQlA+xKChOGUfuEC1/OwO3rKnc/aVBQRIaDenNz8n80qdCSSCBw0HToeXPgOZwfGFZcrnU/VX1wTuqHg+uGwn5I4fmCtw30+05mLp/ZWVZ0V3KKxP5h6E2SV9WoG10Iubs5yCGGXQdKAu5X710cJ3AEtSkAGx2x3QvctEJSBF+NAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIoBCsOO0bXiVspoXkqdOts4+3sULbbp5aEwQscmLX017Zvv5jxdkZxUYk8L08lNB+WlC1ES4VlmtE06D0cWYErGpArJzVBKgYSA3CkA9veBEugHviMqfwg3suNc8S+GtaRBvpbVZtXydjjqA8GZ4eKhPoJLHHCX6X2Ad33Cdt0/ftucjTqAKVzzzgWZejy+ZKP6ybAqYJ+EZoPUXlyWT3uwcpGEJ3nzOYYGTfxOSmAwnH2v5Z/JWr9ex5o/+QBuBhFcg0z8NcHa3Z0E6ZC9GGxV7XztBqYicO+nONHTLCctoJmyXvLM4j8qIG2UQgPIiwIL0Jkz6xQAYyXvsb+LhM8="],"x5t":"BFrni6MoX-CJwtMT4vzij1HBSTI","x5t#S256":"-Ge3y4JRezxhGTDfbkNoz7prkokzYtbKQ9ardPtfcz4"}]}', headers: {}) + + header "Authorization", "Bearer #{token}" + end + + context "when token is issued by provider not configured in OP" do + let(:token) do + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJpc3N1ZXIuY29tIn0.C9gEPaqdNSEZ4dZHz0z51VCylScIEqRLnwMCkNXuz6g" + end + + it do + get resource + expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq("Bearer realm=\"OpenProject API\" error=\"invalid_token\" error_description=\"The access token issuer is unknown\"") + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end + end + + context "when token is issued by provider configured in OP" do + context "when token signature algorithm is not supported" do + let(:token) do + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJodHRwczovL2tleWNsb2FrLmxvY2FsL3JlYWxtcy9tYXN0ZXIifQ.Pwod8ZJqq3jWsbnrGw4ZU1-aLS2bSicb8PgiF78JHUc" + end + + it do + get resource + expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq("Bearer realm=\"OpenProject API\" error=\"invalid_token\" error_description=\"Token signature algorithm is not supported\"") + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end + end + + context "when access token has not expired yet" do + context "when aud does not contain client_id" do + let(:token) { rsa_signed_access_token_without_aud } + + it do + Timecop.freeze(token_exp - 20.seconds) do + get resource + end + expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="invalid_token" error_description="The access token audience claim is wrong"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end + end + + context "when aud contains client_id" do + let(:token) { rsa_signed_access_token_with_aud } + + it do + Timecop.freeze(token_exp - 20.seconds) do + get resource + end + expect(last_response).to have_http_status :ok + end + end + end + + context "when access token has expired already" do + let(:token) { rsa_signed_access_token_without_aud } + + it do + Timecop.freeze(token_exp + 20.seconds) do + get resource + end + + expect(last_response).to have_http_status :unauthorized + expect(last_response.header["WWW-Authenticate"]).to eq('Bearer realm="OpenProject API" error="invalid_token" error_description="The access token expired"') + expect(JSON.parse(last_response.body)).to eq(error_response_body) + end + end + end + end end diff --git a/spec/support/wait_for.rb b/spec/support/wait_for.rb index 6a2a93f3f8a8..8fcde59104fd 100644 --- a/spec/support/wait_for.rb +++ b/spec/support/wait_for.rb @@ -25,8 +25,7 @@ module Wait # # Examples: # wait_for(ticker.tape).to eq("··-·") - # wait_for(ticker.tape).to eq("··-· ---") - # wait_for(ticker.tape).to eq("··-· --- ---", timeout: 5) + # wait_for { ticker.tape }.to eq("··-· ---") def wait_for(value = Target::UndefinedValue, &block) Target.for(value, block) end @@ -36,6 +35,10 @@ def wait_for(value = Target::UndefinedValue, &block) # @param timeout [Numeric] time in seconds to wait up for assertions to pass # @param delay [Numeric] time in seconds elapsing between two checks of an # assertion + # Examples: + # with_wait(timeout: 10, delay: 1) do + # wait_for { ticker.tape }.to eq("··-· ---") + # end def with_wait(timeout: nil, delay: nil) original_timeout = RSpec.configuration.wait_timeout original_delay = RSpec.configuration.wait_delay From a0fa3bb1f1b78fc2f05b02a98a9cda8fb2472bf8 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Wed, 24 Jul 2024 09:52:06 +0200 Subject: [PATCH 2/5] Remove unnecessary double negation. Co-authored-by: Marcello Rocha --- lib_static/open_project/authentication.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib_static/open_project/authentication.rb b/lib_static/open_project/authentication.rb index 3dc9d2e19d32..2752e0bbddbb 100644 --- a/lib_static/open_project/authentication.rb +++ b/lib_static/open_project/authentication.rb @@ -279,7 +279,7 @@ def auth_schemes(scope) strategies = Array(Manager.scope_config(scope).strategies) Manager.auth_schemes - .select { |_, info| scope.nil? or not !info.strategies.intersect?(strategies) } + .select { |_, info| scope.nil? or info.strategies.intersect?(strategies) } .keys end end From 1462f37ef21701eb3bc3d873874b244d195f10d7 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Wed, 24 Jul 2024 11:06:27 +0200 Subject: [PATCH 3/5] Refactor oauth strategies valid? method. Co-authored-by: Marcello Rocha --- .../strategies/warden/doorkeeper_oauth.rb | 16 +++++++++------- .../authentication/strategies/warden/jwt_oidc.rb | 13 ++++++------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb index 11cd7c23eccb..d3d2a82ea1f4 100644 --- a/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb +++ b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb @@ -10,13 +10,15 @@ class DoorkeeperOAuth < ::Warden::Strategies::Base def valid? access_token = ::Doorkeeper::OAuth::Token .from_request(decorated_request, *Doorkeeper.configuration.access_token_methods) - access_token.present? && - begin - JWT.decode(access_token, nil, false) - false - rescue JWT::DecodeError - true - end + + # No access token found, so invalid strategy. + return false if access_token.blank? + + # We don't want JWT as our OAuth Bearer token + JWT.decode(access_token, nil, false) + false + rescue JWT::DecodeError + true end def authenticate! diff --git a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb index 19a5a1a75952..453881cc070b 100644 --- a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb +++ b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb @@ -15,13 +15,12 @@ def valid? @access_token = ::Doorkeeper::OAuth::Token.from_bearer_authorization( ::Doorkeeper::Grape::AuthorizationDecorator.new(request) ) - @access_token.present? && - begin - @unverified_payload, @unverified_header = JWT.decode(@access_token, nil, false) - @unverified_header.present? && @unverified_payload.present? - rescue JWT::DecodeError - false - end + return false if @access_token.blank? + + @unverified_payload, @unverified_header = JWT.decode(@access_token, nil, false) + @unverified_header.present? && @unverified_payload.present? + rescue JWT::DecodeError + false end def authenticate! From 118b911e496464bfa4bc54a197e70527eb940307 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Wed, 24 Jul 2024 11:10:34 +0200 Subject: [PATCH 4/5] Refactor complex authenticate method. - Merge fail and return. - Use guard clauses to reduce nesting a bit. --- .../strategies/warden/jwt_oidc.rb | 83 +++++++++---------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb index 453881cc070b..3eceb6d977a6 100644 --- a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb +++ b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb @@ -16,7 +16,7 @@ def valid? ::Doorkeeper::Grape::AuthorizationDecorator.new(request) ) return false if @access_token.blank? - + @unverified_payload, @unverified_header = JWT.decode(@access_token, nil, false) @unverified_header.present? && @unverified_payload.present? rescue JWT::DecodeError @@ -26,53 +26,46 @@ def valid? def authenticate! issuer = @unverified_payload["iss"] provider = OpenProject::OpenIDConnect.providers.find { |p| p.configuration[:issuer] == issuer } if issuer.present? - if provider.present? - client_id = provider.configuration.fetch(:identifier) - alg = @unverified_header.fetch("alg") - key = - if SUPPORTED_ALG.include?(alg) - kid = @unverified_header.fetch("kid") - jwks_uri = provider.configuration[:jwks_uri] - JSON::JWK::Set::Fetcher.fetch(jwks_uri, kid:).to_key - else - fail_with_header!(error: "invalid_token", error_description: "Token signature algorithm is not supported") - return - end - begin - verified_payload, = JWT.decode( - @access_token, - key, - true, - { - algorithm: alg, - verify_iss: true, - verify_aud: true, - iss: issuer, - aud: client_id, - required_claims: ["sub", "iss", "aud"] - } - ) - rescue JWT::ExpiredSignature - fail_with_header!(error: "invalid_token", error_description: "The access token expired") - return - rescue JWT::ImmatureSignature - # happens when nbf time is less than current - fail_with_header!(error: "invalid_token", error_description: "The access token is used too early") + if provider.blank? + return fail_with_header!(error: "invalid_token", error_description: "The access token issuer is unknown") + end - return - rescue JWT::InvalidIssuerError - fail_with_header!(error: "invalid_token", error_description: "The access token issuer is wrong") - return - rescue JWT::InvalidAudError - fail_with_header!(error: "invalid_token", error_description: "The access token audience claim is wrong") - return - end + client_id = provider.configuration.fetch(:identifier) + alg = @unverified_header.fetch("alg") + if SUPPORTED_ALG.exclude?(alg) + return fail_with_header!(error: "invalid_token", error_description: "Token signature algorithm is not supported") + end - user = User.find_by(identity_url: "#{provider.name}:#{verified_payload['sub']}") - success!(user) if user - else - fail_with_header!(error: "invalid_token", error_description: "The access token issuer is unknown") + kid = @unverified_header.fetch("kid") + jwks_uri = provider.configuration[:jwks_uri] + key = JSON::JWK::Set::Fetcher.fetch(jwks_uri, kid:).to_key + begin + verified_payload, = JWT.decode( + @access_token, + key, + true, + { + algorithm: alg, + verify_iss: true, + verify_aud: true, + iss: issuer, + aud: client_id, + required_claims: ["sub", "iss", "aud"] + } + ) + rescue JWT::ExpiredSignature + return fail_with_header!(error: "invalid_token", error_description: "The access token expired") + rescue JWT::ImmatureSignature + # happens when nbf time is less than current + return fail_with_header!(error: "invalid_token", error_description: "The access token is used too early") + rescue JWT::InvalidIssuerError + return fail_with_header!(error: "invalid_token", error_description: "The access token issuer is wrong") + rescue JWT::InvalidAudError + return fail_with_header!(error: "invalid_token", error_description: "The access token audience claim is wrong") end + + user = User.find_by(identity_url: "#{provider.name}:#{verified_payload['sub']}") + success!(user) if user end end end From 5c57582a1a365c5dd9ba9120216a0f8805c736e4 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Wed, 24 Jul 2024 11:37:44 +0200 Subject: [PATCH 5/5] Add comments to bearer token stategies. --- .../authentication/strategies/warden/doorkeeper_oauth.rb | 2 ++ .../open_project/authentication/strategies/warden/jwt_oidc.rb | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb index d3d2a82ea1f4..ac80d71effe3 100644 --- a/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb +++ b/lib_static/open_project/authentication/strategies/warden/doorkeeper_oauth.rb @@ -7,6 +7,8 @@ module Warden class DoorkeeperOAuth < ::Warden::Strategies::Base include FailWithHeader + # The strategy is supposed to handle bearer tokens that are not JWT. + # These tokens are issued by OpenProject def valid? access_token = ::Doorkeeper::OAuth::Token .from_request(decorated_request, *Doorkeeper.configuration.access_token_methods) diff --git a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb index 3eceb6d977a6..e1d829d19194 100644 --- a/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb +++ b/lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb @@ -11,6 +11,8 @@ class JwtOidc < ::Warden::Strategies::Base RS512 ].freeze + # The strategy is supposed to only handle JWT. + # These tokens are supposed to be issued by configured OIDC. def valid? @access_token = ::Doorkeeper::OAuth::Token.from_bearer_authorization( ::Doorkeeper::Grape::AuthorizationDecorator.new(request)