diff --git a/cmd/mattermost/main.go b/cmd/mattermost/main.go index b5f73a31f9..441ed9a752 100644 --- a/cmd/mattermost/main.go +++ b/cmd/mattermost/main.go @@ -11,9 +11,6 @@ import ( _ "github.com/mattermost/mattermost-server/v6/app/slashcommands" // Plugins _ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/gitlab" - _ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/google" - _ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/office365" - _ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/openid" // Enterprise Imports _ "github.com/mattermost/mattermost-server/v6/imports" diff --git a/config/client.go b/config/client.go index 7d05dda917..b8160993fd 100644 --- a/config/client.go +++ b/config/client.go @@ -303,11 +303,11 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m props["SamlLoginButtonColor"] = "" props["SamlLoginButtonBorderColor"] = "" props["SamlLoginButtonTextColor"] = "" - props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable) - props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor - props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText - props["EnableSignUpWithGoogle"] = strconv.FormatBool(*c.GoogleSettings.Enable) - props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable) + props["EnableSignUpWithGoogle"] = "false" + props["EnableSignUpWithOffice365"] = "false" + props["EnableSignUpWithOpenId"] = "false" + props["OpenIdButtonText"] = "" + props["OpenIdButtonColor"] = "" props["CWSURL"] = "" props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand) props["CustomBrandText"] = *c.TeamSettings.CustomBrandText @@ -342,6 +342,27 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m if *license.Features.MFA { props["EnforceMultifactorAuthentication"] = strconv.FormatBool(*c.ServiceSettings.EnforceMultifactorAuthentication) } + + if license.IsCloud() { + // MM-48727: enable SSO options for free cloud - not in self hosted + *license.Features.GoogleOAuth = true + *license.Features.Office365OAuth = true + *license.Features.OpenId = true + } + + if *license.Features.GoogleOAuth { + props["EnableSignUpWithGoogle"] = strconv.FormatBool(*c.GoogleSettings.Enable) + } + + if *license.Features.Office365OAuth { + props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable) + } + + if *license.Features.OpenId { + props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable) + props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor + props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText + } } for key, value := range c.FeatureFlags.ToMap() { diff --git a/model/license.go b/model/license.go index d04a88acef..94f0b81da4 100644 --- a/model/license.go +++ b/model/license.go @@ -78,18 +78,12 @@ type TrialLicenseRequest struct { } type Features struct { - Users *int `json:"users"` - LDAP *bool `json:"ldap"` - LDAPGroups *bool `json:"ldap_groups"` - MFA *bool `json:"mfa"` - - // Deprecated: This feature will be removed from the license because it's available without a license. - GoogleOAuth *bool `json:"google_oauth"` - - // Deprecated: This feature will be removed from the license because it's available without a license. - Office365OAuth *bool `json:"office365_oauth"` - - // Deprecated: This feature will be removed from the license because it's available without a license. + Users *int `json:"users"` + LDAP *bool `json:"ldap"` + LDAPGroups *bool `json:"ldap_groups"` + MFA *bool `json:"mfa"` + GoogleOAuth *bool `json:"google_oauth"` + Office365OAuth *bool `json:"office365_oauth"` OpenId *bool `json:"openid"` Compliance *bool `json:"compliance"` Cluster *bool `json:"cluster"` @@ -171,15 +165,15 @@ func (f *Features) SetDefaults() { } if f.GoogleOAuth == nil { - f.GoogleOAuth = NewBool(true) + f.GoogleOAuth = NewBool(*f.FutureFeatures) } if f.Office365OAuth == nil { - f.Office365OAuth = NewBool(true) + f.Office365OAuth = NewBool(*f.FutureFeatures) } if f.OpenId == nil { - f.OpenId = NewBool(true) + f.OpenId = NewBool(*f.FutureFeatures) } if f.Compliance == nil { diff --git a/model/oauthproviders/google/google.go b/model/oauthproviders/google/google.go deleted file mode 100644 index 19c4cf3353..0000000000 --- a/model/oauthproviders/google/google.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package oauthgoogle - -import ( - "encoding/json" - "errors" - "io" - "strings" - - "github.com/mattermost/mattermost-server/v6/einterfaces" - "github.com/mattermost/mattermost-server/v6/model" -) - -type GoogleProvider struct { -} - -type SourceElement struct { - Type string `json:"type"` - ID string `json:"id"` - Etag string `json:"etag"` - ProfileMetadata ProfileMetadata `json:"profileMetadata"` -} - -type ProfileMetadata struct { - ObjectType string `json:"objectType"` - UserTypes []string `json:"userTypes"` -} - -type GoogleUserRootMetadata struct { - Sources []SourceElement `json:"sources"` -} - -type GoogleUserMetadata struct { - Source map[string]string `json:"source"` -} - -type GoogleUserNameNode struct { - Metadata GoogleUserMetadata `json:"metadata"` - GivenName string `json:"givenName"` - FamilyName string `json:"familyName"` -} - -type GoogleGenericInfoNode struct { - Metadata GoogleUserMetadata `json:"metadata"` - Value string `json:"value"` -} - -type GoogleUser struct { - Metadata GoogleUserRootMetadata `json:"metadata"` - Nicknames []GoogleGenericInfoNode `json:"nicknames"` - Emails []GoogleGenericInfoNode `json:"emailAddresses"` - Names []GoogleUserNameNode `json:"names"` -} - -func init() { - provider := &GoogleProvider{} - einterfaces.RegisterOAuthProvider(model.ServiceGoogle, provider) -} - -func userFromGoogleUser(gu *GoogleUser) *model.User { - user := &model.User{} - - for _, e := range gu.Emails { - if e.Metadata.Source["type"] == "ACCOUNT" || e.Metadata.Source["type"] == "DOMAIN_PROFILE" { - user.Email = e.Value - user.Username = model.CleanUsername(strings.Split(user.Email, "@")[0]) - break - } - } - - for _, e := range gu.Names { - if e.Metadata.Source["type"] == "PROFILE" || e.Metadata.Source["type"] == "DOMAIN_PROFILE" { - user.FirstName = e.GivenName - user.LastName = e.FamilyName - break - } - } - - if len(gu.Nicknames) > 0 { - user.Nickname = gu.Nicknames[0].Value - } - - user.AuthData = new(string) - *user.AuthData = gu.getAuthData() - user.AuthService = model.ServiceGoogle - - return user -} - -func googleUserFromJSON(data io.Reader) (*GoogleUser, error) { - decoder := json.NewDecoder(data) - var gu GoogleUser - err := decoder.Decode(&gu) - if err != nil { - return nil, err - } - - return &gu, nil -} - -func (gu *GoogleUser) IsValid() error { - if len(gu.Metadata.Sources) == 0 { - return errors.New("invalid metadata sources") - } - - if len(gu.Emails) == 0 { - return errors.New("invalid emails") - } - - return nil -} - -func (gu *GoogleUser) getAuthData() string { - if len(gu.Metadata.Sources) > 0 { - return gu.Metadata.Sources[0].ID - } - - return "" -} - -func (m *GoogleProvider) GetIdentifier() string { - return model.ServiceGoogle -} - -func (m *GoogleProvider) GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error) { - gu, err := googleUserFromJSON(data) - if err != nil { - return nil, err - } - return userFromGoogleUser(gu), nil -} - -func (m *GoogleProvider) GetAuthDataFromJSON(data io.Reader) (string, error) { - gu, err := googleUserFromJSON(data) - if err != nil { - return "", err - } - - if err = gu.IsValid(); err != nil { - return "", err - } - - return gu.getAuthData(), nil -} - -func (m *GoogleProvider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) { - return &config.GoogleSettings, nil -} - -func (m *GoogleProvider) GetUserFromIdToken(idToken string) (*model.User, error) { - return nil, nil -} - -func (m *GoogleProvider) IsSameUser(dbUser, oauthUser *model.User) bool { - return dbUser.AuthData == oauthUser.AuthData -} diff --git a/model/oauthproviders/google/google_test.go b/model/oauthproviders/google/google_test.go deleted file mode 100644 index 1f30efb8eb..0000000000 --- a/model/oauthproviders/google/google_test.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package oauthgoogle - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestGoogleUserFromJSON(t *testing.T) { - gu := GoogleUser{ - Metadata: GoogleUserRootMetadata{ - Sources: []SourceElement{ - { - Etag: "tag", - }, - }, - }, - Emails: []GoogleGenericInfoNode{ - { - Value: "ali@test.com", - }, - }, - Names: []GoogleUserNameNode{ - { - GivenName: "ali", - }, - }, - Nicknames: []GoogleGenericInfoNode{ - { - Value: "ila", - }, - }, - } - - provider := &GoogleProvider{} - - t.Run("valid google user", func(t *testing.T) { - b, err := json.Marshal(gu) - require.NoError(t, err) - - _, err = provider.GetUserFromJSON(bytes.NewReader(b), nil) - require.NoError(t, err) - - _, err = provider.GetAuthDataFromJSON(bytes.NewReader(b)) - require.NoError(t, err) - }) - - t.Run("empty body should fail without panic", func(t *testing.T) { - _, err := provider.GetUserFromJSON(strings.NewReader("{}"), nil) - require.NoError(t, err) - - _, err = provider.GetAuthDataFromJSON(strings.NewReader("{}")) - require.Error(t, err) - }) -} diff --git a/model/oauthproviders/office365/office365.go b/model/oauthproviders/office365/office365.go deleted file mode 100644 index 7f23d9e51c..0000000000 --- a/model/oauthproviders/office365/office365.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package oauthoffice365 - -import ( - "encoding/json" - "errors" - "io" - "strings" - - "github.com/mattermost/mattermost-server/v6/einterfaces" - "github.com/mattermost/mattermost-server/v6/model" -) - -type Office365Provider struct { -} - -type Office365User struct { - Id string `json:"id"` - FirstName string `json:"givenName"` - LastName string `json:"surname"` - Mail string `json:"mail"` - UserPrincipalName string `json:"userPrincipalName"` -} - -func init() { - provider := &Office365Provider{} - einterfaces.RegisterOAuthProvider(model.ServiceOffice365, provider) -} - -func userFromOffice365User(of *Office365User) *model.User { - user := &model.User{} - user.FirstName = of.FirstName - user.LastName = of.LastName - - if of.Mail != "" { - user.Email = of.Mail - } else if strings.Contains(of.UserPrincipalName, "@") { - user.Email = of.UserPrincipalName - } - - if user.Email != "" { - user.Username = model.CleanUsername(strings.Split(user.Email, "@")[0]) - } - - user.AuthData = new(string) - *user.AuthData = of.Id - user.AuthService = model.ServiceOffice365 - - return user -} - -func office365UserFromJSON(data io.Reader) (*Office365User, error) { - decoder := json.NewDecoder(data) - var of Office365User - err := decoder.Decode(&of) - if err != nil { - return nil, err - } - - return &of, nil -} - -func (of *Office365User) IsValid() error { - if of.Id == "" { - return errors.New("invalid user id") - } - - if of.Mail == "" && !strings.Contains(of.UserPrincipalName, "@") { - return errors.New("invalid email") - } - - return nil -} - -func (of *Office365User) getAuthData() string { - return of.Id -} - -func (m *Office365Provider) GetIdentifier() string { - return model.ServiceOffice365 -} - -func (m *Office365Provider) GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error) { - of, err := office365UserFromJSON(data) - if err != nil { - return nil, err - } - return userFromOffice365User(of), nil -} - -func (m *Office365Provider) GetAuthDataFromJSON(data io.Reader) (string, error) { - of, err := office365UserFromJSON(data) - if err != nil { - return "", err - } - - if err = of.IsValid(); err != nil { - return "", err - } - - return of.getAuthData(), nil -} - -func (m *Office365Provider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) { - return config.Office365Settings.SSOSettings(), nil -} - -func (m *Office365Provider) GetUserFromIdToken(idToken string) (*model.User, error) { - return nil, nil -} - -func (m *Office365Provider) IsSameUser(dbUser, oauthUser *model.User) bool { - return dbUser.AuthData == oauthUser.AuthData -} diff --git a/model/oauthproviders/office365/office365_test.go b/model/oauthproviders/office365/office365_test.go deleted file mode 100644 index 85496dd7ab..0000000000 --- a/model/oauthproviders/office365/office365_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package oauthoffice365 - -import ( - "bytes" - "encoding/json" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestOffice365UserFromJSON(t *testing.T) { - ou := Office365User{ - FirstName: "ali", - Id: "12345", - LastName: "maya", - Mail: "ali@test.com", - } - - provider := &Office365Provider{} - - t.Run("valid office365 user", func(t *testing.T) { - b, err := json.Marshal(ou) - require.NoError(t, err) - - _, err = provider.GetUserFromJSON(bytes.NewReader(b), nil) - require.NoError(t, err) - - _, err = provider.GetAuthDataFromJSON(bytes.NewReader(b)) - require.NoError(t, err) - }) - - t.Run("empty body should fail without panic", func(t *testing.T) { - _, err := provider.GetUserFromJSON(strings.NewReader("{}"), nil) - require.NoError(t, err) - - _, err = provider.GetAuthDataFromJSON(strings.NewReader("{}")) - require.Error(t, err) - }) -} diff --git a/model/oauthproviders/openid/openid.go b/model/oauthproviders/openid/openid.go deleted file mode 100644 index e48be5d913..0000000000 --- a/model/oauthproviders/openid/openid.go +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package oauthopenid - -import ( - "encoding/base64" - "encoding/json" - "errors" - "io" - "net/http" - "strconv" - "strings" - "time" - - "github.com/mattermost/mattermost-server/v6/einterfaces" - "github.com/mattermost/mattermost-server/v6/model" -) - -type CacheData struct { - Service string - Expires int64 - Settings model.SSOSettings -} - -type OpenIdMetadata struct { - Issuer string `json:"issuer"` - AuthorizationEndpoint string `json:"authorization_endpoint"` - TokenEndpoint string `json:"token_endpoint"` - UserEndpoint string `json:"userinfo_endpoint"` - JwksURI string `json:"jwks_uri"` - Algorithms []string `json:"id_token_signing_alg_values_supported"` -} - -type OpenIdProvider struct { - CacheData *CacheData -} - -type OpenIdUser struct { - Id string `json:"sub"` - Oid string `json:"oid"` //Office 365 only - FirstName string `json:"given_name"` - LastName string `json:"family_name"` - Name string `json:"name"` - Nickname string `json:"nickname"` - Email string `json:"email"` -} - -func init() { - provider := &OpenIdProvider{} - einterfaces.RegisterOAuthProvider(model.ServiceOpenid, provider) -} - -func (o *OpenIdProvider) userFromOpenIdUser(u *OpenIdUser) *model.User { - user := &model.User{} - - user.Email = u.Email - user.Username = model.CleanUsername(strings.Split(user.Email, "@")[0]) - if o.CacheData.Service == model.ServiceGitlab && u.Nickname != "" { - user.Username = u.Nickname - } - - user.FirstName = u.FirstName - user.LastName = u.LastName - user.Nickname = u.Nickname - - user.AuthData = new(string) - *user.AuthData = o.getAuthData(u) - - return user -} - -func (o *OpenIdProvider) getAuthData(u *OpenIdUser) string { - if o.CacheData.Service == model.ServiceOffice365 { - if u.Oid != "" { - return u.Oid - } - } - return u.Id -} - -func openIDUserFromJSON(data io.Reader) (*OpenIdUser, error) { - decoder := json.NewDecoder(data) - var u OpenIdUser - err := decoder.Decode(&u) - if err != nil { - return nil, err - } - return &u, nil -} - -func (u *OpenIdUser) IsValid() error { - if u.Id == "" { - return errors.New("invalid id") - } - - if u.Email == "" { - return errors.New("invalid emails") - } - return nil -} - -func (u *OpenIdUser) GetIdentifier() string { - return model.ServiceOpenid -} - -func (o *OpenIdProvider) GetUserFromJSON(data io.Reader, tokenUser *model.User) (*model.User, error) { - oid, err := openIDUserFromJSON(data) - if err != nil { - return nil, err - } - jsonUser := o.userFromOpenIdUser(oid) - - if tokenUser != nil { - jsonUser = o.combineUsers(jsonUser, tokenUser) - } - return jsonUser, nil -} - -func (o *OpenIdProvider) combineUsers(jsonUser *model.User, tokenUser *model.User) *model.User { - if o.CacheData.Service == model.ServiceOffice365 { - jsonUser.AuthData = tokenUser.AuthData - } - return jsonUser -} - -func (o *OpenIdProvider) GetAuthDataFromJSON(data io.Reader) (string, error) { - u, err := openIDUserFromJSON(data) - if err != nil { - return "", err - } - - err = u.IsValid() - if err != nil { - return "", err - } - return o.getAuthData(u), nil -} - -// GetSSOSettings returns SSO Settings from Cache or Discovery Document -func (o *OpenIdProvider) GetSSOSettings(config *model.Config, service string) (*model.SSOSettings, error) { - settings := config.OpenIdSettings - if service == model.ServiceOffice365 { - settings = *config.Office365Settings.SSOSettings() - } else if service == model.ServiceGoogle { - settings = config.GoogleSettings - } else if service == model.ServiceGitlab { - settings = config.GitLabSettings - } - - if o.CacheData != nil && !settingsChanged(*o.CacheData, service, settings) && o.CacheData.Expires > time.Now().Unix() { - return &o.CacheData.Settings, nil - } - - var age int64 = 0 - if *settings.DiscoveryEndpoint != "" { - response, err := http.Get(*settings.DiscoveryEndpoint) - if err != nil { - return nil, err - } - defer response.Body.Close() - - for _, v := range strings.Split(response.Header.Get("Cache-Control"), ",") { - if strings.Contains(v, "max-age") { - ageValue := strings.Split(v, "=")[1] - age, _ = strconv.ParseInt(ageValue, 10, 64) - } - } - responseData, err := io.ReadAll(response.Body) - if err != nil { - return nil, err - } - - var openIDResponse OpenIdMetadata - err = json.Unmarshal(responseData, &openIDResponse) - if err != nil { - return nil, err - } - - settings.AuthEndpoint = &openIDResponse.AuthorizationEndpoint - settings.TokenEndpoint = &openIDResponse.TokenEndpoint - settings.UserAPIEndpoint = &openIDResponse.UserEndpoint - } - expires := time.Now().Unix() + age - - o.CacheData = &CacheData{ - Service: service, - Expires: expires, - Settings: settings, - } - return &settings, nil -} - -func settingsChanged(cacheData CacheData, service string, configSettings model.SSOSettings) bool { - if cacheData.Service == service && - cacheData.Settings.DiscoveryEndpoint == configSettings.DiscoveryEndpoint && - cacheData.Settings.Secret == configSettings.Secret && - cacheData.Settings.Id == configSettings.Id { - return false - } - return true -} - -func (o *OpenIdProvider) GetUserFromIdToken(idToken string) (*model.User, error) { - parts := strings.Split(idToken, ".") - if len(parts) != 3 { - return nil, errors.New("invalid Id Token") - } - - b, err := base64.RawURLEncoding.DecodeString(parts[1]) - if err != nil { - return nil, err - } - - claims := &OpenIdUser{} - json.Unmarshal(b, &claims) - - return o.userFromOpenIdUser(claims), nil -} - -func (o *OpenIdProvider) IsSameUser(dbUser, oauthUser *model.User) bool { - // Office365 OAuth would store Ids without dashes. (ie. 0e8fddd450d344999a93a390ee8cb83d) - // Office365 OpenId will return as a formatted GUID (ie. '0e8fddd4-50d3-4499-9a93-a390ee8cb83d') - // If this is a UUID that starts with all zero. (ie. 00000000-0000-0000-be95-fe607df5dbeb) - // For backwards compatibility we store the auth data from OAuth as be95fe607df5dbeb - if dbUser.AuthData == nil || oauthUser.AuthData == nil { - return false - } - dbID := *dbUser.AuthData - oauthID := *oauthUser.AuthData - if dbID == "" || oauthID == "" { - return false - } - parts := strings.Split(oauthID, "-") - for _, part := range parts { - if strings.Count(part, "0") != len(part) { - if !strings.Contains(dbID, part) { - return false - } - } - } - return true -} diff --git a/model/oauthproviders/openid/openid_test.go b/model/oauthproviders/openid/openid_test.go deleted file mode 100644 index 7e157c6a88..0000000000 --- a/model/oauthproviders/openid/openid_test.go +++ /dev/null @@ -1,352 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package oauthopenid - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/v6/model" -) - -func TestGetAuthData(t *testing.T) { - ou := OpenIdUser{ - Id: "12345", - FirstName: "firstname", - LastName: "lastname", - Nickname: "nickname", - Email: "name@test.com", - Oid: "0e8fddd4-50d3-4499-9a93-a390ee8cb83d", - } - - provider := &OpenIdProvider{ - CacheData: &CacheData{ - Service: model.ServiceGitlab, - }, - } - - t.Run("validate return id", func(t *testing.T) { - authData := provider.getAuthData(&ou) - assert.Equal(t, ou.Id, authData) - }) - - provider.CacheData.Service = model.ServiceOffice365 - - fmt.Println(provider.CacheData.Service) - t.Run("validate Oid return", func(t *testing.T) { - authData := provider.getAuthData(&ou) - assert.Equal(t, ou.Oid, authData) - }) -} -func TestOpenIdUserFromJSON(t *testing.T) { - ou := OpenIdUser{ - Id: "12345", - FirstName: "firstname", - LastName: "lastname", - Nickname: "nickname", - Email: "name@test.com", - } - - provider := &OpenIdProvider{ - CacheData: &CacheData{ - Service: model.ServiceOpenid, - }, - } - - t.Run("valid OpenId user", func(t *testing.T) { - b, err := json.Marshal(ou) - require.NoError(t, err) - - _, err = provider.GetUserFromJSON(bytes.NewReader(b), nil) - require.NoError(t, err) - - _, err = provider.GetAuthDataFromJSON(bytes.NewReader(b)) - require.NoError(t, err) - }) - - t.Run("empty body should fail without panic", func(t *testing.T) { - _, err := provider.GetUserFromJSON(strings.NewReader("{}"), nil) - require.NoError(t, err) - - _, err = provider.GetAuthDataFromJSON(strings.NewReader("{}")) - require.Error(t, err) - }) - - t.Run("test getUserFromIdToken", func(t *testing.T) { - header := "dummyHeader" - payload := "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMDIyOTIwNzU1ODQ2LWtyM2JrMjBxdDRhMTlkODhqMWt1cjNqcnM2MmI2ZXFjLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTAyMjkyMDc1NTg0Ni1rcjNiazIwcXQ0YTE5ZDg4ajFrdXIzanJzNjJiNmVxYy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExMDIxNjMwMDI2MzA5MTY3MzQ2MSIsImhkIjoibWF0dGVybW9zdC5jb20iLCJlbWFpbCI6InNjb3R0LmJpc2hlbEBtYXR0ZXJtb3N0LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiWTVscFFoQlR0UkxHUGZqZ1BLSUhzUSIsIm5hbWUiOiJTY290dCBCaXNoZWwiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2dMR1Nfa19KV2dacmc1Y1BGLU9JNV9oUkhaREFvUUNoUFUyVE1VPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IlNjb3R0IiwiZmFtaWx5X25hbWUiOiJCaXNoZWwiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTYwODI0OTg5MSwiZXhwIjoxNjA4MjUzNDkxfQ" - signature := "dummysignature" - - testToken := header - _, err := provider.GetUserFromIdToken(testToken) - require.Error(t, err) - - testToken = header + "." + payload - _, err = provider.GetUserFromIdToken(testToken) - require.Error(t, err) - - t.Run("non ascii string encoded in the payload", func(t *testing.T) { - cases := []struct { - payload string - expectedName string - }{ - { - payload: "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMDIyOTIwNzU1ODQ2LWtyM2JrMjBxdDRhMTlkODhqMWt1cjNqcnM2MmI2ZXFjLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTAyMjkyMDc1NTg0Ni1rcjNiazIwcXQ0YTE5ZDg4ajFrdXIzanJzNjJiNmVxYy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExMDIxNjMwMDI2MzA5MTY3MzQ2MSIsImhkIjoibWF0dGVybW9zdC5jb20iLCJlbWFpbCI6InNjb3R0LmJpc2hlbEBtYXR0ZXJtb3N0LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiWTVscFFoQlR0UkxHUGZqZ1BLSUhzUSIsIm5hbWUiOiJTY290dCBCaXNoZWwiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2dMR1Nfa19KV2dacmc1Y1BGLU9JNV9oUkhaREFvUUNoUFUyVE1VPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6InRlc3TFiMWhxb4iLCJmYW1pbHlfbmFtZSI6IkJpc2hlbCIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNjA4MjQ5ODkxLCJleHAiOjE2MDgyNTM0OTF9", - expectedName: "testňšž", - }, - { - payload: "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMDIyOTIwNzU1ODQ2LWtyM2JrMjBxdDRhMTlkODhqMWt1cjNqcnM2MmI2ZXFjLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTAyMjkyMDc1NTg0Ni1rcjNiazIwcXQ0YTE5ZDg4ajFrdXIzanJzNjJiNmVxYy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExMDIxNjMwMDI2MzA5MTY3MzQ2MSIsImhkIjoibWF0dGVybW9zdC5jb20iLCJlbWFpbCI6InNjb3R0LmJpc2hlbEBtYXR0ZXJtb3N0LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiWTVscFFoQlR0UkxHUGZqZ1BLSUhzUSIsIm5hbWUiOiJTY290dCBCaXNoZWwiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2dMR1Nfa19KV2dacmc1Y1BGLU9JNV9oUkhaREFvUUNoUFUyVE1VPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6IlNjb3R0IiwiZmFtaWx5X25hbWUiOiJCaXNoZWwiLCJsb2NhbGUiOiJlbiIsImlhdCI6MTYwODI0OTg5MSwiZXhwIjoxNjA4MjUzNDkxfQ", - expectedName: "Scott", - }, - { - payload: "eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiIxMDIyOTIwNzU1ODQ2LWtyM2JrMjBxdDRhMTlkODhqMWt1cjNqcnM2MmI2ZXFjLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiMTAyMjkyMDc1NTg0Ni1rcjNiazIwcXQ0YTE5ZDg4ajFrdXIzanJzNjJiNmVxYy5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsInN1YiI6IjExMDIxNjMwMDI2MzA5MTY3MzQ2MSIsImhkIjoibWF0dGVybW9zdC5jb20iLCJlbWFpbCI6InNjb3R0LmJpc2hlbEBtYXR0ZXJtb3N0LmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiWTVscFFoQlR0UkxHUGZqZ1BLSUhzUSIsIm5hbWUiOiJTY290dCBCaXNoZWwiLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EtL0FPaDE0R2dMR1Nfa19KV2dacmc1Y1BGLU9JNV9oUkhaREFvUUNoUFUyVE1VPXM5Ni1jIiwiZ2l2ZW5fbmFtZSI6InRlc3TEjcSNxI0iLCJmYW1pbHlfbmFtZSI6IkJpc2hlbCIsImxvY2FsZSI6ImVuIiwiaWF0IjoxNjA4MjQ5ODkxLCJleHAiOjE2MDgyNTM0OTF9", - expectedName: "testččč", - }, - } - for _, c := range cases { - testToken = header + "." + c.payload + "." + signature - user, err := provider.GetUserFromIdToken(testToken) - require.NoError(t, err) - require.NotNil(t, user) - require.Equal(t, c.expectedName, user.FirstName) - } - }) - - }) -} - -func TestGetSSOSettings(t *testing.T) { - provider := &OpenIdProvider{ - CacheData: &CacheData{ - Service: model.ServiceOpenid, - }, - } - validJSON := `{ - "issuer": "issuer", - "authorization_endpoint": "authorization_endpoint", - "token_endpoint": "token_endpoint", - "userinfo_endpoint": "userinfo_endpoint", - "jwks_uri": "jwks_uri", - "id_token_signing_alg_values_supported": ["RS256"] - }` - var validFunctionCalled int - validServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", "max-age=3600") - fmt.Fprintln(w, validJSON) - validFunctionCalled++ - })) - defer validServer.Close() - - validConfig := model.Config{ - OpenIdSettings: model.SSOSettings{ - Enable: model.NewBool(true), - Secret: model.NewString("secret string"), - Id: model.NewString("id"), - Scope: model.NewString("profile openid email"), - AuthEndpoint: model.NewString(""), - TokenEndpoint: model.NewString(""), - UserAPIEndpoint: model.NewString(""), - DiscoveryEndpoint: model.NewString(validServer.URL), - }, - } - - t.Run("Error", func(t *testing.T) { - errorFunctionCalled := 0 - errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - errorFunctionCalled++ - w.Header().Add("Cache-Control", "max-age=3600") - http.Error(w, "Not found", 404) - })) - - errCfg := validConfig - errCfg.OpenIdSettings.DiscoveryEndpoint = model.NewString(errorServer.URL) - _, err := provider.GetSSOSettings(&errCfg, model.ServiceOpenid) - assert.Error(t, err) - assert.Equal(t, 1, errorFunctionCalled) - }) - - t.Run("UseCache", func(t *testing.T) { - validFunctionCalled = 0 - - settings, _ := provider.GetSSOSettings(&validConfig, model.ServiceOpenid) - assert.Equal(t, "authorization_endpoint", *settings.AuthEndpoint) - assert.Equal(t, "token_endpoint", *settings.TokenEndpoint) - assert.Equal(t, "userinfo_endpoint", *settings.UserAPIEndpoint) - assert.Equal(t, 1, validFunctionCalled) - // Should set cache - assert.Equal(t, provider.CacheData.Settings, *settings) - assert.True(t, provider.CacheData.Expires > 0) - currentCacheExpires := provider.CacheData.Expires - - // Call again should come from cache - settings, _ = provider.GetSSOSettings(&validConfig, model.ServiceOpenid) - assert.Equal(t, provider.CacheData.Settings, *settings) - assert.Equal(t, currentCacheExpires, provider.CacheData.Expires) - // should still be 1 - assert.Equal(t, 1, validFunctionCalled) - }) - - t.Run("CacheExpired", func(t *testing.T) { - // reset to original cache settings - settings, _ := provider.GetSSOSettings(&validConfig, model.ServiceOpenid) - // Should set cache - assert.Equal(t, provider.CacheData.Settings, *settings) - - // set cache to expired - provider.CacheData.Expires = time.Now().Add(time.Duration(-1) * time.Minute).Unix() - - // same config, should call endpoint - validFunctionCalled = 0 - provider.GetSSOSettings(&validConfig, model.ServiceOpenid) - assert.Equal(t, 1, validFunctionCalled) - assert.True(t, provider.CacheData.Expires > time.Now().Unix()) - }) - - t.Run("NoCache", func(t *testing.T) { - noCacheFunctionCalled := 0 - noCacheServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, validJSON) - noCacheFunctionCalled++ - })) - defer noCacheServer.Close() - - newCfg := validConfig - newCfg.OpenIdSettings.DiscoveryEndpoint = model.NewString(noCacheServer.URL) - - settings, err := provider.GetSSOSettings(&newCfg, model.ServiceOpenid) - require.NoError(t, err) - assert.Equal(t, "authorization_endpoint", *settings.AuthEndpoint) - assert.Equal(t, "token_endpoint", *settings.TokenEndpoint) - assert.Equal(t, "userinfo_endpoint", *settings.UserAPIEndpoint) - assert.Equal(t, 1, noCacheFunctionCalled) - // Should set cache - assert.Equal(t, provider.CacheData.Settings, *settings) - // Cache Expires, set, less than, equal now. - assert.True(t, provider.CacheData.Expires <= time.Now().Unix()) - - // Call again, should call server again - _, err = provider.GetSSOSettings(&newCfg, model.ServiceOpenid) - require.NoError(t, err) - assert.Equal(t, 2, noCacheFunctionCalled) - }) - - t.Run("ChangeService", func(t *testing.T) { - // reset to original cache settings - settings, _ := provider.GetSSOSettings(&validConfig, model.ServiceOpenid) - // Should set cache - assert.Equal(t, provider.CacheData.Settings, *settings) - assert.True(t, provider.CacheData.Expires > time.Now().Unix()) - - // create identical setting for Google - googleCfg := model.Config{ - GoogleSettings: model.SSOSettings{}, - } - googleCfg.GoogleSettings = validConfig.OpenIdSettings - - // call with different service, same config settings - validFunctionCalled = 0 - provider.GetSSOSettings(&googleCfg, model.ServiceGoogle) - assert.Equal(t, model.ServiceGoogle, provider.CacheData.Service) - assert.Equal(t, 1, validFunctionCalled) - }) - - t.Run("ChangeConfigSettings", func(t *testing.T) { - secondFunctionCalled := 0 - secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Cache-Control", "max-age=3600") - fmt.Fprintln(w, validJSON) - secondFunctionCalled++ - })) - defer secondServer.Close() - - newCfg := validConfig - newCfg.OpenIdSettings.DiscoveryEndpoint = model.NewString(secondServer.URL) - - // new URL - settings, err := provider.GetSSOSettings(&newCfg, model.ServiceOpenid) - require.NoError(t, err) - assert.Equal(t, "authorization_endpoint", *settings.AuthEndpoint) - assert.Equal(t, "token_endpoint", *settings.TokenEndpoint) - assert.Equal(t, "userinfo_endpoint", *settings.UserAPIEndpoint) - assert.Equal(t, 1, secondFunctionCalled) - - // new secret - newCfg.OpenIdSettings.Secret = model.NewString("NewSecret") - _, err = provider.GetSSOSettings(&newCfg, model.ServiceOpenid) - require.NoError(t, err) - assert.Equal(t, newCfg.OpenIdSettings.Secret, provider.CacheData.Settings.Secret) - assert.Equal(t, 2, secondFunctionCalled) - - // new Id - newCfg.OpenIdSettings.Id = model.NewString("NewId") - _, err = provider.GetSSOSettings(&newCfg, model.ServiceOpenid) - require.NoError(t, err) - assert.Equal(t, newCfg.OpenIdSettings.Id, provider.CacheData.Settings.Id) - assert.Equal(t, 3, secondFunctionCalled) - }) -} - -func TestCacheControlPanic(t *testing.T) { - provider := &OpenIdProvider{ - CacheData: &CacheData{ - Service: model.ServiceOpenid, - }, - } - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "no header") - })) - defer ts.Close() - - cfg := &model.Config{ - OpenIdSettings: model.SSOSettings{ - DiscoveryEndpoint: model.NewString(ts.URL), - }, - } - - require.NotPanics(t, func() { - provider.GetSSOSettings(cfg, model.ServiceOpenid) - }) -} - -func TestIsSameUser(t *testing.T) { - provider := &OpenIdProvider{ - CacheData: &CacheData{ - Service: model.ServiceOpenid, - }, - } - cases := []struct { - dbUser model.User - oauthUser model.User - verified bool - }{ - {model.User{AuthData: model.NewString("202993a800824dc1b4496d598d47c58a")}, model.User{AuthData: model.NewString("202993a8-0082-4dc1-b449-6d598d47c58a")}, true}, - {model.User{AuthData: model.NewString("202993a85a824dc1b4496d598d47c58a")}, model.User{AuthData: model.NewString("")}, false}, - {model.User{AuthData: model.NewString("")}, model.User{AuthData: model.NewString("202993a8-5a82-4dc1-b449-6d598d47c58a")}, false}, - {model.User{AuthData: model.NewString("be95fe607df5dbeb")}, model.User{AuthData: model.NewString("00000000-0000-0000-be95-fe607df5dbeb")}, true}, - {model.User{AuthData: model.NewString("be95fe607df5dbeb")}, model.User{AuthData: model.NewString("00000000-0000-0000-be90-fe607df5dbeb")}, false}, - {model.User{AuthData: model.NewString("be95fe607df5dbeb")}, model.User{AuthData: model.NewString("00000000-0000-0000-be95-fe607df5dbe0")}, false}, - {model.User{AuthData: model.NewString("hello")}, model.User{}, false}, - } - for _, c := range cases { - verified := provider.IsSameUser(&c.dbUser, &c.oauthUser) - if verified != c.verified { - if c.verified { - t.Logf("'%v' should have matched '%v'", c.dbUser, c.oauthUser) - } else { - t.Logf("'%v' should not have matched '%v'", c.dbUser, c.oauthUser) - } - t.FailNow() - } - } -}