Skip to content

Commit a7b2304

Browse files
authored
Merge pull request #4445 from tonistiigi/auth-refactor
docker: split private token helper functions to reusable pkg
2 parents bacf07f + b5185ea commit a7b2304

4 files changed

Lines changed: 317 additions & 239 deletions

File tree

remotes/docker/auth/fetch.go

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package auth
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"fmt"
23+
"io"
24+
"io/ioutil"
25+
"net/http"
26+
"net/url"
27+
"strings"
28+
"time"
29+
30+
"github.com/containerd/containerd/log"
31+
"github.com/pkg/errors"
32+
"golang.org/x/net/context/ctxhttp"
33+
)
34+
35+
var (
36+
// ErrNoToken is returned if a request is successful but the body does not
37+
// contain an authorization token.
38+
ErrNoToken = errors.New("authorization server did not include a token in the response")
39+
)
40+
41+
// ErrUnexpectedStatus is returned if a token request returned with unexpected HTTP status
42+
type ErrUnexpectedStatus struct {
43+
Status string
44+
StatusCode int
45+
Body []byte
46+
}
47+
48+
func (e ErrUnexpectedStatus) Error() string {
49+
return fmt.Sprintf("unexpected status: %s", e.Status)
50+
}
51+
52+
func newUnexpectedStatusErr(resp *http.Response) error {
53+
var b []byte
54+
if resp.Body != nil {
55+
b, _ = ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
56+
}
57+
return ErrUnexpectedStatus{Status: resp.Status, StatusCode: resp.StatusCode, Body: b}
58+
}
59+
60+
// GenerateTokenOptions generates options for fetching a token based on a challenge
61+
func GenerateTokenOptions(ctx context.Context, host, username, secret string, c Challenge) (TokenOptions, error) {
62+
realm, ok := c.Parameters["realm"]
63+
if !ok {
64+
return TokenOptions{}, errors.New("no realm specified for token auth challenge")
65+
}
66+
67+
realmURL, err := url.Parse(realm)
68+
if err != nil {
69+
return TokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
70+
}
71+
72+
to := TokenOptions{
73+
Realm: realmURL.String(),
74+
Service: c.Parameters["service"],
75+
Username: username,
76+
Secret: secret,
77+
}
78+
79+
scope, ok := c.Parameters["scope"]
80+
if ok {
81+
to.Scopes = append(to.Scopes, scope)
82+
} else {
83+
log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge")
84+
}
85+
86+
return to, nil
87+
}
88+
89+
// TokenOptions are optios for requesting a token
90+
type TokenOptions struct {
91+
Realm string
92+
Service string
93+
Scopes []string
94+
Username string
95+
Secret string
96+
}
97+
98+
// OAuthTokenResponse is response from fetching token with a OAuth POST request
99+
type OAuthTokenResponse struct {
100+
AccessToken string `json:"access_token"`
101+
RefreshToken string `json:"refresh_token"`
102+
ExpiresIn int `json:"expires_in"`
103+
IssuedAt time.Time `json:"issued_at"`
104+
Scope string `json:"scope"`
105+
}
106+
107+
// FetchTokenWithOAuth fetches a token using a POST request
108+
func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.Header, clientID string, to TokenOptions) (*OAuthTokenResponse, error) {
109+
form := url.Values{}
110+
if len(to.Scopes) > 0 {
111+
form.Set("scope", strings.Join(to.Scopes, " "))
112+
}
113+
form.Set("service", to.Service)
114+
form.Set("client_id", clientID)
115+
116+
if to.Username == "" {
117+
form.Set("grant_type", "refresh_token")
118+
form.Set("refresh_token", to.Secret)
119+
} else {
120+
form.Set("grant_type", "password")
121+
form.Set("username", to.Username)
122+
form.Set("password", to.Secret)
123+
}
124+
125+
req, err := http.NewRequest("POST", to.Realm, strings.NewReader(form.Encode()))
126+
if err != nil {
127+
return nil, err
128+
}
129+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
130+
if headers != nil {
131+
for k, v := range headers {
132+
req.Header[k] = append(req.Header[k], v...)
133+
}
134+
}
135+
136+
resp, err := ctxhttp.Do(ctx, client, req)
137+
if err != nil {
138+
return nil, err
139+
}
140+
defer resp.Body.Close()
141+
142+
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
143+
return nil, errors.WithStack(newUnexpectedStatusErr(resp))
144+
}
145+
146+
decoder := json.NewDecoder(resp.Body)
147+
148+
var tr OAuthTokenResponse
149+
if err = decoder.Decode(&tr); err != nil {
150+
return nil, errors.Wrap(err, "unable to decode token response")
151+
}
152+
153+
if tr.AccessToken == "" {
154+
return nil, errors.WithStack(ErrNoToken)
155+
}
156+
157+
return &tr, nil
158+
}
159+
160+
// FetchTokenResponse is response from fetching token with GET request
161+
type FetchTokenResponse struct {
162+
Token string `json:"token"`
163+
AccessToken string `json:"access_token"`
164+
ExpiresIn int `json:"expires_in"`
165+
IssuedAt time.Time `json:"issued_at"`
166+
RefreshToken string `json:"refresh_token"`
167+
}
168+
169+
// FetchToken fetches a token using a GET request
170+
func FetchToken(ctx context.Context, client *http.Client, headers http.Header, to TokenOptions) (*FetchTokenResponse, error) {
171+
req, err := http.NewRequest("GET", to.Realm, nil)
172+
if err != nil {
173+
return nil, err
174+
}
175+
176+
if headers != nil {
177+
for k, v := range headers {
178+
req.Header[k] = append(req.Header[k], v...)
179+
}
180+
}
181+
182+
reqParams := req.URL.Query()
183+
184+
if to.Service != "" {
185+
reqParams.Add("service", to.Service)
186+
}
187+
188+
for _, scope := range to.Scopes {
189+
reqParams.Add("scope", scope)
190+
}
191+
192+
if to.Secret != "" {
193+
req.SetBasicAuth(to.Username, to.Secret)
194+
}
195+
196+
req.URL.RawQuery = reqParams.Encode()
197+
198+
resp, err := ctxhttp.Do(ctx, client, req)
199+
if err != nil {
200+
return nil, err
201+
}
202+
defer resp.Body.Close()
203+
204+
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
205+
return nil, errors.WithStack(newUnexpectedStatusErr(resp))
206+
}
207+
208+
decoder := json.NewDecoder(resp.Body)
209+
210+
var tr FetchTokenResponse
211+
if err = decoder.Decode(&tr); err != nil {
212+
return nil, errors.Wrap(err, "unable to decode token response")
213+
}
214+
215+
// `access_token` is equivalent to `token` and if both are specified
216+
// the choice is undefined. Canonicalize `access_token` by sticking
217+
// things in `token`.
218+
if tr.AccessToken != "" {
219+
tr.Token = tr.AccessToken
220+
}
221+
222+
if tr.Token == "" {
223+
return nil, errors.WithStack(ErrNoToken)
224+
}
225+
226+
return &tr, nil
227+
}
Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,39 +14,43 @@
1414
limitations under the License.
1515
*/
1616

17-
package docker
17+
package auth
1818

1919
import (
2020
"net/http"
2121
"sort"
2222
"strings"
2323
)
2424

25-
type authenticationScheme byte
25+
// AuthenticationScheme defines scheme of the authentication method
26+
type AuthenticationScheme byte
2627

2728
const (
28-
basicAuth authenticationScheme = 1 << iota // Defined in RFC 7617
29-
digestAuth // Defined in RFC 7616
30-
bearerAuth // Defined in RFC 6750
29+
// BasicAuth is scheme for Basic HTTP Authentication RFC 7617
30+
BasicAuth AuthenticationScheme = 1 << iota
31+
// DigestAuth is scheme for HTTP Digest Access Authentication RFC 7616
32+
DigestAuth
33+
// BearerAuth is scheme for OAuth 2.0 Bearer Tokens RFC 6750
34+
BearerAuth
3135
)
3236

33-
// challenge carries information from a WWW-Authenticate response header.
37+
// Challenge carries information from a WWW-Authenticate response header.
3438
// See RFC 2617.
35-
type challenge struct {
39+
type Challenge struct {
3640
// scheme is the auth-scheme according to RFC 2617
37-
scheme authenticationScheme
41+
Scheme AuthenticationScheme
3842

3943
// parameters are the auth-params according to RFC 2617
40-
parameters map[string]string
44+
Parameters map[string]string
4145
}
4246

43-
type byScheme []challenge
47+
type byScheme []Challenge
4448

4549
func (bs byScheme) Len() int { return len(bs) }
4650
func (bs byScheme) Swap(i, j int) { bs[i], bs[j] = bs[j], bs[i] }
4751

4852
// Sort in priority order: token > digest > basic
49-
func (bs byScheme) Less(i, j int) bool { return bs[i].scheme > bs[j].scheme }
53+
func (bs byScheme) Less(i, j int) bool { return bs[i].Scheme > bs[j].Scheme }
5054

5155
// Octet types from RFC 2616.
5256
type octetType byte
@@ -90,22 +94,23 @@ func init() {
9094
}
9195
}
9296

93-
func parseAuthHeader(header http.Header) []challenge {
94-
challenges := []challenge{}
97+
// ParseAuthHeader parses challenges from WWW-Authenticate header
98+
func ParseAuthHeader(header http.Header) []Challenge {
99+
challenges := []Challenge{}
95100
for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] {
96101
v, p := parseValueAndParams(h)
97-
var s authenticationScheme
102+
var s AuthenticationScheme
98103
switch v {
99104
case "basic":
100-
s = basicAuth
105+
s = BasicAuth
101106
case "digest":
102-
s = digestAuth
107+
s = DigestAuth
103108
case "bearer":
104-
s = bearerAuth
109+
s = BearerAuth
105110
default:
106111
continue
107112
}
108-
challenges = append(challenges, challenge{scheme: s, parameters: p})
113+
challenges = append(challenges, Challenge{Scheme: s, Parameters: p})
109114
}
110115
sort.Stable(byScheme(challenges))
111116
return challenges

0 commit comments

Comments
 (0)