Skip to content

fix(vcs): send the GitHub App ID as an integer in the JWT issuer claim - #204

Open
levivannoort wants to merge 2 commits into
mainfrom
fix/vcs-github-jwt-issuer
Open

fix(vcs): send the GitHub App ID as an integer in the JWT issuer claim#204
levivannoort wants to merge 2 commits into
mainfrom
fix/vcs-github-jwt-issuer

Conversation

@levivannoort

Copy link
Copy Markdown
Contributor

What breaks

generateAccessToken() puts the App ID into the JWT iss claim as the string it was read from:

$appIdentifier = $appId;   // ?string
$payload = ['iat' => $iat, 'exp' => $exp, 'iss' => $appIdentifier];

That encodes as "iss":"1234567". GitHub reads the claim as a JSON integer and answers every token request:

Status: 401 · {"message":"'Issuer' claim ('iss') must be an Integer"}

No installation token is ever issued, so all 94 GitHub e2e tests error. Six Bitbucket 403s are a separate issue, likely masked behind this.

It has been red for two days

test (vcs, linked) has failed on every run since 2026-09-02. The runs that looked green in between had zero vcs jobs — vcs only runs when the changed set reaches it, so a nats-only or queue-only PR never exercises it. It surfaced on #201 only because that touches bin/, which puts every package in scope.

This is not the staging cluster and not NATS: the vcs compose stack is gitea, forgejo, gogs, gitlab and a request-catcher, with no NATS service. The credentials are present too — GitHubTest::setupAdapter() calls markTestSkipped() when the App ID is empty, and the tests error rather than skip.

The fix

-$appIdentifier = $appId;
+$appIdentifier = ctype_digit((string) $appId) ? (int) $appId : $appId;

A client ID (Iv23li...) is a legitimate string issuer, so only an all-digit identifier is cast.

The test froze the defect

testInitializeVariablesSignsJwtWithPemString verified the signature and the path correctly, but its last assertion was:

$this->assertSame('5678', $claims['iss']);   // the exact shape GitHub rejects

So it passed the whole time the suite was down. It now asserts the type, and a second case covers the client-ID path. Seen red: reverting the one-line fix gives Failed asserting that '5678' is of type int.

Unit suite: 559 tests, 1127 assertions, all passing.

generateAccessToken() put the App ID into `iss` as the string it was read
from. GitHub reads that claim as a JSON integer and answers every token
request 401 "'Issuer' claim ('iss') must be an Integer", so no installation
token is ever issued and all 94 GitHub e2e tests error. The suite has been
red on every run since 2026-09-02; the runs that looked green in between
had no vcs job at all, because vcs only runs when the changed set reaches
it.

A client ID is a legitimate string issuer, so only an all-digit identifier
is cast.

The unit test froze the defect rather than catching it: it asserted
`iss` was the string '5678' -- the exact value GitHub rejects -- so it
passed throughout. It now asserts the type, and a second case covers the
client-ID path the cast must not touch. Reverting the one-line fix turns
the first red with "Failed asserting that '5678' is of type int."
The cast alone did not clear CI: the 94 GitHub 401s came back unchanged,
which can only happen if ctype_digit() saw a non-digit and the identifier
passed through as a string. The likeliest reason is invisible whitespace --
the value arrives from a CI secret, where a trailing newline does not show
up anywhere.

Trimming is right regardless of whether that is the cause here. It also
makes the empty case honest: a whitespace-only secret now collapses to ''
and the e2e suite skips with "GitHub App credentials not configured"
instead of erroring 94 times.

If the identifier is genuinely not numeric, the failure now says so. The
exception carries the issuer's type and length -- never its value -- because
GitHub's response cannot distinguish a badly shaped claim from a wrong
credential behind it, and this cost a full CI round trip to establish.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant