* add discord auth * add vendor for discord * fix syntax error * make fmt * update version of goth in use * update markbates/gothtags/v1.21.12.1
@@ -588,12 +588,13 @@ | |||||
revision = "e3534c89ef969912856dfa39e56b09e58c5f5daf" | revision = "e3534c89ef969912856dfa39e56b09e58c5f5daf" | ||||
[[projects]] | [[projects]] | ||||
digest = "1:4b992ec853d0ea9bac3dcf09a64af61de1a392e6cb0eef2204c0c92f4ae6b911" | |||||
digest = "1:aa7dcd6a0db70d514821f8739d0a22e7df33b499d8d399cf15b2858d44f8319e" | |||||
name = "github.com/markbates/goth" | name = "github.com/markbates/goth" | ||||
packages = [ | packages = [ | ||||
".", | ".", | ||||
"gothic", | "gothic", | ||||
"providers/bitbucket", | "providers/bitbucket", | ||||
"providers/discord", | |||||
"providers/dropbox", | "providers/dropbox", | ||||
"providers/facebook", | "providers/facebook", | ||||
"providers/github", | "providers/github", | ||||
@@ -603,8 +604,8 @@ | |||||
"providers/twitter", | "providers/twitter", | ||||
] | ] | ||||
pruneopts = "NUT" | pruneopts = "NUT" | ||||
revision = "bc6d8ddf751a745f37ca5567dbbfc4157bbf5da9" | |||||
version = "v1.47.2" | |||||
revision = "157987f620ff2fc5e1f6a1427a3685219fbf6ff4" | |||||
version = "v1.49.0" | |||||
[[projects]] | [[projects]] | ||||
digest = "1:c9724c929d27a14475a45b17a267dbc60671c0bc2c5c05ed21f011f7b5bc9fb5" | digest = "1:c9724c929d27a14475a45b17a267dbc60671c0bc2c5c05ed21f011f7b5bc9fb5" | ||||
@@ -1179,6 +1180,7 @@ | |||||
"github.com/markbates/goth", | "github.com/markbates/goth", | ||||
"github.com/markbates/goth/gothic", | "github.com/markbates/goth/gothic", | ||||
"github.com/markbates/goth/providers/bitbucket", | "github.com/markbates/goth/providers/bitbucket", | ||||
"github.com/markbates/goth/providers/discord", | |||||
"github.com/markbates/goth/providers/dropbox", | "github.com/markbates/goth/providers/dropbox", | ||||
"github.com/markbates/goth/providers/facebook", | "github.com/markbates/goth/providers/facebook", | ||||
"github.com/markbates/goth/providers/github", | "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"}, | "gplus": {Name: "gplus", DisplayName: "Google+", Image: "/img/auth/google_plus.png"}, | ||||
"openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, | "openidConnect": {Name: "openidConnect", DisplayName: "OpenID Connect", Image: "/img/auth/openid_connect.png"}, | ||||
"twitter": {Name: "twitter", DisplayName: "Twitter", Image: "/img/auth/twitter.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 | // 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" | ||||
"github.com/markbates/goth/gothic" | "github.com/markbates/goth/gothic" | ||||
"github.com/markbates/goth/providers/bitbucket" | "github.com/markbates/goth/providers/bitbucket" | ||||
"github.com/markbates/goth/providers/discord" | |||||
"github.com/markbates/goth/providers/dropbox" | "github.com/markbates/goth/providers/dropbox" | ||||
"github.com/markbates/goth/providers/facebook" | "github.com/markbates/goth/providers/facebook" | ||||
"github.com/markbates/goth/providers/github" | "github.com/markbates/goth/providers/github" | ||||
@@ -172,6 +173,8 @@ func createProvider(providerName, providerType, clientID, clientSecret, openIDCo | |||||
} | } | ||||
case "twitter": | case "twitter": | ||||
provider = twitter.NewAuthenticate(clientID, clientSecret, callbackURL) | 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 | // 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.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.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.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.edit = Edit Authentication Source | ||||
auths.activated = This Authentication Source is Activated | auths.activated = This Authentication Source is Activated | ||||
auths.new_success = The authentication '%s' has been added. | auths.new_success = The authentication '%s' has been added. | ||||
@@ -108,6 +108,8 @@ | |||||
<span>{{.i18n.Tr "admin.auths.tip.openid_connect"}}</span> | <span>{{.i18n.Tr "admin.auths.tip.openid_connect"}}</span> | ||||
<li>Twitter</li> | <li>Twitter</li> | ||||
<span>{{.i18n.Tr "admin.auths.tip.twitter"}}</span> | <span>{{.i18n.Tr "admin.auths.tip.twitter"}}</span> | ||||
<li>Discord</li> | |||||
<span>{{.i18n.Tr "admin.auths.tip.discord"}}</span> | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
@@ -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 | 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. | 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 | package gothic | ||||
@@ -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 | |||||
} |
@@ -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 | |||||
} |
@@ -37,6 +37,7 @@ func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { | |||||
providerName: "facebook", | providerName: "facebook", | ||||
} | } | ||||
p.config = newConfig(p, scopes) | p.config = newConfig(p, scopes) | ||||
p.Fields = "email,first_name,last_name,link,about,id,name,picture,location" | |||||
return p | return p | ||||
} | } | ||||
@@ -46,6 +47,7 @@ type Provider struct { | |||||
Secret string | Secret string | ||||
CallbackURL string | CallbackURL string | ||||
HTTPClient *http.Client | HTTPClient *http.Client | ||||
Fields string | |||||
config *oauth2.Config | config *oauth2.Config | ||||
providerName string | providerName string | ||||
} | } | ||||
@@ -60,6 +62,16 @@ func (p *Provider) SetName(name string) { | |||||
p.providerName = name | 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 { | func (p *Provider) Client() *http.Client { | ||||
return goth.HTTPClientWithFallBack(p.HTTPClient) | return goth.HTTPClientWithFallBack(p.HTTPClient) | ||||
} | } | ||||
@@ -99,7 +111,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { | |||||
reqUrl := fmt.Sprint( | reqUrl := fmt.Sprint( | ||||
endpointProfile, | endpointProfile, | ||||
strings.Join(p.config.Scopes, ","), | |||||
p.Fields, | |||||
"&access_token=", | "&access_token=", | ||||
url.QueryEscape(sess.AccessToken), | url.QueryEscape(sess.AccessToken), | ||||
"&appsecret_proof=", | "&appsecret_proof=", | ||||
@@ -177,31 +189,17 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config { | |||||
}, | }, | ||||
Scopes: []string{ | Scopes: []string{ | ||||
"email", | "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 | |||||
} | |||||
} | |||||
if !found { | |||||
c.Scopes = append(c.Scopes, sc) | |||||
defaultScopes := map[string]struct{}{ | |||||
"email": {}, | |||||
} | |||||
for _, scope := range scopes { | |||||
if _, exists := defaultScopes[scope]; !exists { | |||||
c.Scopes = append(c.Scopes, scope) | |||||
} | } | ||||
found = false | |||||
} | } | ||||
return c | return c | ||||