diff options
author | techknowlogick <hello@techknowlogick.com> | 2019-01-13 14:06:22 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-01-13 14:06:22 -0500 |
commit | 5c44f751a30517914ad232607a1202111cf4f0fa (patch) | |
tree | 5c2efcfa35e80d092bfd246f1c3a966792f9bb5f | |
parent | beab2df1227f9b7e556aa5716d94feb3a3e2088e (diff) | |
download | gitea-5c44f751a30517914ad232607a1202111cf4f0fa.tar.gz gitea-5c44f751a30517914ad232607a1202111cf4f0fa.zip |
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
-rw-r--r-- | Gopkg.lock | 8 | ||||
-rw-r--r-- | models/oauth2.go | 1 | ||||
-rw-r--r-- | modules/auth/oauth2/oauth2.go | 3 | ||||
-rw-r--r-- | options/locale/locale_en-US.ini | 1 | ||||
-rw-r--r-- | public/img/auth/discord.png | bin | 0 -> 1559 bytes | |||
-rw-r--r-- | templates/admin/auth/new.tmpl | 2 | ||||
-rw-r--r-- | vendor/github.com/markbates/goth/gothic/gothic.go | 2 | ||||
-rw-r--r-- | vendor/github.com/markbates/goth/providers/discord/discord.go | 210 | ||||
-rw-r--r-- | vendor/github.com/markbates/goth/providers/discord/session.go | 65 | ||||
-rw-r--r-- | vendor/github.com/markbates/goth/providers/facebook/facebook.go | 42 |
10 files changed, 308 insertions, 26 deletions
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 (<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. diff --git a/public/img/auth/discord.png b/public/img/auth/discord.png Binary files differnew file mode 100644 index 0000000000..db0e70d5d4 --- /dev/null +++ b/public/img/auth/discord.png 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 @@ <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> 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 - } - } - 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 |