1 package token
2
3 import (
4 "context"
5 "crypto"
6 "crypto/x509"
7 "encoding/pem"
8 "errors"
9 "fmt"
10 "io/ioutil"
11 "net/http"
12 "os"
13 "strings"
14
15 dcontext "github.com/docker/distribution/context"
16 "github.com/docker/distribution/registry/auth"
17 "github.com/docker/libtrust"
18 )
19
20
21
22 type accessSet map[auth.Resource]actionSet
23
24
25
26 func newAccessSet(accessItems ...auth.Access) accessSet {
27 accessSet := make(accessSet, len(accessItems))
28
29 for _, access := range accessItems {
30 resource := auth.Resource{
31 Type: access.Type,
32 Name: access.Name,
33 }
34
35 set, exists := accessSet[resource]
36 if !exists {
37 set = newActionSet()
38 accessSet[resource] = set
39 }
40
41 set.add(access.Action)
42 }
43
44 return accessSet
45 }
46
47
48 func (s accessSet) contains(access auth.Access) bool {
49 actionSet, ok := s[access.Resource]
50 if ok {
51 return actionSet.contains(access.Action)
52 }
53
54 return false
55 }
56
57
58
59
60 func (s accessSet) scopeParam() string {
61 scopes := make([]string, 0, len(s))
62
63 for resource, actionSet := range s {
64 actions := strings.Join(actionSet.keys(), ",")
65 scopes = append(scopes, fmt.Sprintf("%s:%s:%s", resource.Type, resource.Name, actions))
66 }
67
68 return strings.Join(scopes, " ")
69 }
70
71
72 var (
73 ErrInsufficientScope = errors.New("insufficient scope")
74 ErrTokenRequired = errors.New("authorization token required")
75 )
76
77
78 type authChallenge struct {
79 err error
80 realm string
81 autoRedirect bool
82 service string
83 accessSet accessSet
84 }
85
86 var _ auth.Challenge = authChallenge{}
87
88
89 func (ac authChallenge) Error() string {
90 return ac.err.Error()
91 }
92
93
94 func (ac authChallenge) Status() int {
95 return http.StatusUnauthorized
96 }
97
98
99
100
101 func (ac authChallenge) challengeParams(r *http.Request) string {
102 var realm string
103 if ac.autoRedirect {
104 realm = fmt.Sprintf("https://%s/auth/token", r.Host)
105 } else {
106 realm = ac.realm
107 }
108 str := fmt.Sprintf("Bearer realm=%q,service=%q", realm, ac.service)
109
110 if scope := ac.accessSet.scopeParam(); scope != "" {
111 str = fmt.Sprintf("%s,scope=%q", str, scope)
112 }
113
114 if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken {
115 str = fmt.Sprintf("%s,error=%q", str, "invalid_token")
116 } else if ac.err == ErrInsufficientScope {
117 str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
118 }
119
120 return str
121 }
122
123
124 func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
125 w.Header().Add("WWW-Authenticate", ac.challengeParams(r))
126 }
127
128
129 type accessController struct {
130 realm string
131 autoRedirect bool
132 issuer string
133 service string
134 rootCerts *x509.CertPool
135 trustedKeys map[string]libtrust.PublicKey
136 }
137
138
139
140 type tokenAccessOptions struct {
141 realm string
142 autoRedirect bool
143 issuer string
144 service string
145 rootCertBundle string
146 }
147
148
149
150 func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
151 var opts tokenAccessOptions
152
153 keys := []string{"realm", "issuer", "service", "rootcertbundle"}
154 vals := make([]string, 0, len(keys))
155 for _, key := range keys {
156 val, ok := options[key].(string)
157 if !ok {
158 return opts, fmt.Errorf("token auth requires a valid option string: %q", key)
159 }
160 vals = append(vals, val)
161 }
162
163 opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3]
164
165 autoRedirectVal, ok := options["autoredirect"]
166 if ok {
167 autoRedirect, ok := autoRedirectVal.(bool)
168 if !ok {
169 return opts, fmt.Errorf("token auth requires a valid option bool: autoredirect")
170 }
171 opts.autoRedirect = autoRedirect
172 }
173
174 return opts, nil
175 }
176
177
178 func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
179 config, err := checkOptions(options)
180 if err != nil {
181 return nil, err
182 }
183
184 fp, err := os.Open(config.rootCertBundle)
185 if err != nil {
186 return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
187 }
188 defer fp.Close()
189
190 rawCertBundle, err := ioutil.ReadAll(fp)
191 if err != nil {
192 return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err)
193 }
194
195 var rootCerts []*x509.Certificate
196 pemBlock, rawCertBundle := pem.Decode(rawCertBundle)
197 for pemBlock != nil {
198 if pemBlock.Type == "CERTIFICATE" {
199 cert, err := x509.ParseCertificate(pemBlock.Bytes)
200 if err != nil {
201 return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err)
202 }
203
204 rootCerts = append(rootCerts, cert)
205 }
206
207 pemBlock, rawCertBundle = pem.Decode(rawCertBundle)
208 }
209
210 if len(rootCerts) == 0 {
211 return nil, errors.New("token auth requires at least one token signing root certificate")
212 }
213
214 rootPool := x509.NewCertPool()
215 trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts))
216 for _, rootCert := range rootCerts {
217 rootPool.AddCert(rootCert)
218 pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey))
219 if err != nil {
220 return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err)
221 }
222 trustedKeys[pubKey.KeyID()] = pubKey
223 }
224
225 return &accessController{
226 realm: config.realm,
227 autoRedirect: config.autoRedirect,
228 issuer: config.issuer,
229 service: config.service,
230 rootCerts: rootPool,
231 trustedKeys: trustedKeys,
232 }, nil
233 }
234
235
236
237 func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) {
238 challenge := &authChallenge{
239 realm: ac.realm,
240 autoRedirect: ac.autoRedirect,
241 service: ac.service,
242 accessSet: newAccessSet(accessItems...),
243 }
244
245 req, err := dcontext.GetRequest(ctx)
246 if err != nil {
247 return nil, err
248 }
249
250 parts := strings.Split(req.Header.Get("Authorization"), " ")
251
252 if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
253 challenge.err = ErrTokenRequired
254 return nil, challenge
255 }
256
257 rawToken := parts[1]
258
259 token, err := NewToken(rawToken)
260 if err != nil {
261 challenge.err = err
262 return nil, challenge
263 }
264
265 verifyOpts := VerifyOptions{
266 TrustedIssuers: []string{ac.issuer},
267 AcceptedAudiences: []string{ac.service},
268 Roots: ac.rootCerts,
269 TrustedKeys: ac.trustedKeys,
270 }
271
272 if err = token.Verify(verifyOpts); err != nil {
273 challenge.err = err
274 return nil, challenge
275 }
276
277 accessSet := token.accessSet()
278 for _, access := range accessItems {
279 if !accessSet.contains(access) {
280 challenge.err = ErrInsufficientScope
281 return nil, challenge
282 }
283 }
284
285 ctx = auth.WithResources(ctx, token.resources())
286
287 return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil
288 }
289
290
291 func init() {
292 auth.Register("token", auth.InitFunc(newAccessController))
293 }
294
View as plain text