* 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" | |||
[[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. | |||
@@ -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> | |||
@@ -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 | |||
@@ -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", | |||
} | |||
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 | |||
} | |||
} | |||
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 | |||