From fae606d6c4c254870d6dee25e335cbee97ad334c Mon Sep 17 00:00:00 2001 From: fadymak Date: Mon, 12 Jan 2026 16:23:15 +0700 Subject: [PATCH] fix: additional provider and issuer checks --- internal/api/token_oidc.go | 49 ++++++++++------ internal/api/token_oidc_test.go | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 18 deletions(-) diff --git a/internal/api/token_oidc.go b/internal/api/token_oidc.go index e62940abc9..de129d17ff 100644 --- a/internal/api/token_oidc.go +++ b/internal/api/token_oidc.go @@ -6,6 +6,7 @@ import ( "fmt" "net/http" "slices" + "strings" "github.com/coreos/go-oidc/v3/oidc" "github.com/supabase/auth/internal/api/apierrors" @@ -36,23 +37,29 @@ func (p *IdTokenGrantParams) getProvider(ctx context.Context, config *conf.Globa var providerType string var acceptableClientIDs []string + if p.Issuer != "" { + log.WithField("issuer", p.Issuer).WithField("provider", p.Provider).Info("Issuer provided in request.") + } + switch true { case p.Provider == "apple" || provider.IsAppleIssuer(p.Issuer): cfg = &config.External.Apple providerType = "apple" - issuer = p.Issuer - if issuer == "" { - detectedIssuer, err := provider.DetectAppleIDTokenIssuer(ctx, p.IdToken) - if err != nil { - return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Unable to detect issuer in ID token for Apple provider").WithInternalError(err) - } - if provider.IsAppleIssuer(detectedIssuer) { - issuer = detectedIssuer - } else { - return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Detected ID token issuer is not an Apple ID token issuer") - } + detectedIssuer, err := provider.DetectAppleIDTokenIssuer(ctx, p.IdToken) + if err != nil { + return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Unable to detect issuer in ID token for Apple provider").WithInternalError(err) + } + + if !provider.IsAppleIssuer(detectedIssuer) { + return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Detected ID token issuer is not an Apple ID token issuer") + } + + if p.Issuer != "" && p.Issuer != detectedIssuer { + return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Provided issuer does not match ID token issuer") } + + issuer = detectedIssuer acceptableClientIDs = append(acceptableClientIDs, config.External.Apple.ClientID...) if config.External.IosBundleId != "" { @@ -66,14 +73,20 @@ func (p *IdTokenGrantParams) getProvider(ctx context.Context, config *conf.Globa acceptableClientIDs = append(acceptableClientIDs, config.External.Google.ClientID...) case p.Provider == "azure" || provider.IsAzureIssuer(p.Issuer): - issuer = p.Issuer - if issuer == "" || !provider.IsAzureIssuer(issuer) { - detectedIssuer, err := provider.DetectAzureIDTokenIssuer(ctx, p.IdToken) - if err != nil { - return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Unable to detect issuer in ID token for Azure provider").WithInternalError(err) - } - issuer = detectedIssuer + detectedIssuer, err := provider.DetectAzureIDTokenIssuer(ctx, p.IdToken) + if err != nil { + return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Unable to detect issuer in ID token for Azure provider").WithInternalError(err) + } + + if !strings.HasPrefix(detectedIssuer, "https://login.microsoftonline.com/") && !strings.HasPrefix(detectedIssuer, "https://sts.windows.net/") { + return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Detected ID token issuer is not an Azure ID token issuer") + } + + if p.Issuer != "" && p.Issuer != detectedIssuer { + return nil, false, "", nil, false, apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "Provided issuer does not match ID token issuer") } + + issuer = detectedIssuer cfg = &config.External.Azure providerType = "azure" acceptableClientIDs = append(acceptableClientIDs, config.External.Azure.ClientID...) diff --git a/internal/api/token_oidc_test.go b/internal/api/token_oidc_test.go index 23e872eb1e..2ca589370d 100644 --- a/internal/api/token_oidc_test.go +++ b/internal/api/token_oidc_test.go @@ -2,6 +2,8 @@ package api import ( "context" + "encoding/base64" + "encoding/json" "net/http" "net/http/httptest" "testing" @@ -68,3 +70,101 @@ func (ts *TokenOIDCTestSuite) TestGetProvider() { require.Equal(ts.T(), params.Provider, providerType) require.NotEmpty(ts.T(), acceptableClientIds) } + +// createFakeIDToken creates a fake JWT token with a specific issuer claim for testing +// WARNING: This is for testing purposes only and creates an unsigned token +func createFakeIDToken(issuer string, sub string) string { + header := map[string]interface{}{ + "alg": "RS256", + "typ": "JWT", + } + + payload := map[string]interface{}{ + "iss": issuer, + "sub": sub, + "aud": "test-client-id", + "exp": 9999999999, + "iat": 1234567890, + } + + headerJSON, _ := json.Marshal(header) + payloadJSON, _ := json.Marshal(payload) + + headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON) + payloadEncoded := base64.RawURLEncoding.EncodeToString(payloadJSON) + + // Note: signature is fake, but the issuer detection only looks at the payload + return headerEncoded + "." + payloadEncoded + ".fake-signature" +} + +func (ts *TokenOIDCTestSuite) TestGetProviderAppleWithIncorrectIssuer() { + incorrectIssuer := SetupTestOIDCProvider(ts) + defer incorrectIssuer.Close() + + ts.Config.External.Apple.Enabled = true + ts.Config.External.Apple.ClientID = []string{"com.example.app"} + + // Create a token with an invalid issuer + nonAppleToken := createFakeIDToken(incorrectIssuer.URL, "user123") + + // provider="apple" but with an incorrect issuer in the token + params := &IdTokenGrantParams{ + IdToken: nonAppleToken, + Provider: "apple", + Issuer: incorrectIssuer.URL, + } + + req := httptest.NewRequest(http.MethodPost, "http://localhost", nil) + _, _, _, _, _, err := params.getProvider(context.Background(), ts.Config, req) + + require.Error(ts.T(), err) + require.Contains(ts.T(), err.Error(), "not an Apple ID token issuer") +} + +// TestGetProviderAzureWithNonAzureTokenIssuer tests that Azure provider only +// accepts tokens from login.microsoftonline.com and sts.windows.net +func (ts *TokenOIDCTestSuite) TestGetProviderAzureWithNonAzureTokenIssuer() { + ts.Config.External.Azure.Enabled = true + ts.Config.External.Azure.ClientID = []string{"test-client-id"} + + // Create a token with an incorrect issuer + nonAzureIssuer := "https://non-azure-issuer.example.com" + nonAzureToken := createFakeIDToken(nonAzureIssuer, "user123") + + params := &IdTokenGrantParams{ + IdToken: nonAzureToken, + Provider: "azure", + Issuer: nonAzureIssuer, + } + + req := httptest.NewRequest(http.MethodPost, "http://localhost", nil) + _, _, _, _, _, err := params.getProvider(context.Background(), ts.Config, req) + + // This should fail - the token's issuer is not an accepted issuer + require.Error(ts.T(), err) + require.Contains(ts.T(), err.Error(), "not an Azure ID token issuer") +} + +// TestGetProviderAppleWithInvalidIssuerInToken tests that Apple provider rejects +// tokens when the actual token issuer does not match the expected issuer +func (ts *TokenOIDCTestSuite) TestGetProviderAppleWithNonAppleIssuerInToken() { + ts.Config.External.Apple.Enabled = true + ts.Config.External.Apple.ClientID = []string{"com.example.app"} + + // Create a token with an incorrect issuer + nonAppleIssuer := "https://non-apple-issuer.example.com" + nonAppleToken := createFakeIDToken(nonAppleIssuer, "user123") + + params := &IdTokenGrantParams{ + IdToken: nonAppleToken, + Provider: "apple", + Issuer: "https://appleid.apple.com", + } + + req := httptest.NewRequest(http.MethodPost, "http://localhost", nil) + _, _, _, _, _, err := params.getProvider(context.Background(), ts.Config, req) + + // This should fail - the token's actual issuer is not appleid.apple.com + require.Error(ts.T(), err) + require.Contains(ts.T(), err.Error(), "not an Apple ID token issuer") +}