1package auth
2
3import (
4	"encoding/json"
5	"errors"
6	"fmt"
7	"net/http"
8	"net/url"
9	"strings"
10	"sync"
11	"time"
12
13	"github.com/docker/distribution/registry/client"
14	"github.com/docker/distribution/registry/client/auth/challenge"
15	"github.com/docker/distribution/registry/client/transport"
16)
17
18var (
19	// ErrNoBasicAuthCredentials is returned if a request can't be authorized with
20	// basic auth due to lack of credentials.
21	ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
22
23	// ErrNoToken is returned if a request is successful but the body does not
24	// contain an authorization token.
25	ErrNoToken = errors.New("authorization server did not include a token in the response")
26)
27
28const defaultClientID = "registry-client"
29
30// AuthenticationHandler is an interface for authorizing a request from
31// params from a "WWW-Authenicate" header for a single scheme.
32type AuthenticationHandler interface {
33	// Scheme returns the scheme as expected from the "WWW-Authenicate" header.
34	Scheme() string
35
36	// AuthorizeRequest adds the authorization header to a request (if needed)
37	// using the parameters from "WWW-Authenticate" method. The parameters
38	// values depend on the scheme.
39	AuthorizeRequest(req *http.Request, params map[string]string) error
40}
41
42// CredentialStore is an interface for getting credentials for
43// a given URL
44type CredentialStore interface {
45	// Basic returns basic auth for the given URL
46	Basic(*url.URL) (string, string)
47
48	// RefreshToken returns a refresh token for the
49	// given URL and service
50	RefreshToken(*url.URL, string) string
51
52	// SetRefreshToken sets the refresh token if none
53	// is provided for the given url and service
54	SetRefreshToken(realm *url.URL, service, token string)
55}
56
57// NewAuthorizer creates an authorizer which can handle multiple authentication
58// schemes. The handlers are tried in order, the higher priority authentication
59// methods should be first. The challengeMap holds a list of challenges for
60// a given root API endpoint (for example "https://registry-1.docker.io/v2/").
61func NewAuthorizer(manager challenge.Manager, handlers ...AuthenticationHandler) transport.RequestModifier {
62	return &endpointAuthorizer{
63		challenges: manager,
64		handlers:   handlers,
65	}
66}
67
68type endpointAuthorizer struct {
69	challenges challenge.Manager
70	handlers   []AuthenticationHandler
71	transport  http.RoundTripper
72}
73
74func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
75	pingPath := req.URL.Path
76	if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 {
77		pingPath = pingPath[:v2Root+4]
78	} else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 {
79		pingPath = pingPath[:v1Root] + "/v2/"
80	} else {
81		return nil
82	}
83
84	ping := url.URL{
85		Host:   req.URL.Host,
86		Scheme: req.URL.Scheme,
87		Path:   pingPath,
88	}
89
90	challenges, err := ea.challenges.GetChallenges(ping)
91	if err != nil {
92		return err
93	}
94
95	if len(challenges) > 0 {
96		for _, handler := range ea.handlers {
97			for _, c := range challenges {
98				if c.Scheme != handler.Scheme() {
99					continue
100				}
101				if err := handler.AuthorizeRequest(req, c.Parameters); err != nil {
102					return err
103				}
104			}
105		}
106	}
107
108	return nil
109}
110
111// This is the minimum duration a token can last (in seconds).
112// A token must not live less than 60 seconds because older versions
113// of the Docker client didn't read their expiration from the token
114// response and assumed 60 seconds.  So to remain compatible with
115// those implementations, a token must live at least this long.
116const minimumTokenLifetimeSeconds = 60
117
118// Private interface for time used by this package to enable tests to provide their own implementation.
119type clock interface {
120	Now() time.Time
121}
122
123type tokenHandler struct {
124	header    http.Header
125	creds     CredentialStore
126	transport http.RoundTripper
127	clock     clock
128
129	offlineAccess bool
130	forceOAuth    bool
131	clientID      string
132	scopes        []Scope
133
134	tokenLock       sync.Mutex
135	tokenCache      string
136	tokenExpiration time.Time
137
138	logger Logger
139}
140
141// Scope is a type which is serializable to a string
142// using the allow scope grammar.
143type Scope interface {
144	String() string
145}
146
147// RepositoryScope represents a token scope for access
148// to a repository.
149type RepositoryScope struct {
150	Repository string
151	Class      string
152	Actions    []string
153}
154
155// String returns the string representation of the repository
156// using the scope grammar
157func (rs RepositoryScope) String() string {
158	repoType := "repository"
159	// Keep existing format for image class to maintain backwards compatibility
160	// with authorization servers which do not support the expanded grammar.
161	if rs.Class != "" && rs.Class != "image" {
162		repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class)
163	}
164	return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ","))
165}
166
167// RegistryScope represents a token scope for access
168// to resources in the registry.
169type RegistryScope struct {
170	Name    string
171	Actions []string
172}
173
174// String returns the string representation of the user
175// using the scope grammar
176func (rs RegistryScope) String() string {
177	return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ","))
178}
179
180// Logger defines the injectable logging interface, used on TokenHandlers.
181type Logger interface {
182	Debugf(format string, args ...interface{})
183}
184
185func logDebugf(logger Logger, format string, args ...interface{}) {
186	if logger == nil {
187		return
188	}
189	logger.Debugf(format, args...)
190}
191
192// TokenHandlerOptions is used to configure a new token handler
193type TokenHandlerOptions struct {
194	Transport   http.RoundTripper
195	Credentials CredentialStore
196
197	OfflineAccess bool
198	ForceOAuth    bool
199	ClientID      string
200	Scopes        []Scope
201	Logger        Logger
202}
203
204// An implementation of clock for providing real time data.
205type realClock struct{}
206
207// Now implements clock
208func (realClock) Now() time.Time { return time.Now() }
209
210// NewTokenHandler creates a new AuthenicationHandler which supports
211// fetching tokens from a remote token server.
212func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
213	// Create options...
214	return NewTokenHandlerWithOptions(TokenHandlerOptions{
215		Transport:   transport,
216		Credentials: creds,
217		Scopes: []Scope{
218			RepositoryScope{
219				Repository: scope,
220				Actions:    actions,
221			},
222		},
223	})
224}
225
226// NewTokenHandlerWithOptions creates a new token handler using the provided
227// options structure.
228func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
229	handler := &tokenHandler{
230		transport:     options.Transport,
231		creds:         options.Credentials,
232		offlineAccess: options.OfflineAccess,
233		forceOAuth:    options.ForceOAuth,
234		clientID:      options.ClientID,
235		scopes:        options.Scopes,
236		clock:         realClock{},
237		logger:        options.Logger,
238	}
239
240	return handler
241}
242
243func (th *tokenHandler) client() *http.Client {
244	return &http.Client{
245		Transport: th.transport,
246		Timeout:   15 * time.Second,
247	}
248}
249
250func (th *tokenHandler) Scheme() string {
251	return "bearer"
252}
253
254func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
255	var additionalScopes []string
256	if fromParam := req.URL.Query().Get("from"); fromParam != "" {
257		additionalScopes = append(additionalScopes, RepositoryScope{
258			Repository: fromParam,
259			Actions:    []string{"pull"},
260		}.String())
261	}
262
263	token, err := th.getToken(params, additionalScopes...)
264	if err != nil {
265		return err
266	}
267
268	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
269
270	return nil
271}
272
273func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) {
274	th.tokenLock.Lock()
275	defer th.tokenLock.Unlock()
276	scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
277	for _, scope := range th.scopes {
278		scopes = append(scopes, scope.String())
279	}
280	var addedScopes bool
281	for _, scope := range additionalScopes {
282		if hasScope(scopes, scope) {
283			continue
284		}
285		scopes = append(scopes, scope)
286		addedScopes = true
287	}
288
289	now := th.clock.Now()
290	if now.After(th.tokenExpiration) || addedScopes {
291		token, expiration, err := th.fetchToken(params, scopes)
292		if err != nil {
293			return "", err
294		}
295
296		// do not update cache for added scope tokens
297		if !addedScopes {
298			th.tokenCache = token
299			th.tokenExpiration = expiration
300		}
301
302		return token, nil
303	}
304
305	return th.tokenCache, nil
306}
307
308func hasScope(scopes []string, scope string) bool {
309	for _, s := range scopes {
310		if s == scope {
311			return true
312		}
313	}
314	return false
315}
316
317type postTokenResponse struct {
318	AccessToken  string    `json:"access_token"`
319	RefreshToken string    `json:"refresh_token"`
320	ExpiresIn    int       `json:"expires_in"`
321	IssuedAt     time.Time `json:"issued_at"`
322	Scope        string    `json:"scope"`
323}
324
325func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
326	form := url.Values{}
327	form.Set("scope", strings.Join(scopes, " "))
328	form.Set("service", service)
329
330	clientID := th.clientID
331	if clientID == "" {
332		// Use default client, this is a required field
333		clientID = defaultClientID
334	}
335	form.Set("client_id", clientID)
336
337	if refreshToken != "" {
338		form.Set("grant_type", "refresh_token")
339		form.Set("refresh_token", refreshToken)
340	} else if th.creds != nil {
341		form.Set("grant_type", "password")
342		username, password := th.creds.Basic(realm)
343		form.Set("username", username)
344		form.Set("password", password)
345
346		// attempt to get a refresh token
347		form.Set("access_type", "offline")
348	} else {
349		// refuse to do oauth without a grant type
350		return "", time.Time{}, fmt.Errorf("no supported grant type")
351	}
352
353	resp, err := th.client().PostForm(realm.String(), form)
354	if err != nil {
355		return "", time.Time{}, err
356	}
357	defer resp.Body.Close()
358
359	if !client.SuccessStatus(resp.StatusCode) {
360		err := client.HandleErrorResponse(resp)
361		return "", time.Time{}, err
362	}
363
364	decoder := json.NewDecoder(resp.Body)
365
366	var tr postTokenResponse
367	if err = decoder.Decode(&tr); err != nil {
368		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
369	}
370
371	if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
372		th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
373	}
374
375	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
376		// The default/minimum lifetime.
377		tr.ExpiresIn = minimumTokenLifetimeSeconds
378		logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn)
379	}
380
381	if tr.IssuedAt.IsZero() {
382		// issued_at is optional in the token response.
383		tr.IssuedAt = th.clock.Now().UTC()
384	}
385
386	return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
387}
388
389type getTokenResponse struct {
390	Token        string    `json:"token"`
391	AccessToken  string    `json:"access_token"`
392	ExpiresIn    int       `json:"expires_in"`
393	IssuedAt     time.Time `json:"issued_at"`
394	RefreshToken string    `json:"refresh_token"`
395}
396
397func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) {
398
399	req, err := http.NewRequest("GET", realm.String(), nil)
400	if err != nil {
401		return "", time.Time{}, err
402	}
403
404	reqParams := req.URL.Query()
405
406	if service != "" {
407		reqParams.Add("service", service)
408	}
409
410	for _, scope := range scopes {
411		reqParams.Add("scope", scope)
412	}
413
414	if th.offlineAccess {
415		reqParams.Add("offline_token", "true")
416		clientID := th.clientID
417		if clientID == "" {
418			clientID = defaultClientID
419		}
420		reqParams.Add("client_id", clientID)
421	}
422
423	if th.creds != nil {
424		username, password := th.creds.Basic(realm)
425		if username != "" && password != "" {
426			reqParams.Add("account", username)
427			req.SetBasicAuth(username, password)
428		}
429	}
430
431	req.URL.RawQuery = reqParams.Encode()
432
433	resp, err := th.client().Do(req)
434	if err != nil {
435		return "", time.Time{}, err
436	}
437	defer resp.Body.Close()
438
439	if !client.SuccessStatus(resp.StatusCode) {
440		err := client.HandleErrorResponse(resp)
441		return "", time.Time{}, err
442	}
443
444	decoder := json.NewDecoder(resp.Body)
445
446	var tr getTokenResponse
447	if err = decoder.Decode(&tr); err != nil {
448		return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
449	}
450
451	if tr.RefreshToken != "" && th.creds != nil {
452		th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
453	}
454
455	// `access_token` is equivalent to `token` and if both are specified
456	// the choice is undefined.  Canonicalize `access_token` by sticking
457	// things in `token`.
458	if tr.AccessToken != "" {
459		tr.Token = tr.AccessToken
460	}
461
462	if tr.Token == "" {
463		return "", time.Time{}, ErrNoToken
464	}
465
466	if tr.ExpiresIn < minimumTokenLifetimeSeconds {
467		// The default/minimum lifetime.
468		tr.ExpiresIn = minimumTokenLifetimeSeconds
469		logDebugf(th.logger, "Increasing token expiration to: %d seconds", tr.ExpiresIn)
470	}
471
472	if tr.IssuedAt.IsZero() {
473		// issued_at is optional in the token response.
474		tr.IssuedAt = th.clock.Now().UTC()
475	}
476
477	return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
478}
479
480func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
481	realm, ok := params["realm"]
482	if !ok {
483		return "", time.Time{}, errors.New("no realm specified for token auth challenge")
484	}
485
486	// TODO(dmcgowan): Handle empty scheme and relative realm
487	realmURL, err := url.Parse(realm)
488	if err != nil {
489		return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err)
490	}
491
492	service := params["service"]
493
494	var refreshToken string
495
496	if th.creds != nil {
497		refreshToken = th.creds.RefreshToken(realmURL, service)
498	}
499
500	if refreshToken != "" || th.forceOAuth {
501		return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
502	}
503
504	return th.fetchTokenWithBasicAuth(realmURL, service, scopes)
505}
506
507type basicHandler struct {
508	creds CredentialStore
509}
510
511// NewBasicHandler creaters a new authentiation handler which adds
512// basic authentication credentials to a request.
513func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
514	return &basicHandler{
515		creds: creds,
516	}
517}
518
519func (*basicHandler) Scheme() string {
520	return "basic"
521}
522
523func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
524	if bh.creds != nil {
525		username, password := bh.creds.Basic(req.URL)
526		if username != "" && password != "" {
527			req.SetBasicAuth(username, password)
528			return nil
529		}
530	}
531	return ErrNoBasicAuthCredentials
532}
533