Add microsoft oauth2 providers (#16544)

* 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>
This commit is contained in:
zeripath 2021-08-06 02:11:08 +01:00 committed by GitHub
parent 7e7006e00d
commit ab9bb54144
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 2132 additions and 260 deletions

1
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=

@ -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

BIN
public/img/auth/azuread.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

@ -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 {

@ -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
}

@ -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

@ -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{}

@ -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
},
))
}

@ -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{})
}

@ -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...)
},
))
}

@ -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()
}

@ -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
}

@ -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
}

@ -62,6 +62,7 @@ type AuthenticationForm struct {
Oauth2ProfileURL string
Oauth2EmailURL string
Oauth2IconURL string
Oauth2Tenant string
SSPIAutoCreateUsers bool
SSPIAutoActivateUsers bool
SSPIStripDomainNames bool

@ -203,8 +203,8 @@
<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>
@ -248,11 +248,18 @@
<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}}

@ -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>
@ -51,12 +51,17 @@
<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>

22
vendor/github.com/markbates/going/LICENSE.txt generated vendored Normal file

@ -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.

36
vendor/github.com/markbates/going/defaults/defaults.go generated vendored Normal file

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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 organizations 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
// devices 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 organizations security events on behalf of the signed-in
// user.
// requires admin consent
SecurityEventsReadAllScope ScopeType = "SecurityEvents.Read.All"
// SecurityEventsReadWriteAllScope allows the app to read your organizations 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"
)

@ -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
}

@ -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)
}

@ -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
}

5
vendor/modules.txt vendored

@ -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

@ -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();
}
}
}
}