]> source.dussan.org Git - gitea.git/commitdiff
Add microsoft oauth2 providers (#16544)
authorzeripath <art27@cantab.net>
Fri, 6 Aug 2021 01:11:08 +0000 (02:11 +0100)
committerGitHub <noreply@github.com>
Fri, 6 Aug 2021 01:11:08 +0000 (21:11 -0400)
* Clean up oauth2 providers

Signed-off-by: Andrew Thornton <art27@cantab.net>
* Add AzureAD, AzureADv2, MicrosoftOnline OAuth2 providers

Signed-off-by: Andrew Thornton <art27@cantab.net>
* Apply suggestions from code review

* remove unused Scopes

Signed-off-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: techknowlogick <techknowlogick@gitea.io>
29 files changed:
go.sum
options/locale/locale_en-US.ini
public/img/auth/azuread.png [new file with mode: 0644]
public/img/auth/azureadv2.png [new file with mode: 0644]
public/img/auth/microsoftonline.png [new file with mode: 0644]
routers/web/admin/auths.go
routers/web/user/setting/security.go
services/auth/source/oauth2/providers.go
services/auth/source/oauth2/providers_base.go [new file with mode: 0644]
services/auth/source/oauth2/providers_custom.go [new file with mode: 0644]
services/auth/source/oauth2/providers_openid.go [new file with mode: 0644]
services/auth/source/oauth2/providers_simple.go [new file with mode: 0644]
services/auth/source/oauth2/source_name.go [new file with mode: 0644]
services/auth/source/oauth2/source_register.go
services/auth/source/oauth2/urlmapping.go
services/forms/auth_form.go
templates/admin/auth/edit.tmpl
templates/admin/auth/source/oauth.tmpl
vendor/github.com/markbates/going/LICENSE.txt [new file with mode: 0644]
vendor/github.com/markbates/going/defaults/defaults.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/azuread/azuread.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/azuread/session.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/azureadv2/azureadv2.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/azureadv2/scopes.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/azureadv2/session.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/microsoftonline/microsoftonline.go [new file with mode: 0644]
vendor/github.com/markbates/goth/providers/microsoftonline/session.go [new file with mode: 0644]
vendor/modules.txt
web_src/js/index.js

diff --git a/go.sum b/go.sum
index b1e5a1f96bc2a6c3744c4df36410d60b103ca941..24ac6a65a3249cef5b69d61c5d656d92bef92485 100644 (file)
--- 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=
index 23d7b2387871aa59da4e79e602426510fd2e520d..ccf19293ff8bf4e1b1a43e1277cb3976509fd503 100644 (file)
@@ -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 (file)
index 0000000..1adbf15
Binary files /dev/null and b/public/img/auth/azuread.png differ
diff --git a/public/img/auth/azureadv2.png b/public/img/auth/azureadv2.png
new file mode 100644 (file)
index 0000000..1adbf15
Binary files /dev/null and b/public/img/auth/azureadv2.png differ
diff --git a/public/img/auth/microsoftonline.png b/public/img/auth/microsoftonline.png
new file mode 100644 (file)
index 0000000..a8129dc
Binary files /dev/null and b/public/img/auth/microsoftonline.png differ
index 20efd4a2aca03950450ca6a6b3150a8c66cfd821..2e9697533a22554efd1ce90895ef1f1a25b3e19d 100644 (file)
@@ -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 {
index 02969fb1e6bce38fac62d9dc53207afdaaf9f02f..36c6d7df7276ed06b1a9706f03eb43163810ecf8 100644 (file)
@@ -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
                        }
index 8df8d629617af01d1fb94efbd520e7f1da87e97e..2196e304928e86c9ac1f20d2d967ee5198fb685f 100644 (file)
@@ -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 (file)
index 0000000..b6b6d0b
--- /dev/null
@@ -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 (file)
index 0000000..de1a169
--- /dev/null
@@ -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 (file)
index 0000000..b725cf9
--- /dev/null
@@ -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 (file)
index 0000000..5a7062e
--- /dev/null
@@ -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 (file)
index 0000000..0b794ad
--- /dev/null
@@ -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()
+}
index b61cc3fe7948f0214c284ab31dd5116b7ff12858..24c61a9a568de6c120c3bb03dcd34d08cb23ee9e 100644 (file)
@@ -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
 }
 
index 68829fba2167ff2ee239483f36dbaaa2a330db95..43c8dde9a5dc48712053c5d603e4f6c649cd6886 100644 (file)
@@ -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
 }
index 30621cadffc2f19d8ae04dbf722210839ab8002a..b78fa9217eab056d58cfa4fd42d15518c28435b5 100644 (file)
@@ -62,6 +62,7 @@ type AuthenticationForm struct {
        Oauth2ProfileURL              string
        Oauth2EmailURL                string
        Oauth2IconURL                 string
+       Oauth2Tenant                  string
        SSPIAutoCreateUsers           bool
        SSPIAutoActivateUsers         bool
        SSPIStripDomainNames          bool
index 22a2903b273f723466033e3bc1489dc333388e5b..2b499c7c762796682b3180c185ddceda1c3860b4 100644 (file)
                                                        <div class="text">{{.CurrentOAuth2Provider.DisplayName}}</div>
                                                        {{svg "octicon-triangle-down" 14 "dropdown icon"}}
                                                        <div class="menu">
-                                                               {{range $key, $value := .OAuth2Providers}}
-                                                                       <div class="item" data-value="{{$key}}">{{$value.DisplayName}}</div>
+                                                               {{range .OAuth2Providers}}
+                                                                       <div class="item" data-value="{{.Name}}">{{.DisplayName}}</div>
                                                                {{end}}
                                                        </div>
                                                </div>
                                                <label for="oauth2_email_url">{{.i18n.Tr "admin.auths.oauth2_emailURL"}}</label>
                                                <input id="oauth2_email_url" name="oauth2_email_url" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.EmailURL}}{{end}}">
                                        </div>
-                                       {{if .OAuth2DefaultCustomURLMappings}}{{range $key, $value := .OAuth2DefaultCustomURLMappings}}
-                                       <input id="{{$key}}_token_url" value="{{$value.TokenURL}}" type="hidden" />
-                                       <input id="{{$key}}_auth_url" value="{{$value.AuthURL}}" type="hidden" />
-                                       <input id="{{$key}}_profile_url" value="{{$value.ProfileURL}}" type="hidden" />
-                                       <input id="{{$key}}_email_url" value="{{$value.EmailURL}}" type="hidden" />
+                                       <div class="oauth2_use_custom_url_field oauth2_tenant required field">
+                                               <label for="oauth2_tenant">{{.i18n.Tr "admin.auths.oauth2_tenant"}}</label>
+                                               <input id="oauth2_tenant" name="oauth2_tenant" value="{{if $cfg.CustomURLMapping}}{{$cfg.CustomURLMapping.Tenant}}{{end}}">
+                                       </div>
+
+                                       {{range .OAuth2Providers}}{{if .CustomURLSettings}}
+                                               <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
+                                               <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden" />
+                                               <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden" />
+                                               <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden" />
+                                               <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden" />
+                                               <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden" />
                                        {{end}}{{end}}
                                {{end}}
 
index 787e29873d762f7af4c28f52c41548ef7440b50e..b19fe3d42825de5077ed067efd03ef221a0eb8de 100644 (file)
@@ -2,12 +2,12 @@
        <div class="inline required field">
                <label>{{.i18n.Tr "admin.auths.oauth2_provider"}}</label>
                <div class="ui selection type dropdown">
-                       <input type="hidden" id="oauth2_provider" name="oauth2_provider" value="{{.oauth2_provider}}">
-                       <div class="text">{{.oauth2_provider}}</div>
+                       <input type="hidden" id="oauth2_provider" name="oauth2_provider" value="{{.oauth2_provider.Name}}">
+                       <div class="text">{{.oauth2_provider.Name}}</div>
                        {{svg "octicon-triangle-down" 14 "dropdown icon"}}
                        <div class="menu">
-                               {{range $key, $value := .OAuth2Providers}}
-                                       <div class="item" data-value="{{$key}}">{{$value.DisplayName}}</div>
+                               {{range .OAuth2Providers}}
+                                       <div class="item" data-value="{{.Name}}">{{.DisplayName}}</div>
                                {{end}}
                        </div>
                </div>
                <label for="oauth2_email_url">{{.i18n.Tr "admin.auths.oauth2_emailURL"}}</label>
                <input id="oauth2_email_url" name="oauth2_email_url" value="{{.oauth2_email_url}}">
        </div>
-       {{if .OAuth2DefaultCustomURLMappings}}
-               {{range $key, $value := .OAuth2DefaultCustomURLMappings}}
-                       <input id="{{$key}}_token_url" value="{{$value.TokenURL}}" type="hidden" />
-                       <input id="{{$key}}_auth_url" value="{{$value.AuthURL}}" type="hidden" />
-                       <input id="{{$key}}_profile_url" value="{{$value.ProfileURL}}" type="hidden" />
-                       <input id="{{$key}}_email_url" value="{{$value.EmailURL}}" type="hidden" />
-               {{end}}
-       {{end}}
+       <div class="oauth2_use_custom_url_field oauth2_tenant required field">
+               <label for="oauth2_tenant">{{.i18n.Tr "admin.auths.oauth2_tenant"}}</label>
+               <input id="oauth2_tenant" name="oauth2_tenant" value="{{.oauth2_tenant}}">
+       </div>
+
+       {{range .OAuth2Providers}}{{if .CustomURLSettings}}
+               <input id="{{.Name}}_customURLSettings" type="hidden" data-required="{{.CustomURLSettings.Required}}" data-available="true">
+               <input id="{{.Name}}_token_url" value="{{.CustomURLSettings.TokenURL.Value}}" data-available="{{.CustomURLSettings.TokenURL.Available}}" data-required="{{.CustomURLSettings.TokenURL.Required}}" type="hidden" />
+               <input id="{{.Name}}_auth_url" value="{{.CustomURLSettings.AuthURL.Value}}" data-available="{{.CustomURLSettings.AuthURL.Available}}" data-required="{{.CustomURLSettings.AuthURL.Required}}" type="hidden" />
+               <input id="{{.Name}}_profile_url" value="{{.CustomURLSettings.ProfileURL.Value}}" data-available="{{.CustomURLSettings.ProfileURL.Available}}" data-required="{{.CustomURLSettings.ProfileURL.Required}}" type="hidden" />
+               <input id="{{.Name}}_email_url" value="{{.CustomURLSettings.EmailURL.Value}}" data-available="{{.CustomURLSettings.EmailURL.Available}}" data-required="{{.CustomURLSettings.EmailURL.Required}}" type="hidden" />
+               <input id="{{.Name}}_tenant" value="{{.CustomURLSettings.Tenant.Value}}" data-available="{{.CustomURLSettings.Tenant.Available}}" data-required="{{.CustomURLSettings.Tenant.Required}}" type="hidden" />
+       {{end}}{{end}}
 </div>
diff --git a/vendor/github.com/markbates/going/LICENSE.txt b/vendor/github.com/markbates/going/LICENSE.txt
new file mode 100644 (file)
index 0000000..f8e6d5b
--- /dev/null
@@ -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 (file)
index 0000000..0d81de6
--- /dev/null
@@ -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 (file)
index 0000000..50fb479
--- /dev/null
@@ -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 (file)
index 0000000..098a9dc
--- /dev/null
@@ -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 (file)
index 0000000..f293816
--- /dev/null
@@ -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 (file)
index 0000000..8dcedef
--- /dev/null
@@ -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 (file)
index 0000000..f2f0cd0
--- /dev/null
@@ -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 (file)
index 0000000..0ee1bec
--- /dev/null
@@ -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 (file)
index 0000000..0747ab5
--- /dev/null
@@ -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
+}
index 2a616ad9cdd5af50b0289d19536754e0dff0343f..a9eabbbba4c96964386ad34989b7427cc0534fbb 100644 (file)
@@ -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
index 4900d37f3ff764cbacecffd6e60b68f04acdb8d2..17bf31d6a6673f180939d7d554eaad6a083da837 100644 (file)
@@ -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();
+        }
       }
     }
   }