github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+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 113 // tokenScope represents the scope at which a token will be requested. 114 // This represents a specific action on a registry resource. 115 type tokenScope struct { 116 Resource string 117 Scope string 118 Actions []string 119 } 120 121 func (ts tokenScope) String() string { 122 return fmt.Sprintf("%s:%s:%s", ts.Resource, ts.Scope, strings.Join(ts.Actions, ",")) 123 } 124 125 // An implementation of clock for providing real time data. 126 type realClock struct{} 127 128 // Now implements clock 129 func (realClock) Now() time.Time { return time.Now() } 130 131 // NewTokenHandler creates a new AuthenicationHandler which supports 132 // fetching tokens from a remote token server. 133 func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { 134 return newTokenHandler(transport, creds, realClock{}, scope, actions...) 135 } 136 137 // newTokenHandler exposes the option to provide a clock to manipulate time in unit testing. 138 func newTokenHandler(transport http.RoundTripper, creds CredentialStore, c clock, scope string, actions ...string) AuthenticationHandler { 139 return &tokenHandler{ 140 transport: transport, 141 creds: creds, 142 clock: c, 143 scope: tokenScope{ 144 Resource: "repository", 145 Scope: scope, 146 Actions: actions, 147 }, 148 } 149 } 150 151 func (th *tokenHandler) client() *http.Client { 152 return &http.Client{ 153 Transport: th.transport, 154 Timeout: 15 * time.Second, 155 } 156 } 157 158 func (th *tokenHandler) Scheme() string { 159 return "bearer" 160 } 161 162 func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 163 if err := th.refreshToken(params); err != nil { 164 return err 165 } 166 167 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", th.tokenCache)) 168 169 return nil 170 } 171 172 func (th *tokenHandler) refreshToken(params map[string]string) error { 173 th.tokenLock.Lock() 174 defer th.tokenLock.Unlock() 175 now := th.clock.Now() 176 if now.After(th.tokenExpiration) { 177 tr, err := th.fetchToken(params) 178 if err != nil { 179 return err 180 } 181 th.tokenCache = tr.Token 182 th.tokenExpiration = tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second) 183 } 184 185 return nil 186 } 187 188 type tokenResponse struct { 189 Token string `json:"token"` 190 AccessToken string `json:"access_token"` 191 ExpiresIn int `json:"expires_in"` 192 IssuedAt time.Time `json:"issued_at"` 193 } 194 195 func (th *tokenHandler) fetchToken(params map[string]string) (token *tokenResponse, err error) { 196 //log.Debugf("Getting bearer token with %s for %s", challenge.Parameters, ta.auth.Username) 197 realm, ok := params["realm"] 198 if !ok { 199 return nil, errors.New("no realm specified for token auth challenge") 200 } 201 202 // TODO(dmcgowan): Handle empty scheme 203 204 realmURL, err := url.Parse(realm) 205 if err != nil { 206 return nil, fmt.Errorf("invalid token auth challenge realm: %s", err) 207 } 208 209 req, err := http.NewRequest("GET", realmURL.String(), nil) 210 if err != nil { 211 return nil, err 212 } 213 214 reqParams := req.URL.Query() 215 service := params["service"] 216 scope := th.scope.String() 217 218 if service != "" { 219 reqParams.Add("service", service) 220 } 221 222 for _, scopeField := range strings.Fields(scope) { 223 reqParams.Add("scope", scopeField) 224 } 225 226 if th.creds != nil { 227 username, password := th.creds.Basic(realmURL) 228 if username != "" && password != "" { 229 reqParams.Add("account", username) 230 req.SetBasicAuth(username, password) 231 } 232 } 233 234 req.URL.RawQuery = reqParams.Encode() 235 236 resp, err := th.client().Do(req) 237 if err != nil { 238 return nil, err 239 } 240 defer resp.Body.Close() 241 242 if !client.SuccessStatus(resp.StatusCode) { 243 err := client.HandleErrorResponse(resp) 244 return nil, err 245 } 246 247 decoder := json.NewDecoder(resp.Body) 248 249 tr := new(tokenResponse) 250 if err = decoder.Decode(tr); err != nil { 251 return nil, fmt.Errorf("unable to decode token response: %s", err) 252 } 253 254 // `access_token` is equivalent to `token` and if both are specified 255 // the choice is undefined. Canonicalize `access_token` by sticking 256 // things in `token`. 257 if tr.AccessToken != "" { 258 tr.Token = tr.AccessToken 259 } 260 261 if tr.Token == "" { 262 return nil, errors.New("authorization server did not include a token in the response") 263 } 264 265 if tr.ExpiresIn < minimumTokenLifetimeSeconds { 266 logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn) 267 // The default/minimum lifetime. 268 tr.ExpiresIn = minimumTokenLifetimeSeconds 269 } 270 271 if tr.IssuedAt.IsZero() { 272 // issued_at is optional in the token response. 273 tr.IssuedAt = th.clock.Now() 274 } 275 276 return tr, nil 277 } 278 279 type basicHandler struct { 280 creds CredentialStore 281 } 282 283 // NewBasicHandler creaters a new authentiation handler which adds 284 // basic authentication credentials to a request. 285 func NewBasicHandler(creds CredentialStore) AuthenticationHandler { 286 return &basicHandler{ 287 creds: creds, 288 } 289 } 290 291 func (*basicHandler) Scheme() string { 292 return "basic" 293 } 294 295 func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { 296 if bh.creds != nil { 297 username, password := bh.creds.Basic(req.URL) 298 if username != "" && password != "" { 299 req.SetBasicAuth(username, password) 300 return nil 301 } 302 } 303 return errors.New("no basic auth credentials") 304 }