From dae8ece2cf2e7a4477ef592f44e710c1d3fc6596 Mon Sep 17 00:00:00 2001 From: Kenneth Jenkins <51246568+kenjenkins@users.noreply.github.com> Date: Thu, 18 Jul 2024 18:15:33 -0700 Subject: [PATCH] identity: add tests for identity manager refresh Add a test exercising the combined effect of session claims unmarshaling and SetRawIDToken(), in preparation for refactoring how unmarshaling works. This test should serve as an invariant, as in it should pass both before and after the upcoming change. --- pkg/identity/legacymanager/data_test.go | 62 +++++++++++++++++++++++++ pkg/identity/manager/data_test.go | 62 +++++++++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/pkg/identity/legacymanager/data_test.go b/pkg/identity/legacymanager/data_test.go index 14bb18e84..1324c4a26 100644 --- a/pkg/identity/legacymanager/data_test.go +++ b/pkg/identity/legacymanager/data_test.go @@ -1,12 +1,20 @@ package legacymanager import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" "encoding/json" "fmt" "testing" "time" + go_oidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -72,3 +80,57 @@ func TestSession_UnmarshalJSON(t *testing.T) { "some-other-claim": {Values: []*structpb.Value{protoutil.ToStruct("xyz")}}, }, s.Claims) } + +// Simulate the behavior during an oidc.Authenticator Refresh() call: +// SetRawIDToken() followed by a Claims() unmarshal call. +func TestSession_RefreshUpdate(t *testing.T) { + // Create a valid go_oidc.IDToken. This requires a real signing key. + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + iat := time.Now().Unix() + exp := iat + 3600 + payload := map[string]any{ + "iss": "https://issuer.example.com", + "aud": "https://client.example.com", + "sub": "subject", + "exp": exp, + "iat": iat, + "some-other-claim": "xyz", + } + jwtSigner, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: privateKey}, nil) + require.NoError(t, err) + rawIDToken, err := jwt.Signed(jwtSigner).Claims(payload).CompactSerialize() + require.NoError(t, err) + + keySet := &go_oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{privateKey.Public()}} + verifier := go_oidc.NewVerifier("https://issuer.example.com", keySet, &go_oidc.Config{ + ClientID: "https://client.example.com", + }) + + // Finally, we can obtain a go_oidc.IDToken. + idToken, err := verifier.Verify(context.Background(), rawIDToken) + require.NoError(t, err) + + // This is the behavior under test. + var s session.Session + v := &Session{Session: &s} + v.SetRawIDToken(rawIDToken) + err = idToken.Claims(v) + + assert.NoError(t, err) + assert.NotNil(t, s.IdToken) + assert.Equal(t, "https://issuer.example.com", s.IdToken.Issuer) + assert.Equal(t, "subject", s.IdToken.Subject) + assert.Equal(t, ×tamppb.Timestamp{Seconds: exp}, s.IdToken.ExpiresAt) + assert.Equal(t, ×tamppb.Timestamp{Seconds: iat}, s.IdToken.IssuedAt) + assert.Equal(t, map[string]*structpb.ListValue{ + "aud": { + Values: []*structpb.Value{structpb.NewStringValue("https://client.example.com")}, + }, + "some-other-claim": { + Values: []*structpb.Value{structpb.NewStringValue("xyz")}, + }, + }, s.Claims) + assert.Equal(t, rawIDToken, s.IdToken.Raw) +} diff --git a/pkg/identity/manager/data_test.go b/pkg/identity/manager/data_test.go index 3432589d2..244c96f03 100644 --- a/pkg/identity/manager/data_test.go +++ b/pkg/identity/manager/data_test.go @@ -1,12 +1,20 @@ package manager import ( + "context" + "crypto" + "crypto/rand" + "crypto/rsa" "encoding/json" "fmt" "testing" "time" + go_oidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/go-jose/go-jose/v3" + "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -68,3 +76,57 @@ func TestSession_UnmarshalJSON(t *testing.T) { "some-other-claim": {Values: []*structpb.Value{protoutil.ToStruct("xyz")}}, }, s.Claims) } + +// Simulate the behavior during an oidc.Authenticator Refresh() call: +// SetRawIDToken() followed by a Claims() unmarshal call. +func TestSession_RefreshUpdate(t *testing.T) { + // Create a valid go_oidc.IDToken. This requires a real signing key. + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + iat := time.Now().Unix() + exp := iat + 3600 + payload := map[string]any{ + "iss": "https://issuer.example.com", + "aud": "https://client.example.com", + "sub": "subject", + "exp": exp, + "iat": iat, + "some-other-claim": "xyz", + } + jwtSigner, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: privateKey}, nil) + require.NoError(t, err) + rawIDToken, err := jwt.Signed(jwtSigner).Claims(payload).CompactSerialize() + require.NoError(t, err) + + keySet := &go_oidc.StaticKeySet{PublicKeys: []crypto.PublicKey{privateKey.Public()}} + verifier := go_oidc.NewVerifier("https://issuer.example.com", keySet, &go_oidc.Config{ + ClientID: "https://client.example.com", + }) + + // Finally, we can obtain a go_oidc.IDToken. + idToken, err := verifier.Verify(context.Background(), rawIDToken) + require.NoError(t, err) + + // This is the behavior under test. + var s session.Session + v := newSessionUnmarshaler(&s) + v.SetRawIDToken(rawIDToken) + err = idToken.Claims(v) + + assert.NoError(t, err) + assert.NotNil(t, s.IdToken) + assert.Equal(t, "https://issuer.example.com", s.IdToken.Issuer) + assert.Equal(t, "subject", s.IdToken.Subject) + assert.Equal(t, ×tamppb.Timestamp{Seconds: exp}, s.IdToken.ExpiresAt) + assert.Equal(t, ×tamppb.Timestamp{Seconds: iat}, s.IdToken.IssuedAt) + assert.Equal(t, map[string]*structpb.ListValue{ + "aud": { + Values: []*structpb.Value{structpb.NewStringValue("https://client.example.com")}, + }, + "some-other-claim": { + Values: []*structpb.Value{structpb.NewStringValue("xyz")}, + }, + }, s.Claims) + assert.Equal(t, rawIDToken, s.IdToken.Raw) +}