chatai/auth_v2.169.0/internal/api/external_azure_test.go

270 lines
9.4 KiB
Go

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", "")
}