session.go 14 KB

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