270 lines
9.4 KiB
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", "")
|
|
}
|