authorizer.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. /*
  2. Copyright The containerd Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package docker
  14. import (
  15. "context"
  16. "encoding/base64"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "io/ioutil"
  21. "net/http"
  22. "net/url"
  23. "strings"
  24. "sync"
  25. "time"
  26. "github.com/containerd/containerd/errdefs"
  27. "github.com/containerd/containerd/log"
  28. "github.com/pkg/errors"
  29. "github.com/sirupsen/logrus"
  30. "golang.org/x/net/context/ctxhttp"
  31. )
  32. type dockerAuthorizer struct {
  33. credentials func(string) (string, string, error)
  34. client *http.Client
  35. header http.Header
  36. mu sync.Mutex
  37. // indexed by host name
  38. handlers map[string]*authHandler
  39. }
  40. // NewAuthorizer creates a Docker authorizer using the provided function to
  41. // get credentials for the token server or basic auth.
  42. // Deprecated: Use NewDockerAuthorizer
  43. func NewAuthorizer(client *http.Client, f func(string) (string, string, error)) Authorizer {
  44. return NewDockerAuthorizer(WithAuthClient(client), WithAuthCreds(f))
  45. }
  46. type authorizerConfig struct {
  47. credentials func(string) (string, string, error)
  48. client *http.Client
  49. header http.Header
  50. }
  51. // AuthorizerOpt configures an authorizer
  52. type AuthorizerOpt func(*authorizerConfig)
  53. // WithAuthClient provides the HTTP client for the authorizer
  54. func WithAuthClient(client *http.Client) AuthorizerOpt {
  55. return func(opt *authorizerConfig) {
  56. opt.client = client
  57. }
  58. }
  59. // WithAuthCreds provides a credential function to the authorizer
  60. func WithAuthCreds(creds func(string) (string, string, error)) AuthorizerOpt {
  61. return func(opt *authorizerConfig) {
  62. opt.credentials = creds
  63. }
  64. }
  65. // WithAuthHeader provides HTTP headers for authorization
  66. func WithAuthHeader(hdr http.Header) AuthorizerOpt {
  67. return func(opt *authorizerConfig) {
  68. opt.header = hdr
  69. }
  70. }
  71. // NewDockerAuthorizer creates an authorizer using Docker's registry
  72. // authentication spec.
  73. // See https://docs.docker.com/registry/spec/auth/
  74. func NewDockerAuthorizer(opts ...AuthorizerOpt) Authorizer {
  75. var ao authorizerConfig
  76. for _, opt := range opts {
  77. opt(&ao)
  78. }
  79. if ao.client == nil {
  80. ao.client = http.DefaultClient
  81. }
  82. return &dockerAuthorizer{
  83. credentials: ao.credentials,
  84. client: ao.client,
  85. header: ao.header,
  86. handlers: make(map[string]*authHandler),
  87. }
  88. }
  89. // Authorize handles auth request.
  90. func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) error {
  91. // skip if there is no auth handler
  92. ah := a.getAuthHandler(req.URL.Host)
  93. if ah == nil {
  94. return nil
  95. }
  96. auth, err := ah.authorize(ctx)
  97. if err != nil {
  98. return err
  99. }
  100. req.Header.Set("Authorization", auth)
  101. return nil
  102. }
  103. func (a *dockerAuthorizer) getAuthHandler(host string) *authHandler {
  104. a.mu.Lock()
  105. defer a.mu.Unlock()
  106. return a.handlers[host]
  107. }
  108. func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.Response) error {
  109. last := responses[len(responses)-1]
  110. host := last.Request.URL.Host
  111. a.mu.Lock()
  112. defer a.mu.Unlock()
  113. for _, c := range parseAuthHeader(last.Header) {
  114. if c.scheme == bearerAuth {
  115. if err := invalidAuthorization(c, responses); err != nil {
  116. delete(a.handlers, host)
  117. return err
  118. }
  119. // reuse existing handler
  120. //
  121. // assume that one registry will return the common
  122. // challenge information, including realm and service.
  123. // and the resource scope is only different part
  124. // which can be provided by each request.
  125. if _, ok := a.handlers[host]; ok {
  126. return nil
  127. }
  128. common, err := a.generateTokenOptions(ctx, host, c)
  129. if err != nil {
  130. return err
  131. }
  132. a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
  133. return nil
  134. } else if c.scheme == basicAuth && a.credentials != nil {
  135. username, secret, err := a.credentials(host)
  136. if err != nil {
  137. return err
  138. }
  139. if username != "" && secret != "" {
  140. common := tokenOptions{
  141. username: username,
  142. secret: secret,
  143. }
  144. a.handlers[host] = newAuthHandler(a.client, a.header, c.scheme, common)
  145. return nil
  146. }
  147. }
  148. }
  149. return errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
  150. }
  151. func (a *dockerAuthorizer) generateTokenOptions(ctx context.Context, host string, c challenge) (tokenOptions, error) {
  152. realm, ok := c.parameters["realm"]
  153. if !ok {
  154. return tokenOptions{}, errors.New("no realm specified for token auth challenge")
  155. }
  156. realmURL, err := url.Parse(realm)
  157. if err != nil {
  158. return tokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
  159. }
  160. to := tokenOptions{
  161. realm: realmURL.String(),
  162. service: c.parameters["service"],
  163. }
  164. scope, ok := c.parameters["scope"]
  165. if ok {
  166. to.scopes = append(to.scopes, scope)
  167. } else {
  168. log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge")
  169. }
  170. if a.credentials != nil {
  171. to.username, to.secret, err = a.credentials(host)
  172. if err != nil {
  173. return tokenOptions{}, err
  174. }
  175. }
  176. return to, nil
  177. }
  178. // authResult is used to control limit rate.
  179. type authResult struct {
  180. sync.WaitGroup
  181. token string
  182. err error
  183. }
  184. // authHandler is used to handle auth request per registry server.
  185. type authHandler struct {
  186. sync.Mutex
  187. header http.Header
  188. client *http.Client
  189. // only support basic and bearer schemes
  190. scheme authenticationScheme
  191. // common contains common challenge answer
  192. common tokenOptions
  193. // scopedTokens caches token indexed by scopes, which used in
  194. // bearer auth case
  195. scopedTokens map[string]*authResult
  196. }
  197. func newAuthHandler(client *http.Client, hdr http.Header, scheme authenticationScheme, opts tokenOptions) *authHandler {
  198. return &authHandler{
  199. header: hdr,
  200. client: client,
  201. scheme: scheme,
  202. common: opts,
  203. scopedTokens: map[string]*authResult{},
  204. }
  205. }
  206. func (ah *authHandler) authorize(ctx context.Context) (string, error) {
  207. switch ah.scheme {
  208. case basicAuth:
  209. return ah.doBasicAuth(ctx)
  210. case bearerAuth:
  211. return ah.doBearerAuth(ctx)
  212. default:
  213. return "", errors.Wrap(errdefs.ErrNotImplemented, "failed to find supported auth scheme")
  214. }
  215. }
  216. func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
  217. username, secret := ah.common.username, ah.common.secret
  218. if username == "" || secret == "" {
  219. return "", fmt.Errorf("failed to handle basic auth because missing username or secret")
  220. }
  221. auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + secret))
  222. return fmt.Sprintf("Basic %s", auth), nil
  223. }
  224. func (ah *authHandler) doBearerAuth(ctx context.Context) (string, error) {
  225. // copy common tokenOptions
  226. to := ah.common
  227. to.scopes = getTokenScopes(ctx, to.scopes)
  228. // Docs: https://docs.docker.com/registry/spec/auth/scope
  229. scoped := strings.Join(to.scopes, " ")
  230. ah.Lock()
  231. if r, exist := ah.scopedTokens[scoped]; exist {
  232. ah.Unlock()
  233. r.Wait()
  234. return r.token, r.err
  235. }
  236. // only one fetch token job
  237. r := new(authResult)
  238. r.Add(1)
  239. ah.scopedTokens[scoped] = r
  240. ah.Unlock()
  241. // fetch token for the resource scope
  242. var (
  243. token string
  244. err error
  245. )
  246. if to.secret != "" {
  247. // credential information is provided, use oauth POST endpoint
  248. token, err = ah.fetchTokenWithOAuth(ctx, to)
  249. err = errors.Wrap(err, "failed to fetch oauth token")
  250. } else {
  251. // do request anonymously
  252. token, err = ah.fetchToken(ctx, to)
  253. err = errors.Wrap(err, "failed to fetch anonymous token")
  254. }
  255. token = fmt.Sprintf("Bearer %s", token)
  256. r.token, r.err = token, err
  257. r.Done()
  258. return r.token, r.err
  259. }
  260. type tokenOptions struct {
  261. realm string
  262. service string
  263. scopes []string
  264. username string
  265. secret string
  266. }
  267. type postTokenResponse struct {
  268. AccessToken string `json:"access_token"`
  269. RefreshToken string `json:"refresh_token"`
  270. ExpiresIn int `json:"expires_in"`
  271. IssuedAt time.Time `json:"issued_at"`
  272. Scope string `json:"scope"`
  273. }
  274. func (ah *authHandler) fetchTokenWithOAuth(ctx context.Context, to tokenOptions) (string, error) {
  275. form := url.Values{}
  276. if len(to.scopes) > 0 {
  277. form.Set("scope", strings.Join(to.scopes, " "))
  278. }
  279. form.Set("service", to.service)
  280. // TODO: Allow setting client_id
  281. form.Set("client_id", "containerd-client")
  282. if to.username == "" {
  283. form.Set("grant_type", "refresh_token")
  284. form.Set("refresh_token", to.secret)
  285. } else {
  286. form.Set("grant_type", "password")
  287. form.Set("username", to.username)
  288. form.Set("password", to.secret)
  289. }
  290. req, err := http.NewRequest("POST", to.realm, strings.NewReader(form.Encode()))
  291. if err != nil {
  292. return "", err
  293. }
  294. req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
  295. if ah.header != nil {
  296. for k, v := range ah.header {
  297. req.Header[k] = append(req.Header[k], v...)
  298. }
  299. }
  300. resp, err := ctxhttp.Do(ctx, ah.client, req)
  301. if err != nil {
  302. return "", err
  303. }
  304. defer resp.Body.Close()
  305. // Registries without support for POST may return 404 for POST /v2/token.
  306. // As of September 2017, GCR is known to return 404.
  307. // As of February 2018, JFrog Artifactory is known to return 401.
  308. if (resp.StatusCode == 405 && to.username != "") || resp.StatusCode == 404 || resp.StatusCode == 401 {
  309. return ah.fetchToken(ctx, to)
  310. } else if resp.StatusCode < 200 || resp.StatusCode >= 400 {
  311. b, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 64000)) // 64KB
  312. log.G(ctx).WithFields(logrus.Fields{
  313. "status": resp.Status,
  314. "body": string(b),
  315. }).Debugf("token request failed")
  316. // TODO: handle error body and write debug output
  317. return "", errors.Errorf("unexpected status: %s", resp.Status)
  318. }
  319. decoder := json.NewDecoder(resp.Body)
  320. var tr postTokenResponse
  321. if err = decoder.Decode(&tr); err != nil {
  322. return "", fmt.Errorf("unable to decode token response: %s", err)
  323. }
  324. return tr.AccessToken, nil
  325. }
  326. type getTokenResponse struct {
  327. Token string `json:"token"`
  328. AccessToken string `json:"access_token"`
  329. ExpiresIn int `json:"expires_in"`
  330. IssuedAt time.Time `json:"issued_at"`
  331. RefreshToken string `json:"refresh_token"`
  332. }
  333. // fetchToken fetches a token using a GET request
  334. func (ah *authHandler) fetchToken(ctx context.Context, to tokenOptions) (string, error) {
  335. req, err := http.NewRequest("GET", to.realm, nil)
  336. if err != nil {
  337. return "", err
  338. }
  339. if ah.header != nil {
  340. for k, v := range ah.header {
  341. req.Header[k] = append(req.Header[k], v...)
  342. }
  343. }
  344. reqParams := req.URL.Query()
  345. if to.service != "" {
  346. reqParams.Add("service", to.service)
  347. }
  348. for _, scope := range to.scopes {
  349. reqParams.Add("scope", scope)
  350. }
  351. if to.secret != "" {
  352. req.SetBasicAuth(to.username, to.secret)
  353. }
  354. req.URL.RawQuery = reqParams.Encode()
  355. resp, err := ctxhttp.Do(ctx, ah.client, req)
  356. if err != nil {
  357. return "", err
  358. }
  359. defer resp.Body.Close()
  360. if resp.StatusCode < 200 || resp.StatusCode >= 400 {
  361. // TODO: handle error body and write debug output
  362. return "", errors.Errorf("unexpected status: %s", resp.Status)
  363. }
  364. decoder := json.NewDecoder(resp.Body)
  365. var tr getTokenResponse
  366. if err = decoder.Decode(&tr); err != nil {
  367. return "", fmt.Errorf("unable to decode token response: %s", err)
  368. }
  369. // `access_token` is equivalent to `token` and if both are specified
  370. // the choice is undefined. Canonicalize `access_token` by sticking
  371. // things in `token`.
  372. if tr.AccessToken != "" {
  373. tr.Token = tr.AccessToken
  374. }
  375. if tr.Token == "" {
  376. return "", ErrNoToken
  377. }
  378. return tr.Token, nil
  379. }
  380. func invalidAuthorization(c challenge, responses []*http.Response) error {
  381. errStr := c.parameters["error"]
  382. if errStr == "" {
  383. return nil
  384. }
  385. n := len(responses)
  386. if n == 1 || (n > 1 && !sameRequest(responses[n-2].Request, responses[n-1].Request)) {
  387. return nil
  388. }
  389. return errors.Wrapf(ErrInvalidAuthorization, "server message: %s", errStr)
  390. }
  391. func sameRequest(r1, r2 *http.Request) bool {
  392. if r1.Method != r2.Method {
  393. return false
  394. }
  395. if *r1.URL != *r2.URL {
  396. return false
  397. }
  398. return true
  399. }