From 5c44f751a30517914ad232607a1202111cf4f0fa Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Sun, 13 Jan 2019 14:06:22 -0500 Subject: [PATCH] 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 --- Gopkg.lock | 8 +- models/oauth2.go | 1 + modules/auth/oauth2/oauth2.go | 3 + options/locale/locale_en-US.ini | 1 + public/img/auth/discord.png | Bin 0 -> 1559 bytes templates/admin/auth/new.tmpl | 2 + .../markbates/goth/gothic/gothic.go | 2 +- .../goth/providers/discord/discord.go | 210 ++++++++++++++++++ .../goth/providers/discord/session.go | 65 ++++++ .../goth/providers/facebook/facebook.go | 42 ++-- 10 files changed, 308 insertions(+), 26 deletions(-) create mode 100644 public/img/auth/discord.png create mode 100644 vendor/github.com/markbates/goth/providers/discord/discord.go create mode 100644 vendor/github.com/markbates/goth/providers/discord/session.go diff --git a/Gopkg.lock b/Gopkg.lock index 48a8c52621..ca02fe933b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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", diff --git a/models/oauth2.go b/models/oauth2.go index 0640471a48..10bce31924 100644 --- a/models/oauth2.go +++ b/models/oauth2.go @@ -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 diff --git a/modules/auth/oauth2/oauth2.go b/modules/auth/oauth2/oauth2.go index de125c6195..5684f44a89 100644 --- a/modules/auth/oauth2/oauth2.go +++ b/modules/auth/oauth2/oauth2.go @@ -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 diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index db4989cae8..fd51ca678e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -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 (/.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. diff --git a/public/img/auth/discord.png b/public/img/auth/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..db0e70d5d42d5a4e9df0db6491f647b3f33bea76 GIT binary patch literal 1559 zcmb7Edo&XY9G)H&d0dp;v2w8MeVH|3lGiqpVP>mYPyKh#`F_9ecYf!5|9t0sUz$G#t*#1E1pol* z-d>(q1+OYbx^1&!FO>{S6a@1>ghMKrnl}Ew5KRfpQ^>TUv6w)BYl7CprQ*R*+bP4d z{&iWJtIY@iU~7`MryGu;wD|n@yeeDG-!&?T(Y5fuTrQ(iMeiG5Xp|7A#B%9-@XptnMd3^YRW!BRa-Md=M|NLpK{9eS{qY# z#+lk91@5qxyHYr42->07qd8BIROvpgNvj)4AUg^$YEEz|%VJ*CO|U5SgN2xu)j-5u|JfpgU@0am_iI zdX8qG;>=E*)BI$sROL*@AH`G2&ih_@2nik7S{_|F%j~gU;kxx;yspl4m86!d(c#p4 zG0^d34V2LXY?TkM+rG4%tu3Q}>NUWpHI5`OJHiNIwzOtX8Z@3g2Q7s+b{^pBUz0;b z3cq!qslOK{pZgLAR>o@fV1Fu{ZCk0UFc ze(I|Br?qji^DFL-@(lxhNBLG*@SP}3$$048byr&3yO@|a-?i#VDpVH8yME=Ib49Y9 zrV~)7!rPV9+eI)9^CwtSgJCltUxzZc8%&`a()-QjB%(=$iUDp{S*C-P4VrT9uv_N% z%RASB{H~Pbup0Q~a3QfkIX1PEcM=C1oETp64U3x%jSaZM^n|F&`yLv9KjrK+VT3H2 z=-TF^Zh5-8!PNW>mvV+x)V#3GJ9V6L6ULEZ$C^y7>_qgDhd>Y=#gPgOJ@!+G+v9rs z^p^zz$!zJ}r?%NE!dzjh|9S?3FhOl%9W)YYES+}b)(88|lx*QY=h3ahlRoxqi%x0M zgzME?_+t$F`%Q-aJzoBiU}YP){7d4&BMB|HhDRH2uLPsny0_^1n(U*!nEj=A#}KeR zM~|JHKB*y|{1l!}#DRXe{@j5mr3#!VAj*Dh$ECNInQO89Hs!m5#~62Lgdvq5IeO%Xkx5BIYTG(SrV_cTr4=Y(nr#cn`J$2PFYMk5ZoWN zdV%Z$jL5`Rb2?EjNBetIblMxjB2IxBCBJ-D&y5lnO1TRRmi}I(<;@3>vGX4t;DxTA zW8^0Lo-GrBQf@YjXBlxQGyYZHkQUh(mfSrkw$sfw^C$m)87EeE*L3Kr=4MZ=ee%V)+ep>WE;)lU~aV+DuNAs{_e5mfpn5 zC~ipg1GEo?cbMt{LY)t*cg^9~_E?fNoWHp`$mHi;7dYDbssI20 literal 0 HcmV?d00001 diff --git a/templates/admin/auth/new.tmpl b/templates/admin/auth/new.tmpl index 46db82c3a7..91d3cde308 100644 --- a/templates/admin/auth/new.tmpl +++ b/templates/admin/auth/new.tmpl @@ -108,6 +108,8 @@ {{.i18n.Tr "admin.auths.tip.openid_connect"}}
  • Twitter
  • {{.i18n.Tr "admin.auths.tip.twitter"}} +
  • Discord
  • + {{.i18n.Tr "admin.auths.tip.discord"}} diff --git a/vendor/github.com/markbates/goth/gothic/gothic.go b/vendor/github.com/markbates/goth/gothic/gothic.go index 19dacb44b0..bea87d963d 100644 --- a/vendor/github.com/markbates/goth/gothic/gothic.go +++ b/vendor/github.com/markbates/goth/gothic/gothic.go @@ -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 diff --git a/vendor/github.com/markbates/goth/providers/discord/discord.go b/vendor/github.com/markbates/goth/providers/discord/discord.go new file mode 100644 index 0000000000..e93ec60cd8 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/discord/discord.go @@ -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 +} diff --git a/vendor/github.com/markbates/goth/providers/discord/session.go b/vendor/github.com/markbates/goth/providers/discord/session.go new file mode 100644 index 0000000000..b3078f09a2 --- /dev/null +++ b/vendor/github.com/markbates/goth/providers/discord/session.go @@ -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 +} diff --git a/vendor/github.com/markbates/goth/providers/facebook/facebook.go b/vendor/github.com/markbates/goth/providers/facebook/facebook.go index 5c80ca747b..dd13580a34 100644 --- a/vendor/github.com/markbates/goth/providers/facebook/facebook.go +++ b/vendor/github.com/markbates/goth/providers/facebook/facebook.go @@ -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