* 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 | ||||