session.go 14 KB

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