From 4752aa2ab7433b9f8b312aeb94af485cc8c94dec Mon Sep 17 00:00:00 2001 From: "Chris (ChrisJr404)" <11917633+ChrisJr404@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:01:25 -0400 Subject: [PATCH 1/2] fix(op): guard against nil claims for opaque access tokens in token exchange Exchanging an opaque access token (an encrypted tokenID:subject string) panicked, because getTokenIDAndClaims returns nil claims with ok true and the caller dereferenced them unconditionally. Skip the dereference when there are no claims and add a test covering that path. --- pkg/op/token_exchange.go | 6 +++++- pkg/op/token_exchange_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 pkg/op/token_exchange_test.go diff --git a/pkg/op/token_exchange.go b/pkg/op/token_exchange.go index f8dc0f84..08b3a9d6 100644 --- a/pkg/op/token_exchange.go +++ b/pkg/op/token_exchange.go @@ -311,7 +311,11 @@ func GetTokenIDAndSubjectFromToken( if !ok { break } - claims = accessTokenClaims.Claims + // opaque access tokens carry no JWT claims, so accessTokenClaims can be + // nil even when ok is true. Leave claims empty instead of dereferencing. + if accessTokenClaims != nil { + claims = accessTokenClaims.Claims + } case oidc.RefreshTokenType: refreshTokenRequest, err := exchanger.Storage().TokenRequestByRefreshToken(ctx, token) if err != nil { diff --git a/pkg/op/token_exchange_test.go b/pkg/op/token_exchange_test.go new file mode 100644 index 00000000..53d4eef8 --- /dev/null +++ b/pkg/op/token_exchange_test.go @@ -0,0 +1,35 @@ +package op_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/oidc/v3/pkg/oidc" + "github.com/zitadel/oidc/v3/pkg/op" +) + +func TestGetTokenIDAndSubjectFromToken_OpaqueAccessToken(t *testing.T) { + ctx := context.Background() + + // Opaque access tokens are stored as the encrypted string "tokenID:subject" + // and have no JWT claims attached. This used to panic because the returned + // claims pointer was dereferenced unconditionally. + token, err := testProvider.Crypto().Encrypt("tokenID:subject") + require.NoError(t, err) + + var ( + tokenIDOrToken, subject string + claims map[string]any + ok bool + ) + require.NotPanics(t, func() { + tokenIDOrToken, subject, claims, ok = op.GetTokenIDAndSubjectFromToken(ctx, testProvider, token, oidc.AccessTokenType, false) + }) + + assert.True(t, ok) + assert.Equal(t, "tokenID", tokenIDOrToken) + assert.Equal(t, "subject", subject) + assert.Nil(t, claims) +} From ab98dbe5d9954d9515636501fdb7a7291263ae79 Mon Sep 17 00:00:00 2001 From: Wim Van Laer Date: Thu, 27 Aug 2026 11:00:48 +0200 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pkg/op/token_exchange_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/op/token_exchange_test.go b/pkg/op/token_exchange_test.go index 53d4eef8..be3946a8 100644 --- a/pkg/op/token_exchange_test.go +++ b/pkg/op/token_exchange_test.go @@ -31,5 +31,5 @@ func TestGetTokenIDAndSubjectFromToken_OpaqueAccessToken(t *testing.T) { assert.True(t, ok) assert.Equal(t, "tokenID", tokenIDOrToken) assert.Equal(t, "subject", subject) - assert.Nil(t, claims) + assert.Empty(t, claims) }