1// Package gitlab implements the OAuth2 protocol for authenticating users through gitlab. 2// This package can be used as a reference implementation of an OAuth2 provider for Goth. 3package gitlab 4 5import ( 6 "bytes" 7 "encoding/json" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/url" 12 "strconv" 13 14 "fmt" 15 "github.com/markbates/goth" 16 "golang.org/x/oauth2" 17) 18 19// These vars define the Authentication, Token, and Profile URLS for Gitlab. If 20// using Gitlab CE or EE, you should change these values before calling New. 21// 22// Examples: 23// gitlab.AuthURL = "https://gitlab.acme.com/oauth/authorize 24// gitlab.TokenURL = "https://gitlab.acme.com/oauth/token 25// gitlab.ProfileURL = "https://gitlab.acme.com/api/v3/user 26var ( 27 AuthURL = "https://gitlab.com/oauth/authorize" 28 TokenURL = "https://gitlab.com/oauth/token" 29 ProfileURL = "https://gitlab.com/api/v3/user" 30) 31 32// Provider is the implementation of `goth.Provider` for accessing Gitlab. 33type Provider struct { 34 ClientKey string 35 Secret string 36 CallbackURL string 37 HTTPClient *http.Client 38 config *oauth2.Config 39 providerName string 40 authURL string 41 tokenURL string 42 profileURL string 43} 44 45// New creates a new Gitlab provider and sets up important connection details. 46// You should always call `gitlab.New` to get a new provider. Never try to 47// create one manually. 48func New(clientKey, secret, callbackURL string, scopes ...string) *Provider { 49 return NewCustomisedURL(clientKey, secret, callbackURL, AuthURL, TokenURL, ProfileURL, scopes...) 50} 51 52// NewCustomisedURL is similar to New(...) but can be used to set custom URLs to connect to 53func NewCustomisedURL(clientKey, secret, callbackURL, authURL, tokenURL, profileURL string, scopes ...string) *Provider { 54 p := &Provider{ 55 ClientKey: clientKey, 56 Secret: secret, 57 CallbackURL: callbackURL, 58 providerName: "gitlab", 59 profileURL: profileURL, 60 } 61 p.config = newConfig(p, authURL, tokenURL, scopes) 62 return p 63} 64 65// Name is the name used to retrieve this provider later. 66func (p *Provider) Name() string { 67 return p.providerName 68} 69 70// SetName is to update the name of the provider (needed in case of multiple providers of 1 type) 71func (p *Provider) SetName(name string) { 72 p.providerName = name 73} 74 75func (p *Provider) Client() *http.Client { 76 return goth.HTTPClientWithFallBack(p.HTTPClient) 77} 78 79// Debug is a no-op for the gitlab package. 80func (p *Provider) Debug(debug bool) {} 81 82// BeginAuth asks Gitlab for an authentication end-point. 83func (p *Provider) BeginAuth(state string) (goth.Session, error) { 84 return &Session{ 85 AuthURL: p.config.AuthCodeURL(state), 86 }, nil 87} 88 89// FetchUser will go to Gitlab and access basic information about the user. 90func (p *Provider) FetchUser(session goth.Session) (goth.User, error) { 91 sess := session.(*Session) 92 user := goth.User{ 93 AccessToken: sess.AccessToken, 94 Provider: p.Name(), 95 RefreshToken: sess.RefreshToken, 96 ExpiresAt: sess.ExpiresAt, 97 } 98 99 if user.AccessToken == "" { 100 // data is not yet retrieved since accessToken is still empty 101 return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName) 102 } 103 104 response, err := p.Client().Get(p.profileURL + "?access_token=" + url.QueryEscape(sess.AccessToken)) 105 if err != nil { 106 if response != nil { 107 response.Body.Close() 108 } 109 return user, err 110 } 111 112 defer response.Body.Close() 113 114 if response.StatusCode != http.StatusOK { 115 return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, response.StatusCode) 116 } 117 118 bits, err := ioutil.ReadAll(response.Body) 119 if err != nil { 120 return user, err 121 } 122 123 err = json.NewDecoder(bytes.NewReader(bits)).Decode(&user.RawData) 124 if err != nil { 125 return user, err 126 } 127 128 err = userFromReader(bytes.NewReader(bits), &user) 129 130 return user, err 131} 132 133func newConfig(provider *Provider, authURL, tokenURL string, scopes []string) *oauth2.Config { 134 c := &oauth2.Config{ 135 ClientID: provider.ClientKey, 136 ClientSecret: provider.Secret, 137 RedirectURL: provider.CallbackURL, 138 Endpoint: oauth2.Endpoint{ 139 AuthURL: authURL, 140 TokenURL: tokenURL, 141 }, 142 Scopes: []string{}, 143 } 144 145 if len(scopes) > 0 { 146 for _, scope := range scopes { 147 c.Scopes = append(c.Scopes, scope) 148 } 149 } 150 return c 151} 152 153func userFromReader(r io.Reader, user *goth.User) error { 154 u := struct { 155 Name string `json:"name"` 156 Email string `json:"email"` 157 NickName string `json:"username"` 158 ID int `json:"id"` 159 AvatarURL string `json:"avatar_url"` 160 }{} 161 err := json.NewDecoder(r).Decode(&u) 162 if err != nil { 163 return err 164 } 165 user.Email = u.Email 166 user.Name = u.Name 167 user.NickName = u.NickName 168 user.UserID = strconv.Itoa(u.ID) 169 user.AvatarURL = u.AvatarURL 170 return nil 171} 172 173//RefreshTokenAvailable refresh token is provided by auth provider or not 174func (p *Provider) RefreshTokenAvailable() bool { 175 return true 176} 177 178//RefreshToken get new access token based on the refresh token 179func (p *Provider) RefreshToken(refreshToken string) (*oauth2.Token, error) { 180 token := &oauth2.Token{RefreshToken: refreshToken} 181 ts := p.config.TokenSource(goth.ContextForClient(p.Client()), token) 182 newToken, err := ts.Token() 183 if err != nil { 184 return nil, err 185 } 186 return newToken, err 187} 188