From be931e3d1192acbb871d3e8b5872cef621db751d Mon Sep 17 00:00:00 2001 From: Jordan May Date: Thu, 15 Aug 2024 14:07:41 -0400 Subject: [PATCH 1/2] Add id_token support to microsoft oidc provider. --- selfservice/strategy/oidc/provider_microsoft.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/selfservice/strategy/oidc/provider_microsoft.go b/selfservice/strategy/oidc/provider_microsoft.go index d69206ec4d87..85328806c50f 100644 --- a/selfservice/strategy/oidc/provider_microsoft.go +++ b/selfservice/strategy/oidc/provider_microsoft.go @@ -6,6 +6,7 @@ package oidc import ( "context" "encoding/json" + "fmt" "net/url" "strings" @@ -25,6 +26,7 @@ import ( type ProviderMicrosoft struct { *ProviderGenericOIDC + JWKSUrl string } func NewProviderMicrosoft( @@ -36,6 +38,7 @@ func NewProviderMicrosoft( config: config, reg: reg, }, + JWKSUrl: "https://login.microsoftonline.com/common/discovery/keys", } } @@ -127,3 +130,9 @@ type microsoftUnverifiedClaims struct { func (c *microsoftUnverifiedClaims) Valid() error { return nil } + +func (p *ProviderMicrosoft) Verify(ctx context.Context, rawIDToken string) (*Claims, error) { + keySet := gooidc.NewRemoteKeySet(ctx, p.JWKSUrl) + ctx = gooidc.ClientContext(ctx, p.reg.HTTPClient(ctx).HTTPClient) + return verifyToken(ctx, keySet, p.config, rawIDToken, fmt.Sprintf("https://login.microsoftonline.com/%s/v2.0", p.config.Tenant)) +} From 9c1808dc832717d2aaff392be6c5e1b9bba56799 Mon Sep 17 00:00:00 2001 From: Jordan May Date: Thu, 15 Aug 2024 15:36:22 -0400 Subject: [PATCH 2/2] Add tests for microsoft oidc provider id_token. --- .../strategy/oidc/provider_microsoft_test.go | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 selfservice/strategy/oidc/provider_microsoft_test.go diff --git a/selfservice/strategy/oidc/provider_microsoft_test.go b/selfservice/strategy/oidc/provider_microsoft_test.go new file mode 100644 index 000000000000..241dbeefa3e7 --- /dev/null +++ b/selfservice/strategy/oidc/provider_microsoft_test.go @@ -0,0 +1,112 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package oidc_test + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + _ "embed" + + "github.com/golang-jwt/jwt/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/strategy/oidc" +) + +func TestMicrosoftVerify(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(publicJWKS) + })) + + tsOtherJWKS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + w.Write(publicJWKS2) + })) + makeClaims := func(aud string) jwt.RegisteredClaims { + return jwt.RegisteredClaims{ + Issuer: "https://login.microsoftonline.com/tenant_id/v2.0", + Subject: "acme@ory.sh", + Audience: jwt.ClaimStrings{aud}, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)), + } + } + t.Run("case=successful verification", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderMicrosoft(&oidc.Configuration{ + ClientID: "com.example.app", + Tenant: "tenant_id", + }, reg).(*oidc.ProviderMicrosoft) + apple.JWKSUrl = ts.URL + token := createIdToken(t, makeClaims("com.example.app")) + + c, err := apple.Verify(context.Background(), token) + require.NoError(t, err) + assert.Equal(t, "acme@ory.sh", c.Email) + assert.Equal(t, "acme@ory.sh", c.Subject) + assert.Equal(t, "https://login.microsoftonline.com/tenant_id/v2.0", c.Issuer) + }) + + t.Run("case=fails due to client_id mismatch", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderMicrosoft(&oidc.Configuration{ + ClientID: "com.example.app", + Tenant: "tenant_id", + }, reg).(*oidc.ProviderMicrosoft) + apple.JWKSUrl = ts.URL + token := createIdToken(t, makeClaims("com.different-example.app")) + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, `token audience didn't match allowed audiences: [com.example.app] oidc: expected audience "com.example.app" got ["com.different-example.app"]`, err.Error()) + }) + + t.Run("case=fails due to jwks mismatch", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderMicrosoft(&oidc.Configuration{ + ClientID: "com.example.app", + Tenant: "tenant_id", + }, reg).(*oidc.ProviderMicrosoft) + apple.JWKSUrl = tsOtherJWKS.URL + token := createIdToken(t, makeClaims("com.example.app")) + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, "failed to verify signature: failed to verify id token signature", err.Error()) + }) + + t.Run("case=fails due to wrong issuer tenant", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderMicrosoft(&oidc.Configuration{ + ClientID: "com.example.app", + Tenant: "wrong_tenant_id", + }, reg).(*oidc.ProviderMicrosoft) + apple.JWKSUrl = tsOtherJWKS.URL + token := createIdToken(t, makeClaims("com.example.app")) + + _, err := apple.Verify(context.Background(), token) + require.Error(t, err) + assert.Equal(t, "oidc: id token issued by a different provider, expected \"https://login.microsoftonline.com/wrong_tenant_id/v2.0\" got \"https://login.microsoftonline.com/tenant_id/v2.0\"", err.Error()) + }) + + t.Run("case=succeedes with additional id token audience", func(t *testing.T) { + _, reg := internal.NewFastRegistryWithMocks(t) + apple := oidc.NewProviderMicrosoft(&oidc.Configuration{ + ClientID: "something.else.app", + Tenant: "tenant_id", + AdditionalIDTokenAudiences: []string{"com.example.app"}, + }, reg).(*oidc.ProviderMicrosoft) + apple.JWKSUrl = ts.URL + token := createIdToken(t, makeClaims("com.example.app")) + + _, err := apple.Verify(context.Background(), token) + require.NoError(t, err) + }) +}