github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/client/auth/session.go (about) 1 package auth 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "net/http" 8 "net/url" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/Sirupsen/logrus" 14 "github.com/docker/distribution/registry/client" 15 "github.com/docker/distribution/registry/client/transport" 16 ) 17 18 // AuthenticationHandler is an interface for authorizing a request from 19 // params from a "WWW-Authenicate" header for a single scheme. 20 type AuthenticationHandler interface { 21 // Scheme returns the scheme as expected from the "WWW-Authenicate" header. 22 Scheme() string 23 24 // AuthorizeRequest adds the authorization header to a request (if needed) 25 // using the parameters from "WWW-Authenticate" method. The parameters 26 // values depend on the scheme. 27 AuthorizeRequest(req *http.Request, params map[string]string) error 28 } 29 30 // CredentialStore is an interface for getting credentials for 31 // a given URL 32 type CredentialStore interface { 33 // Basic returns basic auth for the given URL 34 Basic(*url.URL) (string, string) 35 } 36 37 // NewAuthorizer creates an authorizer which can handle multiple authentication 38 // schemes. The handlers are tried in order, the higher priority authentication 39 // methods should be first. The challengeMap holds a list of challenges for 40 // a given root API endpoint (for example "https://registry-1.docker.io/v2/"). 41 func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler) transport.RequestModifier { 42 return &endpointAuthorizer{ 43 challenges: manager, 44 handlers: handlers, 45 } 46 } 47 48 type endpointAuthorizer struct { 49 challenges ChallengeManager 50 handlers []AuthenticationHandler 51 transport http.RoundTripper 52 } 53 54 func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error { 55 v2Root := strings.Index(req.URL.Path, "/v2/") 56 if v2Root == -1 { 57 return nil 58 } 59 60 ping := url.URL{ 61 Host: req.URL.Host, 62 Scheme: req.URL.Scheme, 63 Path: req.URL.Path[:v2Root+4], 64 } 65 66 pingEndpoint := ping.String() 67 68 challenges, err := ea.challenges.GetChallenges(pingEndpoint) 69 if err != nil { 70 return err 71 } 72 73 if len(challenges) > 0 { 74 for _, handler := range ea.handlers { 75 for _, challenge := range challenges { 76 if challenge.Scheme != handler.Scheme() { 77 continue 78 } 79 if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil { 80 return err 81 } 82 } 83 } 84 } 85 86 return nil 87 } 88 89 // This is the minimum duration a token can last (in seconds). 90 // A token must not live less than 60 seconds because older versions 91 // of the Docker client didn't read their expiration from the token 92 // response and assumed 60 seconds. So to remain compatible with 93 // those implementations, a token must live at least this long. 94 const minimumTokenLifetimeSeconds = 60 95 96 // Private interface for time used by this package to enable tests to provide their own implementation. 97 type clock interface { 98 Now() time.Time 99 } 100 101 type tokenHandler struct { 102 header http.Header 103 creds CredentialStore 104 scope tokenScope 105 transport http.RoundTripper 106 clock clock 107 108 tokenLock sync.Mutex 109 tokenCache string 110 tokenExpiration time.Time 111 112 additionalScopes map[string]struct{} 113 } 114 115 // tokenScope represents the scope at which a token will be requested. 116 // This represents a specific action on a registry resource. 117 type tokenScope struct { 118 Resource string 119 Scope string 120 Actions []string 121 } 122 123 func (ts tokenScope) String() string { 124 return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) 125 } 126 127 // An implementation of clock for providing real time data. 128 type realClock struct{} 129 130 // Now implements clock 131 func (realClock) Now() time.Time { return time.Now() } 132 133 // NewTokenHandler creates a new AuthenicationHandler which supports 134 // fetching tokens from a remote token server. 135 func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { 136 return newTokenHandler(transport, creds, realClock{}, scope, actions...) 137 } 138 139 // newTokenHandler exposes the option to provide a clock to manipulate time in unit testing. 140 func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler { 141 return &tokenHandler{ 142 transport: transport, 143 creds: creds, 144 clock: c, 145 scope: tokenScope{ 146 Resource: "repository", 147 Scope: scope, 148 Actions: actions, 149 }, 150 additionalScopes: map[string]struct{}{}, 151 } 152 } 153 154 func (th *tokenHandler) client() *http.Client { 155 return &http.Client{ 156 Transport: th.transport, 157 Timeout: 15 * time.Second, 158 } 159 } 160 161 func (th *tokenHandler) Scheme() string { 162 return "bearer" 163 } 164 165 func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 166 var additionalScopes []string 167 if fromParam := req.URL.Query().Get("from"); fromParam != "" { 168 additionalScopes = append(additionalScopes, tokenScope{ 169 Resource: "repository", 170 Scope: fromParam, 171 Actions: []string{"pull"}, 172 }.String()) 173 } 174 if err := th.refreshToken(params, additionalScopes...); err != nil { 175 return err 176 } 177 178 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache)) 179 180 return nil 181 } 182 183 func (th *tokenHandler) refreshToken(params map[string]string, additionalScopes ...string) error { 184 th.tokenLock.Lock() 185 defer th.tokenLock.Unlock() 186 var addedScopes bool 187 for _, scope := range additionalScopes { 188 if _, ok := th.additionalScopes[scope]; !ok { 189 th.additionalScopes[scope] = struct{}{} 190 addedScopes = true 191 } 192 } 193 now := th.clock.Now() 194 if now.After(th.tokenExpiration) || addedScopes { 195 tr, err := th.fetchToken(params) 196 if err != nil { 197 return err 198 } 199 th.tokenCache = tr.Token 200 th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second) 201 } 202 203 return nil 204 } 205 206 type tokenResponse struct { 207 Token string `json:"token"` 208 AccessToken string `json:"access_token"` 209 ExpiresIn int `json:"expires_in"` 210 IssuedAt time.Time `json:"issued_at"` 211 } 212 213 func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) { 214 //log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username) 215 realm, ok := params["realm"] 216 if !ok { 217 return nil, errors.New("no realm specified for token auth challenge") 218 } 219 220 // TODO(dmcgowan): Handle empty scheme 221 222 realmURL, err := url.Parse(realm) 223 if err != nil { 224 return nil, fmt.Errorf("invalid token auth challenge realm: %s", err) 225 } 226 227 req, err := http.NewRequest("GET", realmURL.String(), nil) 228 if err != nil { 229 return nil, err 230 } 231 232 reqParams := req.URL.Query() 233 service := params["service"] 234 scope := th.scope.String() 235 236 if service != "" { 237 reqParams.Add("service", service) 238 } 239 240 for _, scopeField := range strings.Fields(scope) { 241 reqParams.Add("scope", scopeField) 242 } 243 244 for scope := range th.additionalScopes { 245 reqParams.Add("scope", scope) 246 } 247 248 if th.creds != nil { 249 username, password := th.creds.Basic(realmURL) 250 if username != "" && password != "" { 251 reqParams.Add("account", username) 252 req.SetBasicAuth(username, password) 253 } 254 } 255 256 req.URL.RawQuery = reqParams.Encode() 257 258 resp, err := th.client().Do(req) 259 if err != nil { 260 return nil, err 261 } 262 defer resp.Body.Close() 263 264 if !client.SuccessStatus(resp.StatusCode) { 265 err := client.HandleErrorResponse(resp) 266 return nil, err 267 } 268 269 decoder := json.NewDecoder(resp.Body) 270 271 tr := new(tokenResponse) 272 if err = decoder.Decode(tr); err != nil { 273 return nil, fmt.Errorf("unable to decode token response: %s", err) 274 } 275 276 // `access_token` is equivalent to `token` and if both are specified 277 // the choice is undefined. Canonicalize `access_token` by sticking 278 // things in `token`. 279 if tr.AccessToken != "" { 280 tr.Token = tr.AccessToken 281 } 282 283 if tr.Token == "" { 284 return nil, errors.New("authorization server did not include a token in the response") 285 } 286 287 if tr.ExpiresIn < minimumTokenLifetimeSeconds { 288 logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn) 289 // The default/minimum lifetime. 290 tr.ExpiresIn = minimumTokenLifetimeSeconds 291 } 292 293 if tr.IssuedAt.IsZero() { 294 // issued_at is optional in the token response. 295 tr.IssuedAt = th.clock.Now() 296 } 297 298 return tr, nil 299 } 300 301 type basicHandler struct { 302 creds CredentialStore 303 } 304 305 // NewBasicHandler creaters a new authentiation handler which adds 306 // basic authentication credentials to a request. 307 func NewBasicHandler(creds CredentialStore) AuthenticationHandler { 308 return &basicHandler{ 309 creds: creds, 310 } 311 } 312 313 func (*basicHandler) Scheme() string { 314 return "basic" 315 } 316 317 func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 318 if bh.creds != nil { 319 username, password := bh.creds.Basic(req.URL) 320 if username != "" && password != "" { 321 req.SetBasicAuth(username, password) 322 return nil 323 } 324 } 325 return errors.New("no basic auth credentials") 326 }