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