1
0
Fork 0
forked from forgejo/forgejo

Additional OAuth2 providers (#1010)

* add google+

* sort signin oauth2 providers based on the name so order is always the same

* update auth tip for google+

* add gitlab provider

* add bitbucket provider (and some go fmt)

* add twitter provider

* add facebook provider

* add dropbox provider

* add openid connect provider incl. new format of tips section in "Add New Source"

* lower the amount of disk storage for each session to prevent issues while building cross platform (and disk overflow)

* imports according to goimport and code style

* make it possible to set custom urls to gitlab and github provider (only these could have a different host)

* split up oauth2 into multiple files

* small typo in comment

* fix indention

* fix indentation

* fix new line before external import

* fix layout of signin part

* update "broken" dependency
This commit is contained in:
Willem van Dreumel 2017-05-01 15:26:53 +02:00 committed by Lunny Xiao
parent 2368bbb672
commit 950f2e2074
44 changed files with 4164 additions and 159 deletions

View file

@ -0,0 +1,206 @@
// Package bitbucket implements the OAuth2 protocol for authenticating users through Bitbucket.
package bitbucket
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"fmt"
)
const (
authURL string = "https://bitbucket.org/site/oauth2/authorize"
tokenURL string = "https://bitbucket.org/site/oauth2/access_token"
endpointProfile string = "https://api.bitbucket.org/2.0/user"
endpointEmail string = "https://api.bitbucket.org/2.0/user/emails"
)
// New creates a new Bitbucket provider, and sets up important connection details.
// You should always call `bitbucket.New` to get a new Provider. Never try to create
// one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "bitbucket",
}
p.config = newConfig(p, scopes)
return p
}
// Provider is the implementation of `goth.Provider` for accessing Bitbucket.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
}
// Name is the name used to retrieve this provider later.
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 a no-op for the bitbucket package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Bitbucket for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
url := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: url,
}
return session, nil
}
// FetchUser will go to Bitbucket and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
RefreshToken: sess.RefreshToken,
ExpiresAt: sess.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)
}
response, err := goth.HTTPClientWithFallBack(p.Client()).Get(endpointProfile + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
return user, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
}
bits, err := ioutil.ReadAll(response.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)
response, err = goth.HTTPClientWithFallBack(p.Client()).Get(endpointEmail + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
return user, err
}
defer response.Body.Close()
bits, err = ioutil.ReadAll(response.Body)
if err != nil {
return user, err
}
err = emailFromReader(bytes.NewReader(bits), &user)
return user, err
}
func userFromReader(reader io.Reader, user *goth.User) error {
u := struct {
ID string `json:"uuid"`
Links struct {
Avatar struct {
URL string `json:"href"`
} `json:"avatar"`
} `json:"links"`
Email string `json:"email"`
Username string `json:"username"`
Name string `json:"display_name"`
Location string `json:"location"`
}{}
err := json.NewDecoder(reader).Decode(&u)
if err != nil {
return err
}
user.Name = u.Name
user.NickName = u.Username
user.AvatarURL = u.Links.Avatar.URL
user.UserID = u.ID
user.Location = u.Location
return err
}
func emailFromReader(reader io.Reader, user *goth.User) error {
e := struct {
Values []struct {
Email string `json:"email"`
} `json:"values"`
}{}
err := json.NewDecoder(reader).Decode(&e)
if err != nil {
return err
}
if len(e.Values) > 0 {
user.Email = e.Values[0].Email
}
return err
}
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{},
}
for _, scope := range scopes {
c.Scopes = append(c.Scopes, scope)
}
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(goth.ContextForClient(p.Client()), token)
newToken, err := ts.Token()
if err != nil {
return nil, err
}
return newToken, err
}

View file

@ -0,0 +1,61 @@
package bitbucket
import (
"encoding/json"
"errors"
"strings"
"time"
"github.com/markbates/goth"
)
// Session stores data during the auth process with Bitbucket.
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 Bitbucket provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Authorize the session with Bitbucket and return 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(goth.ContextForClient(p.Client()), 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 the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
// UnmarshalSession will unmarshal a JSON string into a session.
func (p *Provider) UnmarshalSession(data string) (goth.Session, error) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}
func (s Session) String() string {
return s.Marshal()
}

View file

@ -0,0 +1,191 @@
// Package dropbox implements the OAuth2 protocol for authenticating users through Dropbox.
package dropbox
import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"fmt"
)
const (
authURL = "https://www.dropbox.com/1/oauth2/authorize"
tokenURL = "https://api.dropbox.com/1/oauth2/token"
accountURL = "https://api.dropbox.com/1/account/info"
)
// Provider is the implementation of `goth.Provider` for accessing Dropbox.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
}
// Session stores data during the auth process with Dropbox.
type Session struct {
AuthURL string
Token string
}
// New creates a new Dropbox provider and sets up important connection details.
// You should always call `dropbox.New` to get a new provider. Never try to
// create one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "dropbox",
}
p.config = newConfig(p, scopes)
return p
}
// Name is the name used to retrieve this provider later.
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 a no-op for the dropbox package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Dropbox for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
return &Session{
AuthURL: p.config.AuthCodeURL(state),
}, nil
}
// FetchUser will go to Dropbox and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
s := session.(*Session)
user := goth.User{
AccessToken: s.Token,
Provider: p.Name(),
}
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", accountURL, nil)
if err != nil {
return user, err
}
req.Header.Set("Authorization", "Bearer "+s.Token)
resp, err := p.Client().Do(req)
if err != nil {
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)
}
err = userFromReader(resp.Body, &user)
return user, err
}
// UnmarshalSession wil 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
}
// GetAuthURL gets the URL set by calling the `BeginAuth` function on the Dropbox provider.
func (s *Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New("dropbox: missing AuthURL")
}
return s.AuthURL, nil
}
// Authorize the session with Dropbox and return 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(goth.ContextForClient(p.Client()), params.Get("code"))
if err != nil {
return "", err
}
if !token.Valid() {
return "", errors.New("Invalid token received from provider")
}
s.Token = token.AccessToken
return token.AccessToken, nil
}
// Marshal the session into a string
func (s *Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
func (s Session) String() string {
return s.Marshal()
}
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,
},
}
return c
}
func userFromReader(r io.Reader, user *goth.User) error {
u := struct {
Name string `json:"display_name"`
NameDetails struct {
NickName string `json:"familiar_name"`
} `json:"name_details"`
Location string `json:"country"`
Email string `json:"email"`
}{}
err := json.NewDecoder(r).Decode(&u)
if err != nil {
return err
}
user.Email = u.Email
user.Name = u.Name
user.NickName = u.NameDetails.NickName
user.UserID = u.Email // Dropbox doesn't provide a separate user ID
user.Location = u.Location
return nil
}
//RefreshToken refresh token is not provided by dropbox
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, errors.New("Refresh token is not provided by dropbox")
}
//RefreshTokenAvailable refresh token is not provided by dropbox
func (p *Provider) RefreshTokenAvailable() bool {
return false
}

View file

@ -0,0 +1,195 @@
// Package facebook implements the OAuth2 protocol for authenticating users through Facebook.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package facebook
import (
"bytes"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"fmt"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
const (
authURL string = "https://www.facebook.com/dialog/oauth"
tokenURL string = "https://graph.facebook.com/oauth/access_token"
endpointProfile string = "https://graph.facebook.com/me?fields=email,first_name,last_name,link,about,id,name,picture,location"
)
// New creates a new Facebook provider, and sets up important connection details.
// You should always call `facebook.New` to get a new Provider. Never try to create
// one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "facebook",
}
p.config = newConfig(p, scopes)
return p
}
// Provider is the implementation of `goth.Provider` for accessing Facebook.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
}
// Name is the name used to retrieve this provider later.
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 a no-op for the facebook package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Facebook for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
url := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: url,
}
return session, nil
}
// FetchUser will go to Facebook and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
ExpiresAt: sess.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)
}
// always add appsecretProof to make calls more protected
// https://github.com/markbates/goth/issues/96
// https://developers.facebook.com/docs/graph-api/securing-requests
hash := hmac.New(sha256.New, []byte(p.Secret))
hash.Write([]byte(sess.AccessToken))
appsecretProof := hex.EncodeToString(hash.Sum(nil))
response, err := p.Client().Get(endpointProfile + "&access_token=" + url.QueryEscape(sess.AccessToken) + "&appsecret_proof=" + appsecretProof)
if err != nil {
return user, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
}
bits, err := ioutil.ReadAll(response.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)
return user, err
}
func userFromReader(reader io.Reader, user *goth.User) error {
u := struct {
ID string `json:"id"`
Email string `json:"email"`
About string `json:"about"`
Name string `json:"name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Link string `json:"link"`
Picture struct {
Data struct {
URL string `json:"url"`
} `json:"data"`
} `json:"picture"`
Location struct {
Name string `json:"name"`
} `json:"location"`
}{}
err := json.NewDecoder(reader).Decode(&u)
if err != nil {
return err
}
user.Name = u.Name
user.FirstName = u.FirstName
user.LastName = u.LastName
user.NickName = u.Name
user.Email = u.Email
user.Description = u.About
user.AvatarURL = u.Picture.Data.URL
user.UserID = u.ID
user.Location = u.Location.Name
return err
}
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{
"email",
},
}
defaultScopes := map[string]struct{}{
"email": {},
}
for _, scope := range scopes {
if _, exists := defaultScopes[scope]; !exists {
c.Scopes = append(c.Scopes, scope)
}
}
return c
}
//RefreshToken refresh token is not provided by facebook
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, errors.New("Refresh token is not provided by facebook")
}
//RefreshTokenAvailable refresh token is not provided by facebook
func (p *Provider) RefreshTokenAvailable() bool {
return false
}

View file

@ -0,0 +1,59 @@
package facebook
import (
"encoding/json"
"errors"
"strings"
"time"
"github.com/markbates/goth"
)
// Session stores data during the auth process with Facebook.
type Session struct {
AuthURL string
AccessToken string
ExpiresAt time.Time
}
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Facebook provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Authorize the session with Facebook and return 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(goth.ContextForClient(p.Client()), params.Get("code"))
if err != nil {
return "", err
}
if !token.Valid() {
return "", errors.New("Invalid token received from provider")
}
s.AccessToken = token.AccessToken
s.ExpiresAt = token.Expiry
return token.AccessToken, err
}
// Marshal the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
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) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}

View file

@ -37,13 +37,20 @@ var (
// You should always call `github.New` to get a new Provider. Never try to create
// one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, EmailURL, scopes...)
}
// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to
func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL, emailURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "github",
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "github",
profileURL: profileURL,
emailURL: emailURL,
}
p.config = newConfig(p, scopes)
p.config = newConfig(p, authURL, tokenURL, scopes)
return p
}
@ -55,6 +62,8 @@ type Provider struct {
HTTPClient *http.Client
config *oauth2.Config
providerName string
profileURL string
emailURL string
}
// Name is the name used to retrieve this provider later.
@ -96,7 +105,7 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}
response, err := p.Client().Get(ProfileURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
response, err := p.Client().Get(p.profileURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
return user, err
}
@ -163,7 +172,7 @@ func userFromReader(reader io.Reader, user *goth.User) error {
}
func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
response, err := p.Client().Get(EmailURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
response, err := p.Client().Get(p.emailURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
if response != nil {
response.Body.Close()
@ -194,14 +203,14 @@ func getPrivateMail(p *Provider, sess *Session) (email string, err error) {
return
}
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: AuthURL,
TokenURL: TokenURL,
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{},
}

View file

@ -0,0 +1,187 @@
// Package gitlab implements the OAuth2 protocol for authenticating users through gitlab.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package gitlab
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"fmt"
)
// These vars define the Authentication, Token, and Profile URLS for Gitlab. If
// using Gitlab CE or EE, you should change these values before calling New.
//
// Examples:
// gitlab.AuthURL = "https://gitlab.acme.com/oauth/authorize
// gitlab.TokenURL = "https://gitlab.acme.com/oauth/token
// gitlab.ProfileURL = "https://gitlab.acme.com/api/v3/user
var (
AuthURL = "https://gitlab.com/oauth/authorize"
TokenURL = "https://gitlab.com/oauth/token"
ProfileURL = "https://gitlab.com/api/v3/user"
)
// Provider is the implementation of `goth.Provider` for accessing Gitlab.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
providerName string
authURL string
tokenURL string
profileURL string
}
// New creates a new Gitlab provider and sets up important connection details.
// You should always call `gitlab.New` to get a new provider. Never try to
// create one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...)
}
// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to
func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "gitlab",
profileURL: profileURL,
}
p.config = newConfig(p, authURL, tokenURL, scopes)
return p
}
// Name is the name used to retrieve this provider later.
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 a no-op for the gitlab package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Gitlab for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
return &Session{
AuthURL: p.config.AuthCodeURL(state),
}, nil
}
// FetchUser will go to Gitlab and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
RefreshToken: sess.RefreshToken,
ExpiresAt: sess.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)
}
response, err := p.Client().Get(p.profileURL + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
if response != nil {
response.Body.Close()
}
return user, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
}
bits, err := ioutil.ReadAll(response.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)
return user, err
}
func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: authURL,
TokenURL: tokenURL,
},
Scopes: []string{},
}
if len(scopes) > 0 {
for _, scope := range scopes {
c.Scopes = append(c.Scopes, scope)
}
}
return c
}
func userFromReader(r io.Reader, user *goth.User) error {
u := struct {
Name string `json:"name"`
Email string `json:"email"`
NickName string `json:"username"`
ID int `json:"id"`
AvatarURL string `json:"avatar_url"`
}{}
err := json.NewDecoder(r).Decode(&u)
if err != nil {
return err
}
user.Email = u.Email
user.Name = u.Name
user.NickName = u.NickName
user.UserID = strconv.Itoa(u.ID)
user.AvatarURL = u.AvatarURL
return nil
}
//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(goth.ContextForClient(p.Client()), token)
newToken, err := ts.Token()
if err != nil {
return nil, err
}
return newToken, err
}

View file

@ -0,0 +1,63 @@
package gitlab
import (
"encoding/json"
"errors"
"strings"
"time"
"github.com/markbates/goth"
)
// Session stores data during the auth process with Gitlab.
type Session struct {
AuthURL string
AccessToken string
RefreshToken string
ExpiresAt time.Time
}
var _ goth.Session = &Session{}
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Gitlab provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Authorize the session with Gitlab and return 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(goth.ContextForClient(p.Client()), 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 the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
func (s Session) String() string {
return s.Marshal()
}
// UnmarshalSession wil 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
}

View file

@ -0,0 +1,195 @@
// Package gplus implements the OAuth2 protocol for authenticating users through Google+.
// This package can be used as a reference implementation of an OAuth2 provider for Goth.
package gplus
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/markbates/goth"
"golang.org/x/oauth2"
"fmt"
)
const (
authURL string = "https://accounts.google.com/o/oauth2/auth?access_type=offline"
tokenURL string = "https://accounts.google.com/o/oauth2/token"
endpointProfile string = "https://www.googleapis.com/oauth2/v2/userinfo"
)
// New creates a new Google+ provider, and sets up important connection details.
// You should always call `gplus.New` to get a new Provider. Never try to create
// one manually.
func New(clientKey, secret, callbackURL string, scopes ...string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "gplus",
}
p.config = newConfig(p, scopes)
return p
}
// Provider is the implementation of `goth.Provider` for accessing Google+.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
prompt oauth2.AuthCodeOption
providerName string
}
// Name is the name used to retrieve this provider later.
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 a no-op for the gplus package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks Google+ for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
var opts []oauth2.AuthCodeOption
if p.prompt != nil {
opts = append(opts, p.prompt)
}
url := p.config.AuthCodeURL(state, opts...)
session := &Session{
AuthURL: url,
}
return session, nil
}
// FetchUser will go to Google+ and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
RefreshToken: sess.RefreshToken,
ExpiresAt: sess.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)
}
response, err := p.Client().Get(endpointProfile + "?access_token=" + url.QueryEscape(sess.AccessToken))
if err != nil {
return user, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
}
bits, err := ioutil.ReadAll(response.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)
return user, err
}
func userFromReader(reader io.Reader, user *goth.User) error {
u := struct {
ID string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
FirstName string `json:"given_name"`
LastName string `json:"family_name"`
Link string `json:"link"`
Picture string `json:"picture"`
}{}
err := json.NewDecoder(reader).Decode(&u)
if err != nil {
return err
}
user.Name = u.Name
user.FirstName = u.FirstName
user.LastName = u.LastName
user.NickName = u.Name
user.Email = u.Email
//user.Description = u.Bio
user.AvatarURL = u.Picture
user.UserID = u.ID
//user.Location = u.Location.Name
return err
}
func newConfig(provider *Provider, scopes []string) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.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{"profile", "email", "openid"}
}
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(goth.ContextForClient(p.Client()), token)
newToken, err := ts.Token()
if err != nil {
return nil, err
}
return newToken, err
}
// SetPrompt sets the prompt values for the GPlus OAuth call. Use this to
// force users to choose and account every time by passing "select_account",
// for example.
// See https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters
func (p *Provider) SetPrompt(prompt ...string) {
if len(prompt) == 0 {
return
}
p.prompt = oauth2.SetAuthURLParam("prompt", strings.Join(prompt, " "))
}

View file

@ -0,0 +1,61 @@
package gplus
import (
"encoding/json"
"errors"
"strings"
"time"
"github.com/markbates/goth"
)
// Session stores data during the auth process with Google+.
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 Google+ provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Authorize the session with Google+ and return 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(goth.ContextForClient(p.Client()), 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 the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
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) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}

View file

@ -0,0 +1,384 @@
package openidConnect
import (
"net/http"
"strings"
"fmt"
"encoding/json"
"encoding/base64"
"io/ioutil"
"errors"
"golang.org/x/oauth2"
"github.com/markbates/goth"
"time"
"bytes"
)
const (
// Standard Claims http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
// fixed, cannot be changed
subjectClaim = "sub"
expiryClaim = "exp"
audienceClaim = "aud"
issuerClaim = "iss"
PreferredUsernameClaim = "preferred_username"
EmailClaim = "email"
NameClaim = "name"
NicknameClaim = "nickname"
PictureClaim = "picture"
GivenNameClaim = "given_name"
FamilyNameClaim = "family_name"
AddressClaim = "address"
// Unused but available to set in Provider claims
MiddleNameClaim = "middle_name"
ProfileClaim = "profile"
WebsiteClaim = "website"
EmailVerifiedClaim = "email_verified"
GenderClaim = "gender"
BirthdateClaim = "birthdate"
ZoneinfoClaim = "zoneinfo"
LocaleClaim = "locale"
PhoneNumberClaim = "phone_number"
PhoneNumberVerifiedClaim = "phone_number_verified"
UpdatedAtClaim = "updated_at"
clockSkew = 10 * time.Second
)
// Provider is the implementation of `goth.Provider` for accessing OpenID Connect provider
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
config *oauth2.Config
openIDConfig *OpenIDConfig
providerName string
UserIdClaims []string
NameClaims []string
NickNameClaims []string
EmailClaims []string
AvatarURLClaims []string
FirstNameClaims []string
LastNameClaims []string
LocationClaims []string
SkipUserInfoRequest bool
}
type OpenIDConfig struct {
AuthEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"userinfo_endpoint"`
Issuer string `json:"issuer"`
}
// New creates a new OpenID Connect provider, and sets up important connection details.
// You should always call `openidConnect.New` to get a new Provider. Never try to create
// one manually.
// New returns an implementation of an OpenID Connect Authorization Code Flow
// See http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
// ID Token decryption is not (yet) supported
// UserInfo decryption is not (yet) supported
func New(clientKey, secret, callbackURL, openIDAutoDiscoveryURL string, scopes ...string) (*Provider, error) {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
UserIdClaims: []string{subjectClaim},
NameClaims: []string{NameClaim},
NickNameClaims: []string{NicknameClaim, PreferredUsernameClaim},
EmailClaims: []string{EmailClaim},
AvatarURLClaims:[]string{PictureClaim},
FirstNameClaims:[]string{GivenNameClaim},
LastNameClaims: []string{FamilyNameClaim},
LocationClaims: []string{AddressClaim},
providerName: "openid-connect",
}
openIDConfig, err := getOpenIDConfig(p, openIDAutoDiscoveryURL)
if err != nil {
return nil, err
}
p.openIDConfig = openIDConfig
p.config = newConfig(p, scopes, openIDConfig)
return p, nil
}
// Name is the name used to retrieve this provider later.
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 a no-op for the openidConnect package.
func (p *Provider) Debug(debug bool) {}
// BeginAuth asks the OpenID Connect provider for an authentication end-point.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
url := p.config.AuthCodeURL(state)
session := &Session{
AuthURL: url,
}
return session, nil
}
// FetchUser will use the the id_token and access requested information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
expiresAt := sess.ExpiresAt
if sess.IDToken == "" {
return goth.User{}, fmt.Errorf("%s cannot get user information without id_token", p.providerName)
}
// decode returned id token to get expiry
claims, err := decodeJWT(sess.IDToken)
if err != nil {
return goth.User{}, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
}
expiry, err := p.validateClaims(claims)
if err != nil {
return goth.User{}, fmt.Errorf("oauth2: error validating JWT token: %v", err)
}
if expiry.Before(expiresAt) {
expiresAt = expiry
}
if err := p.getUserInfo(sess.AccessToken, claims); err != nil {
return goth.User{}, err
}
user := goth.User{
AccessToken: sess.AccessToken,
Provider: p.Name(),
RefreshToken: sess.RefreshToken,
ExpiresAt: expiresAt,
RawData: claims,
}
p.userFromClaims(claims, &user)
return user, err
}
//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
}
// validate according to standard, returns expiry
// http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
func (p *Provider) validateClaims(claims map[string]interface{}) (time.Time, error) {
audience := getClaimValue(claims, []string{audienceClaim})
if audience != p.ClientKey {
return time.Time{}, errors.New("audience in token does not match client key")
}
issuer := getClaimValue(claims, []string{issuerClaim})
if issuer != p.openIDConfig.Issuer {
return time.Time{}, errors.New("issuer in token does not match issuer in OpenIDConfig discovery")
}
// expiry is required for JWT, not for UserInfoResponse
// is actually a int64, so force it in to that type
expiryClaim := int64(claims[expiryClaim].(float64))
expiry := time.Unix(expiryClaim, 0)
if expiry.Add(clockSkew).Before(time.Now()) {
return time.Time{}, errors.New("user info JWT token is expired")
}
return expiry, nil
}
func (p *Provider) userFromClaims(claims map[string]interface{}, user *goth.User) {
// required
user.UserID = getClaimValue(claims, p.UserIdClaims)
user.Name = getClaimValue(claims, p.NameClaims)
user.NickName = getClaimValue(claims, p.NickNameClaims)
user.Email = getClaimValue(claims, p.EmailClaims)
user.AvatarURL = getClaimValue(claims, p.AvatarURLClaims)
user.FirstName = getClaimValue(claims, p.FirstNameClaims)
user.LastName = getClaimValue(claims, p.LastNameClaims)
user.Location = getClaimValue(claims, p.LocationClaims)
}
func (p *Provider) getUserInfo(accessToken string, claims map[string]interface{}) error {
// skip if there is no UserInfoEndpoint or is explicitly disabled
if p.openIDConfig.UserInfoEndpoint == "" || p.SkipUserInfoRequest {
return nil
}
userInfoClaims, err := p.fetchUserInfo(p.openIDConfig.UserInfoEndpoint, accessToken)
if err != nil {
return err
}
// The sub (subject) Claim MUST always be returned in the UserInfo Response.
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
userInfoSubject := getClaimValue(userInfoClaims, []string{subjectClaim})
if userInfoSubject == "" {
return fmt.Errorf("userinfo response did not contain a 'sub' claim: %#v", userInfoClaims)
}
// The sub Claim in the UserInfo Response MUST be verified to exactly match the sub Claim in the ID Token;
// if they do not match, the UserInfo Response values MUST NOT be used.
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
subject := getClaimValue(claims, []string{subjectClaim})
if userInfoSubject != subject {
return fmt.Errorf("userinfo 'sub' claim (%s) did not match id_token 'sub' claim (%s)", userInfoSubject, subject)
}
// Merge in userinfo claims in case id_token claims contained some that userinfo did not
for k, v := range userInfoClaims {
claims[k] = v
}
return nil
}
// fetch and decode JSON from the given UserInfo URL
func (p *Provider) fetchUserInfo(url, accessToken string) (map[string]interface{}, error) {
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := p.Client().Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("Non-200 response from UserInfo: %d, WWW-Authenticate=%s", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
}
// The UserInfo Claims MUST be returned as the members of a JSON object
// http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return unMarshal(data)
}
func getOpenIDConfig(p *Provider, openIDAutoDiscoveryURL string) (*OpenIDConfig, error) {
res, err := p.Client().Get(openIDAutoDiscoveryURL)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
openIDConfig := &OpenIDConfig{}
err = json.Unmarshal(body, openIDConfig)
if err != nil {
return nil, err
}
return openIDConfig, nil
}
func newConfig(provider *Provider, scopes []string, openIDConfig *OpenIDConfig) *oauth2.Config {
c := &oauth2.Config{
ClientID: provider.ClientKey,
ClientSecret: provider.Secret,
RedirectURL: provider.CallbackURL,
Endpoint: oauth2.Endpoint{
AuthURL: openIDConfig.AuthEndpoint,
TokenURL: openIDConfig.TokenEndpoint,
},
Scopes: []string{},
}
if len(scopes) > 0 {
foundOpenIDScope := false
for _, scope := range scopes {
if scope == "openid" {
foundOpenIDScope = true
}
c.Scopes = append(c.Scopes, scope)
}
if !foundOpenIDScope {
c.Scopes = append(c.Scopes, "openid")
}
} else {
c.Scopes = []string{"openid"}
}
return c
}
func getClaimValue(data map[string]interface{}, claims []string) string {
for _, claim := range claims {
if value, ok := data[claim]; ok {
if stringValue, ok := value.(string); ok && len(stringValue) > 0 {
return stringValue
}
}
}
return ""
}
// decodeJWT decodes a JSON Web Token into a simple map
// http://openid.net/specs/draft-jones-json-web-token-07.html
func decodeJWT(jwt string) (map[string]interface{}, error) {
jwtParts := strings.Split(jwt, ".")
if len(jwtParts) != 3 {
return nil, errors.New("jws: invalid token received, not all parts available")
}
// Re-pad, if needed
encodedPayload := jwtParts[1]
if l := len(encodedPayload) % 4; l != 0 {
encodedPayload += strings.Repeat("=", 4-l)
}
decodedPayload, err := base64.StdEncoding.DecodeString(encodedPayload)
if err != nil {
return nil, err
}
return unMarshal(decodedPayload)
}
func unMarshal(payload []byte) (map[string]interface{}, error) {
data := make(map[string]interface{})
return data, json.NewDecoder(bytes.NewBuffer(payload)).Decode(&data)
}

View file

@ -0,0 +1,63 @@
package openidConnect
import (
"errors"
"github.com/markbates/goth"
"encoding/json"
"strings"
"time"
"golang.org/x/oauth2"
)
// Session stores data during the auth process with the OpenID Connect provider.
type Session struct {
AuthURL string
AccessToken string
RefreshToken string
ExpiresAt time.Time
IDToken string
}
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the OpenID Connect provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New("an AuthURL has not be set")
}
return s.AuthURL, nil
}
// Authorize the session with the OpenID Connect provider and return 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
s.IDToken = token.Extra("id_token").(string)
return token.AccessToken, err
}
// Marshal the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
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) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}

View file

@ -0,0 +1,54 @@
package twitter
import (
"encoding/json"
"errors"
"strings"
"github.com/markbates/goth"
"github.com/mrjones/oauth"
)
// Session stores data during the auth process with Twitter.
type Session struct {
AuthURL string
AccessToken *oauth.AccessToken
RequestToken *oauth.RequestToken
}
// GetAuthURL will return the URL set by calling the `BeginAuth` function on the Twitter provider.
func (s Session) GetAuthURL() (string, error) {
if s.AuthURL == "" {
return "", errors.New(goth.NoAuthUrlErrorMessage)
}
return s.AuthURL, nil
}
// Authorize the session with Twitter and return the access token to be stored for future use.
func (s *Session) Authorize(provider goth.Provider, params goth.Params) (string, error) {
p := provider.(*Provider)
accessToken, err := p.consumer.AuthorizeToken(s.RequestToken, params.Get("oauth_verifier"))
if err != nil {
return "", err
}
s.AccessToken = accessToken
return accessToken.Token, err
}
// Marshal the session into a string
func (s Session) Marshal() string {
b, _ := json.Marshal(s)
return string(b)
}
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) {
sess := &Session{}
err := json.NewDecoder(strings.NewReader(data)).Decode(sess)
return sess, err
}

View file

@ -0,0 +1,160 @@
// Package twitter implements the OAuth protocol for authenticating users through Twitter.
// This package can be used as a reference implementation of an OAuth provider for Goth.
package twitter
import (
"bytes"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"github.com/markbates/goth"
"github.com/mrjones/oauth"
"golang.org/x/oauth2"
"fmt"
)
var (
requestURL = "https://api.twitter.com/oauth/request_token"
authorizeURL = "https://api.twitter.com/oauth/authorize"
authenticateURL = "https://api.twitter.com/oauth/authenticate"
tokenURL = "https://api.twitter.com/oauth/access_token"
endpointProfile = "https://api.twitter.com/1.1/account/verify_credentials.json"
)
// New creates a new Twitter provider, and sets up important connection details.
// You should always call `twitter.New` to get a new Provider. Never try to create
// one manually.
//
// If you'd like to use authenticate instead of authorize, use NewAuthenticate instead.
func New(clientKey, secret, callbackURL string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "twitter",
}
p.consumer = newConsumer(p, authorizeURL)
return p
}
// NewAuthenticate is the almost same as New.
// NewAuthenticate uses the authenticate URL instead of the authorize URL.
func NewAuthenticate(clientKey, secret, callbackURL string) *Provider {
p := &Provider{
ClientKey: clientKey,
Secret: secret,
CallbackURL: callbackURL,
providerName: "twitter",
}
p.consumer = newConsumer(p, authenticateURL)
return p
}
// Provider is the implementation of `goth.Provider` for accessing Twitter.
type Provider struct {
ClientKey string
Secret string
CallbackURL string
HTTPClient *http.Client
debug bool
consumer *oauth.Consumer
providerName string
}
// Name is the name used to retrieve this provider later.
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 sets the logging of the OAuth client to verbose.
func (p *Provider) Debug(debug bool) {
p.debug = debug
}
// BeginAuth asks Twitter for an authentication end-point and a request token for a session.
// Twitter does not support the "state" variable.
func (p *Provider) BeginAuth(state string) (goth.Session, error) {
requestToken, url, err := p.consumer.GetRequestTokenAndUrl(p.CallbackURL)
session := &Session{
AuthURL: url,
RequestToken: requestToken,
}
return session, err
}
// FetchUser will go to Twitter and access basic information about the user.
func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
sess := session.(*Session)
user := goth.User{
Provider: p.Name(),
}
if sess.AccessToken == nil {
// data is not yet retrieved since accessToken is still empty
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}
response, err := p.consumer.Get(
endpointProfile,
map[string]string{"include_entities": "false", "skip_status": "true"},
sess.AccessToken)
if err != nil {
return user, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode)
}
bits, err := ioutil.ReadAll(response.Body)
err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData)
if err != nil {
return user, err
}
user.Name = user.RawData["name"].(string)
user.NickName = user.RawData["screen_name"].(string)
user.Description = user.RawData["description"].(string)
user.AvatarURL = user.RawData["profile_image_url"].(string)
user.UserID = user.RawData["id_str"].(string)
user.Location = user.RawData["location"].(string)
user.AccessToken = sess.AccessToken.Token
user.AccessTokenSecret = sess.AccessToken.Secret
return user, err
}
func newConsumer(provider *Provider, authURL string) *oauth.Consumer {
c := oauth.NewConsumer(
provider.ClientKey,
provider.Secret,
oauth.ServiceProvider{
RequestTokenUrl: requestURL,
AuthorizeTokenUrl: authURL,
AccessTokenUrl: tokenURL,
})
c.Debug(provider.debug)
return c
}
//RefreshToken refresh token is not provided by twitter
func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) {
return nil, errors.New("Refresh token is not provided by twitter")
}
//RefreshTokenAvailable refresh token is not provided by twitter
func (p *Provider) RefreshTokenAvailable() bool {
return false
}

7
vendor/github.com/mrjones/oauth/MIT-LICENSE.txt generated vendored Normal file
View file

@ -0,0 +1,7 @@
Copyright (C) 2013 Matthew R. Jones
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

51
vendor/github.com/mrjones/oauth/README.md generated vendored Normal file
View file

@ -0,0 +1,51 @@
OAuth 1.0 Library for [Go](http://golang.org)
========================
[![GoDoc](http://godoc.org/github.com/mrjones/oauth?status.png)](http://godoc.org/github.com/mrjones/oauth)
[![CircleCI](https://circleci.com/gh/mrjones/oauth/tree/master.svg?style=svg)](https://circleci.com/gh/mrjones/oauth/tree/master)
(If you need an OAuth 2.0 library, check out: https://godoc.org/golang.org/x/oauth2)
Developing your own apps, with this library
-------------------------------------------
* First, install the library
go get github.com/mrjones/oauth
* Then, check out the comments in oauth.go
* Or, have a look at the examples:
* Netflix
go run examples/netflix/netflix.go --consumerkey [key] --consumersecret [secret] --appname [appname]
* Twitter
Command line:
go run examples/twitter/twitter.go --consumerkey [key] --consumersecret [secret]
Or, in the browser (using an HTTP server):
go run examples/twitterserver/twitterserver.go --consumerkey [key] --consumersecret [secret] --port 8888
* The Google Latitude example is broken, now that Google uses OAuth 2.0
Contributing to this library
----------------------------
* Please install the pre-commit hook, which will run tests, and go-fmt before committing.
ln -s $PWD/pre-commit.sh .git/hooks/pre-commit
* Running tests and building is as you'd expect:
go test *.go
go build *.go

1412
vendor/github.com/mrjones/oauth/oauth.go generated vendored Normal file

File diff suppressed because it is too large Load diff

21
vendor/github.com/mrjones/oauth/pre-commit.sh generated vendored Executable file
View file

@ -0,0 +1,21 @@
#!/bin/bash
# ln -s $PWD/pre-commit.sh .git/hooks/pre-commit
go test *.go
RESULT=$?
if [[ $RESULT != 0 ]]; then
echo "REJECTING COMMIT (test failed with status: $RESULT)"
exit 1;
fi
go fmt *.go
for e in $(ls examples); do
go build examples/$e/*.go
RESULT=$?
if [[ $RESULT != 0 ]]; then
echo "REJECTING COMMIT (Examples failed to compile)"
exit $RESULT;
fi
go fmt examples/$e/*.go
done
exit 0

163
vendor/github.com/mrjones/oauth/provider.go generated vendored Normal file
View file

@ -0,0 +1,163 @@
package oauth
import (
"bytes"
"fmt"
"math"
"net/http"
"net/url"
"strconv"
"strings"
)
//
// OAuth1 2-legged provider
// Contributed by https://github.com/jacobpgallagher
//
// Provide an buffer reader which implements the Close() interface
type oauthBufferReader struct {
*bytes.Buffer
}
// So that it implements the io.ReadCloser interface
func (m oauthBufferReader) Close() error { return nil }
type ConsumerGetter func(key string, header map[string]string) (*Consumer, error)
// Provider provides methods for a 2-legged Oauth1 provider
type Provider struct {
ConsumerGetter ConsumerGetter
// For mocking
clock clock
}
// NewProvider takes a function to get the consumer secret from a datastore.
// Returns a Provider
func NewProvider(secretGetter ConsumerGetter) *Provider {
provider := &Provider{
secretGetter,
&defaultClock{},
}
return provider
}
// Combine a URL and Request to make the URL absolute
func makeURLAbs(url *url.URL, request *http.Request) {
if !url.IsAbs() {
url.Host = request.Host
if request.TLS != nil || request.Header.Get("X-Forwarded-Proto") == "https" {
url.Scheme = "https"
} else {
url.Scheme = "http"
}
}
}
// IsAuthorized takes an *http.Request and returns a pointer to a string containing the consumer key,
// or nil if not authorized
func (provider *Provider) IsAuthorized(request *http.Request) (*string, error) {
var err error
var userParams map[string]string
// start with the body/query params
userParams, err = parseBody(request)
if err != nil {
return nil, err
}
// if the oauth params are in the Authorization header, grab them, and
// let them override what's in userParams
authHeader := request.Header.Get(HTTP_AUTH_HEADER)
if len(authHeader) > 6 && strings.EqualFold(OAUTH_HEADER, authHeader[0:6]) {
authHeader = authHeader[6:]
params := strings.Split(authHeader, ",")
for _, param := range params {
vals := strings.SplitN(param, "=", 2)
k := strings.Trim(vals[0], " ")
v := strings.Trim(strings.Trim(vals[1], "\""), " ")
if strings.HasPrefix(k, "oauth") {
userParams[k], err = url.QueryUnescape(v)
if err != nil {
return nil, err
}
}
}
}
// pop the request's signature, it's not included in our signature
// calculation
oauthSignature, ok := userParams[SIGNATURE_PARAM]
if !ok {
return nil, fmt.Errorf("no oauth signature")
}
delete(userParams, SIGNATURE_PARAM)
// get the oauth consumer key
consumerKey, ok := userParams[CONSUMER_KEY_PARAM]
if !ok || consumerKey == "" {
return nil, fmt.Errorf("no consumer key")
}
// use it to create a consumer object
consumer, err := provider.ConsumerGetter(consumerKey, userParams)
if err != nil {
return nil, err
}
// Make sure timestamp is no more than 10 digits
timestamp := userParams[TIMESTAMP_PARAM]
if len(timestamp) > 10 {
timestamp = timestamp[0:10]
}
// Check the timestamp
if !consumer.serviceProvider.IgnoreTimestamp {
oauthTimeNumber, err := strconv.Atoi(timestamp)
if err != nil {
return nil, err
}
if math.Abs(float64(int64(oauthTimeNumber)-provider.clock.Seconds())) > 5*60 {
return nil, fmt.Errorf("too much clock skew")
}
}
// Include the query string params in the base string
if consumer.serviceProvider.SignQueryParams {
for k, v := range request.URL.Query() {
userParams[k] = strings.Join(v, "")
}
}
// if our consumer supports bodyhash, check it
if consumer.serviceProvider.BodyHash {
bodyHash, err := calculateBodyHash(request, consumer.signer)
if err != nil {
return nil, err
}
sentHash, ok := userParams[BODY_HASH_PARAM]
if bodyHash == "" && ok {
return nil, fmt.Errorf("body_hash must not be set")
} else if sentHash != bodyHash {
return nil, fmt.Errorf("body_hash mismatch")
}
}
allParams := NewOrderedParams()
for key, value := range userParams {
allParams.Add(key, value)
}
makeURLAbs(request.URL, request)
baseString := consumer.requestString(request.Method, canonicalizeUrl(request.URL), allParams)
err = consumer.signer.Verify(baseString, oauthSignature)
if err != nil {
return nil, err
}
return &consumerKey, nil
}