package api import ( "context" "crypto" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "time" "github.com/coreos/go-oidc/v3/oidc" jwt "github.com/golang-jwt/jwt/v5" "github.com/supabase/auth/internal/api/provider" ) const ( azureUser string = `{"name":"Azure Test","email":"azure@example.com","sub":"azuretestid"}` azureUserNoEmail string = `{"name":"Azure Test","sub":"azuretestid"}` ) func idTokenPrivateKey() *rsa.PrivateKey { // #nosec der, err := base64.StdEncoding.DecodeString("MIIEpAIBAAKCAQEAvklrFDsVgbhs3DOQICMqm4xdFoi/MHj/T6XH8S7wXWd0roqdWVarwCLV4y3DILkLre4PzNK+hEY5NAnoAKrsCMyyCb4Wdl8HCdJk4ojDqAig+DJw67imqZoxJMFJyIhfMJhwVK1V8GRUPATn855rygLo7wThahMJeEHNiJr3TtV6Rf35KSs7DuyoWIUSjISYabQozKqIvpdUpTpSqjlOQvjdAxggRyycBZSgLzjWhsA8metnAMO48bX4bgiHLR6Kzu/dfPyEVPfgeYpA2ebIY6GzIUxVS0yX8+ExA6jeLCkuepjLHuz5XCJtd6zzGDXr1eX7nA6ZIeUNdFbWRDnPawIDAQABAoIBABH4Qvl1HvHSJc2hvPGcAJER71SKc2uzcYDnCfu30BEyDO3Sv0tJiQyq/YHnt26mqviw66MPH9jD/PDyIou1mHa4RfPvlJV3IeYGjWprOfbrYbAuq0VHec24dv2el0YtwreHHcyRVfVOtDm6yODTzCAWqEKyNktbIuDNbgiBgetayaJecDRoFMF9TOCeMCL92iZytzAr7fi+JWtLkRS/GZRIBjbr8LJ/ueYoCRmIx3MIw0WdPp7v2ZfeRTxP7LxJZ+MAsrq2pstmZYP7K0305e0bCJX1HexfXLs2Ul7u8zaxrXL8zw4/9+/GMsAeU3ffCVnGz/RKL5+T6iuz2RotjFECgYEA+Xk7DGwRXfDg9xba1GVFGeiC4nybqZw/RfZKcz/RRJWSHRJV/ps1avtbca3B19rjI6rewZMO1NWNv/tI2BdXP8vAKUnI9OHJZ+J/eZzmqDE6qu0v0ddRFUDzCMWE0j8BjrUdy44n4NQgopcv14u0iyr9tuhGO6YXn2SuuvEkZokCgYEAw0PNnT55kpkEhXSp7An2hdBJEub9ST7hS6Kcd8let62/qUZ/t5jWigSkWC1A2bMtH55+LgudIFjiehwVzRs7jym2j4jkKZGonyAX1l9IWgXwKl7Pn49lEQH5Yk6MhnXdyLGoFTzXiUyk/fKvgXX7jow1bD3j6sAc8P495I7TyVMCgYAHg6VJrH+har37805IE3zPWPeIRuSRaUlmnBKGAigVfsPV6FV6w8YKIOQSOn+aNtecnWr0Pa+2rXAFllYNXDaej06Mb9KDvcFJRcM9MIKqEkGIIHjOQ0QH9drcKsbjZk5vs/jfxrpgxULuYstoHKclgff+aGSlK02O2YOB0f2csQKBgQCEC/MdNiWCpKXxFg7fB3HF1i/Eb56zjKlQu7uyKeQ6tG3bLEisQNg8Z5034Apt7gRC0KyluMbeHB2z1BBOLu9dBill8X3SOqVcTpiwKKlF76QVEx622YLQOJSMDXBscYK0+KchDY74U3N0JEzZcI7YPCrYcxYRJy+rLVNvn8LK7wKBgQDE8THsZ589e10F0zDBvPK56o8PJnPeH71sgdM2Co4oLzBJ6g0rpJOKfcc03fLHsoJVOAya9WZeIy6K8+WVdcPTadR07S4p8/tcK1eguu5qlmCUOzswrTKAaJoIHO7cddQp3nySIqgYtkGdHKuvlQDMQkEKJS0meOm+vdeAG2rkaA==") if err != nil { panic(err) } privateKey, err := x509.ParsePKCS1PrivateKey(der) if err != nil { panic(err) } privateKey.E = 65537 return privateKey } func setupAzureOverrideVerifiers() { provider.OverrideVerifiers["https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/oauth2/v2.0/authorize"] = func(ctx context.Context, config *oidc.Config) *oidc.IDTokenVerifier { pk := idTokenPrivateKey() return oidc.NewVerifier( provider.IssuerAzureMicrosoft, &oidc.StaticKeySet{ PublicKeys: []crypto.PublicKey{ &pk.PublicKey, }, }, config, ) } } func mintIDToken(user string) string { var idToken struct { Issuer string `json:"iss"` IssuedAt int `json:"iat"` ExpiresAt int `json:"exp"` Audience string `json:"aud"` Sub string `json:"sub,omitempty"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` XmsEdov any `json:"xms_edov,omitempty"` } if err := json.Unmarshal([]byte(user), &idToken); err != nil { panic(err) } now := time.Now() idToken.Issuer = provider.IssuerAzureMicrosoft idToken.IssuedAt = int(now.Unix()) idToken.ExpiresAt = int(now.Unix() + 60*60) idToken.Audience = "testclientid" header := base64.RawURLEncoding.EncodeToString([]byte(`{"typ":"JWT","alg":"RS256"}`)) data, err := json.Marshal(idToken) if err != nil { panic(err) } payload := base64.RawURLEncoding.EncodeToString(data) sum := sha256.Sum256([]byte(header + "." + payload)) pk := idTokenPrivateKey() sig, err := rsa.SignPKCS1v15(nil, pk, crypto.SHA256, sum[:]) if err != nil { panic(err) } token := header + "." + payload + "." + base64.RawURLEncoding.EncodeToString(sig) return token } func (ts *ExternalTestSuite) TestSignupExternalAzure() { req := httptest.NewRequest(http.MethodGet, "http://localhost/authorize?provider=azure", nil) w := httptest.NewRecorder() ts.API.handler.ServeHTTP(w, req) ts.Require().Equal(http.StatusFound, w.Code) u, err := url.Parse(w.Header().Get("Location")) ts.Require().NoError(err, "redirect url parse failed") q := u.Query() ts.Equal(ts.Config.External.Azure.RedirectURI, q.Get("redirect_uri")) ts.Equal(ts.Config.External.Azure.ClientID, []string{q.Get("client_id")}) ts.Equal("code", q.Get("response_type")) ts.Equal("openid", q.Get("scope")) claims := ExternalProviderClaims{} p := jwt.NewParser(jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Name})) _, err = p.ParseWithClaims(q.Get("state"), &claims, func(token *jwt.Token) (interface{}, error) { return []byte(ts.Config.JWT.Secret), nil }) ts.Require().NoError(err) ts.Equal("azure", claims.Provider) ts.Equal(ts.Config.SiteURL, claims.SiteURL) } func AzureTestSignupSetup(ts *ExternalTestSuite, tokenCount *int, code string, user string) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/oauth2/v2.0/token": *tokenCount++ ts.Equal(code, r.FormValue("code")) ts.Equal("authorization_code", r.FormValue("grant_type")) ts.Equal(ts.Config.External.Azure.RedirectURI, r.FormValue("redirect_uri")) w.Header().Add("Content-Type", "application/json") fmt.Fprintf(w, `{"access_token":"azure_token","expires_in":100000,"id_token":%q}`, mintIDToken(user)) default: w.WriteHeader(500) ts.Fail("unknown azure oauth call %s", r.URL.Path) } })) ts.Config.External.Azure.URL = server.URL ts.Config.External.Azure.ApiURL = server.URL return server } func (ts *ExternalTestSuite) TestSignupExternalAzure_AuthorizationCode() { setupAzureOverrideVerifiers() ts.Config.DisableSignup = false tokenCount := 0 code := "authcode" server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "") assertAuthorizationSuccess(ts, u, tokenCount, -1, "azure@example.com", "Azure Test", "azuretestid", "") } func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoUser() { setupAzureOverrideVerifiers() ts.Config.DisableSignup = true tokenCount := 0 code := "authcode" server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "") assertAuthorizationFailure(ts, u, "Signups not allowed for this instance", "access_denied", "azure@example.com") } func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupErrorWhenNoEmail() { setupAzureOverrideVerifiers() ts.Config.DisableSignup = true tokenCount := 0 code := "authcode" server := AzureTestSignupSetup(ts, &tokenCount, code, azureUserNoEmail) defer server.Close() u := performAuthorization(ts, "azure", code, "") assertAuthorizationFailure(ts, u, "Error getting user email from external provider", "server_error", "azure@example.com") } func (ts *ExternalTestSuite) TestSignupExternalAzureDisableSignupSuccessWithPrimaryEmail() { setupAzureOverrideVerifiers() ts.Config.DisableSignup = true ts.createUser("azuretestid", "azure@example.com", "Azure Test", "", "") tokenCount := 0 code := "authcode" server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "") assertAuthorizationSuccess(ts, u, tokenCount, -1, "azure@example.com", "Azure Test", "azuretestid", "") } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureSuccessWhenMatchingToken() { setupAzureOverrideVerifiers() // name should be populated from Azure API ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") tokenCount := 0 code := "authcode" server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "invite_token") assertAuthorizationSuccess(ts, u, tokenCount, -1, "azure@example.com", "Azure Test", "azuretestid", "") } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenNoMatchingToken() { setupAzureOverrideVerifiers() tokenCount := 0 code := "authcode" azureUser := `{"name":"Azure Test","avatar":{"href":"http://example.com/avatar"}}` server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() w := performAuthorizationRequest(ts, "azure", "invite_token") ts.Require().Equal(http.StatusNotFound, w.Code) } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenWrongToken() { setupAzureOverrideVerifiers() ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") tokenCount := 0 code := "authcode" azureUser := `{"name":"Azure Test","avatar":{"href":"http://example.com/avatar"}}` server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() w := performAuthorizationRequest(ts, "azure", "wrong_token") ts.Require().Equal(http.StatusNotFound, w.Code) } func (ts *ExternalTestSuite) TestInviteTokenExternalAzureErrorWhenEmailDoesntMatch() { setupAzureOverrideVerifiers() ts.createUser("azuretestid", "azure@example.com", "", "", "invite_token") tokenCount := 0 code := "authcode" azureUser := `{"name":"Azure Test", "email":"other@example.com", "avatar":{"href":"http://example.com/avatar"}}` server := AzureTestSignupSetup(ts, &tokenCount, code, azureUser) defer server.Close() u := performAuthorization(ts, "azure", code, "invite_token") assertAuthorizationFailure(ts, u, "Invited email does not match emails from external provider", "invalid_request", "") }