mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
4d305df714
We generate those tokens with a prefix, so that we can decide by looking at a token, whether it's an API Token or a different kind of token, so that we can decide which code path to choose for validating the token. The usage of access tokens as Bearer token has the usability advantage, that you can paste them as plaintext into tools that expect you to specify the token as a header. Also the Basic auth approach for our old tokens usually rather caused issues, such as browsers prompting for credentials in surprising situations. If we were to deprecate basic authentication one day, this change today could've been the first step towards that.
724 lines
26 KiB
Ruby
724 lines
26 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
#-- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) 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.
|
|
#++
|
|
|
|
require "spec_helper"
|
|
|
|
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
|
|
let(:resource_metadata) { 'resource_metadata="http://test.host/.well-known/oauth-protected-resource"' }
|
|
|
|
describe "oauth" do
|
|
let(:oauth_access_token) { "" }
|
|
let(:expected_message) { "You did not provide the correct credentials." }
|
|
|
|
before do
|
|
user
|
|
|
|
header "Authorization", "Bearer #{oauth_access_token}"
|
|
|
|
get resource
|
|
end
|
|
|
|
context "with a valid access token" do
|
|
let(:token) { create(:oauth_access_token, resource_owner: user) }
|
|
let(:oauth_access_token) { token.plaintext_token }
|
|
|
|
it "authenticates successfully" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
end
|
|
|
|
context "with an invalid access token" do
|
|
let(:oauth_access_token) { "1337" }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
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 }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when the token's application is disabled" do
|
|
let(:token) { create(:oauth_access_token, resource_owner: user, application: create(:oauth_application, enabled: false)) }
|
|
let(:oauth_access_token) { token.plaintext_token }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "with an expired access token" do
|
|
let(:token) { create(:oauth_access_token, resource_owner: user) }
|
|
let(:oauth_access_token) { token.plaintext_token }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
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(expected_www_auth_header)
|
|
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 }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="insufficient_scope"}
|
|
end
|
|
|
|
it "returns forbidden" do
|
|
expect(last_response).to have_http_status :forbidden
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when the token's resource owner can't be found" do
|
|
let(:token) { create(:oauth_access_token, resource_owner: user, application:) }
|
|
let(:application) { create(:oauth_application) }
|
|
let(:oauth_access_token) { token.plaintext_token }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
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(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
|
|
context "when the application has a client credentials user configured" do
|
|
let(:application) { create(:oauth_application, client_credentials_user_id: create(:user).id) }
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when the token's resource owner is locked" do
|
|
let(:token) { create(:oauth_access_token, resource_owner: user) }
|
|
let(:oauth_access_token) { token.plaintext_token }
|
|
let(:user) { create(:user, :locked) }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when there is no resource owner on the token" do
|
|
let(:token) { create(:oauth_access_token, resource_owner: nil, application:) }
|
|
let(:application) { create(:oauth_application) }
|
|
let(:oauth_access_token) { token.plaintext_token }
|
|
|
|
# Note: This is just caused by DoorkeeperOauth rejecting to handle this case and auth falling through to basic auth
|
|
# more specific examples can be found at spec/requests/oauth/client_credentials_flow_spec.rb
|
|
let(:expected_message) { "You need to be authenticated to access this resource." }
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
|
|
# Note: This is just caused by DoorkeeperOauth rejecting to handle this case and auth falling through to basic auth
|
|
# more specific examples can be found at spec/requests/oauth/client_credentials_flow_spec.rb
|
|
expect(last_response.header["WWW-Authenticate"]).to eq('Basic realm="OpenProject API"')
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
|
|
context "when the application has a client credentials user configured" do
|
|
let(:application) { create(:oauth_application, client_credentials_user_id: user.id) }
|
|
|
|
it "authenticates successfully" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
|
|
context "and the client credentials user is locked" do
|
|
let(:user) { create(:user, :locked) }
|
|
let(:expected_message) { "You did not provide the correct credentials." }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "API Key as Bearer token" do
|
|
let(:token) { create(:api_token, user:) }
|
|
let(:bearer_token) { token.plain_value }
|
|
let(:expected_message) { "You did not provide the correct credentials." }
|
|
|
|
before do
|
|
user
|
|
|
|
header "Authorization", "Bearer #{bearer_token}"
|
|
|
|
get resource
|
|
end
|
|
|
|
context "with a valid access token" do
|
|
it "authenticates successfully" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
end
|
|
|
|
context "with an invalid access token" do
|
|
let(:bearer_token) { "opapi-1337" }
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when the token's user can't be found" do
|
|
let(:expected_www_auth_header) do
|
|
%{Bearer realm="OpenProject API", #{resource_metadata}, scope="api_v3", error="invalid_token"}
|
|
end
|
|
|
|
around do |ex|
|
|
# create the token before deleting the user; right now it especially works, because a foreign key constraint prevents
|
|
# tokens without users
|
|
token
|
|
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(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when the token's user is locked" do
|
|
let(:user) { create(:user, :locked) }
|
|
let(:expected_www_auth_header) do
|
|
"Bearer realm=\"OpenProject API\", #{resource_metadata}, scope=\"api_v3\", error=\"invalid_token\", " \
|
|
"error_description=\"#{expected_error_description}\""
|
|
end
|
|
let(:expected_error_description) { "The user account is locked" }
|
|
|
|
it "returns unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
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
|
|
|
|
def set_basic_auth_header(user, password)
|
|
credentials = ActionController::HttpAuthentication::Basic.encode_credentials user, password
|
|
header "Authorization", credentials
|
|
end
|
|
|
|
shared_examples "it is basic auth protected" do
|
|
context "when not allowed", with_config: { apiv3_enable_basic_auth: false } do
|
|
context "with valid credentials" do
|
|
before do
|
|
set_basic_auth_header(username, password)
|
|
get resource
|
|
end
|
|
|
|
it "returns 401 unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when allowed", with_config: { apiv3_enable_basic_auth: true } do
|
|
context "without credentials" do
|
|
before do
|
|
get resource
|
|
end
|
|
|
|
it "returns 401 unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
end
|
|
|
|
it "returns the correct JSON response" do
|
|
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"'
|
|
end
|
|
end
|
|
|
|
context "with invalid credentials" do
|
|
let(:expected_message) { "You did not provide the correct credentials." }
|
|
|
|
before do
|
|
set_basic_auth_header(username, password.reverse)
|
|
get resource
|
|
end
|
|
|
|
it "returns 401 unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
end
|
|
|
|
it "returns the correct JSON response" do
|
|
expect(JSON.parse(last_response.body)).to eq error_response_body
|
|
end
|
|
|
|
it "returns the correct content type header" do
|
|
expect(last_response.headers["Content-Type"]).to eq "application/hal+json; charset=utf-8"
|
|
end
|
|
|
|
it "returns the WWW-Authenticate header" do
|
|
expect(last_response.header["WWW-Authenticate"])
|
|
.to include 'Basic realm="OpenProject API"'
|
|
end
|
|
end
|
|
|
|
context "with no credentials" do
|
|
let(:expected_message) { "You need to be authenticated to access this resource." }
|
|
|
|
before do
|
|
post "/api/v3/time_entries/form"
|
|
end
|
|
|
|
it "returns 401 unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
end
|
|
|
|
it "returns the correct JSON response" do
|
|
expect(JSON.parse(last_response.body)).to eq error_response_body
|
|
end
|
|
|
|
it "returns the correct content type header" do
|
|
expect(last_response.headers["Content-Type"]).to eq "application/hal+json; charset=utf-8"
|
|
end
|
|
|
|
it "returns the WWW-Authenticate header" do
|
|
expect(last_response.header["WWW-Authenticate"])
|
|
.to include 'Basic realm="OpenProject API"'
|
|
end
|
|
end
|
|
|
|
context 'with invalid credentials an X-Authentication-Scheme "Session"' do
|
|
let(:expected_message) { "You did not provide the correct credentials." }
|
|
|
|
before do
|
|
set_basic_auth_header(username, password.reverse)
|
|
header "X-Authentication-Scheme", "Session"
|
|
get resource
|
|
end
|
|
|
|
it "returns 401 unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
end
|
|
|
|
it "returns the correct JSON response" do
|
|
expect(JSON.parse(last_response.body)).to eq error_response_body
|
|
end
|
|
|
|
it "returns the correct content type header" do
|
|
expect(last_response.headers["Content-Type"]).to eq "application/hal+json; charset=utf-8"
|
|
end
|
|
|
|
it "returns the WWW-Authenticate header" do
|
|
expect(last_response.header["WWW-Authenticate"])
|
|
.to include 'Session realm="OpenProject API"'
|
|
end
|
|
end
|
|
|
|
context "with valid credentials" do
|
|
before do
|
|
set_basic_auth_header(username, password)
|
|
get resource
|
|
end
|
|
|
|
it "returns 200 OK" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with login required" do
|
|
before do
|
|
allow(Setting).to receive_messages(login_required: true, login_required?: true)
|
|
end
|
|
|
|
context "with global basic auth configured" do
|
|
let(:username) { "root" }
|
|
let(:password) { "toor" }
|
|
|
|
before do
|
|
strategies::GlobalBasicAuth.configure! user: "root", password: "toor"
|
|
end
|
|
|
|
it_behaves_like "it is basic auth protected"
|
|
|
|
describe "user basic auth" do
|
|
let(:api_key) { create(:api_token) }
|
|
|
|
let(:username) { "apikey" }
|
|
let(:password) { api_key.plain_value }
|
|
|
|
# check that user basic auth is tried when global basic auth fails
|
|
it_behaves_like "it is basic auth protected"
|
|
end
|
|
end
|
|
|
|
describe "user basic auth" do
|
|
let(:api_key) { create(:api_token) }
|
|
|
|
let(:username) { "apikey" }
|
|
let(:password) { api_key.plain_value }
|
|
|
|
# check that user basic auth works on its own too
|
|
it_behaves_like "it is basic auth protected"
|
|
end
|
|
end
|
|
|
|
context "when enabled", with_config: { apiv3_enable_basic_auth: true } do
|
|
context "without login required" do
|
|
before do
|
|
allow(Setting).to receive_messages(login_required: false, login_required?: false)
|
|
end
|
|
|
|
context "with global and user basic auth enabled" do
|
|
let(:username) { "hancholo" }
|
|
let(:password) { "olooleol" }
|
|
|
|
let(:api_user) { create(:user, login: "user_account") }
|
|
let(:api_key) { create(:api_token, user: api_user) }
|
|
|
|
before do
|
|
config = { user: "global_account", password: "global_password" }
|
|
strategies::GlobalBasicAuth.configure! config
|
|
end
|
|
|
|
context "without credentials" do
|
|
before do
|
|
get resource
|
|
end
|
|
|
|
it "returns 200 OK" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
|
|
it '"login"s the anonymous user' do
|
|
expect(User.current).to be_anonymous
|
|
end
|
|
end
|
|
|
|
context "with invalid credentials" do
|
|
before do
|
|
set_basic_auth_header(username, password)
|
|
get resource
|
|
end
|
|
|
|
it "returns 401 unauthorized" do
|
|
expect(last_response).to have_http_status :unauthorized
|
|
end
|
|
end
|
|
|
|
context "with valid global credentials" do
|
|
before do
|
|
set_basic_auth_header("global_account", "global_password")
|
|
get resource
|
|
end
|
|
|
|
it "returns 200 OK" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
end
|
|
|
|
context "with valid user credentials" do
|
|
before do
|
|
set_basic_auth_header("apikey", api_key.plain_value)
|
|
get resource
|
|
end
|
|
|
|
it "returns 200 OK" do
|
|
expect(last_response).to have_http_status :ok
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe("OIDC", :webmock) do
|
|
let(:jwk) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: "my-kid", use: "sig", alg: "RS256") }
|
|
let(:payload) do
|
|
{
|
|
"exp" => token_exp.to_i,
|
|
"iat" => 1721283370,
|
|
"jti" => "c526b435-991f-474a-ad1b-c371456d1fd0",
|
|
"iss" => token_issuer,
|
|
"aud" => token_aud,
|
|
"sub" => token_sub,
|
|
"typ" => "Bearer",
|
|
"azp" => "https://openproject.local",
|
|
"session_state" => "eb235240-0b47-48fa-8b3e-f3b310d352e3",
|
|
"acr" => "1",
|
|
"allowed-origins" => ["https://openproject.local"],
|
|
"realm_access" => { "roles" => ["create-realm", "default-roles-master", "offline_access", "admin", "uma_authorization"] },
|
|
"resource_access" =>
|
|
{ "master-realm" =>
|
|
{ "roles" =>
|
|
["view-realm",
|
|
"view-identity-providers",
|
|
"manage-identity-providers",
|
|
"impersonation",
|
|
"create-client",
|
|
"manage-users",
|
|
"query-realms",
|
|
"view-authorization",
|
|
"query-clients",
|
|
"query-users",
|
|
"manage-events",
|
|
"manage-realm",
|
|
"view-events",
|
|
"view-users",
|
|
"view-clients",
|
|
"manage-authorization",
|
|
"manage-clients",
|
|
"query-groups"] },
|
|
"account" => { "roles" => ["manage-account", "manage-account-links", "view-profile"] } },
|
|
"scope" => token_scope,
|
|
"sid" => "eb235240-0b47-48fa-8b3e-f3b310d352e3",
|
|
"email_verified" => false,
|
|
"preferred_username" => "admin"
|
|
}
|
|
end
|
|
let(:token) { JWT.encode(payload, jwk.signing_key, jwk[:alg], { kid: jwk[:kid] }) }
|
|
let(:token_exp) { 5.minutes.from_now }
|
|
let(:token_sub) { "b70e2fbf-ea68-420c-a7a5-0a287cb689c6" }
|
|
let(:token_aud) { ["https://openproject.local", "master-realm", "account"] }
|
|
let(:token_issuer) { "https://keycloak.local/realms/master" }
|
|
let(:token_scope) { "email profile api_v3" }
|
|
let(:expected_message) { "You did not provide the correct credentials." }
|
|
let(:expected_www_auth_header) do
|
|
"Bearer realm=\"OpenProject API\", #{resource_metadata}, scope=\"api_v3\", error=\"#{expected_error}\", " \
|
|
"error_description=\"#{expected_error_description}\""
|
|
end
|
|
let(:expected_error) { "invalid_token" }
|
|
let(:keys_request_stub) do
|
|
stub_request(:get, "https://keycloak.local/realms/master/protocol/openid-connect/certs")
|
|
.to_return(status: 200, body: JWT::JWK::Set.new(jwk_response).export.to_json, headers: {})
|
|
end
|
|
let(:jwk_response) { jwk }
|
|
let(:user) { create(:user, authentication_provider: create(:oidc_provider), external_id: token_sub) }
|
|
|
|
before do
|
|
user.save!
|
|
keys_request_stub
|
|
|
|
header "Authorization", "Bearer #{token}"
|
|
end
|
|
|
|
it "succeeds" do
|
|
get resource
|
|
|
|
expect(last_response).to have_http_status :ok
|
|
expect(last_response.header["WWW-Authenticate"]).to be_blank
|
|
end
|
|
|
|
context "when token is issued by provider not configured in OP" do
|
|
let(:token_issuer) { "https://eve.example.com" }
|
|
let(:expected_error_description) { "The access token issuer is unknown" }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when token signature algorithm is not supported" do
|
|
let(:token) { JWT.encode(payload, "secret", "HS256", { kid: "97AmyvoS8BFFRfm585GPgA16G1H2V22EdxxuAYUuoKk" }) }
|
|
let(:expected_error_description) { "Token signature algorithm HS256 is not supported" }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when aud does not contain client_id" do
|
|
let(:token_aud) { ["Lisa", "Bart"] }
|
|
let(:expected_error_description) { 'Invalid audience. Expected https://openproject.local, received [\"Lisa\", \"Bart\"]' }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when the scope does not permit access to APIv3" do
|
|
let(:token_scope) { "profile email" }
|
|
let(:expected_error) { "insufficient_scope" }
|
|
let(:expected_error_description) { "Requires scope api_v3 to access this resource." }
|
|
|
|
it "fails with HTTP 403 Forbidden" do
|
|
get resource
|
|
|
|
expect(last_response).to have_http_status :forbidden
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when access token has expired already" do
|
|
let(:token_exp) { 5.minutes.ago }
|
|
let(:expected_error_description) { "Signature has expired" }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
|
|
it "caches keys request to keycloak" do
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
|
|
expect(keys_request_stub).to have_been_made.once
|
|
end
|
|
end
|
|
|
|
context "when kid is absent in keycloak keys response" do
|
|
let(:jwk_response) { JWT::JWK.new(OpenSSL::PKey::RSA.new(2048), kid: "your-kid", use: "sig", alg: "RS256") }
|
|
let(:expected_error_description) { "The signature key ID is unknown" }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when user identified by token is not known" do
|
|
let(:user) { create(:user, authentication_provider: create(:oidc_provider)) }
|
|
let(:expected_error_description) { "The user identified by the token is not known" }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
|
|
context "when user identified by token is locked" do
|
|
let(:user) { create(:user, :locked, authentication_provider: create(:oidc_provider), external_id: token_sub) }
|
|
let(:expected_error_description) { "The user account is locked" }
|
|
|
|
it "fails with HTTP 401 Unauthorized" do
|
|
get resource
|
|
expect(last_response).to have_http_status :unauthorized
|
|
expect(last_response.header["WWW-Authenticate"]).to eq(expected_www_auth_header)
|
|
expect(JSON.parse(last_response.body)).to eq(error_response_body)
|
|
end
|
|
end
|
|
end
|
|
end
|