Skip to content

Commit 957bcb3

Browse files
committed
docker: split private token helper functions to reusable pkg
Signed-off-by: Tonis Tiigi <[email protected]>
1 parent bd92d56 commit 957bcb3

4 files changed

Lines changed: 292 additions & 228 deletions

File tree

remotes/docker/auth/fetch.go

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
func GenerateTokenOptions(ctx context.Context, host, username, secret string, c Challenge) (TokenOptions, error) {
61+
realm, ok := c.Parameters["realm"]
62+
if !ok {
63+
return TokenOptions{}, errors.New("no realm specified for token auth challenge")
64+
}
65+
66+
realmURL, err := url.Parse(realm)
67+
if err != nil {
68+
return TokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
69+
}
70+
71+
to := TokenOptions{
72+
Realm: realmURL.String(),
73+
Service: c.Parameters["service"],
74+
Username: username,
75+
Secret: secret,
76+
}
77+
78+
scope, ok := c.Parameters["scope"]
79+
if ok {
80+
to.Scopes = append(to.Scopes, scope)
81+
} else {
82+
log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge")
83+
}
84+
85+
return to, nil
86+
}
87+
88+
// TokenOptions are optios for requesting a token
89+
type TokenOptions struct {
90+
Realm string
91+
Service string
92+
Scopes []string
93+
Username string
94+
Secret string
95+
}
96+
97+
type postTokenResponse struct {
98+
AccessToken string `json:"access_token"`
99+
RefreshToken string `json:"refresh_token"`
100+
ExpiresIn int `json:"expires_in"`
101+
IssuedAt time.Time `json:"issued_at"`
102+
Scope string `json:"scope"`
103+
}
104+
105+
func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.Header, clientID string, to TokenOptions) (string, error) {
106+
form := url.Values{}
107+
if len(to.Scopes) > 0 {
108+
form.Set("scope", strings.Join(to.Scopes, " "))
109+
}
110+
form.Set("service", to.Service)
111+
form.Set("client_id", clientID)
112+
113+
if to.Username == "" {
114+
form.Set("grant_type", "refresh_token")
115+
form.Set("refresh_token", to.Secret)
116+
} else {
117+
form.Set("grant_type", "password")
118+
form.Set("username", to.Username)
119+
form.Set("password", to.Secret)
120+
}
121+
122+
req, err := http.NewRequest("POST", to.Realm, strings.NewReader(form.Encode()))
123+
if err != nil {
124+
return "", err
125+
}
126+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
127+
if headers != nil {
128+
for k, v := range headers {
129+
req.Header[k] = append(req.Header[k], v...)
130+
}
131+
}
132+
133+
resp, err := ctxhttp.Do(ctx, client, req)
134+
if err != nil {
135+
return "", err
136+
}
137+
defer resp.Body.Close()
138+
139+
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
140+
return "", errors.WithStack(newUnexpectedStatusErr(resp))
141+
}
142+
143+
decoder := json.NewDecoder(resp.Body)
144+
145+
var tr postTokenResponse
146+
if err = decoder.Decode(&tr); err != nil {
147+
return "", errors.Errorf("unable to decode token response: %s", err)
148+
}
149+
150+
return tr.AccessToken, nil
151+
}
152+
153+
type getTokenResponse struct {
154+
Token string `json:"token"`
155+
AccessToken string `json:"access_token"`
156+
ExpiresIn int `json:"expires_in"`
157+
IssuedAt time.Time `json:"issued_at"`
158+
RefreshToken string `json:"refresh_token"`
159+
}
160+
161+
// FetchToken fetches a token using a GET request
162+
func FetchToken(ctx context.Context, client *http.Client, headers http.Header, to TokenOptions) (string, error) {
163+
req, err := http.NewRequest("GET", to.Realm, nil)
164+
if err != nil {
165+
return "", err
166+
}
167+
168+
if headers != nil {
169+
for k, v := range headers {
170+
req.Header[k] = append(req.Header[k], v...)
171+
}
172+
}
173+
174+
reqParams := req.URL.Query()
175+
176+
if to.Service != "" {
177+
reqParams.Add("service", to.Service)
178+
}
179+
180+
for _, scope := range to.Scopes {
181+
reqParams.Add("scope", scope)
182+
}
183+
184+
if to.Secret != "" {
185+
req.SetBasicAuth(to.Username, to.Secret)
186+
}
187+
188+
req.URL.RawQuery = reqParams.Encode()
189+
190+
resp, err := ctxhttp.Do(ctx, client, req)
191+
if err != nil {
192+
return "", err
193+
}
194+
defer resp.Body.Close()
195+
196+
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
197+
return "", errors.WithStack(newUnexpectedStatusErr(resp))
198+
}
199+
200+
decoder := json.NewDecoder(resp.Body)
201+
202+
var tr getTokenResponse
203+
if err = decoder.Decode(&tr); err != nil {
204+
return "", errors.Errorf("unable to decode token response: %s", err)
205+
}
206+
207+
// `access_token` is equivalent to `token` and if both are specified
208+
// the choice is undefined. Canonicalize `access_token` by sticking
209+
// things in `token`.
210+
if tr.AccessToken != "" {
211+
tr.Token = tr.AccessToken
212+
}
213+
214+
if tr.Token == "" {
215+
return "", ErrNoToken
216+
}
217+
218+
return tr.Token, nil
219+
}
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)