github.com/containerd/containerd@v22.0.0-20200918172823-438c87b8e050+incompatible/remotes/docker/auth/fetch.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package auth
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"net/http"
    23  	"net/url"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/containerd/containerd/log"
    28  	remoteserrors "github.com/containerd/containerd/remotes/errors"
    29  	"github.com/pkg/errors"
    30  	"golang.org/x/net/context/ctxhttp"
    31  )
    32  
    33  var (
    34  	// ErrNoToken is returned if a request is successful but the body does not
    35  	// contain an authorization token.
    36  	ErrNoToken = errors.New("authorization server did not include a token in the response")
    37  )
    38  
    39  // GenerateTokenOptions generates options for fetching a token based on a challenge
    40  func GenerateTokenOptions(ctx context.Context, host, username, secret string, c Challenge) (TokenOptions, error) {
    41  	realm, ok := c.Parameters["realm"]
    42  	if !ok {
    43  		return TokenOptions{}, errors.New("no realm specified for token auth challenge")
    44  	}
    45  
    46  	realmURL, err := url.Parse(realm)
    47  	if err != nil {
    48  		return TokenOptions{}, errors.Wrap(err, "invalid token auth challenge realm")
    49  	}
    50  
    51  	to := TokenOptions{
    52  		Realm:    realmURL.String(),
    53  		Service:  c.Parameters["service"],
    54  		Username: username,
    55  		Secret:   secret,
    56  	}
    57  
    58  	scope, ok := c.Parameters["scope"]
    59  	if ok {
    60  		to.Scopes = append(to.Scopes, scope)
    61  	} else {
    62  		log.G(ctx).WithField("host", host).Debug("no scope specified for token auth challenge")
    63  	}
    64  
    65  	return to, nil
    66  }
    67  
    68  // TokenOptions are optios for requesting a token
    69  type TokenOptions struct {
    70  	Realm    string
    71  	Service  string
    72  	Scopes   []string
    73  	Username string
    74  	Secret   string
    75  }
    76  
    77  // OAuthTokenResponse is response from fetching token with a OAuth POST request
    78  type OAuthTokenResponse struct {
    79  	AccessToken  string    `json:"access_token"`
    80  	RefreshToken string    `json:"refresh_token"`
    81  	ExpiresIn    int       `json:"expires_in"`
    82  	IssuedAt     time.Time `json:"issued_at"`
    83  	Scope        string    `json:"scope"`
    84  }
    85  
    86  // FetchTokenWithOAuth fetches a token using a POST request
    87  func FetchTokenWithOAuth(ctx context.Context, client *http.Client, headers http.Header, clientID string, to TokenOptions) (*OAuthTokenResponse, error) {
    88  	form := url.Values{}
    89  	if len(to.Scopes) > 0 {
    90  		form.Set("scope", strings.Join(to.Scopes, " "))
    91  	}
    92  	form.Set("service", to.Service)
    93  	form.Set("client_id", clientID)
    94  
    95  	if to.Username == "" {
    96  		form.Set("grant_type", "refresh_token")
    97  		form.Set("refresh_token", to.Secret)
    98  	} else {
    99  		form.Set("grant_type", "password")
   100  		form.Set("username", to.Username)
   101  		form.Set("password", to.Secret)
   102  	}
   103  
   104  	req, err := http.NewRequest("POST", to.Realm, strings.NewReader(form.Encode()))
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
   109  	if headers != nil {
   110  		for k, v := range headers {
   111  			req.Header[k] = append(req.Header[k], v...)
   112  		}
   113  	}
   114  
   115  	resp, err := ctxhttp.Do(ctx, client, req)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	defer resp.Body.Close()
   120  
   121  	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
   122  		return nil, errors.WithStack(remoteserrors.NewUnexpectedStatusErr(resp))
   123  	}
   124  
   125  	decoder := json.NewDecoder(resp.Body)
   126  
   127  	var tr OAuthTokenResponse
   128  	if err = decoder.Decode(&tr); err != nil {
   129  		return nil, errors.Wrap(err, "unable to decode token response")
   130  	}
   131  
   132  	if tr.AccessToken == "" {
   133  		return nil, errors.WithStack(ErrNoToken)
   134  	}
   135  
   136  	return &tr, nil
   137  }
   138  
   139  // FetchTokenResponse is response from fetching token with GET request
   140  type FetchTokenResponse struct {
   141  	Token        string    `json:"token"`
   142  	AccessToken  string    `json:"access_token"`
   143  	ExpiresIn    int       `json:"expires_in"`
   144  	IssuedAt     time.Time `json:"issued_at"`
   145  	RefreshToken string    `json:"refresh_token"`
   146  }
   147  
   148  // FetchToken fetches a token using a GET request
   149  func FetchToken(ctx context.Context, client *http.Client, headers http.Header, to TokenOptions) (*FetchTokenResponse, error) {
   150  	req, err := http.NewRequest("GET", to.Realm, nil)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	if headers != nil {
   156  		for k, v := range headers {
   157  			req.Header[k] = append(req.Header[k], v...)
   158  		}
   159  	}
   160  
   161  	reqParams := req.URL.Query()
   162  
   163  	if to.Service != "" {
   164  		reqParams.Add("service", to.Service)
   165  	}
   166  
   167  	for _, scope := range to.Scopes {
   168  		reqParams.Add("scope", scope)
   169  	}
   170  
   171  	if to.Secret != "" {
   172  		req.SetBasicAuth(to.Username, to.Secret)
   173  	}
   174  
   175  	req.URL.RawQuery = reqParams.Encode()
   176  
   177  	resp, err := ctxhttp.Do(ctx, client, req)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	defer resp.Body.Close()
   182  
   183  	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
   184  		return nil, errors.WithStack(remoteserrors.NewUnexpectedStatusErr(resp))
   185  	}
   186  
   187  	decoder := json.NewDecoder(resp.Body)
   188  
   189  	var tr FetchTokenResponse
   190  	if err = decoder.Decode(&tr); err != nil {
   191  		return nil, errors.Wrap(err, "unable to decode token response")
   192  	}
   193  
   194  	// `access_token` is equivalent to `token` and if both are specified
   195  	// the choice is undefined.  Canonicalize `access_token` by sticking
   196  	// things in `token`.
   197  	if tr.AccessToken != "" {
   198  		tr.Token = tr.AccessToken
   199  	}
   200  
   201  	if tr.Token == "" {
   202  		return nil, errors.WithStack(ErrNoToken)
   203  	}
   204  
   205  	return &tr, nil
   206  }