Skip to content

Commit ef65ef5

Browse files
Validate scope of JWTs
So far we are only accepting JWTs for requests to APIv3 and our scopes are not more fine-grained than that. However, warden strategies are generally responsible for validating the scopes of a token (e.g. compare DoorkeeperOAuth) to the scope of the currently accessed API.
1 parent a2d010f commit ef65ef5

File tree

2 files changed

+28
-1
lines changed

2 files changed

+28
-1
lines changed

lib_static/open_project/authentication/strategies/warden/jwt_oidc.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ def authenticate!
2525
::OpenIDConnect::JwtParser.new(required_claims: ["sub"]).parse(@access_token).either(
2626
->(payload_and_provider) do
2727
payload, provider = payload_and_provider
28+
unless valid_scope?(payload)
29+
return fail_with_header! error: "insufficient_scope",
30+
error_description: "Requires scope #{scope} to access this resource."
31+
end
32+
2833
user = User.find_by(identity_url: "#{provider.slug}:#{payload['sub']}")
2934
if user
3035
success!(user)
@@ -35,6 +40,13 @@ def authenticate!
3540
->(error) { fail_with_header!(error: "invalid_token", error_description: error) }
3641
)
3742
end
43+
44+
private
45+
46+
def valid_scope?(payload)
47+
scopes = (payload["scope"] || "").split
48+
scopes.include?(scope.to_s)
49+
end
3850
end
3951
end
4052
end

spec/requests/api/v3/authentication_spec.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ def set_basic_auth_header(user, password)
406406
"manage-clients",
407407
"query-groups"] },
408408
"account" => { "roles" => ["manage-account", "manage-account-links", "view-profile"] } },
409-
"scope" => "email profile",
409+
"scope" => token_scope,
410410
"sid" => "eb235240-0b47-48fa-8b3e-f3b310d352e3",
411411
"email_verified" => false,
412412
"preferred_username" => "admin"
@@ -417,6 +417,7 @@ def set_basic_auth_header(user, password)
417417
let(:token_sub) { "b70e2fbf-ea68-420c-a7a5-0a287cb689c6" }
418418
let(:token_aud) { ["https://openproject.local", "master-realm", "account"] }
419419
let(:token_issuer) { "https://keycloak.local/realms/master" }
420+
let(:token_scope) { "email profile api_v3" }
420421
let(:expected_message) { "You did not provide the correct credentials." }
421422
let(:keys_request_stub) do
422423
stub_request(:get, "https://keycloak.local/realms/master/protocol/openid-connect/certs")
@@ -479,6 +480,20 @@ def set_basic_auth_header(user, password)
479480
end
480481
end
481482

483+
context "when the scope does not permit access to APIv3" do
484+
let(:token_scope) { "profile email" }
485+
486+
it "fails with HTTP 403 Forbidden" do
487+
get resource
488+
489+
expect(last_response).to have_http_status :forbidden
490+
error = "Requires scope api_v3 to access this resource."
491+
expect(last_response.header["WWW-Authenticate"])
492+
.to eq(%{Bearer realm="OpenProject API", error="insufficient_scope", error_description="#{error}"})
493+
expect(JSON.parse(last_response.body)).to eq(error_response_body)
494+
end
495+
end
496+
482497
context "when access token has expired already" do
483498
let(:token_exp) { 5.minutes.ago }
484499

0 commit comments

Comments
 (0)