diff --git a/selfservice/strategy/oidc/provider_apple_test.go b/selfservice/strategy/oidc/provider_apple_test.go index 422ae643708a..a466f417cc90 100644 --- a/selfservice/strategy/oidc/provider_apple_test.go +++ b/selfservice/strategy/oidc/provider_apple_test.go @@ -81,6 +81,7 @@ func TestAppleVerify(t *testing.T) { assert.Equal(t, "acme@ory.sh", c.Email) assert.Equal(t, "acme@ory.sh", c.Subject) assert.Equal(t, "https://appleid.apple.com", c.Issuer) + assert.Equal(t, "acme@ory.sh", c.RawClaims["sub"]) }) t.Run("case=fails due to client_id mismatch", func(t *testing.T) { diff --git a/selfservice/strategy/oidc/provider_google_test.go b/selfservice/strategy/oidc/provider_google_test.go index c1a1f65b9348..76abd6f9023d 100644 --- a/selfservice/strategy/oidc/provider_google_test.go +++ b/selfservice/strategy/oidc/provider_google_test.go @@ -101,6 +101,7 @@ func TestGoogleVerify(t *testing.T) { assert.Equal(t, "acme@ory.sh", c.Email) assert.Equal(t, "acme@ory.sh", c.Subject) assert.Equal(t, "https://accounts.google.com", c.Issuer) + assert.Equal(t, "acme@ory.sh", c.RawClaims["sub"]) }) t.Run("case=fails due to client_id mismatch", func(t *testing.T) { diff --git a/selfservice/strategy/oidc/provider_microsoft_test.go b/selfservice/strategy/oidc/provider_microsoft_test.go index 207ffc27b4b0..5e80319b7b62 100644 --- a/selfservice/strategy/oidc/provider_microsoft_test.go +++ b/selfservice/strategy/oidc/provider_microsoft_test.go @@ -5,6 +5,7 @@ package oidc_test import ( "context" + "encoding/json" "net/http" "net/http/httptest" "net/url" @@ -76,6 +77,58 @@ func TestMicrosoftVerify(t *testing.T) { assert.Equal(t, issuerFor(tenantA), c.Issuer) }) + t.Run("case=preserves verified raw claims for the mapper", func(t *testing.T) { + for _, tc := range []struct { + name string + value interface{} + }{ + {name: "boolean true", value: true}, + {name: "boolean false", value: false}, + {name: "string one", value: "1"}, + {name: "string zero", value: "0"}, + {name: "string true", value: "true"}, + {name: "string false", value: "false"}, + {name: "number one", value: float64(1)}, + {name: "number zero", value: float64(0)}, + {name: "absent"}, + } { + t.Run(tc.name, func(t *testing.T) { + cl := makeClaimsWithRawClaims(issuerFor(tenantA), tenantA, tc.value) + c, err := newProvider(t, &oidc.Configuration{ + Tenant: tenantA, SubjectSource: "oid", + }).Verify(context.Background(), signIdToken(t, cl)) + require.NoError(t, err) + require.NotEmpty(t, c.RawClaims) + + // The mapper receives the JSON-encoded Claims object, not the JWT. + encoded, err := json.Marshal(c) + require.NoError(t, err) + var mapperClaims map[string]interface{} + require.NoError(t, json.Unmarshal(encoded, &mapperClaims)) + raw := mapperClaims["raw_claims"].(map[string]interface{}) + if tc.value == nil { + assert.NotContains(t, raw, "xms_edov") + } else { + assert.Equal(t, tc.value, raw["xms_edov"]) + } + assert.Equal(t, cl.Object, c.Subject) + assert.Equal(t, cl.Subject, raw["sub"]) + assert.Equal(t, cl.Object, raw["oid"]) + assert.Equal(t, cl.Email, c.Email) + assert.Equal(t, cl.TenantID, raw["tid"]) + }) + } + }) + + t.Run("case=rejects expired tokens even with xms_edov", func(t *testing.T) { + cl := makeClaimsWithRawClaims(issuerFor(tenantA), tenantA, "1") + cl.ExpiresAt = jwt.NewNumericDate(time.Now().Add(-time.Hour)) + c, err := newProvider(t, &oidc.Configuration{Tenant: tenantA}).Verify(context.Background(), signIdToken(t, cl)) + require.Error(t, err) + assert.Contains(t, err.Error(), "token is expired") + assert.Nil(t, c) + }) + t.Run("case=fails due to client_id mismatch", func(t *testing.T) { _, err := newProvider(t, &oidc.Configuration{Tenant: tenantA}).Verify(context.Background(), signIdToken(t, makeClaims("com.different-example.app", tenantA))) require.Error(t, err) @@ -221,4 +274,36 @@ func TestMicrosoftClaims(t *testing.T) { assert.Equal(t, http.StatusBadRequest, herr.StatusCode()) assert.Contains(t, herr.Reason(), "`oid` claim") }) + + t.Run("case=browser and native expose the same verified claims", func(t *testing.T) { + for _, value := range []interface{}{true, false, "1", "0", float64(1), nil} { + cl := makeClaimsWithRawClaims(issuer, tenant, value) + cl.Audience = jwt.ClaimStrings{"foo"} + raw := signIdToken(t, cl) + exchange := (&oauth2.Token{AccessToken: "foo"}).WithExtra(map[string]interface{}{"id_token": raw}) + browserClaims, err := provider.Claims(context.Background(), exchange, url.Values{}) + require.NoError(t, err) + + native := provider.(*oidc.ProviderMicrosoft) + native.JWKSUrl = issuer + "/keys" + nativeClaims, err := native.Verify(context.Background(), raw) + require.NoError(t, err) + assert.Equal(t, browserClaims, nativeClaims) + } + }) +} + +func makeClaimsWithRawClaims(issuer, tenant string, xmsEdov interface{}) *claims { + return &claims{ + RegisteredClaims: &jwt.RegisteredClaims{ + Issuer: issuer, + Subject: "pairwise-sub", + Audience: jwt.ClaimStrings{"com.example.app"}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), + }, + Email: "acme@ory.sh", + Object: "00000000-0000-0000-0000-00000000c0de", + TenantID: tenant, + XmsEdov: xmsEdov, + } } diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index 384bc2076d79..910c25f31ca0 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -390,9 +390,10 @@ var publicJWKS2 []byte type claims struct { *jwt.RegisteredClaims - Email string `json:"email"` - Object string `json:"oid,omitempty"` - TenantID string `json:"tid,omitempty"` + Email string `json:"email"` + Object string `json:"oid,omitempty"` + TenantID string `json:"tid,omitempty"` + XmsEdov interface{} `json:"xms_edov,omitempty"` } func createIdToken(t *testing.T, cl jwt.RegisteredClaims) string { diff --git a/selfservice/strategy/oidc/token_verifier.go b/selfservice/strategy/oidc/token_verifier.go index ce9cb8b3d3ee..42b16767a041 100644 --- a/selfservice/strategy/oidc/token_verifier.go +++ b/selfservice/strategy/oidc/token_verifier.go @@ -35,8 +35,19 @@ func verifyToken(ctx context.Context, keySet oidc.KeySet, config *Configuration, return nil, fmt.Errorf("token audience didn't match allowed audiences: %+v %w", tokenAudiences, err) } claims := &Claims{} + var rawClaims map[string]any + + if token == nil { + return nil, fmt.Errorf("token is nil") + } + if err := token.Claims(claims); err != nil { return nil, err } + if err = token.Claims(&rawClaims); err != nil { + return nil, err + } + claims.RawClaims = rawClaims + return claims, nil }