Discord Oauth2 support (#4476)
* add discord auth * add vendor for discord * fix syntax error * make fmt * update version of goth in use * update markbates/goth
This commit is contained in:
		
							parent
							
								
									beab2df122
								
							
						
					
					
						commit
						5c44f751a3
					
				
					 10 changed files with 308 additions and 26 deletions
				
			
		
							
								
								
									
										8
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								Gopkg.lock
									
									
									
										generated
									
									
									
								
							|  | @ -588,12 +588,13 @@ | |||
|   revision = "e3534c89ef969912856dfa39e56b09e58c5f5daf" | ||||
| 
 | ||||
| [[projects]] | ||||
|   digest = "1:4b992ec853d0ea9bac3dcf09a64af61de1a392e6cb0eef2204c0c92f4ae6b911" | ||||
|   digest = "1:aa7dcd6a0db70d514821f8739d0a22e7df33b499d8d399cf15b2858d44f8319e" | ||||
|   name = "github.com/markbates/goth" | ||||
|   packages = [ | ||||
|     ".", | ||||
|     "gothic", | ||||
|     "providers/bitbucket", | ||||
|     "providers/discord", | ||||
|     "providers/dropbox", | ||||
|     "providers/facebook", | ||||
|     "providers/github", | ||||
|  | @ -603,8 +604,8 @@ | |||
|     "providers/twitter", | ||||
|   ] | ||||
|   pruneopts = "NUT" | ||||
|   revision = "bc6d8ddf751a745f37ca5567dbbfc4157bbf5da9" | ||||
|   version = "v1.47.2" | ||||
|   revision = "157987f620ff2fc5e1f6a1427a3685219fbf6ff4" | ||||
|   version = "v1.49.0" | ||||
| 
 | ||||
| [[projects]] | ||||
|   digest = "1:c9724c929d27a14475a45b17a267dbc60671c0bc2c5c05ed21f011f7b5bc9fb5" | ||||
|  | @ -1179,6 +1180,7 @@ | |||
|     "github.com/markbates/goth", | ||||
|     "github.com/markbates/goth/gothic", | ||||
|     "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/github", | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ var OAuth2Providers = map[string]OAuth2Provider{ | |||
| 	"gplus":         {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, | ||||
| 	"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, | ||||
| 	"twitter":       {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.png"}, | ||||
| 	"discord":       {Name: "discord", DisplayName: "Discord", Image: "/img/auth/discord.png"}, | ||||
| } | ||||
| 
 | ||||
| // OAuth2DefaultCustomURLMappings contains the map of default URL's for OAuth2 providers that are allowed to have custom urls
 | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ import ( | |||
| 	"github.com/markbates/goth" | ||||
| 	"github.com/markbates/goth/gothic" | ||||
| 	"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/github" | ||||
|  | @ -172,6 +173,8 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo | |||
| 		} | ||||
| 	case "twitter": | ||||
| 		provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL) | ||||
| 	case "discord": | ||||
| 		provider = discord.New(clientID, clientSecret, callbackURL, discord.ScopeIdentify, discord.ScopeEmail) | ||||
| 	} | ||||
| 
 | ||||
| 	// always set the name if provider is created so we can support multiple setups of 1 provider
 | ||||
|  |  | |||
|  | @ -1523,6 +1523,7 @@ auths.tip.gitlab = Register a new application on https://gitlab.com/profile/appl | |||
| auths.tip.google_plus = Obtain OAuth2 client credentials from the Google API console at https://console.developers.google.com/ | ||||
| auths.tip.openid_connect = Use the OpenID Connect Discovery URL (<server>/.well-known/openid-configuration) to specify the endpoints | ||||
| auths.tip.twitter = Go to https://dev.twitter.com/apps, create an application and ensure that the “Allow this application to be used to Sign in with Twitter” option is enabled | ||||
| auths.tip.discord = Register a new application on https://discordapp.com/developers/applications/me | ||||
| auths.edit = Edit Authentication Source | ||||
| auths.activated = This Authentication Source is Activated | ||||
| auths.new_success = The authentication '%s' has been added. | ||||
|  |  | |||
							
								
								
									
										
											BIN
										
									
								
								public/img/auth/discord.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/img/auth/discord.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.5 KiB | 
|  | @ -108,6 +108,8 @@ | |||
| 				<span>{{.i18n.Tr "admin.auths.tip.openid_connect"}}</span> | ||||
| 				<li>Twitter</li> | ||||
| 				<span>{{.i18n.Tr "admin.auths.tip.twitter"}}</span> | ||||
| 				<li>Discord</li> | ||||
| 				<span>{{.i18n.Tr "admin.auths.tip.discord"}}</span> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  |  | |||
							
								
								
									
										2
									
								
								vendor/github.com/markbates/goth/gothic/gothic.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								vendor/github.com/markbates/goth/gothic/gothic.go
									
									
									
										generated
									
									
										vendored
									
									
								
							|  | @ -3,7 +3,7 @@ Package gothic wraps common behaviour when using Goth. This makes it quick, and | |||
| and running with Goth. Of course, if you want complete control over how things flow, in regards | ||||
| to the authentication process, feel free and use Goth directly. | ||||
| 
 | ||||
| See https://github.com/markbates/goth/examples/main.go to see this in action.
 | ||||
| See https://github.com/markbates/goth/blob/master/examples/main.go to see this in action.
 | ||||
| */ | ||||
| package gothic | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										210
									
								
								vendor/github.com/markbates/goth/providers/discord/discord.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								vendor/github.com/markbates/goth/providers/discord/discord.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,210 @@ | |||
| // Package discord implements the OAuth2 protocol for authenticating users through Discord.
 | ||||
| // This package can be used as a reference implementation of an OAuth2 provider for Discord.
 | ||||
| package discord | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/json" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 
 | ||||
| 	"github.com/markbates/goth" | ||||
| 	"golang.org/x/oauth2" | ||||
| 
 | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	authURL      string = "https://discordapp.com/api/oauth2/authorize" | ||||
| 	tokenURL     string = "https://discordapp.com/api/oauth2/token" | ||||
| 	userEndpoint string = "https://discordapp.com/api/users/@me" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	// allows /users/@me without email
 | ||||
| 	ScopeIdentify string = "identify" | ||||
| 	// enables /users/@me to return an email
 | ||||
| 	ScopeEmail string = "email" | ||||
| 	// allows /users/@me/connections to return linked Twitch and YouTube accounts
 | ||||
| 	ScopeConnections string = "connections" | ||||
| 	// allows /users/@me/guilds to return basic information about all of a user's guilds
 | ||||
| 	ScopeGuilds string = "guilds" | ||||
| 	// allows /invites/{invite.id} to be used for joining a user's guild
 | ||||
| 	ScopeJoinGuild string = "guilds.join" | ||||
| 	// allows your app to join users to a group dm
 | ||||
| 	ScopeGroupDMjoin string = "gdm.join" | ||||
| 	// for oauth2 bots, this puts the bot in the user's selected guild by default
 | ||||
| 	ScopeBot string = "bot" | ||||
| 	// 	this generates a webhook that is returned in the oauth token response for authorization code grants
 | ||||
| 	ScopeWebhook string = "webhook.incoming" | ||||
| ) | ||||
| 
 | ||||
| // New creates a new Discord provider, and sets up important connection details.
 | ||||
| // You should always call `discord.New` to get a new Provider. Never try to create
 | ||||
| // one manually.
 | ||||
| func New(clientKey string, secret string, callbackURL string, scopes ...string) *Provider { | ||||
| 	p := &Provider{ | ||||
| 		ClientKey:    clientKey, | ||||
| 		Secret:       secret, | ||||
| 		CallbackURL:  callbackURL, | ||||
| 		providerName: "discord", | ||||
| 	} | ||||
| 	p.config = newConfig(p, scopes) | ||||
| 	return p | ||||
| } | ||||
| 
 | ||||
| // Provider is the implementation of `goth.Provider` for accessing Discord
 | ||||
| type Provider struct { | ||||
| 	ClientKey    string | ||||
| 	Secret       string | ||||
| 	CallbackURL  string | ||||
| 	HTTPClient   *http.Client | ||||
| 	config       *oauth2.Config | ||||
| 	providerName string | ||||
| } | ||||
| 
 | ||||
| // Name gets the name used to retrieve this provider.
 | ||||
| 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 | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) Client() *http.Client { | ||||
| 	return goth.HTTPClientWithFallBack(p.HTTPClient) | ||||
| } | ||||
| 
 | ||||
| // Debug is no-op for the Discord package.
 | ||||
| func (p *Provider) Debug(debug bool) {} | ||||
| 
 | ||||
| // BeginAuth asks Discord for an authentication end-point.
 | ||||
| func (p *Provider) BeginAuth(state string) (goth.Session, error) { | ||||
| 
 | ||||
| 	url := p.config.AuthCodeURL(state, oauth2.AccessTypeOnline) | ||||
| 
 | ||||
| 	s := &Session{ | ||||
| 		AuthURL: url, | ||||
| 	} | ||||
| 	return s, nil | ||||
| } | ||||
| 
 | ||||
| // FetchUser will go to Discord and access basic info about the user.
 | ||||
| func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { | ||||
| 
 | ||||
| 	s := session.(*Session) | ||||
| 
 | ||||
| 	user := goth.User{ | ||||
| 		AccessToken:  s.AccessToken, | ||||
| 		Provider:     p.Name(), | ||||
| 		RefreshToken: s.RefreshToken, | ||||
| 		ExpiresAt:    s.ExpiresAt, | ||||
| 	} | ||||
| 
 | ||||
| 	if user.AccessToken == "" { | ||||
| 		// data is not yet retrieved since accessToken is still empty
 | ||||
| 		return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) | ||||
| 	} | ||||
| 
 | ||||
| 	req, err := http.NewRequest("GET", userEndpoint, nil) | ||||
| 	if err != nil { | ||||
| 		return user, err | ||||
| 	} | ||||
| 	req.Header.Set("Accept", "application/json") | ||||
| 	req.Header.Set("Authorization", "Bearer "+s.AccessToken) | ||||
| 	resp, err := p.Client().Do(req) | ||||
| 	if err != nil { | ||||
| 		if resp != nil { | ||||
| 			resp.Body.Close() | ||||
| 		} | ||||
| 		return user, err | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
| 
 | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode) | ||||
| 	} | ||||
| 
 | ||||
| 	bits, err := ioutil.ReadAll(resp.Body) | ||||
| 	if err != nil { | ||||
| 		return user, err | ||||
| 	} | ||||
| 
 | ||||
| 	err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) | ||||
| 	if err != nil { | ||||
| 		return user, err | ||||
| 	} | ||||
| 
 | ||||
| 	err = userFromReader(bytes.NewReader(bits), &user) | ||||
| 	if err != nil { | ||||
| 		return user, err | ||||
| 	} | ||||
| 
 | ||||
| 	return user, err | ||||
| } | ||||
| 
 | ||||
| func userFromReader(r io.Reader, user *goth.User) error { | ||||
| 	u := struct { | ||||
| 		Name          string `json:"username"` | ||||
| 		Email         string `json:"email"` | ||||
| 		AvatarID      string `json:"avatar"` | ||||
| 		MFAEnabled    bool   `json:"mfa_enabled"` | ||||
| 		Discriminator string `json:"discriminator"` | ||||
| 		Verified      bool   `json:"verified"` | ||||
| 		ID            string `json:"id"` | ||||
| 	}{} | ||||
| 
 | ||||
| 	err := json.NewDecoder(r).Decode(&u) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	user.Name = u.Name | ||||
| 	user.Email = u.Email | ||||
| 	user.AvatarURL = "https://media.discordapp.net/avatars/" + u.ID + "/" + u.AvatarID + ".jpg" | ||||
| 	user.UserID = u.ID | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func newConfig(p *Provider, scopes []string) *oauth2.Config { | ||||
| 	c := &oauth2.Config{ | ||||
| 		ClientID:     p.ClientKey, | ||||
| 		ClientSecret: p.Secret, | ||||
| 		RedirectURL:  p.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 = []string{ScopeIdentify} | ||||
| 	} | ||||
| 
 | ||||
| 	return c | ||||
| } | ||||
| 
 | ||||
| //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(oauth2.NoContext, token) | ||||
| 	newToken, err := ts.Token() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return newToken, err | ||||
| } | ||||
							
								
								
									
										65
									
								
								vendor/github.com/markbates/goth/providers/discord/session.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								vendor/github.com/markbates/goth/providers/discord/session.go
									
									
									
										generated
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | |||
| package discord | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"github.com/markbates/goth" | ||||
| 	"golang.org/x/oauth2" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // Session stores data during the auth process with Discord
 | ||||
| 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 Discord provider.
 | ||||
| func (s Session) GetAuthURL() (string, error) { | ||||
| 	if s.AuthURL == "" { | ||||
| 		return "", errors.New(goth.NoAuthUrlErrorMessage) | ||||
| 	} | ||||
| 	return s.AuthURL, nil | ||||
| } | ||||
| 
 | ||||
| // Authorize completes the authorization with Discord and returns 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(oauth2.NoContext, 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 marshals a session into a JSON string.
 | ||||
| func (s Session) Marshal() string { | ||||
| 	j, _ := json.Marshal(s) | ||||
| 	return string(j) | ||||
| } | ||||
| 
 | ||||
| // String is equivalent to Marshal. It returns a JSON representation of the
 | ||||
| // of the session.
 | ||||
| func (s Session) String() string { | ||||
| 	return s.Marshal() | ||||
| } | ||||
| 
 | ||||
| // UnmarshalSession will unmarshal a JSON string into a session.
 | ||||
| func (p *Provider) UnmarshalSession(data string) (goth.Session, error) { | ||||
| 	s := &Session{} | ||||
| 	err := json.NewDecoder(strings.NewReader(data)).Decode(s) | ||||
| 	return s, err | ||||
| } | ||||
							
								
								
									
										42
									
								
								vendor/github.com/markbates/goth/providers/facebook/facebook.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								vendor/github.com/markbates/goth/providers/facebook/facebook.go
									
									
									
										generated
									
									
										vendored
									
									
								
							|  | @ -37,6 +37,7 @@ func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { | |||
| 		providerName: "facebook", | ||||
| 	} | ||||
| 	p.config = newConfig(p, scopes) | ||||
| 	p.Fields = "email,first_name,last_name,link,about,id,name,picture,location" | ||||
| 	return p | ||||
| } | ||||
| 
 | ||||
|  | @ -46,6 +47,7 @@ type Provider struct { | |||
| 	Secret       string | ||||
| 	CallbackURL  string | ||||
| 	HTTPClient   *http.Client | ||||
| 	Fields       string | ||||
| 	config       *oauth2.Config | ||||
| 	providerName string | ||||
| } | ||||
|  | @ -60,6 +62,16 @@ func (p *Provider) SetName(name string) { | |||
| 	p.providerName = name | ||||
| } | ||||
| 
 | ||||
| // SetCustomFields sets the fields used to return information
 | ||||
| // for a user.
 | ||||
| //
 | ||||
| // A list of available field values can be found at
 | ||||
| // https://developers.facebook.com/docs/graph-api/reference/user
 | ||||
| func (p *Provider) SetCustomFields(fields []string) *Provider { | ||||
| 	p.Fields = strings.Join(fields, ",") | ||||
| 	return p | ||||
| } | ||||
| 
 | ||||
| func (p *Provider) Client() *http.Client { | ||||
| 	return goth.HTTPClientWithFallBack(p.HTTPClient) | ||||
| } | ||||
|  | @ -99,7 +111,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { | |||
| 
 | ||||
| 	reqUrl := fmt.Sprint( | ||||
| 		endpointProfile, | ||||
| 		strings.Join(p.config.Scopes, ","), | ||||
| 		p.Fields, | ||||
| 		"&access_token=", | ||||
| 		url.QueryEscape(sess.AccessToken), | ||||
| 		"&appsecret_proof=", | ||||
|  | @ -177,31 +189,17 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config { | |||
| 		}, | ||||
| 		Scopes: []string{ | ||||
| 			"email", | ||||
| 			"first_name", | ||||
| 			"last_name", | ||||
| 			"link", | ||||
| 			"about", | ||||
| 			"id", | ||||
| 			"name", | ||||
| 			"picture", | ||||
| 			"location", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	// creates possibility to invoke field method like 'picture.type(large)'
 | ||||
| 	var found bool | ||||
| 	for _, sc := range scopes { | ||||
| 		sc := sc | ||||
| 		for i, defScope := range c.Scopes { | ||||
| 			if defScope == strings.Split(sc, ".")[0] { | ||||
| 				c.Scopes[i] = sc | ||||
| 				found = true | ||||
| 			} | ||||
| 	defaultScopes := map[string]struct{}{ | ||||
| 		"email": {}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, scope := range scopes { | ||||
| 		if _, exists := defaultScopes[scope]; !exists { | ||||
| 			c.Scopes = append(c.Scopes, scope) | ||||
| 		} | ||||
| 		if !found { | ||||
| 			c.Scopes = append(c.Scopes, sc) | ||||
| 		} | ||||
| 		found = false | ||||
| 	} | ||||
| 
 | ||||
| 	return c | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue