From ab9bb54144f136bbbba2ce2e94fd88c0be0ee1cf Mon Sep 17 00:00:00 2001 From: zeripath Date: Fri, 6 Aug 2021 02:11:08 +0100 Subject: [PATCH] Add microsoft oauth2 providers (#16544) * Clean up oauth2 providers Signed-off-by: Andrew Thornton * Add AzureAD, AzureADv2, MicrosoftOnline OAuth2 providers Signed-off-by: Andrew Thornton * Apply suggestions from code review * remove unused Scopes Signed-off-by: Andrew Thornton Co-authored-by: techknowlogick --- go.sum | 1 + options/locale/locale_en-US.ini | 1 + public/img/auth/azuread.png | Bin 0 -> 3099 bytes public/img/auth/azureadv2.png | Bin 0 -> 3099 bytes public/img/auth/microsoftonline.png | Bin 0 -> 792 bytes routers/web/admin/auths.go | 34 +- routers/web/user/setting/security.go | 17 +- services/auth/source/oauth2/providers.go | 252 ++----- services/auth/source/oauth2/providers_base.go | 33 + .../auth/source/oauth2/providers_custom.go | 118 +++ .../auth/source/oauth2/providers_openid.go | 52 ++ .../auth/source/oauth2/providers_simple.go | 108 +++ services/auth/source/oauth2/source_name.go | 19 + .../auth/source/oauth2/source_register.go | 4 +- services/auth/source/oauth2/urlmapping.go | 80 +- services/forms/auth_form.go | 1 + templates/admin/auth/edit.tmpl | 21 +- templates/admin/auth/source/oauth.tmpl | 29 +- vendor/github.com/markbates/going/LICENSE.txt | 22 + .../markbates/going/defaults/defaults.go | 36 + .../goth/providers/azuread/azuread.go | 187 +++++ .../goth/providers/azuread/session.go | 63 ++ .../goth/providers/azureadv2/azureadv2.go | 233 ++++++ .../goth/providers/azureadv2/scopes.go | 714 ++++++++++++++++++ .../goth/providers/azureadv2/session.go | 63 ++ .../microsoftonline/microsoftonline.go | 190 +++++ .../goth/providers/microsoftonline/session.go | 62 ++ vendor/modules.txt | 5 + web_src/js/index.js | 47 +- 29 files changed, 2132 insertions(+), 260 deletions(-) create mode 100644 public/img/auth/azuread.png create mode 100644 public/img/auth/azureadv2.png create mode 100644 public/img/auth/microsoftonline.png create mode 100644 services/auth/source/oauth2/providers_base.go create mode 100644 services/auth/source/oauth2/providers_custom.go create mode 100644 services/auth/source/oauth2/providers_openid.go create mode 100644 services/auth/source/oauth2/providers_simple.go create mode 100644 services/auth/source/oauth2/source_name.go create mode 100644 vendor/github.com/markbates/going/LICENSE.txt create mode 100644 vendor/github.com/markbates/going/defaults/defaults.go create mode 100644 vendor/github.com/markbates/goth/providers/azuread/azuread.go create mode 100644 vendor/github.com/markbates/goth/providers/azuread/session.go create mode 100644 vendor/github.com/markbates/goth/providers/azureadv2/azureadv2.go create mode 100644 vendor/github.com/markbates/goth/providers/azureadv2/scopes.go create mode 100644 vendor/github.com/markbates/goth/providers/azureadv2/session.go create mode 100644 vendor/github.com/markbates/goth/providers/microsoftonline/microsoftonline.go create mode 100644 vendor/github.com/markbates/goth/providers/microsoftonline/session.go diff --git a/go.sum b/go.sum index b1e5a1f96..24ac6a65a 100644 --- a/go.sum +++ b/go.sum @@ -762,6 +762,7 @@ github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/going v1.0.0 h1:DQw0ZP7NbNlFGcKbcE/IVSOAFzScxRtLpd0rLMzLhq0= github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA= github.com/markbates/goth v1.68.0 h1:90sKvjRAKHcl9V2uC9x/PJXeD78cFPiBsyP1xVhoQfA= github.com/markbates/goth v1.68.0/go.mod h1:V2VcDMzDiMHW+YmqYl7i0cMiAUeCkAe4QE6jRKBhXZw= diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 23d7b2387..ccf19293f 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2441,6 +2441,7 @@ auths.oauth2_tokenURL = Token URL auths.oauth2_authURL = Authorize URL auths.oauth2_profileURL = Profile URL auths.oauth2_emailURL = Email URL +auths.oauth2_tenant = Tenant auths.enable_auto_register = Enable Auto Registration auths.sspi_auto_create_users = Automatically create users auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time diff --git a/public/img/auth/azuread.png b/public/img/auth/azuread.png new file mode 100644 index 0000000000000000000000000000000000000000..1adbf15e28a0da8e8001a97790da0ad234e2b48a GIT binary patch literal 3099 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 TEmG%!shGjj)z4*}Q$iB}$Ddk1 literal 0 HcmV?d00001 diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go index 20efd4a2a..2e9697533 100644 --- a/routers/web/admin/auths.go +++ b/routers/web/admin/auths.go @@ -98,8 +98,8 @@ func NewAuthSource(ctx *context.Context) { ctx.Data["AuthSources"] = authSources ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = smtp.Authenticators - ctx.Data["OAuth2Providers"] = oauth2.Providers - ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings + oauth2providers := oauth2.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = oauth2providers ctx.Data["SSPIAutoCreateUsers"] = true ctx.Data["SSPIAutoActivateUsers"] = true @@ -108,10 +108,7 @@ func NewAuthSource(ctx *context.Context) { ctx.Data["SSPIDefaultLanguage"] = "" // only the first as default - for key := range oauth2.Providers { - ctx.Data["oauth2_provider"] = key - break - } + ctx.Data["oauth2_provider"] = oauth2providers[0] ctx.HTML(http.StatusOK, tplAuthNew) } @@ -170,6 +167,7 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source { AuthURL: form.Oauth2AuthURL, ProfileURL: form.Oauth2ProfileURL, EmailURL: form.Oauth2EmailURL, + Tenant: form.Oauth2Tenant, } } else { customURLMapping = nil @@ -220,8 +218,8 @@ func NewAuthSourcePost(ctx *context.Context) { ctx.Data["AuthSources"] = authSources ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = smtp.Authenticators - ctx.Data["OAuth2Providers"] = oauth2.Providers - ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings + oauth2providers := oauth2.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = oauth2providers ctx.Data["SSPIAutoCreateUsers"] = true ctx.Data["SSPIAutoActivateUsers"] = true @@ -299,8 +297,8 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["SecurityProtocols"] = securityProtocols ctx.Data["SMTPAuths"] = smtp.Authenticators - ctx.Data["OAuth2Providers"] = oauth2.Providers - ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings + oauth2providers := oauth2.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = oauth2providers source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { @@ -311,7 +309,17 @@ func EditAuthSource(ctx *context.Context) { ctx.Data["HasTLS"] = source.HasTLS() if source.IsOAuth2() { - ctx.Data["CurrentOAuth2Provider"] = oauth2.Providers[source.Cfg.(*oauth2.Source).Provider] + type Named interface { + Name() string + } + + for _, provider := range oauth2providers { + if provider.Name() == source.Cfg.(Named).Name() { + ctx.Data["CurrentOAuth2Provider"] = provider + break + } + } + } ctx.HTML(http.StatusOK, tplAuthEdit) } @@ -324,8 +332,8 @@ func EditAuthSourcePost(ctx *context.Context) { ctx.Data["PageIsAdminAuthentications"] = true ctx.Data["SMTPAuths"] = smtp.Authenticators - ctx.Data["OAuth2Providers"] = oauth2.Providers - ctx.Data["OAuth2DefaultCustomURLMappings"] = oauth2.DefaultCustomURLMappings + oauth2providers := oauth2.GetOAuth2Providers() + ctx.Data["OAuth2Providers"] = oauth2providers source, err := models.GetLoginSourceByID(ctx.ParamsInt64(":authid")) if err != nil { diff --git a/routers/web/user/setting/security.go b/routers/web/user/setting/security.go index 02969fb1e..36c6d7df7 100644 --- a/routers/web/user/setting/security.go +++ b/routers/web/user/setting/security.go @@ -12,7 +12,6 @@ import ( "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/services/auth/source/oauth2" ) const ( @@ -92,9 +91,19 @@ func loadSecurityData(ctx *context.Context) { for _, externalAccount := range accountLinks { if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { var providerDisplayName string - if loginSource.IsOAuth2() { - providerTechnicalName := loginSource.Cfg.(*oauth2.Source).Provider - providerDisplayName = oauth2.Providers[providerTechnicalName].DisplayName + + type DisplayNamed interface { + DisplayName() string + } + + type Named interface { + Name() string + } + + if displayNamed, ok := loginSource.Cfg.(DisplayNamed); ok { + providerDisplayName = displayNamed.DisplayName() + } else if named, ok := loginSource.Cfg.(Named); ok { + providerDisplayName = named.Name() } else { providerDisplayName = loginSource.Name } diff --git a/services/auth/source/oauth2/providers.go b/services/auth/source/oauth2/providers.go index 8df8d6296..2196e3049 100644 --- a/services/auth/source/oauth2/providers.go +++ b/services/auth/source/oauth2/providers.go @@ -13,80 +13,72 @@ import ( "code.gitea.io/gitea/modules/setting" "github.com/markbates/goth" - "github.com/markbates/goth/providers/bitbucket" - "github.com/markbates/goth/providers/discord" - "github.com/markbates/goth/providers/dropbox" - "github.com/markbates/goth/providers/facebook" - "github.com/markbates/goth/providers/gitea" - "github.com/markbates/goth/providers/github" - "github.com/markbates/goth/providers/gitlab" - "github.com/markbates/goth/providers/google" - "github.com/markbates/goth/providers/mastodon" - "github.com/markbates/goth/providers/nextcloud" - "github.com/markbates/goth/providers/openidConnect" - "github.com/markbates/goth/providers/twitter" - "github.com/markbates/goth/providers/yandex" ) -// Provider describes the display values of a single OAuth2 provider -type Provider struct { - Name string - DisplayName string - Image string - CustomURLMapping *CustomURLMapping +// Provider is an interface for describing a single OAuth2 provider +type Provider interface { + Name() string + DisplayName() string + Image() string + CustomURLSettings() *CustomURLSettings +} + +// GothProviderCreator provides a function to create a goth.Provider +type GothProviderCreator interface { + CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) +} + +// GothProvider is an interface for describing a single OAuth2 provider +type GothProvider interface { + Provider + GothProviderCreator +} + +// ImagedProvider provide an overrided image setting for the provider +type ImagedProvider struct { + GothProvider + image string +} + +// Image returns the image path for this provider +func (i *ImagedProvider) Image() string { + return i.image +} + +// NewImagedProvider is a constructor function for the ImagedProvider +func NewImagedProvider(image string, provider GothProvider) *ImagedProvider { + return &ImagedProvider{ + GothProvider: provider, + image: image, + } } // Providers contains the map of registered OAuth2 providers in Gitea (based on goth) // key is used to map the OAuth2Provider with the goth provider type (also in LoginSource.OAuth2Config.Provider) // value is used to store display data -var Providers = map[string]Provider{ - "bitbucket": {Name: "bitbucket", DisplayName: "Bitbucket", Image: "/assets/img/auth/bitbucket.png"}, - "dropbox": {Name: "dropbox", DisplayName: "Dropbox", Image: "/assets/img/auth/dropbox.png"}, - "facebook": {Name: "facebook", DisplayName: "Facebook", Image: "/assets/img/auth/facebook.png"}, - "github": { - Name: "github", DisplayName: "GitHub", Image: "/assets/img/auth/github.png", - CustomURLMapping: &CustomURLMapping{ - TokenURL: github.TokenURL, - AuthURL: github.AuthURL, - ProfileURL: github.ProfileURL, - EmailURL: github.EmailURL, - }, - }, - "gitlab": { - Name: "gitlab", DisplayName: "GitLab", Image: "/assets/img/auth/gitlab.png", - CustomURLMapping: &CustomURLMapping{ - TokenURL: gitlab.TokenURL, - AuthURL: gitlab.AuthURL, - ProfileURL: gitlab.ProfileURL, - }, - }, - "gplus": {Name: "gplus", DisplayName: "Google", Image: "/assets/img/auth/google.png"}, - "openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/assets/img/auth/openid_connect.svg"}, - "twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/assets/img/auth/twitter.png"}, - "discord": {Name: "discord", DisplayName: "Discord", Image: "/assets/img/auth/discord.png"}, - "gitea": { - Name: "gitea", DisplayName: "Gitea", Image: "/assets/img/auth/gitea.png", - CustomURLMapping: &CustomURLMapping{ - TokenURL: gitea.TokenURL, - AuthURL: gitea.AuthURL, - ProfileURL: gitea.ProfileURL, - }, - }, - "nextcloud": { - Name: "nextcloud", DisplayName: "Nextcloud", Image: "/assets/img/auth/nextcloud.png", - CustomURLMapping: &CustomURLMapping{ - TokenURL: nextcloud.TokenURL, - AuthURL: nextcloud.AuthURL, - ProfileURL: nextcloud.ProfileURL, - }, - }, - "yandex": {Name: "yandex", DisplayName: "Yandex", Image: "/assets/img/auth/yandex.png"}, - "mastodon": { - Name: "mastodon", DisplayName: "Mastodon", Image: "/assets/img/auth/mastodon.png", - CustomURLMapping: &CustomURLMapping{ - AuthURL: mastodon.InstanceURL, - }, - }, +var gothProviders = map[string]GothProvider{} + +// RegisterGothProvider registers a GothProvider +func RegisterGothProvider(provider GothProvider) { + if _, has := gothProviders[provider.Name()]; has { + log.Fatal("Duplicate oauth2provider type provided: %s", provider.Name()) + } + gothProviders[provider.Name()] = provider +} + +// GetOAuth2Providers returns the map of unconfigured OAuth2 providers +// key is used as technical name (like in the callbackURL) +// values to display +func GetOAuth2Providers() []Provider { + providers := make([]Provider, 0, len(gothProviders)) + + for _, provider := range gothProviders { + providers = append(providers, provider) + } + sort.Slice(providers, func(i, j int) bool { + return providers[i].Name() < providers[j].Name() + }) + return providers } // GetActiveOAuth2Providers returns the map of configured active OAuth2 providers @@ -103,9 +95,9 @@ func GetActiveOAuth2Providers() ([]string, map[string]Provider, error) { var orderedKeys []string providers := make(map[string]Provider) for _, source := range loginSources { - prov := Providers[source.Cfg.(*Source).Provider] + prov := gothProviders[source.Cfg.(*Source).Provider] if source.Cfg.(*Source).IconURL != "" { - prov.Image = source.Cfg.(*Source).IconURL + prov = &ImagedProvider{prov, source.Cfg.(*Source).IconURL} } providers[source.Name] = prov orderedKeys = append(orderedKeys, source.Name) @@ -116,9 +108,9 @@ func GetActiveOAuth2Providers() ([]string, map[string]Provider, error) { return orderedKeys, providers, nil } -// RegisterProvider register a OAuth2 provider in goth lib -func RegisterProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) error { - provider, err := createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL, customURLMapping) +// RegisterProviderWithGothic register a OAuth2 provider in goth lib +func RegisterProviderWithGothic(providerName string, source *Source) error { + provider, err := createProvider(providerName, source) if err == nil && provider != nil { gothRWMutex.Lock() @@ -130,8 +122,8 @@ func RegisterProvider(providerName, providerType, clientID, clientSecret, openID return err } -// RemoveProvider removes the given OAuth2 provider from the goth lib -func RemoveProvider(providerName string) { +// RemoveProviderFromGothic removes the given OAuth2 provider from the goth lib +func RemoveProviderFromGothic(providerName string) { gothRWMutex.Lock() defer gothRWMutex.Unlock() @@ -147,114 +139,20 @@ func ClearProviders() { } // used to create different types of goth providers -func createProvider(providerName, providerType, clientID, clientSecret, openIDConnectAutoDiscoveryURL string, customURLMapping *CustomURLMapping) (goth.Provider, error) { +func createProvider(providerName string, source *Source) (goth.Provider, error) { callbackURL := setting.AppURL + "user/oauth2/" + url.PathEscape(providerName) + "/callback" var provider goth.Provider var err error - switch providerType { - case "bitbucket": - provider = bitbucket.New(clientID, clientSecret, callbackURL, "account") - case "dropbox": - provider = dropbox.New(clientID, clientSecret, callbackURL) - case "facebook": - provider = facebook.New(clientID, clientSecret, callbackURL, "email") - case "github": - authURL := github.AuthURL - tokenURL := github.TokenURL - profileURL := github.ProfileURL - emailURL := github.EmailURL - if customURLMapping != nil { - if len(customURLMapping.AuthURL) > 0 { - authURL = customURLMapping.AuthURL - } - if len(customURLMapping.TokenURL) > 0 { - tokenURL = customURLMapping.TokenURL - } - if len(customURLMapping.ProfileURL) > 0 { - profileURL = customURLMapping.ProfileURL - } - if len(customURLMapping.EmailURL) > 0 { - emailURL = customURLMapping.EmailURL - } - } - scopes := []string{} - if setting.OAuth2Client.EnableAutoRegistration { - scopes = append(scopes, "user:email") - } - provider = github.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, emailURL, scopes...) - case "gitlab": - authURL := gitlab.AuthURL - tokenURL := gitlab.TokenURL - profileURL := gitlab.ProfileURL - if customURLMapping != nil { - if len(customURLMapping.AuthURL) > 0 { - authURL = customURLMapping.AuthURL - } - if len(customURLMapping.TokenURL) > 0 { - tokenURL = customURLMapping.TokenURL - } - if len(customURLMapping.ProfileURL) > 0 { - profileURL = customURLMapping.ProfileURL - } - } - provider = gitlab.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL, "read_user") - case "gplus": // named gplus due to legacy gplus -> google migration (Google killed Google+). This ensures old connections still work - scopes := []string{"email"} - if setting.OAuth2Client.UpdateAvatar || setting.OAuth2Client.EnableAutoRegistration { - scopes = append(scopes, "profile") - } - provider = google.New(clientID, clientSecret, callbackURL, scopes...) - case "openidConnect": - if provider, err = openidConnect.New(clientID, clientSecret, callbackURL, openIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...); err != nil { - log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, openIDConnectAutoDiscoveryURL, err) - } - case "twitter": - provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL) - case "discord": - provider = discord.New(clientID, clientSecret, callbackURL, discord.ScopeIdentify, discord.ScopeEmail) - case "gitea": - authURL := gitea.AuthURL - tokenURL := gitea.TokenURL - profileURL := gitea.ProfileURL - if customURLMapping != nil { - if len(customURLMapping.AuthURL) > 0 { - authURL = customURLMapping.AuthURL - } - if len(customURLMapping.TokenURL) > 0 { - tokenURL = customURLMapping.TokenURL - } - if len(customURLMapping.ProfileURL) > 0 { - profileURL = customURLMapping.ProfileURL - } - } - provider = gitea.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL) - case "nextcloud": - authURL := nextcloud.AuthURL - tokenURL := nextcloud.TokenURL - profileURL := nextcloud.ProfileURL - if customURLMapping != nil { - if len(customURLMapping.AuthURL) > 0 { - authURL = customURLMapping.AuthURL - } - if len(customURLMapping.TokenURL) > 0 { - tokenURL = customURLMapping.TokenURL - } - if len(customURLMapping.ProfileURL) > 0 { - profileURL = customURLMapping.ProfileURL - } - } - provider = nextcloud.NewCustomisedURL(clientID, clientSecret, callbackURL, authURL, tokenURL, profileURL) - case "yandex": - // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ - provider = yandex.New(clientID, clientSecret, callbackURL, "login:email", "login:info", "login:avatar") - case "mastodon": - instanceURL := mastodon.InstanceURL - if customURLMapping != nil && len(customURLMapping.AuthURL) > 0 { - instanceURL = customURLMapping.AuthURL - } - provider = mastodon.NewCustomisedURL(clientID, clientSecret, callbackURL, instanceURL) + p, ok := gothProviders[source.Provider] + if !ok { + return nil, models.ErrLoginSourceNotActived + } + + provider, err = p.CreateGothProvider(providerName, callbackURL, source) + if err != nil { + return provider, err } // always set the name if provider is created so we can support multiple setups of 1 provider diff --git a/services/auth/source/oauth2/providers_base.go b/services/auth/source/oauth2/providers_base.go new file mode 100644 index 000000000..b6b6d0bbd --- /dev/null +++ b/services/auth/source/oauth2/providers_base.go @@ -0,0 +1,33 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package oauth2 + +// BaseProvider represents a common base for Provider +type BaseProvider struct { + name string + displayName string +} + +// Name provides the technical name for this provider +func (b *BaseProvider) Name() string { + return b.name +} + +// DisplayName returns the friendly name for this provider +func (b *BaseProvider) DisplayName() string { + return b.displayName +} + +// Image returns an image path for this provider +func (b *BaseProvider) Image() string { + return "/assets/img/auth/" + b.name + ".png" +} + +// CustomURLSettings returns the custom url settings for this provider +func (b *BaseProvider) CustomURLSettings() *CustomURLSettings { + return nil +} + +var _ (Provider) = &BaseProvider{} diff --git a/services/auth/source/oauth2/providers_custom.go b/services/auth/source/oauth2/providers_custom.go new file mode 100644 index 000000000..de1a1690c --- /dev/null +++ b/services/auth/source/oauth2/providers_custom.go @@ -0,0 +1,118 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/azureadv2" + "github.com/markbates/goth/providers/gitea" + "github.com/markbates/goth/providers/github" + "github.com/markbates/goth/providers/gitlab" + "github.com/markbates/goth/providers/mastodon" + "github.com/markbates/goth/providers/nextcloud" +) + +// CustomProviderNewFn creates a goth.Provider using a custom url mapping +type CustomProviderNewFn func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) + +// CustomProvider is a GothProvider that has CustomURL features +type CustomProvider struct { + BaseProvider + customURLSettings *CustomURLSettings + newFn CustomProviderNewFn +} + +// CustomURLSettings returns the CustomURLSettings for this provider +func (c *CustomProvider) CustomURLSettings() *CustomURLSettings { + return c.customURLSettings +} + +// CreateGothProvider creates a GothProvider from this Provider +func (c *CustomProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) { + custom := c.customURLSettings.OverrideWith(source.CustomURLMapping) + + return c.newFn(source.ClientID, source.ClientSecret, callbackURL, custom) +} + +// NewCustomProvider is a constructor function for custom providers +func NewCustomProvider(name, displayName string, customURLSetting *CustomURLSettings, newFn CustomProviderNewFn) *CustomProvider { + return &CustomProvider{ + BaseProvider: BaseProvider{ + name: name, + displayName: displayName, + }, + customURLSettings: customURLSetting, + newFn: newFn, + } +} + +var _ (GothProvider) = &CustomProvider{} + +func init() { + RegisterGothProvider(NewCustomProvider( + "github", "GitHub", &CustomURLSettings{ + TokenURL: availableAttribute(gitea.TokenURL), + AuthURL: availableAttribute(github.AuthURL), + ProfileURL: availableAttribute(github.ProfileURL), + EmailURL: availableAttribute(github.EmailURL), + }, + func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { + scopes := []string{} + if setting.OAuth2Client.EnableAutoRegistration { + scopes = append(scopes, "user:email") + } + return github.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, custom.EmailURL, scopes...), nil + })) + + RegisterGothProvider(NewCustomProvider( + "gitlab", "GitLab", &CustomURLSettings{ + AuthURL: availableAttribute(gitlab.AuthURL), + TokenURL: availableAttribute(gitlab.TokenURL), + ProfileURL: availableAttribute(gitlab.ProfileURL), + }, func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { + return gitlab.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL, "read_user"), nil + })) + + RegisterGothProvider(NewCustomProvider( + "gitea", "Gitea", &CustomURLSettings{ + TokenURL: requiredAttribute(gitea.TokenURL), + AuthURL: requiredAttribute(gitea.AuthURL), + ProfileURL: requiredAttribute(gitea.ProfileURL), + }, + func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { + return gitea.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL), nil + })) + + RegisterGothProvider(NewCustomProvider( + "nextcloud", "Nextcloud", &CustomURLSettings{ + TokenURL: requiredAttribute(nextcloud.TokenURL), + AuthURL: requiredAttribute(nextcloud.AuthURL), + ProfileURL: requiredAttribute(nextcloud.ProfileURL), + }, + func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { + return nextcloud.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL, custom.TokenURL, custom.ProfileURL), nil + })) + + RegisterGothProvider(NewCustomProvider( + "mastodon", "Mastodon", &CustomURLSettings{ + AuthURL: requiredAttribute(mastodon.InstanceURL), + }, + func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { + return mastodon.NewCustomisedURL(clientID, secret, callbackURL, custom.AuthURL), nil + })) + + RegisterGothProvider(NewCustomProvider( + "azureadv2", "Azure AD v2", &CustomURLSettings{ + Tenant: requiredAttribute("organizations"), + }, + func(clientID, secret, callbackURL string, custom *CustomURLMapping) (goth.Provider, error) { + return azureadv2.New(clientID, secret, callbackURL, azureadv2.ProviderOptions{ + Tenant: azureadv2.TenantType(custom.Tenant), + }), nil + }, + )) +} diff --git a/services/auth/source/oauth2/providers_openid.go b/services/auth/source/oauth2/providers_openid.go new file mode 100644 index 000000000..b725cf960 --- /dev/null +++ b/services/auth/source/oauth2/providers_openid.go @@ -0,0 +1,52 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/openidConnect" +) + +// OpenIDProvider is a GothProvider for OpenID +type OpenIDProvider struct { +} + +// Name provides the technical name for this provider +func (o *OpenIDProvider) Name() string { + return "openidconnect" +} + +// DisplayName returns the friendly name for this provider +func (o *OpenIDProvider) DisplayName() string { + return "OpenID Connect" +} + +// Image returns an image path for this provider +func (o *OpenIDProvider) Image() string { + return "/assets/img/auth/openid_connect.svg" +} + +// CreateGothProvider creates a GothProvider from this Provider +func (o *OpenIDProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) { + provider, err := openidConnect.New(source.ClientID, source.ClientSecret, callbackURL, source.OpenIDConnectAutoDiscoveryURL, setting.OAuth2Client.OpenIDConnectScopes...) + if err != nil { + log.Warn("Failed to create OpenID Connect Provider with name '%s' with url '%s': %v", providerName, source.OpenIDConnectAutoDiscoveryURL, err) + } + return provider, err +} + +// CustomURLSettings returns the custom url settings for this provider +func (o *OpenIDProvider) CustomURLSettings() *CustomURLSettings { + return nil +} + +var _ (GothProvider) = &OpenIDProvider{} + +func init() { + RegisterGothProvider(&OpenIDProvider{}) +} diff --git a/services/auth/source/oauth2/providers_simple.go b/services/auth/source/oauth2/providers_simple.go new file mode 100644 index 000000000..5a7062e6c --- /dev/null +++ b/services/auth/source/oauth2/providers_simple.go @@ -0,0 +1,108 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package oauth2 + +import ( + "code.gitea.io/gitea/modules/setting" + + "github.com/markbates/goth" + "github.com/markbates/goth/providers/azuread" + "github.com/markbates/goth/providers/bitbucket" + "github.com/markbates/goth/providers/discord" + "github.com/markbates/goth/providers/dropbox" + "github.com/markbates/goth/providers/facebook" + "github.com/markbates/goth/providers/google" + "github.com/markbates/goth/providers/microsoftonline" + "github.com/markbates/goth/providers/twitter" + "github.com/markbates/goth/providers/yandex" +) + +// SimpleProviderNewFn create goth.Providers without custom url features +type SimpleProviderNewFn func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider + +// SimpleProvider is a GothProvider which does not have custom url features +type SimpleProvider struct { + BaseProvider + scopes []string + newFn SimpleProviderNewFn +} + +// CreateGothProvider creates a GothProvider from this Provider +func (c *SimpleProvider) CreateGothProvider(providerName, callbackURL string, source *Source) (goth.Provider, error) { + return c.newFn(source.ClientID, source.ClientSecret, callbackURL, c.scopes...), nil +} + +// NewSimpleProvider is a constructor function for simple providers +func NewSimpleProvider(name, displayName string, scopes []string, newFn SimpleProviderNewFn) *SimpleProvider { + return &SimpleProvider{ + BaseProvider: BaseProvider{ + name: name, + displayName: displayName, + }, + scopes: scopes, + newFn: newFn, + } +} + +var _ (GothProvider) = &SimpleProvider{} + +func init() { + RegisterGothProvider( + NewSimpleProvider("bitbucket", "Bitbucket", []string{"account"}, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return bitbucket.New(clientKey, secret, callbackURL, scopes...) + })) + + RegisterGothProvider( + NewSimpleProvider("dropbox", "Dropbox", nil, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return dropbox.New(clientKey, secret, callbackURL, scopes...) + })) + + RegisterGothProvider(NewSimpleProvider("facebook", "Facebook", nil, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return facebook.New(clientKey, secret, callbackURL, scopes...) + })) + + // named gplus due to legacy gplus -> google migration (Google killed Google+). This ensures old connections still work + RegisterGothProvider(NewSimpleProvider("gplus", "Google", []string{"email"}, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + if setting.OAuth2Client.UpdateAvatar || setting.OAuth2Client.EnableAutoRegistration { + scopes = append(scopes, "profile") + } + return google.New(clientKey, secret, callbackURL, scopes...) + })) + + RegisterGothProvider(NewSimpleProvider("twitter", "Twitter", nil, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return twitter.New(clientKey, secret, callbackURL) + })) + + RegisterGothProvider(NewSimpleProvider("discord", "Discord", []string{discord.ScopeIdentify, discord.ScopeEmail}, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return discord.New(clientKey, secret, callbackURL, scopes...) + })) + + // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ + RegisterGothProvider(NewSimpleProvider("yandex", "Yandex", []string{"login:email", "login:info", "login:avatar"}, + func(clientKey, secret, callbackURL string, scopes ...string) goth.Provider { + return yandex.New(clientKey, secret, callbackURL, scopes...) + })) + + RegisterGothProvider(NewSimpleProvider( + "azuread", "Azure AD", nil, + func(clientID, secret, callbackURL string, scopes ...string) goth.Provider { + return azuread.New(clientID, secret, callbackURL, nil, scopes...) + }, + )) + + RegisterGothProvider(NewSimpleProvider( + "microsoftonline", "Microsoft Online", nil, + func(clientID, secret, callbackURL string, scopes ...string) goth.Provider { + return microsoftonline.New(clientID, secret, callbackURL, scopes...) + }, + )) + +} diff --git a/services/auth/source/oauth2/source_name.go b/services/auth/source/oauth2/source_name.go new file mode 100644 index 000000000..0b794ad65 --- /dev/null +++ b/services/auth/source/oauth2/source_name.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package oauth2 + +// Name returns the provider name of this source +func (source *Source) Name() string { + return source.Provider +} + +// DisplayName returns the display name of this source +func (source *Source) DisplayName() string { + provider, has := gothProviders[source.Provider] + if !has { + return source.Provider + } + return provider.DisplayName() +} diff --git a/services/auth/source/oauth2/source_register.go b/services/auth/source/oauth2/source_register.go index b61cc3fe7..24c61a9a5 100644 --- a/services/auth/source/oauth2/source_register.go +++ b/services/auth/source/oauth2/source_register.go @@ -10,13 +10,13 @@ import ( // RegisterSource causes an OAuth2 configuration to be registered func (source *Source) RegisterSource() error { - err := RegisterProvider(source.loginSource.Name, source.Provider, source.ClientID, source.ClientSecret, source.OpenIDConnectAutoDiscoveryURL, source.CustomURLMapping) + err := RegisterProviderWithGothic(source.loginSource.Name, source) return wrapOpenIDConnectInitializeError(err, source.loginSource.Name, source) } // UnregisterSource causes an OAuth2 configuration to be unregistered func (source *Source) UnregisterSource() error { - RemoveProvider(source.loginSource.Name) + RemoveProviderFromGothic(source.loginSource.Name) return nil } diff --git a/services/auth/source/oauth2/urlmapping.go b/services/auth/source/oauth2/urlmapping.go index 68829fba2..43c8dde9a 100644 --- a/services/auth/source/oauth2/urlmapping.go +++ b/services/auth/source/oauth2/urlmapping.go @@ -6,19 +6,73 @@ package oauth2 // CustomURLMapping describes the urls values to use when customizing OAuth2 provider URLs type CustomURLMapping struct { - AuthURL string - TokenURL string - ProfileURL string - EmailURL string + AuthURL string `json:",omitempty"` + TokenURL string `json:",omitempty"` + ProfileURL string `json:",omitempty"` + EmailURL string `json:",omitempty"` + Tenant string `json:",omitempty"` } -// DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls -// key is used to map the OAuth2Provider -// value is the mapping as defined for the OAuth2Provider -var DefaultCustomURLMappings = map[string]*CustomURLMapping{ - "github": Providers["github"].CustomURLMapping, - "gitlab": Providers["gitlab"].CustomURLMapping, - "gitea": Providers["gitea"].CustomURLMapping, - "nextcloud": Providers["nextcloud"].CustomURLMapping, - "mastodon": Providers["mastodon"].CustomURLMapping, +// CustomURLSettings describes the urls values and availability to use when customizing OAuth2 provider URLs +type CustomURLSettings struct { + AuthURL Attribute `json:",omitempty"` + TokenURL Attribute `json:",omitempty"` + ProfileURL Attribute `json:",omitempty"` + EmailURL Attribute `json:",omitempty"` + Tenant Attribute `json:",omitempty"` +} + +// Attribute describes the availability, and required status for a custom url configuration +type Attribute struct { + Value string + Available bool + Required bool +} + +func availableAttribute(value string) Attribute { + return Attribute{Value: value, Available: true} +} + +func requiredAttribute(value string) Attribute { + return Attribute{Value: value, Available: true, Required: true} +} + +// Required is true if any attribute is required +func (c *CustomURLSettings) Required() bool { + if c == nil { + return false + } + if c.AuthURL.Required || c.EmailURL.Required || c.ProfileURL.Required || c.TokenURL.Required || c.Tenant.Required { + return true + } + return false +} + +// OverrideWith copies the current customURLMapping and overrides it with values from the provided mapping +func (c *CustomURLSettings) OverrideWith(override *CustomURLMapping) *CustomURLMapping { + custom := &CustomURLMapping{ + AuthURL: c.AuthURL.Value, + TokenURL: c.TokenURL.Value, + ProfileURL: c.ProfileURL.Value, + EmailURL: c.EmailURL.Value, + Tenant: c.Tenant.Value, + } + if override != nil { + if len(override.AuthURL) > 0 && c.AuthURL.Available { + custom.AuthURL = override.AuthURL + } + if len(override.TokenURL) > 0 && c.TokenURL.Available { + custom.TokenURL = override.TokenURL + } + if len(override.ProfileURL) > 0 && c.ProfileURL.Available { + custom.ProfileURL = override.ProfileURL + } + if len(override.EmailURL) > 0 && c.EmailURL.Available { + custom.EmailURL = override.EmailURL + } + if len(override.Tenant) > 0 && c.Tenant.Available { + custom.Tenant = override.Tenant + } + } + return custom } diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go index 30621cadf..b78fa9217 100644 --- a/services/forms/auth_form.go +++ b/services/forms/auth_form.go @@ -62,6 +62,7 @@ type AuthenticationForm struct { Oauth2ProfileURL string Oauth2EmailURL string Oauth2IconURL string + Oauth2Tenant string SSPIAutoCreateUsers bool SSPIAutoActivateUsers bool SSPIStripDomainNames bool diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl index 22a2903b2..2b499c7c7 100644 --- a/templates/admin/auth/edit.tmpl +++ b/templates/admin/auth/edit.tmpl @@ -203,8 +203,8 @@
{{.CurrentOAuth2Provider.DisplayName}}
{{svg "octicon-triangle-down" 14 "dropdown icon"}} @@ -248,11 +248,18 @@ - {{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}} - - - - +
+ + +
+ + {{range .OAuth2Providers}}{{if .CustomURLSettings}} + + + + + + {{end}}{{end}} {{end}} diff --git a/templates/admin/auth/source/oauth.tmpl b/templates/admin/auth/source/oauth.tmpl index 787e29873..b19fe3d42 100644 --- a/templates/admin/auth/source/oauth.tmpl +++ b/templates/admin/auth/source/oauth.tmpl @@ -2,12 +2,12 @@
@@ -51,12 +51,17 @@
- {{if .OAuth2DefaultCustomURLMappings}} - {{range $key, $value := .OAuth2DefaultCustomURLMappings}} - - - - - {{end}} - {{end}} +
+ + +
+ + {{range .OAuth2Providers}}{{if .CustomURLSettings}} + + + + + + + {{end}}{{end}} diff --git a/vendor/github.com/markbates/going/LICENSE.txt b/vendor/github.com/markbates/going/LICENSE.txt new file mode 100644 index 000000000..f8e6d5b27 --- /dev/null +++ b/vendor/github.com/markbates/going/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2014 Mark Bates + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/markbates/going/defaults/defaults.go b/vendor/github.com/markbates/going/defaults/defaults.go new file mode 100644 index 000000000..0d81de6f4 --- /dev/null +++ b/vendor/github.com/markbates/going/defaults/defaults.go @@ -0,0 +1,36 @@ +package defaults + +func String(s1, s2 string) string { + if s1 == "" { + return s2 + } + return s1 +} + +func Int(i1, i2 int) int { + if i1 == 0 { + return i2 + } + return i1 +} + +func Int64(i1, i2 int64) int64 { + if i1 == 0 { + return i2 + } + return i1 +} + +func Float32(i1, i2 float32) float32 { + if i1 == 0.0 { + return i2 + } + return i1 +} + +func Float64(i1, i2 float64) float64 { + if i1 == 0.0 { + return i2 + } + return i1 +} diff --git a/vendor/github.com/markbates/goth/providers/azuread/azuread.go b/vendor/github.com/markbates/goth/providers/azuread/azuread.go new file mode 100644 index 000000000..50fb47989 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/azuread/azuread.go @@ -0,0 +1,187 @@ +// Package azuread implements the OAuth2 protocol for authenticating users through AzureAD. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +// To use microsoft personal account use microsoftonline provider +package azuread + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +const ( + authURL string = "https://login.microsoftonline.com/common/oauth2/authorize" + tokenURL string = "https://login.microsoftonline.com/common/oauth2/token" + endpointProfile string = "https://graph.windows.net/me?api-version=1.6" + graphAPIResource string = "https://graph.windows.net/" +) + +// New creates a new AzureAD provider, and sets up important connection details. +// You should always call `AzureAD.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey, secret, callbackURL string, resources []string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "azuread", + } + + p.resources = make([]string, 0, 1+len(resources)) + p.resources = append(p.resources, graphAPIResource) + p.resources = append(p.resources, resources...) + + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing AzureAD. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string + resources []string +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +// Client is HTTP client to be used in all fetch operations. +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks AzureAD for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + authURL := p.config.AuthCodeURL(state) + + // Azure ad requires at least one resource + authURL += "&resource=" + url.QueryEscape(strings.Join(p.resources, " ")) + + return &Session{ + AuthURL: authURL, + }, nil +} + +// FetchUser will go to AzureAD and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + msSession := session.(*Session) + user := goth.User{ + AccessToken: msSession.AccessToken, + Provider: p.Name(), + ExpiresAt: msSession.ExpiresAt, + } + + if user.AccessToken == "" { + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", endpointProfile, nil) + if err != nil { + return user, err + } + + req.Header.Set(authorizationHeader(msSession)) + + response, err := p.Client().Do(req) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + err = userFromReader(response.Body, &user) + return user, err +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + Scopes: []string{}, + } + + if len(scopes) > 0 { + for _, scope := range scopes { + c.Scopes = append(c.Scopes, scope) + } + } else { + c.Scopes = append(c.Scopes, "user_impersonation") + } + + return c +} + +func userFromReader(r io.Reader, user *goth.User) error { + u := struct { + Name string `json:"name"` + Email string `json:"mail"` + FirstName string `json:"givenName"` + LastName string `json:"surname"` + NickName string `json:"mailNickname"` + UserPrincipalName string `json:"userPrincipalName"` + Location string `json:"usageLocation"` + }{} + + err := json.NewDecoder(r).Decode(&u) + if err != nil { + return err + } + + user.Email = u.Email + user.Name = u.Name + user.FirstName = u.FirstName + user.LastName = u.LastName + user.NickName = u.Name + user.Location = u.Location + user.UserID = u.UserPrincipalName //AzureAD doesn't provide separate user_id + + return nil +} + +func authorizationHeader(session *Session) (string, string) { + return "Authorization", fmt.Sprintf("Bearer %s", session.AccessToken) +} diff --git a/vendor/github.com/markbates/goth/providers/azuread/session.go b/vendor/github.com/markbates/goth/providers/azuread/session.go new file mode 100644 index 000000000..098a9dc6d --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/azuread/session.go @@ -0,0 +1,63 @@ +package azuread + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session is the implementation of `goth.Session` for accessing AzureAD. +type Session struct { + AuthURL string + AccessToken string + RefreshToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Facebook provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + + return s.AuthURL, nil +} + +// Authorize the session with AzureAD and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + session := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(session) + return session, err +} diff --git a/vendor/github.com/markbates/goth/providers/azureadv2/azureadv2.go b/vendor/github.com/markbates/goth/providers/azureadv2/azureadv2.go new file mode 100644 index 000000000..f293816a7 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/azureadv2/azureadv2.go @@ -0,0 +1,233 @@ +package azureadv2 + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +// also https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints +const ( + authURLTemplate string = "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize" + tokenURLTemplate string = "https://login.microsoftonline.com/%s/oauth2/v2.0/token" + graphAPIResource string = "https://graph.microsoft.com/v1.0/" +) + +type ( + // TenantType are the well known tenant types to scope the users that can authenticate. TenantType is not an + // exclusive list of Azure Tenants which can be used. A consumer can also use their own Tenant ID to scope + // authentication to their specific Tenant either through the Tenant ID or the friendly domain name. + // + // see also https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints + TenantType string + + // Provider is the implementation of `goth.Provider` for accessing AzureAD V2. + Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string + } + + // ProviderOptions are the collection of optional configuration to provide when constructing a Provider + ProviderOptions struct { + Scopes []ScopeType + Tenant TenantType + } +) + +// These are the well known Azure AD Tenants. These are not an exclusive list of all Tenants +// +// See also https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints +const ( + // CommonTenant allows users with both personal Microsoft accounts and work/school accounts from Azure Active + // Directory to sign into the application. + CommonTenant TenantType = "common" + + // OrganizationsTenant allows only users with work/school accounts from Azure Active Directory to sign into the application. + OrganizationsTenant TenantType = "organizations" + + // ConsumersTenant allows only users with personal Microsoft accounts (MSA) to sign into the application. + ConsumersTenant TenantType = "consumers" +) + +// New creates a new AzureAD provider, and sets up important connection details. +// You should always call `AzureAD.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey, secret, callbackURL string, opts ProviderOptions) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "azureadv2", + } + + p.config = newConfig(p, opts) + return p +} + +func newConfig(provider *Provider, opts ProviderOptions) *oauth2.Config { + tenant := opts.Tenant + if tenant == "" { + tenant = CommonTenant + } + + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: fmt.Sprintf(authURLTemplate, tenant), + TokenURL: fmt.Sprintf(tokenURLTemplate, tenant), + }, + Scopes: []string{}, + } + + if len(opts.Scopes) > 0 { + c.Scopes = append(c.Scopes, scopesToStrings(opts.Scopes...)...) + } else { + defaultScopes := scopesToStrings(OpenIDScope, ProfileScope, EmailScope, UserReadScope) + c.Scopes = append(c.Scopes, defaultScopes...) + } + + return c +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +// Client is HTTP client to be used in all fetch operations. +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the package +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks for an authentication end-point for AzureAD. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + authURL := p.config.AuthCodeURL(state) + + return &Session{ + AuthURL: authURL, + }, nil +} + +// FetchUser will go to AzureAD and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + msSession := session.(*Session) + user := goth.User{ + AccessToken: msSession.AccessToken, + Provider: p.Name(), + ExpiresAt: msSession.ExpiresAt, + } + + if user.AccessToken == "" { + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", graphAPIResource+"me", nil) + if err != nil { + return user, err + } + + req.Header.Set(authorizationHeader(msSession)) + + response, err := p.Client().Do(req) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + err = userFromReader(response.Body, &user) + user.AccessToken = msSession.AccessToken + user.RefreshToken = msSession.RefreshToken + user.ExpiresAt = msSession.ExpiresAt + return user, err +} + +//RefreshTokenAvailable refresh token is provided by auth provider or not +func (p *Provider) RefreshTokenAvailable() bool { + return true +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +func authorizationHeader(session *Session) (string, string) { + return "Authorization", fmt.Sprintf("Bearer %s", session.AccessToken) +} + +func userFromReader(r io.Reader, user *goth.User) error { + u := struct { + ID string `json:"id"` // The unique identifier for the user. + BusinessPhones []string `json:"businessPhones"` // The user's phone numbers. + DisplayName string `json:"displayName"` // The name displayed in the address book for the user. + FirstName string `json:"givenName"` // The first name of the user. + JobTitle string `json:"jobTitle"` // The user's job title. + Email string `json:"mail"` // The user's email address. + MobilePhone string `json:"mobilePhone"` // The user's cellphone number. + OfficeLocation string `json:"officeLocation"` // The user's physical office location. + PreferredLanguage string `json:"preferredLanguage"` // The user's language of preference. + LastName string `json:"surname"` // The last name of the user. + UserPrincipalName string `json:"userPrincipalName"` // The user's principal name. + }{} + + userBytes, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + if err := json.Unmarshal(userBytes, &u); err != nil { + return err + } + + user.Email = u.Email + user.Name = u.DisplayName + user.FirstName = u.FirstName + user.LastName = u.LastName + user.NickName = u.DisplayName + user.Location = u.OfficeLocation + user.UserID = u.ID + user.AvatarURL = graphAPIResource + fmt.Sprintf("users/%s/photo/$value", u.ID) + // Make sure all of the information returned is available via RawData + if err := json.Unmarshal(userBytes, &user.RawData); err != nil { + return err + } + + return nil +} + +func scopesToStrings(scopes ...ScopeType) []string { + strs := make([]string, len(scopes)) + for i := 0; i < len(scopes); i++ { + strs[i] = string(scopes[i]) + } + return strs +} diff --git a/vendor/github.com/markbates/goth/providers/azureadv2/scopes.go b/vendor/github.com/markbates/goth/providers/azureadv2/scopes.go new file mode 100644 index 000000000..8dcedef32 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/azureadv2/scopes.go @@ -0,0 +1,714 @@ +package azureadv2 + +type ( + // ScopeType are the well known scopes which can be requested + ScopeType string +) + +// OpenID Permissions +// +// You can use these permissions to specify artifacts that you want returned in Azure AD authorization and token +// requests. They are supported differently by the Azure AD v1.0 and v2.0 endpoints. +// +// With the Azure AD (v1.0) endpoint, only the openid permission is used. You specify it in the scope parameter in an +// authorization request to return an ID token when you use the OpenID Connect protocol to sign in a user to your app. +// For more information, see Authorize access to web applications using OpenID Connect and Azure Active Directory. To +// successfully return an ID token, you must also make sure that the User.Read permission is configured when you +// register your app. +// +// With the Azure AD v2.0 endpoint, you specify the offline_access permission in the scope parameter to explicitly +// request a refresh token when using the OAuth 2.0 or OpenID Connect protocols. With OpenID Connect, you specify the +// openid permission to request an ID token. You can also specify the email permission, profile permission, or both to +// return additional claims in the ID token. You do not need to specify User.Read to return an ID token with the v2.0 +// endpoint. For more information, see OpenID Connect scopes. +const ( + // OpenIDScope shows on the work account consent page as the "Sign you in" permission, and on the personal Microsoft + // account consent page as the "View your profile and connect to apps and services using your Microsoft account" + // permission. With this permission, an app can receive a unique identifier for the user in the form of the sub + // claim. It also gives the app access to the UserInfo endpoint. The openid scope can be used at the v2.0 token + // endpoint to acquire ID tokens, which can be used to secure HTTP calls between different components of an app. + OpenIDScope ScopeType = "openid" + + // EmailScope can be used with the openid scope and any others. It gives the app access to the user's primary + // email address in the form of the email claim. The email claim is included in a token only if an email address is + // associated with the user account, which is not always the case. If it uses the email scope, your app should be + // prepared to handle a case in which the email claim does not exist in the token. + EmailScope ScopeType = "email" + + // ProfileScope can be used with the openid scope and any others. It gives the app access to a substantial + // amount of information about the user. The information it can access includes, but is not limited to, the user's + // given name, surname, preferred username, and object ID. For a complete list of the profile claims available in + // the id_tokens parameter for a specific user, see the v2.0 tokens reference: + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-id-and-access-tokens. + ProfileScope ScopeType = "profile" + + // OfflineAccessScope gives your app access to resources on behalf of the user for an extended time. On the work + // account consent page, this scope appears as the "Access your data anytime" permission. On the personal Microsoft + // account consent page, it appears as the "Access your info anytime" permission. When a user approves the + // offline_access scope, your app can receive refresh tokens from the v2.0 token endpoint. Refresh tokens are + // long-lived. Your app can get new access tokens as older ones expire. + // + // If your app does not request the offline_access scope, it won't receive refresh tokens. This means that when you + // redeem an authorization code in the OAuth 2.0 authorization code flow, you'll receive only an access token from + // the /token endpoint. The access token is valid for a short time. The access token usually expires in one hour. + // At that point, your app needs to redirect the user back to the /authorize endpoint to get a new authorization + // code. During this redirect, depending on the type of app, the user might need to enter their credentials again + // or consent again to permissions. + OfflineAccessScope ScopeType = "offline_access" +) + +// Calendar Permissions +// +// Calendars.Read.Shared and Calendars.ReadWrite.Shared are only valid for work or school accounts. All other +// permissions are valid for both Microsoft accounts and work or school accounts. +// +// See also https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference +const ( + // CalendarsReadScope allows the app to read events in user calendars. + CalendarsReadScope ScopeType = "Calendars.Read" + + // CalendarsReadSharedScope allows the app to read events in all calendars that the user can access, including + // delegate and shared calendars. + CalendarsReadSharedScope ScopeType = "Calendars.Read.Shared" + + // CalendarsReadWriteScope allows the app to create, read, update, and delete events in user calendars. + CalendarsReadWriteScope ScopeType = "Calendars.ReadWrite" + + // CalendarsReadWriteSharedScope allows the app to create, read, update and delete events in all calendars the user + // has permissions to access. This includes delegate and shared calendars. + CalendarsReadWriteSharedScope ScopeType = "Calendars.ReadWrite.Shared" +) + +// Contacts Permissions +// +// Only the Contacts.Read and Contacts.ReadWrite delegated permissions are valid for Microsoft accounts. +// +// See also https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference +const ( + // ContactsReadScope allows the app to read contacts that the user has permissions to access, including the user's + // own and shared contacts. + ContactsReadScope ScopeType = "Contacts.Read" + + // ContactsReadSharedScope allows the app to read contacts that the user has permissions to access, including the + // user's own and shared contacts. + ContactsReadSharedScope ScopeType = "Contacts.Read.Shared" + + // ContactsReadWriteScope allows the app to create, read, update, and delete user contacts. + ContactsReadWriteScope ScopeType = "Contacts.ReadWrite" + + // ContactsReadWriteSharedScope allows the app to create, read, update and delete contacts that the user has + // permissions to, including the user's own and shared contacts. + ContactsReadWriteSharedScope ScopeType = "Contacts.ReadWrite.Shared" +) + +// Device Permissions +// +// The Device.Read and Device.Command delegated permissions are valid only for personal Microsoft accounts. +// +// See also https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference +const ( + // DeviceReadScope allows the app to read a user's list of devices on behalf of the signed-in user. + DeviceReadScope ScopeType = "Device.Read" + + // DeviceCommandScope allows the app to launch another app or communicate with another app on a user's device on + // behalf of the signed-in user. + DeviceCommandScope ScopeType = "Device.Command" +) + +// Directory Permissions +// +// Directory permissions are not supported on Microsoft accounts. +// +// Directory permissions provide the highest level of privilege for accessing directory resources such as User, Group, +// and Device in an organization. +// +// They also exclusively control access to other directory resources like: organizational contacts, schema extension +// APIs, Privileged Identity Management (PIM) APIs, as well as many of the resources and APIs listed under the Azure +// Active Directory node in the v1.0 and beta API reference documentation. These include administrative units, directory +// roles, directory settings, policy, and many more. +// +// The Directory.ReadWrite.All permission grants the following privileges: +// - Full read of all directory resources (both declared properties and navigation properties) +// - Create and update users +// - Disable and enable users (but not company administrator) +// - Set user alternative security id (but not administrators) +// - Create and update groups +// - Manage group memberships +// - Update group owner +// - Manage license assignments +// - Define schema extensions on applications +// - Note: No rights to reset user passwords +// - Note: No rights to delete resources (including users or groups) +// - Note: Specifically excludes create or update for resources not listed above. This includes: application, +// oAauth2Permissiongrant, appRoleAssignment, device, servicePrincipal, organization, domains, and so on. +// +// See also https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference +const ( + // DirectoryReadAllScope allows the app to read data in your organization's directory, such as users, groups and + // apps. + // + // Note: Users may consent to applications that require this permission if the application is registered in their + // own organization’s tenant. + // + // requires admin consent + DirectoryReadAllScope ScopeType = "Directory.Read.All" + + // DirectoryReadWriteAllScope allows the app to read and write data in your organization's directory, such as users, + // and groups. It does not allow the app to delete users or groups, or reset user passwords. + // + // requires admin consent + DirectoryReadWriteAllScope ScopeType = "Directory.ReadWrite.All" + + // DirectoryAccessAsUserAllScope allows the app to have the same access to information in the directory as the + // signed-in user. + // + // requires admin consent + DirectoryAccessAsUserAllScope ScopeType = "Directory.AccessAsUser.All" +) + +// Education Administration Permissions +const ( + // EduAdministrationReadScope allows the app to read education app settings on behalf of the user. + // + // requires admin consent + EduAdministrationReadScope ScopeType = "EduAdministration.Read" + + // EduAdministrationReadWriteScope allows the app to manage education app settings on behalf of the user. + // + // requires admin consent + EduAdministrationReadWriteScope ScopeType = "EduAdministration.ReadWrite" + + // EduAssignmentsReadBasicScope allows the app to read assignments without grades on behalf of the user + // + // requires admin consent + EduAssignmentsReadBasicScope ScopeType = "EduAssignments.ReadBasic" + + // EduAssignmentsReadWriteBasicScope allows the app to read and write assignments without grades on behalf of the + // user + EduAssignmentsReadWriteBasicScope ScopeType = "EduAssignments.ReadWriteBasic" + + // EduAssignmentsReadScope allows the app to read assignments and their grades on behalf of the user + // + // requires admin consent + EduAssignmentsReadScope ScopeType = "EduAssignments.Read" + + // EduAssignmentsReadWriteScope allows the app to read and write assignments and their grades on behalf of the user + // + // requires admin consent + EduAssignmentsReadWriteScope ScopeType = "EduAssignments.ReadWrite" + + // EduRosteringReadBasicScope allows the app to read a limited subset of the data from the structure of schools and + // classes in an organization's roster and education-specific information about users to be read on behalf of the + // user. + // + // requires admin consent + EduRosteringReadBasicScope ScopeType = "EduRostering.ReadBasic" +) + +// Files Permissions +// +// The Files.Read, Files.ReadWrite, Files.Read.All, and Files.ReadWrite.All delegated permissions are valid on both +// personal Microsoft accounts and work or school accounts. Note that for personal accounts, Files.Read and +// Files.ReadWrite also grant access to files shared with the signed-in user. +// +// The Files.Read.Selected and Files.ReadWrite.Selected delegated permissions are only valid on work or school accounts +// and are only exposed for working with Office 365 file handlers (v1.0) +// https://msdn.microsoft.com/office/office365/howto/using-cross-suite-apps. They should not be used for directly +// calling Microsoft Graph APIs. +// +// The Files.ReadWrite.AppFolder delegated permission is only valid for personal accounts and is used for accessing the +// App Root special folder https://dev.onedrive.com/misc/appfolder.htm with the OneDrive Get special folder +// https://developer.microsoft.com/en-us/graph/docs/api-reference/v1.0/api/drive_get_specialfolder Microsoft Graph API. +const ( + // FilesReadScope allows the app to read the signed-in user's files. + FilesReadScope ScopeType = "Files.Read" + + // FilesReadAllScope allows the app to read all files the signed-in user can access. + FilesReadAllScope ScopeType = "Files.Read.All" + + // FilesReadWrite allows the app to read, create, update, and delete the signed-in user's files. + FilesReadWriteScope ScopeType = "Files.ReadWrite" + + // FilesReadWriteAllScope allows the app to read, create, update, and delete all files the signed-in user can access. + FilesReadWriteAllScope ScopeType = "Files.ReadWrite.All" + + // FilesReadWriteAppFolderScope allows the app to read, create, update, and delete files in the application's folder. + FilesReadWriteAppFolderScope ScopeType = "Files.ReadWrite.AppFolder" + + // FilesReadSelectedScope allows the app to read files that the user selects. The app has access for several hours + // after the user selects a file. + // + // preview + FilesReadSelectedScope ScopeType = "Files.Read.Selected" + + // FilesReadWriteSelectedScope allows the app to read and write files that the user selects. The app has access for + // several hours after the user selects a file + // + // preview + FilesReadWriteSelectedScope ScopeType = "Files.ReadWrite.Selected" +) + +// Group Permissions +// +// Group functionality is not supported on personal Microsoft accounts. +// +// For Office 365 groups, Group permissions grant the app access to the contents of the group; for example, +// conversations, files, notes, and so on. +// +// For application permissions, there are some limitations for the APIs that are supported. For more information, see +// known issues. +// +// In some cases, an app may need Directory permissions to read some group properties like member and memberOf. For +// example, if a group has a one or more servicePrincipals as members, the app will need effective permissions to read +// service principals through being granted one of the Directory.* permissions, otherwise Microsoft Graph will return an +// error. (In the case of delegated permissions, the signed-in user will also need sufficient privileges in the +// organization to read service principals.) The same guidance applies for the memberOf property, which can return +// administrativeUnits. +// +// Group permissions are also used to control access to Microsoft Planner resources and APIs. Only delegated permissions +// are supported for Microsoft Planner APIs; application permissions are not supported. Personal Microsoft accounts are +// not supported. +const ( + // GroupReadAllScope allows the app to list groups, and to read their properties and all group memberships on behalf + // of the signed-in user. Also allows the app to read calendar, conversations, files, and other group content for + // all groups the signed-in user can access. + GroupReadAllScope ScopeType = "Group.Read.All" + + // GroupReadWriteAllScope allows the app to create groups and read all group properties and memberships on behalf of + // the signed-in user. Additionally allows group owners to manage their groups and allows group members to update + // group content. + GroupReadWriteAllScope ScopeType = "Group.ReadWrite.All" +) + +// Identity Risk Event Permissions +// +// IdentityRiskEvent.Read.All is valid only for work or school accounts. For an app with delegated permissions to read +// identity risk information, the signed-in user must be a member of one of the following administrator roles: Global +// Administrator, Security Administrator, or Security Reader. For more information about administrator roles, see +// Assigning administrator roles in Azure Active Directory. +const ( + // IdentityRiskEventReadAllScope allows the app to read identity risk event information for all users in your + // organization on behalf of the signed-in user. + // + // requires admin consent + IdentityRiskEventReadAllScope ScopeType = "IdentityRiskEvent.Read.All" +) + +// Identity Provider Permissions +// +// IdentityProvider.Read.All and IdentityProvider.ReadWrite.All are valid only for work or school accounts. For an app +// to read or write identity providers with delegated permissions, the signed-in user must be assigned the Global +// Administrator role. For more information about administrator roles, see Assigning administrator roles in Azure Active +// Directory. +const ( + // IdentityProviderReadAllScope allows the app to read identity providers configured in your Azure AD or Azure AD + // B2C tenant on behalf of the signed-in user. + // + // requires admin consent + IdentityProviderReadAllScope ScopeType = "IdentityProvider.Read.All" + + // IdentityProviderReadWriteAllScope allows the app to read or write identity providers configured in your Azure AD + // or Azure AD B2C tenant on behalf of the signed-in user. + // + // requires admin consent + IdentityProviderReadWriteAllScope ScopeType = "IdentityProvider.ReadWrite.All" +) + +// Device Management Permissions +// +// Using the Microsoft Graph APIs to configure Intune controls and policies still requires that the Intune service is +// correctly licensed by the customer. +// +// These permissions are only valid for work or school accounts. +const ( + // DeviceManagementAppsReadAllScope allows the app to read the properties, group assignments and status of apps, app + // configurations and app protection policies managed by Microsoft Intune. + // + // requires admin consent + DeviceManagementAppsReadAllScope ScopeType = "DeviceManagementApps.Read.All" + + // DeviceManagementAppsReadWriteAllScope allows the app to read and write the properties, group assignments and + // status of apps, app configurations and app protection policies managed by Microsoft Intune. + // + // requires admin consent + DeviceManagementAppsReadWriteAllScope ScopeType = "DeviceManagementApps.ReadWrite.All" + + // DeviceManagementConfigurationReadAllScope allows the app to read properties of Microsoft Intune-managed device + // configuration and device compliance policies and their assignment to groups. + // + // requires admin consent + DeviceManagementConfigurationReadAllScope ScopeType = "DeviceManagementConfiguration.Read.All" + + // DeviceManagementConfigurationReadWriteAllScope allows the app to read and write properties of Microsoft + // Intune-managed device configuration and device compliance policies and their assignment to groups. + // + // requires admin consent + DeviceManagementConfigurationReadWriteAllScope ScopeType = "DeviceManagementConfiguration.ReadWrite.All" + + // DeviceManagementManagedDevicesPrivilegedOperationsAllScope allows the app to perform remote high impact actions + // such as wiping the device or resetting the passcode on devices managed by Microsoft Intune. + // + // requires admin consent + DeviceManagementManagedDevicesPrivilegedOperationsAllScope ScopeType = "DeviceManagementManagedDevices.PrivilegedOperations.All" + + // DeviceManagementManagedDevicesReadAllScope allows the app to read the properties of devices managed by Microsoft + // Intune. + // + // requires admin consent + DeviceManagementManagedDevicesReadAllScope ScopeType = "DeviceManagementManagedDevices.Read.All" + + // DeviceManagementManagedDevicesReadWriteAllScope allows the app to read and write the properties of devices + // managed by Microsoft Intune. Does not allow high impact operations such as remote wipe and password reset on the + // device’s owner. + // + // requires admin consent + DeviceManagementManagedDevicesReadWriteAllScope ScopeType = "DeviceManagementManagedDevices.ReadWrite.All" + + // DeviceManagementRBACReadAllScope allows the app to read the properties relating to the Microsoft Intune + // Role-Based Access Control (RBAC) settings. + // + // requires admin consent + DeviceManagementRBACReadAllScope ScopeType = "DeviceManagementRBAC.Read.All" + + // DeviceManagementRBACReadWriteAllScope allows the app to read and write the properties relating to the Microsoft + // Intune Role-Based Access Control (RBAC) settings. + // + // requires admin consent + DeviceManagementRBACReadWriteAllScope ScopeType = "DeviceManagementRBAC.ReadWrite.All" + + // DeviceManagementServiceConfigReadAllScope allows the app to read Intune service properties including device + // enrollment and third party service connection configuration. + // + // requires admin consent + DeviceManagementServiceConfigReadAllScope ScopeType = "DeviceManagementServiceConfig.Read.All" + + // DeviceManagementServiceConfigReadWriteAllScope allows the app to read and write Microsoft Intune service + // properties including device enrollment and third party service connection configuration. + // + // requires admin consent + DeviceManagementServiceConfigReadWriteAllScope ScopeType = "DeviceManagementServiceConfig.ReadWrite.All" +) + +// Mail Permissions +// +// Mail.Read.Shared, Mail.ReadWrite.Shared, and Mail.Send.Shared are only valid for work or school accounts. All other +// permissions are valid for both Microsoft accounts and work or school accounts. +// +// With the Mail.Send or Mail.Send.Shared permission, an app can send mail and save a copy to the user's Sent Items +// folder, even if the app does not use a corresponding Mail.ReadWrite or Mail.ReadWrite.Shared permission. +const ( + // MailReadScope allows the app to read email in user mailboxes. + MailReadScope ScopeType = "Mail.Read" + + // MailReadWriteScope allows the app to create, read, update, and delete email in user mailboxes. Does not include + // permission to send mail. + MailReadWriteScope ScopeType = "Mail.ReadWrite" + + // MailReadSharedScope allows the app to read mail that the user can access, including the user's own and shared + // mail. + MailReadSharedScope ScopeType = "Mail.Read.Shared" + + // MailReadWriteSharedScope allows the app to create, read, update, and delete mail that the user has permission to + // access, including the user's own and shared mail. Does not include permission to send mail. + MailReadWriteSharedScope ScopeType = "Mail.ReadWrite.Shared" + + // MailSend allowsScope the app to send mail as users in the organization. + MailSendScope ScopeType = "Mail.Send" + + // MailSendSharedScope allows the app to send mail as the signed-in user, including sending on-behalf of others. + MailSendSharedScope ScopeType = "Mail.Send.Shared" + + // MailboxSettingsReadScope allows the app to the read user's mailbox settings. Does not include permission to send + // mail. + MailboxSettingsReadScope ScopeType = "Mailbox.Settings.Read" + + // MailboxSettingsReadWriteScope allows the app to create, read, update, and delete user's mailbox settings. Does + // not include permission to directly send mail, but allows the app to create rules that can forward or redirect + // messages. + MailboxSettingsReadWriteScope ScopeType = "MailboxSettings.ReadWrite" +) + +// Member Permissions +// +// Member.Read.Hidden is valid only on work or school accounts. +// +// Membership in some Office 365 groups can be hidden. This means that only the members of the group can view its +// members. This feature can be used to help comply with regulations that require an organization to hide group +// membership from outsiders (for example, an Office 365 group that represents students enrolled in a class). +const ( + // MemberReadHiddenScope allows the app to read the memberships of hidden groups and administrative units on behalf + // of the signed-in user, for those hidden groups and administrative units that the signed-in user has access to. + // + // requires admin consent + MemberReadHiddenScope ScopeType = "Member.Read.Hidden" +) + +// Notes Permissions +// +// Notes.Read.All and Notes.ReadWrite.All are only valid for work or school accounts. All other permissions are valid +// for both Microsoft accounts and work or school accounts. +// +// With the Notes.Create permission, an app can view the OneNote notebook hierarchy of the signed-in user and create +// OneNote content (notebooks, section groups, sections, pages, etc.). +// +// Notes.ReadWrite and Notes.ReadWrite.All also allow the app to modify the permissions on the OneNote content that can +// be accessed by the signed-in user. +// +// For work or school accounts, Notes.Read.All and Notes.ReadWrite.All allow the app to access other users' OneNote +// content that the signed-in user has permission to within the organization. +const ( + // NotesReadScope allows the app to read OneNote notebooks on behalf of the signed-in user. + NotesReadScope ScopeType = "Notes.Read" + + // NotesCreateScope allows the app to read the titles of OneNote notebooks and sections and to create new pages, + // notebooks, and sections on behalf of the signed-in user. + NotesCreateScope ScopeType = "Notes.Create" + + // NotesReadWriteScope allows the app to read, share, and modify OneNote notebooks on behalf of the signed-in user. + NotesReadWriteScope ScopeType = "Notes.ReadWrite" + + // NotesReadAllScope allows the app to read OneNote notebooks that the signed-in user has access to in the + // organization. + NotesReadAllScope ScopeType = "Notes.Read.All" + + // NotesReadWriteAllScope allows the app to read, share, and modify OneNote notebooks that the signed-in user has + // access to in the organization. + NotesReadWriteAllScope ScopeType = "Notes.ReadWrite.All" +) + +// People Permissions +// +// The People.Read.All permission is only valid for work and school accounts. +const ( + // PeopleReadScope allows the app to read a scored list of people relevant to the signed-in user. The list can + // include local contacts, contacts from social networking or your organization's directory, and people from recent + // communications (such as email and Skype). + PeopleReadScope ScopeType = "People.Read" + + // PeopleReadAllScope allows the app to read a scored list of people relevant to the signed-in user or other users + // in the signed-in user's organization. The list can include local contacts, contacts from social networking or + // your organization's directory, and people from recent communications (such as email and Skype). Also allows the + // app to search the entire directory of the signed-in user's organization. + // + // requires admin consent + PeopleReadAllScope ScopeType = "People.Read.All" +) + +// Report Permissions +// +// Reports permissions are only valid for work or school accounts. +const ( + // ReportsReadAllScope allows an app to read all service usage reports without a signed-in user. Services that + // provide usage reports include Office 365 and Azure Active Directory. + // + // requires admin consent + ReportsReadAllScope ScopeType = "Reports.Read.All" +) + +// Security Permissions +// +// Security permissions are valid only on work or school accounts. +const ( + // SecurityEventsReadAllScope allows the app to read your organization’s security events on behalf of the signed-in + // user. + // requires admin consent + SecurityEventsReadAllScope ScopeType = "SecurityEvents.Read.All" + + // SecurityEventsReadWriteAllScope allows the app to read your organization’s security events on behalf of the + // signed-in user. Also allows the app to update editable properties in security events on behalf of the signed-in + // user. + // + // requires admin consent + SecurityEventsReadWriteAllScope ScopeType = "SecurityEvents.ReadWrite.All" +) + +// Sites Permissions +// +// Sites permissions are valid only on work or school accounts. +const ( + // SitesReadAllScope allows the app to read documents and list items in all site collections on behalf of the + // signed-in user. + SitesReadAllScope ScopeType = "Sites.Read.All" + + // SitesReadWriteAllScope allows the app to edit or delete documents and list items in all site collections on + // behalf of the signed-in user. + SitesReadWriteAllScope ScopeType = "Sites.ReadWrite.All" + + // SitesManageAllScope allows the app to manage and create lists, documents, and list items in all site collections + // on behalf of the signed-in user. + SitesManageAllScope ScopeType = "Sites.Manage.All" + + // SitesFullControlAllScope allows the app to have full control to SharePoint sites in all site collections on + // behalf of the signed-in user. + // + // requires admin consent + SitesFullControlAllScope ScopeType = "Sites.FullControl.All" +) + +// Tasks Permissions +// +// Tasks permissions are used to control access for Outlook tasks. Access for Microsoft Planner tasks is controlled by +// Group permissions. +// +// Shared permissions are currently only supported for work or school accounts. Even with Shared permissions, reads and +// writes may fail if the user who owns the shared content has not granted the accessing user permissions to modify +// content within the folder. +const ( + // TasksReadScope allows the app to read user tasks. + TasksReadScope ScopeType = "Tasks.Read" + + // TasksReadSharedScope allows the app to read tasks a user has permissions to access, including their own and + // shared tasks. + TasksReadSharedScope ScopeType = "Tasks.Read.Shared" + + // TasksReadWriteScope allows the app to create, read, update and delete tasks and containers (and tasks in them) + // that are assigned to or shared with the signed-in user. + TasksReadWriteScope ScopeType = "Tasks.ReadWrite" + + // TasksReadWriteSharedScope allows the app to create, read, update, and delete tasks a user has permissions to, + // including their own and shared tasks. + TasksReadWriteSharedScope ScopeType = "Tasks.ReadWrite.Shared" +) + +// Terms of Use Permissions +// +// All the permissions above are valid only for work or school accounts. +// +// For an app to read or write all agreements or agreement acceptances with delegated permissions, the signed-in user +// must be assigned the Global Administrator, Conditional Access Administrator or Security Administrator role. For more +// information about administrator roles, see Assigning administrator roles in Azure Active Directory +// https://docs.microsoft.com/azure/active-directory/active-directory-assign-admin-roles. +const ( + // AgreementReadAllScope allows the app to read terms of use agreements on behalf of the signed-in user. + // + // requires admin consent + AgreementReadAllScope ScopeType = "Agreement.Read.All" + + // AgreementReadWriteAllScope allows the app to read and write terms of use agreements on behalf of the signed-in + // user. + // + // requires admin consent + AgreementReadWriteAllScope ScopeType = "Agreement.ReadWrite.All" + + // AgreementAcceptanceReadScope allows the app to read terms of use acceptance statuses on behalf of the signed-in + // user. + // + // requires admin consent + AgreementAcceptanceReadScope ScopeType = "AgreementAcceptance.Read" + + // AgreementAcceptanceReadAllScope allows the app to read terms of use acceptance statuses on behalf of the + // signed-in user. + // + // requires admin consent + AgreementAcceptanceReadAllScope ScopeType = "AgreementAcceptance.Read.All" +) + +// User Permissions +// +// The only permissions valid for Microsoft accounts are User.Read and User.ReadWrite. For work or school accounts, all +// permissions are valid. +// +// With the User.Read permission, an app can also read the basic company information of the signed-in user for a work or +// school account through the organization resource. The following properties are available: id, displayName, and +// verifiedDomains. +// +// For work or school accounts, the full profile includes all of the declared properties of the User resource. On reads, +// only a limited number of properties are returned by default. To read properties that are not in the default set, use +// $select. The default properties are: +// displayName +// givenName +// jobTitle +// mail +// mobilePhone +// officeLocation +// preferredLanguage +// surname +// userPrincipalName +// +// User.ReadWrite and User.Readwrite.All delegated permissions allow the app to update the following profile properties +// for work or school accounts: +// aboutMe +// birthday +// hireDate +// interests +// mobilePhone +// mySite +// pastProjects +// photo +// preferredName +// responsibilities +// schools +// skills +// +// With the User.ReadWrite.All application permission, the app can update all of the declared properties of work or +// school accounts except for password. +// +// To read or write direct reports (directReports) or the manager (manager) of a work or school account, the app must +// have either User.Read.All (read only) or User.ReadWrite.All. +// +// The User.ReadBasic.All permission constrains app access to a limited set of properties known as the basic profile. +// This is because the full profile might contain sensitive directory information. The basic profile includes only the +// following properties: +// displayName +// givenName +// mail +// photo +// surname +// userPrincipalName +// +// To read the group memberships of a user (memberOf), the app must have either Group.Read.All or Group.ReadWrite.All. +// However, if the user also has membership in a directoryRole or an administrativeUnit, the app will need effective +// permissions to read those resources too, or Microsoft Graph will return an error. This means the app will also need +// Directory permissions, and, for delegated permissions, the signed-in user will also need sufficient privileges in the +// organization to access directory roles and administrative units. +const ( + // UserReadScope allows users to sign-in to the app, and allows the app to read the profile of signed-in users. It + // also allows the app to read basic company information of signed-in users. + UserReadScope ScopeType = "User.Read" + + // UserReadWriteScope allows the app to read the signed-in user's full profile. It also allows the app to update the + // signed-in user's profile information on their behalf. + UserReadWriteScope ScopeType = "User.ReadWrite" + + // UserReadBasicAllScope allows the app to read a basic set of profile properties of other users in your + // organization on behalf of the signed-in user. This includes display name, first and last name, email address, + // open extensions and photo. Also allows the app to read the full profile of the signed-in user. + UserReadBasicAllScope ScopeType = "User.ReadBasic.All" + + // UserReadAllScope allows the app to read the full set of profile properties, reports, and managers of other users + // in your organization, on behalf of the signed-in user. + // + // requires admin consent + UserReadAllScope ScopeType = "User.Read.All" + + // UserReadWriteAllScope allows the app to read and write the full set of profile properties, reports, and managers + // of other users in your organization, on behalf of the signed-in user. Also allows the app to create and delete + // users as well as reset user passwords on behalf of the signed-in user. + // + // requires admin consent + UserReadWriteAllScope ScopeType = "User.ReadWrite.All" + + // UserInviteAllScope allows the app to invite guest users to your organization, on behalf of the signed-in user. + // + // requires admin consent + UserInviteAllScope ScopeType = "User.Invite.All" + + // UserExportAllScope allows the app to export an organizational user's data, when performed by a Company + // Administrator. + // + // requires admin consent + UserExportAllScope ScopeType = "User.Export.All" +) + +// User Activity Permissions +// +// UserActivity.ReadWrite.CreatedByApp is valid for both Microsoft accounts and work or school accounts. +// +// The CreatedByApp constraint associated with this permission indicates the service will apply implicit filtering to +// results based on the identity of the calling app, either the MSA app id or a set of app ids configured for a +// cross-platform application identity. +const ( + // UserActivityReadWriteCreatedByAppScope allows the app to read and report the signed-in user's activity in the + // app. + UserActivityReadWriteCreatedByAppScope ScopeType = "UserActivity.ReadWrite.CreatedByApp" +) diff --git a/vendor/github.com/markbates/goth/providers/azureadv2/session.go b/vendor/github.com/markbates/goth/providers/azureadv2/session.go new file mode 100644 index 000000000..f2f0cd07c --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/azureadv2/session.go @@ -0,0 +1,63 @@ +package azureadv2 + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session is the implementation of `goth.Session` +type Session struct { + AuthURL string `json:"au"` + AccessToken string `json:"at"` + RefreshToken string `json:"rt"` + ExpiresAt time.Time `json:"exp"` +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` func +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + + return s.AuthURL, nil +} + +// Authorize the session with AzureAD and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.RefreshToken = token.RefreshToken + s.ExpiresAt = token.Expiry + + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + session := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(session) + return session, err +} diff --git a/vendor/github.com/markbates/goth/providers/microsoftonline/microsoftonline.go b/vendor/github.com/markbates/goth/providers/microsoftonline/microsoftonline.go new file mode 100644 index 000000000..0ee1bece6 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/microsoftonline/microsoftonline.go @@ -0,0 +1,190 @@ +// Package microsoftonline implements the OAuth2 protocol for authenticating users through microsoftonline. +// This package can be used as a reference implementation of an OAuth2 provider for Goth. +// To use this package, your application need to be registered in [Application Registration Portal](https://apps.dev.microsoft.com/) +package microsoftonline + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/markbates/going/defaults" + "github.com/markbates/goth" + "golang.org/x/oauth2" +) + +const ( + authURL string = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize" + tokenURL string = "https://login.microsoftonline.com/common/oauth2/v2.0/token" + endpointProfile string = "https://graph.microsoft.com/v1.0/me" +) + +var defaultScopes = []string{"openid", "offline_access", "user.read"} + +// New creates a new microsoftonline provider, and sets up important connection details. +// You should always call `microsoftonline.New` to get a new Provider. Never try to create +// one manually. +func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { + p := &Provider{ + ClientKey: clientKey, + Secret: secret, + CallbackURL: callbackURL, + providerName: "microsoftonline", + } + + p.config = newConfig(p, scopes) + return p +} + +// Provider is the implementation of `goth.Provider` for accessing microsoftonline. +type Provider struct { + ClientKey string + Secret string + CallbackURL string + HTTPClient *http.Client + config *oauth2.Config + providerName string + tenant string +} + +// Name is the name used to retrieve this provider later. +func (p *Provider) Name() string { + return p.providerName +} + +// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) +func (p *Provider) SetName(name string) { + p.providerName = name +} + +// Client is HTTP client to be used in all fetch operations. +func (p *Provider) Client() *http.Client { + return goth.HTTPClientWithFallBack(p.HTTPClient) +} + +// Debug is a no-op for the facebook package. +func (p *Provider) Debug(debug bool) {} + +// BeginAuth asks MicrosoftOnline for an authentication end-point. +func (p *Provider) BeginAuth(state string) (goth.Session, error) { + authURL := p.config.AuthCodeURL(state) + return &Session{ + AuthURL: authURL, + }, nil +} + +// FetchUser will go to MicrosoftOnline and access basic information about the user. +func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { + msSession := session.(*Session) + user := goth.User{ + AccessToken: msSession.AccessToken, + Provider: p.Name(), + ExpiresAt: msSession.ExpiresAt, + } + + if user.AccessToken == "" { + return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) + } + + req, err := http.NewRequest("GET", endpointProfile, nil) + if err != nil { + return user, err + } + + req.Header.Set(authorizationHeader(msSession)) + + response, err := p.Client().Do(req) + if err != nil { + return user, err + } + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) + } + + user.AccessToken = msSession.AccessToken + + err = userFromReader(response.Body, &user) + return user, err +} + +// RefreshTokenAvailable refresh token is provided by auth provider or not +// not available for microsoft online as session size hit the limit of max cookie size +func (p *Provider) RefreshTokenAvailable() bool { + return false +} + +//RefreshToken get new access token based on the refresh token +func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { + if refreshToken == "" { + return nil, fmt.Errorf("No refresh token provided") + } + + token := &oauth2.Token{RefreshToken: refreshToken} + ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) + newToken, err := ts.Token() + if err != nil { + return nil, err + } + return newToken, err +} + +func newConfig(provider *Provider, scopes []string) *oauth2.Config { + c := &oauth2.Config{ + ClientID: provider.ClientKey, + ClientSecret: provider.Secret, + RedirectURL: provider.CallbackURL, + Endpoint: oauth2.Endpoint{ + AuthURL: authURL, + TokenURL: tokenURL, + }, + Scopes: []string{}, + } + + c.Scopes = append(c.Scopes, scopes...) + if len(scopes) == 0 { + c.Scopes = append(c.Scopes, defaultScopes...) + } + + return c +} + +func userFromReader(r io.Reader, user *goth.User) error { + buf := &bytes.Buffer{} + tee := io.TeeReader(r, buf) + + u := struct { + ID string `json:"id"` + Name string `json:"displayName"` + Email string `json:"mail"` + FirstName string `json:"givenName"` + LastName string `json:"surname"` + UserPrincipalName string `json:"userPrincipalName"` + }{} + + if err := json.NewDecoder(tee).Decode(&u); err != nil { + return err + } + + raw := map[string]interface{}{} + if err := json.NewDecoder(buf).Decode(&raw); err != nil { + return err + } + + user.UserID = u.ID + user.Email = defaults.String(u.Email, u.UserPrincipalName) + user.Name = u.Name + user.NickName = u.Name + user.FirstName = u.FirstName + user.LastName = u.LastName + user.RawData = raw + + return nil +} + +func authorizationHeader(session *Session) (string, string) { + return "Authorization", fmt.Sprintf("Bearer %s", session.AccessToken) +} diff --git a/vendor/github.com/markbates/goth/providers/microsoftonline/session.go b/vendor/github.com/markbates/goth/providers/microsoftonline/session.go new file mode 100644 index 000000000..0747ab523 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/microsoftonline/session.go @@ -0,0 +1,62 @@ +package microsoftonline + +import ( + "encoding/json" + "errors" + "strings" + "time" + + "github.com/markbates/goth" +) + +// Session is the implementation of `goth.Session` for accessing microsoftonline. +// Refresh token not available for microsoft online: session size hit the limit of max cookie size +type Session struct { + AuthURL string + AccessToken string + ExpiresAt time.Time +} + +// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Facebook provider. +func (s Session) GetAuthURL() (string, error) { + if s.AuthURL == "" { + return "", errors.New(goth.NoAuthUrlErrorMessage) + } + + return s.AuthURL, nil +} + +// Authorize the session with Facebook and return the access token to be stored for future use. +func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) { + p := provider.(*Provider) + token, err := p.config.Exchange(goth.ContextForClient(p.Client()), params.Get("code")) + if err != nil { + return "", err + } + + if !token.Valid() { + return "", errors.New("Invalid token received from provider") + } + + s.AccessToken = token.AccessToken + s.ExpiresAt = token.Expiry + + return token.AccessToken, err +} + +// Marshal the session into a string +func (s Session) Marshal() string { + b, _ := json.Marshal(s) + return string(b) +} + +func (s Session) String() string { + return s.Marshal() +} + +// UnmarshalSession wil unmarshal a JSON string into a session. +func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { + session := &Session{} + err := json.NewDecoder(strings.NewReader(data)).Decode(session) + return session, err +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 2a616ad9c..a9eabbbba 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -570,10 +570,14 @@ github.com/mailru/easyjson github.com/mailru/easyjson/buffer github.com/mailru/easyjson/jlexer github.com/mailru/easyjson/jwriter +# github.com/markbates/going v1.0.0 +github.com/markbates/going/defaults # github.com/markbates/goth v1.68.0 ## explicit github.com/markbates/goth github.com/markbates/goth/gothic +github.com/markbates/goth/providers/azuread +github.com/markbates/goth/providers/azureadv2 github.com/markbates/goth/providers/bitbucket github.com/markbates/goth/providers/discord github.com/markbates/goth/providers/dropbox @@ -583,6 +587,7 @@ github.com/markbates/goth/providers/github github.com/markbates/goth/providers/gitlab github.com/markbates/goth/providers/google github.com/markbates/goth/providers/mastodon +github.com/markbates/goth/providers/microsoftonline github.com/markbates/goth/providers/nextcloud github.com/markbates/goth/providers/openidConnect github.com/markbates/goth/providers/twitter diff --git a/web_src/js/index.js b/web_src/js/index.js index 4900d37f3..17bf31d6a 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -2027,19 +2027,17 @@ function initAdmin() { const provider = $('#oauth2_provider').val(); switch (provider) { - case 'gitea': - case 'nextcloud': - case 'mastodon': - $('#oauth2_use_custom_url').attr('checked', 'checked'); - // fallthrough intentional - case 'github': - case 'gitlab': - $('.oauth2_use_custom_url').show(); - break; case 'openidConnect': $('.open_id_connect_auto_discovery_url input').attr('required', 'required'); $('.open_id_connect_auto_discovery_url').show(); break; + default: + if ($(`#${provider}_customURLSettings`).data('required')) { + $('#oauth2_use_custom_url').attr('checked', 'checked'); + } + if ($(`#${provider}_customURLSettings`).data('available')) { + $('.oauth2_use_custom_url').show(); + } } onOAuth2UseCustomURLChange(applyDefaultValues); } @@ -2050,29 +2048,14 @@ function initAdmin() { $('.oauth2_use_custom_url_field input[required]').removeAttr('required'); if ($('#oauth2_use_custom_url').is(':checked')) { - if (applyDefaultValues) { - $('#oauth2_token_url').val($(`#${provider}_token_url`).val()); - $('#oauth2_auth_url').val($(`#${provider}_auth_url`).val()); - $('#oauth2_profile_url').val($(`#${provider}_profile_url`).val()); - $('#oauth2_email_url').val($(`#${provider}_email_url`).val()); - } - - switch (provider) { - case 'github': - $('.oauth2_token_url input, .oauth2_auth_url input, .oauth2_profile_url input, .oauth2_email_url input').attr('required', 'required'); - $('.oauth2_token_url, .oauth2_auth_url, .oauth2_profile_url, .oauth2_email_url').show(); - break; - case 'nextcloud': - case 'gitea': - case 'gitlab': - $('.oauth2_token_url input, .oauth2_auth_url input, .oauth2_profile_url input').attr('required', 'required'); - $('.oauth2_token_url, .oauth2_auth_url, .oauth2_profile_url').show(); - $('#oauth2_email_url').val(''); - break; - case 'mastodon': - $('.oauth2_auth_url input').attr('required', 'required'); - $('.oauth2_auth_url').show(); - break; + for (const custom of ['token_url', 'auth_url', 'profile_url', 'email_url', 'tenant']) { + if (applyDefaultValues) { + $(`#oauth2_${custom}`).val($(`#${provider}_${custom}`).val()); + } + if ($(`#${provider}_${custom}`).data('available')) { + $(`.oauth2_${custom} input`).attr('required', 'required'); + $(`.oauth2_${custom}`).show(); + } } } }