Skip to content

Commit 763630d

Browse files
authored
Add integration test for assuming an API Key Role using a Buildkite OIDC token (#5416)
Until recently, Buildkite OIDC tokens did not contain a `jti` claim. At some point in early 2024 it was possible to assume an API Key Role using Buildkite OIDC tokens, but when testing in January 2025 we found the assume role request was failing with an error: > Missing/invalid jti Buildkite has addressed that by adding a `jti` claim to tokens - it's a good claim to include. However, to reduce the risk of regressions in the future, this proposes adding an integration test with a Buildkite-shaped OIDC token. The trait added to the OIDC::Provider factory is based on a real token that I generated then anonymized. I only test the happy path with this token - there's a buncha existing tests for various unhappy paths (expired token, etc) using the Github Actions shaped OIDC token and there's little value in replicating them. Most of the added test is copy-pasted from the happy-path Github Actions test further up the file. Fixes #5412
1 parent 29465ef commit 763630d

File tree

3 files changed

+125
-0
lines changed

3 files changed

+125
-0
lines changed

test/factories/oidc/api_key_role.rb

+18
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,23 @@
1919
]
2020
}
2121
end
22+
23+
trait :buildkite do
24+
provider factory: :oidc_provider_buildkite
25+
sequence(:name) { |n| "Buildkite Pusher #{n}" }
26+
access_policy do
27+
{
28+
statements: [
29+
{ effect: "allow",
30+
principal: { oidc: provider.issuer },
31+
conditions: [
32+
{ operator: "string_equals", claim: "organization_slug", value: "example-org" }
33+
] }
34+
]
35+
}
36+
end
37+
end
38+
39+
factory :oidc_api_key_role_buildkite, traits: [:buildkite]
2240
end
2341
end

test/factories/oidc/provider.rb

+51
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,56 @@
6363
transient do
6464
pkey { OpenSSL::PKey::RSA.generate(2048) }
6565
end
66+
67+
trait :buildkite do
68+
sequence(:issuer) { |n| "https://#{n}.agent.buildkite.com" }
69+
configuration do
70+
{
71+
issuer: issuer,
72+
jwks_uri: "#{issuer}/.well-known/jwks",
73+
id_token_signing_alg_values_supported: [
74+
"RS256"
75+
],
76+
response_types_supported: [
77+
"id_token"
78+
],
79+
scopes_supported: [
80+
"openid"
81+
],
82+
subject_types_supported: %w[
83+
public
84+
pairwise
85+
],
86+
claims_supported: %w[
87+
sub
88+
aud
89+
exp
90+
iat
91+
iss
92+
nbf
93+
jti
94+
organization_id
95+
organization_slug
96+
pipeline_id
97+
pipeline_slug
98+
build_number
99+
build_branch
100+
build_tag
101+
build_commit
102+
build_source
103+
step_key
104+
job_id
105+
agent_id
106+
cluster_id
107+
cluster_name
108+
queue_id
109+
queue_key
110+
runner_environment
111+
]
112+
}
113+
end
114+
end
115+
116+
factory :oidc_provider_buildkite, traits: [:buildkite]
66117
end
67118
end

test/integration/api/v1/oidc/api_key_roles_test.rb

+56
Original file line numberDiff line numberDiff line change
@@ -383,5 +383,61 @@ def jwt(claims = @claims, key: @pkey)
383383
)
384384
end
385385
end
386+
387+
context "with a Buildkite OIDC token" do
388+
setup do
389+
@role = create(:oidc_api_key_role_buildkite, provider: build(:oidc_provider_buildkite, issuer: "https://agent.buildkite.com", pkey: @pkey))
390+
@user = @role.user
391+
392+
@claims = {
393+
"aud" => "rubygems.org",
394+
"exp" => 1_680_020_837,
395+
"iat" => 1_680_020_537,
396+
"iss" => "https://agent.buildkite.com",
397+
"jti" => "0194b014-8517-7cef-b232-76a827315f08",
398+
"nbf" => 1_680_019_937,
399+
"sub" => "organization:example-org:pipeline:example-pipeline:ref:refs/heads/main:commit:b5ffe3aeea51cec6c41aef16e45ee6bce47d8810:step:",
400+
"organization_slug" => "example-org",
401+
"pipeline_slug" => "example-pipeline",
402+
"build_number" => 5,
403+
"build_branch" => "main",
404+
"build_tag" => nil,
405+
"build_commit" => "b5ffe3aeea51cec6c41aef16e45ee6bce47d8810",
406+
"step_key" => nil,
407+
"job_id" => "01945ecf-80f0-41e8-9b83-a2970a9305a1",
408+
"agent_id" => "01945ecf-8bcf-40a6-9d70-a765db9a0928",
409+
"build_source" => "ui",
410+
"runner_environment" => "buildkite-hosted"
411+
}
412+
413+
travel_to Time.zone.at(1_680_020_830) # after the JWT iat, before the exp
414+
end
415+
416+
context "with matching conditions" do
417+
should "return API key" do
418+
post assume_role_api_v1_oidc_api_key_role_path(@role.token),
419+
params: {
420+
jwt: jwt.to_s
421+
},
422+
headers: {}
423+
424+
assert_response :created
425+
426+
resp = response.parsed_body
427+
428+
assert_match(/^rubygems_/, resp["rubygems_api_key"])
429+
assert_equal_hash(
430+
{ "rubygems_api_key" => resp["rubygems_api_key"],
431+
"name" => "#{@role.name}-0194b014-8517-7cef-b232-76a827315f08",
432+
"scopes" => ["push_rubygem"],
433+
"expires_at" => 30.minutes.from_now },
434+
resp
435+
)
436+
hashed_key = @user.api_keys.sole.hashed_key
437+
438+
assert_equal hashed_key, Digest::SHA256.hexdigest(resp["rubygems_api_key"])
439+
end
440+
end
441+
end
386442
end
387443
end

0 commit comments

Comments
 (0)