[MM-47384] Make OpenID Connect free for all (#21556)
* wip: make OpenID Connect free-for-all * Deprecation note: GoogleOAuth, Office365OAuth * Improve deprecation comments Co-authored-by: Martin Kraft <martin@upspin.org> * Lint fix * Add model/oauthproviders, move google, openid, office365 from enterprise * Vet fixes * Remove redundant log Co-authored-by: Martin Kraft <martin@upspin.org> Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
fcd45de73b
Коммит
ee40eb956c
@@ -25,7 +25,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v6/shared/mail"
|
"github.com/mattermost/mattermost-server/v6/shared/mail"
|
||||||
"github.com/mattermost/mattermost-server/v6/utils/testutils"
|
"github.com/mattermost/mattermost-server/v6/utils/testutils"
|
||||||
|
|
||||||
_ "github.com/mattermost/mattermost-server/v6/model/gitlab"
|
_ "github.com/mattermost/mattermost-server/v6/model/oauthproviders/gitlab"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateUser(t *testing.T) {
|
func TestCreateUser(t *testing.T) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
"github.com/mattermost/mattermost-server/v6/einterfaces"
|
||||||
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
|
"github.com/mattermost/mattermost-server/v6/einterfaces/mocks"
|
||||||
"github.com/mattermost/mattermost-server/v6/model"
|
"github.com/mattermost/mattermost-server/v6/model"
|
||||||
oauthgitlab "github.com/mattermost/mattermost-server/v6/model/gitlab"
|
oauthgitlab "github.com/mattermost/mattermost-server/v6/model/oauthproviders/gitlab"
|
||||||
"github.com/mattermost/mattermost-server/v6/store"
|
"github.com/mattermost/mattermost-server/v6/store"
|
||||||
storemocks "github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
storemocks "github.com/mattermost/mattermost-server/v6/store/storetest/mocks"
|
||||||
"github.com/mattermost/mattermost-server/v6/utils/testutils"
|
"github.com/mattermost/mattermost-server/v6/utils/testutils"
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import (
|
|||||||
// Import and register app layer slash commands
|
// Import and register app layer slash commands
|
||||||
_ "github.com/mattermost/mattermost-server/v6/app/slashcommands"
|
_ "github.com/mattermost/mattermost-server/v6/app/slashcommands"
|
||||||
// Plugins
|
// Plugins
|
||||||
_ "github.com/mattermost/mattermost-server/v6/model/gitlab"
|
_ "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
|
// Enterprise Imports
|
||||||
_ "github.com/mattermost/mattermost-server/v6/imports"
|
_ "github.com/mattermost/mattermost-server/v6/imports"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -298,11 +298,11 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
|
|||||||
props["SamlLoginButtonColor"] = ""
|
props["SamlLoginButtonColor"] = ""
|
||||||
props["SamlLoginButtonBorderColor"] = ""
|
props["SamlLoginButtonBorderColor"] = ""
|
||||||
props["SamlLoginButtonTextColor"] = ""
|
props["SamlLoginButtonTextColor"] = ""
|
||||||
props["EnableSignUpWithGoogle"] = "false"
|
props["EnableSignUpWithOpenId"] = strconv.FormatBool(*c.OpenIdSettings.Enable)
|
||||||
props["EnableSignUpWithOffice365"] = "false"
|
props["OpenIdButtonColor"] = *c.OpenIdSettings.ButtonColor
|
||||||
props["EnableSignUpWithOpenId"] = "false"
|
props["OpenIdButtonText"] = *c.OpenIdSettings.ButtonText
|
||||||
props["OpenIdButtonText"] = ""
|
props["EnableSignUpWithGoogle"] = strconv.FormatBool(*c.GoogleSettings.Enable)
|
||||||
props["OpenIdButtonColor"] = ""
|
props["EnableSignUpWithOffice365"] = strconv.FormatBool(*c.Office365Settings.Enable)
|
||||||
props["CWSURL"] = ""
|
props["CWSURL"] = ""
|
||||||
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
|
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
|
||||||
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
|
props["CustomBrandText"] = *c.TeamSettings.CustomBrandText
|
||||||
@@ -329,20 +329,6 @@ func GenerateLimitedClientConfig(c *model.Config, telemetryID string, license *m
|
|||||||
props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
|
props["SamlLoginButtonTextColor"] = *c.SamlSettings.LoginButtonTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if *license.Features.CustomTermsOfService {
|
if *license.Features.CustomTermsOfService {
|
||||||
props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled)
|
props["EnableCustomTermsOfService"] = strconv.FormatBool(*c.SupportSettings.CustomTermsOfServiceEnabled)
|
||||||
props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10)
|
props["CustomTermsOfServiceReAcceptancePeriod"] = strconv.FormatInt(int64(*c.SupportSettings.CustomTermsOfServiceReAcceptancePeriod), 10)
|
||||||
|
|||||||
@@ -77,12 +77,18 @@ type TrialLicenseRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Features struct {
|
type Features struct {
|
||||||
Users *int `json:"users"`
|
Users *int `json:"users"`
|
||||||
LDAP *bool `json:"ldap"`
|
LDAP *bool `json:"ldap"`
|
||||||
LDAPGroups *bool `json:"ldap_groups"`
|
LDAPGroups *bool `json:"ldap_groups"`
|
||||||
MFA *bool `json:"mfa"`
|
MFA *bool `json:"mfa"`
|
||||||
GoogleOAuth *bool `json:"google_oauth"`
|
|
||||||
Office365OAuth *bool `json:"office365_oauth"`
|
// 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.
|
||||||
OpenId *bool `json:"openid"`
|
OpenId *bool `json:"openid"`
|
||||||
Compliance *bool `json:"compliance"`
|
Compliance *bool `json:"compliance"`
|
||||||
Cluster *bool `json:"cluster"`
|
Cluster *bool `json:"cluster"`
|
||||||
@@ -164,15 +170,15 @@ func (f *Features) SetDefaults() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if f.GoogleOAuth == nil {
|
if f.GoogleOAuth == nil {
|
||||||
f.GoogleOAuth = NewBool(*f.FutureFeatures)
|
f.GoogleOAuth = NewBool(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Office365OAuth == nil {
|
if f.Office365OAuth == nil {
|
||||||
f.Office365OAuth = NewBool(*f.FutureFeatures)
|
f.Office365OAuth = NewBool(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.OpenId == nil {
|
if f.OpenId == nil {
|
||||||
f.OpenId = NewBool(*f.FutureFeatures)
|
f.OpenId = NewBool(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.Compliance == nil {
|
if f.Compliance == nil {
|
||||||
|
|||||||
158
model/oauthproviders/google/google.go
Обычный файл
158
model/oauthproviders/google/google.go
Обычный файл
@@ -0,0 +1,158 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
61
model/oauthproviders/google/google_test.go
Обычный файл
61
model/oauthproviders/google/google_test.go
Обычный файл
@@ -0,0 +1,61 @@
|
|||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
116
model/oauthproviders/office365/office365.go
Обычный файл
116
model/oauthproviders/office365/office365.go
Обычный файл
@@ -0,0 +1,116 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
43
model/oauthproviders/office365/office365_test.go
Обычный файл
43
model/oauthproviders/office365/office365_test.go
Обычный файл
@@ -0,0 +1,43 @@
|
|||||||
|
// 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
243
model/oauthproviders/openid/openid.go
Обычный файл
243
model/oauthproviders/openid/openid.go
Обычный файл
@@ -0,0 +1,243 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
352
model/oauthproviders/openid/openid_test.go
Обычный файл
352
model/oauthproviders/openid/openid_test.go
Обычный файл
@@ -0,0 +1,352 @@
|
|||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user