session.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. package auth
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/Sirupsen/logrus"
  12. "github.com/docker/distribution/registry/client"
  13. "github.com/docker/distribution/registry/client/transport"
  14. )
  15. var (
  16. // ErrNoBasicAuthCredentials is returned if a request can't be authorized with
  17. // basic auth due to lack of credentials.
  18. ErrNoBasicAuthCredentials = errors.New("no basic auth credentials")
  19. // ErrNoToken is returned if a request is successful but the body does not
  20. // contain an authorization token.
  21. ErrNoToken = errors.New("authorization server did not include a token in the response")
  22. )
  23. const defaultClientID = "registry-client"
  24. // AuthenticationHandler is an interface for authorizing a request from
  25. // params from a "WWW-Authenicate" header for a single scheme.
  26. type AuthenticationHandler interface {
  27. // Scheme returns the scheme as expected from the "WWW-Authenicate" header.
  28. Scheme() string
  29. // AuthorizeRequest adds the authorization header to a request (if needed)
  30. // using the parameters from "WWW-Authenticate" method. The parameters
  31. // values depend on the scheme.
  32. AuthorizeRequest(req *http.Request, params map[string]string) error
  33. }
  34. // CredentialStore is an interface for getting credentials for
  35. // a given URL
  36. type CredentialStore interface {
  37. // Basic returns basic auth for the given URL
  38. Basic(*url.URL) (string, string)
  39. // RefreshToken returns a refresh token for the
  40. // given URL and service
  41. RefreshToken(*url.URL, string) string
  42. // SetRefreshToken sets the refresh token if none
  43. // is provided for the given url and service
  44. SetRefreshToken(realm *url.URL, service, token string)
  45. }
  46. // NewAuthorizer creates an authorizer which can handle multiple authentication
  47. // schemes. The handlers are tried in order, the higher priority authentication
  48. // methods should be first. The challengeMap holds a list of challenges for
  49. // a given root API endpoint (for example "https://registry-1.docker.io/v2/").
  50. func NewAuthorizer(manager ChallengeManager, handlers ...AuthenticationHandler) transport.RequestModifier {
  51. return &endpointAuthorizer{
  52. challenges: manager,
  53. handlers: handlers,
  54. }
  55. }
  56. type endpointAuthorizer struct {
  57. challenges ChallengeManager
  58. handlers []AuthenticationHandler
  59. transport http.RoundTripper
  60. }
  61. func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error {
  62. v2Root := strings.Index(req.URL.Path, "/v2/")
  63. if v2Root == -1 {
  64. return nil
  65. }
  66. ping := url.URL{
  67. Host: req.URL.Host,
  68. Scheme: req.URL.Scheme,
  69. Path: req.URL.Path[:v2Root+4],
  70. }
  71. challenges, err := ea.challenges.GetChallenges(ping)
  72. if err != nil {
  73. return err
  74. }
  75. if len(challenges) > 0 {
  76. for _, handler := range ea.handlers {
  77. for _, challenge := range challenges {
  78. if challenge.Scheme != handler.Scheme() {
  79. continue
  80. }
  81. if err := handler.AuthorizeRequest(req, challenge.Parameters); err != nil {
  82. return err
  83. }
  84. }
  85. }
  86. }
  87. return nil
  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. // Private interface for time used by this package to enable tests to provide their own implementation.
  96. type clock interface {
  97. Now() time.Time
  98. }
  99. type tokenHandler struct {
  100. header http.Header
  101. creds CredentialStore
  102. transport http.RoundTripper
  103. clock clock
  104. offlineAccess bool
  105. forceOAuth bool
  106. clientID string
  107. scopes []Scope
  108. tokenLock sync.Mutex
  109. tokenCache string
  110. tokenExpiration time.Time
  111. }
  112. // Scope is a type which is serializable to a string
  113. // using the allow scope grammar.
  114. type Scope interface {
  115. String() string
  116. }
  117. // RepositoryScope represents a token scope for access
  118. // to a repository.
  119. type RepositoryScope struct {
  120. Repository string
  121. Actions []string
  122. }
  123. // String returns the string representation of the repository
  124. // using the scope grammar
  125. func (rs RepositoryScope) String() string {
  126. return fmt.Sprintf("repository:%s:%s", rs.Repository, strings.Join(rs.Actions, ","))
  127. }
  128. // TokenHandlerOptions is used to configure a new token handler
  129. type TokenHandlerOptions struct {
  130. Transport http.RoundTripper
  131. Credentials CredentialStore
  132. OfflineAccess bool
  133. ForceOAuth bool
  134. ClientID string
  135. Scopes []Scope
  136. }
  137. // An implementation of clock for providing real time data.
  138. type realClock struct{}
  139. // Now implements clock
  140. func (realClock) Now() time.Time { return time.Now() }
  141. // NewTokenHandler creates a new AuthenicationHandler which supports
  142. // fetching tokens from a remote token server.
  143. func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler {
  144. // Create options...
  145. return NewTokenHandlerWithOptions(TokenHandlerOptions{
  146. Transport: transport,
  147. Credentials: creds,
  148. Scopes: []Scope{
  149. RepositoryScope{
  150. Repository: scope,
  151. Actions: actions,
  152. },
  153. },
  154. })
  155. }
  156. // NewTokenHandlerWithOptions creates a new token handler using the provided
  157. // options structure.
  158. func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler {
  159. handler := &tokenHandler{
  160. transport: options.Transport,
  161. creds: options.Credentials,
  162. offlineAccess: options.OfflineAccess,
  163. forceOAuth: options.ForceOAuth,
  164. clientID: options.ClientID,
  165. scopes: options.Scopes,
  166. clock: realClock{},
  167. }
  168. return handler
  169. }
  170. func (th *tokenHandler) client() *http.Client {
  171. return &http.Client{
  172. Transport: th.transport,
  173. Timeout: 15 * time.Second,
  174. }
  175. }
  176. func (th *tokenHandler) Scheme() string {
  177. return "bearer"
  178. }
  179. func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
  180. var additionalScopes []string
  181. if fromParam := req.URL.Query().Get("from"); fromParam != "" {
  182. additionalScopes = append(additionalScopes, RepositoryScope{
  183. Repository: fromParam,
  184. Actions: []string{"pull"},
  185. }.String())
  186. }
  187. token, err := th.getToken(params, additionalScopes...)
  188. if err != nil {
  189. return err
  190. }
  191. req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
  192. return nil
  193. }
  194. func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) {
  195. th.tokenLock.Lock()
  196. defer th.tokenLock.Unlock()
  197. scopes := make([]string, 0, len(th.scopes)+len(additionalScopes))
  198. for _, scope := range th.scopes {
  199. scopes = append(scopes, scope.String())
  200. }
  201. var addedScopes bool
  202. for _, scope := range additionalScopes {
  203. scopes = append(scopes, scope)
  204. addedScopes = true
  205. }
  206. now := th.clock.Now()
  207. if now.After(th.tokenExpiration) || addedScopes {
  208. token, expiration, err := th.fetchToken(params, scopes)
  209. if err != nil {
  210. return "", err
  211. }
  212. // do not update cache for added scope tokens
  213. if !addedScopes {
  214. th.tokenCache = token
  215. th.tokenExpiration = expiration
  216. }
  217. return token, nil
  218. }
  219. return th.tokenCache, nil
  220. }
  221. type postTokenResponse struct {
  222. AccessToken string `json:"access_token"`
  223. RefreshToken string `json:"refresh_token"`
  224. ExpiresIn int `json:"expires_in"`
  225. IssuedAt time.Time `json:"issued_at"`
  226. Scope string `json:"scope"`
  227. }
  228. func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) {
  229. form := url.Values{}
  230. form.Set("scope", strings.Join(scopes, " "))
  231. form.Set("service", service)
  232. clientID := th.clientID
  233. if clientID == "" {
  234. // Use default client, this is a required field
  235. clientID = defaultClientID
  236. }
  237. form.Set("client_id", clientID)
  238. if refreshToken != "" {
  239. form.Set("grant_type", "refresh_token")
  240. form.Set("refresh_token", refreshToken)
  241. } else if th.creds != nil {
  242. form.Set("grant_type", "password")
  243. username, password := th.creds.Basic(realm)
  244. form.Set("username", username)
  245. form.Set("password", password)
  246. // attempt to get a refresh token
  247. form.Set("access_type", "offline")
  248. } else {
  249. // refuse to do oauth without a grant type
  250. return "", time.Time{}, fmt.Errorf("no supported grant type")
  251. }
  252. resp, err := th.client().PostForm(realm.String(), form)
  253. if err != nil {
  254. return "", time.Time{}, err
  255. }
  256. defer resp.Body.Close()
  257. if !client.SuccessStatus(resp.StatusCode) {
  258. err := client.HandleErrorResponse(resp)
  259. return "", time.Time{}, err
  260. }
  261. decoder := json.NewDecoder(resp.Body)
  262. var tr postTokenResponse
  263. if err = decoder.Decode(&tr); err != nil {
  264. return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
  265. }
  266. if tr.RefreshToken != "" && tr.RefreshToken != refreshToken {
  267. th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
  268. }
  269. if tr.ExpiresIn < minimumTokenLifetimeSeconds {
  270. // The default/minimum lifetime.
  271. tr.ExpiresIn = minimumTokenLifetimeSeconds
  272. logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
  273. }
  274. if tr.IssuedAt.IsZero() {
  275. // issued_at is optional in the token response.
  276. tr.IssuedAt = th.clock.Now().UTC()
  277. }
  278. return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
  279. }
  280. type getTokenResponse struct {
  281. Token string `json:"token"`
  282. AccessToken string `json:"access_token"`
  283. ExpiresIn int `json:"expires_in"`
  284. IssuedAt time.Time `json:"issued_at"`
  285. RefreshToken string `json:"refresh_token"`
  286. }
  287. func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) {
  288. req, err := http.NewRequest("GET", realm.String(), nil)
  289. if err != nil {
  290. return "", time.Time{}, err
  291. }
  292. reqParams := req.URL.Query()
  293. if service != "" {
  294. reqParams.Add("service", service)
  295. }
  296. for _, scope := range scopes {
  297. reqParams.Add("scope", scope)
  298. }
  299. if th.offlineAccess {
  300. reqParams.Add("offline_token", "true")
  301. clientID := th.clientID
  302. if clientID == "" {
  303. clientID = defaultClientID
  304. }
  305. reqParams.Add("client_id", clientID)
  306. }
  307. if th.creds != nil {
  308. username, password := th.creds.Basic(realm)
  309. if username != "" && password != "" {
  310. reqParams.Add("account", username)
  311. req.SetBasicAuth(username, password)
  312. }
  313. }
  314. req.URL.RawQuery = reqParams.Encode()
  315. resp, err := th.client().Do(req)
  316. if err != nil {
  317. return "", time.Time{}, err
  318. }
  319. defer resp.Body.Close()
  320. if !client.SuccessStatus(resp.StatusCode) {
  321. err := client.HandleErrorResponse(resp)
  322. return "", time.Time{}, err
  323. }
  324. decoder := json.NewDecoder(resp.Body)
  325. var tr getTokenResponse
  326. if err = decoder.Decode(&tr); err != nil {
  327. return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err)
  328. }
  329. if tr.RefreshToken != "" && th.creds != nil {
  330. th.creds.SetRefreshToken(realm, service, tr.RefreshToken)
  331. }
  332. // `access_token` is equivalent to `token` and if both are specified
  333. // the choice is undefined. Canonicalize `access_token` by sticking
  334. // things in `token`.
  335. if tr.AccessToken != "" {
  336. tr.Token = tr.AccessToken
  337. }
  338. if tr.Token == "" {
  339. return "", time.Time{}, ErrNoToken
  340. }
  341. if tr.ExpiresIn < minimumTokenLifetimeSeconds {
  342. // The default/minimum lifetime.
  343. tr.ExpiresIn = minimumTokenLifetimeSeconds
  344. logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn)
  345. }
  346. if tr.IssuedAt.IsZero() {
  347. // issued_at is optional in the token response.
  348. tr.IssuedAt = th.clock.Now().UTC()
  349. }
  350. return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil
  351. }
  352. func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) {
  353. realm, ok := params["realm"]
  354. if !ok {
  355. return "", time.Time{}, errors.New("no realm specified for token auth challenge")
  356. }
  357. // TODO(dmcgowan): Handle empty scheme and relative realm
  358. realmURL, err := url.Parse(realm)
  359. if err != nil {
  360. return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err)
  361. }
  362. service := params["service"]
  363. var refreshToken string
  364. if th.creds != nil {
  365. refreshToken = th.creds.RefreshToken(realmURL, service)
  366. }
  367. if refreshToken != "" || th.forceOAuth {
  368. return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes)
  369. }
  370. return th.fetchTokenWithBasicAuth(realmURL, service, scopes)
  371. }
  372. type basicHandler struct {
  373. creds CredentialStore
  374. }
  375. // NewBasicHandler creaters a new authentiation handler which adds
  376. // basic authentication credentials to a request.
  377. func NewBasicHandler(creds CredentialStore) AuthenticationHandler {
  378. return &basicHandler{
  379. creds: creds,
  380. }
  381. }
  382. func (*basicHandler) Scheme() string {
  383. return "basic"
  384. }
  385. func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error {
  386. if bh.creds != nil {
  387. username, password := bh.creds.Basic(req.URL)
  388. if username != "" && password != "" {
  389. req.SetBasicAuth(username, password)
  390. return nil
  391. }
  392. }
  393. return ErrNoBasicAuthCredentials
  394. }