github.com/gophercloud/gophercloud@v1.11.0/openstack/identity/v3/extensions/oauth1/requests.go (about)

     1  package oauth1
     2  
     3  import (
     4  	"crypto/hmac"
     5  	"crypto/sha1"
     6  	"encoding/base64"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"math/rand"
    10  	"net/url"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/gophercloud/gophercloud"
    17  	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
    18  	"github.com/gophercloud/gophercloud/pagination"
    19  )
    20  
    21  // Type SignatureMethod is a OAuth1 SignatureMethod type.
    22  type SignatureMethod string
    23  
    24  const (
    25  	// HMACSHA1 is a recommended OAuth1 signature method.
    26  	HMACSHA1 SignatureMethod = "HMAC-SHA1"
    27  
    28  	// PLAINTEXT signature method is not recommended to be used in
    29  	// production environment.
    30  	PLAINTEXT SignatureMethod = "PLAINTEXT"
    31  
    32  	// OAuth1TokenContentType is a supported content type for an OAuth1
    33  	// token.
    34  	OAuth1TokenContentType = "application/x-www-form-urlencoded"
    35  )
    36  
    37  // AuthOptions represents options for authenticating a user using OAuth1 tokens.
    38  type AuthOptions struct {
    39  	// OAuthConsumerKey is the OAuth1 Consumer Key.
    40  	OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`
    41  
    42  	// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
    43  	// an OAuth1 request signature.
    44  	OAuthConsumerSecret string `required:"true"`
    45  
    46  	// OAuthToken is the OAuth1 Request Token.
    47  	OAuthToken string `q:"oauth_token" required:"true"`
    48  
    49  	// OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate
    50  	// an OAuth1 request signature.
    51  	OAuthTokenSecret string `required:"true"`
    52  
    53  	// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
    54  	// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
    55  	// "PLAINTEXT" is not recommended for production usage.
    56  	OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`
    57  
    58  	// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
    59  	// timestamp will be used.
    60  	OAuthTimestamp *time.Time
    61  
    62  	// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
    63  	// uniquely generated for each request. Will be generated automatically
    64  	// when it is not set.
    65  	OAuthNonce string `q:"oauth_nonce"`
    66  
    67  	// AllowReauth allows Gophercloud to re-authenticate automatically
    68  	// if/when your token expires.
    69  	AllowReauth bool
    70  }
    71  
    72  // ToTokenV3HeadersMap builds the headers required for an OAuth1-based create
    73  // request.
    74  func (opts AuthOptions) ToTokenV3HeadersMap(headerOpts map[string]interface{}) (map[string]string, error) {
    75  	q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "")
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret}
    81  
    82  	method := headerOpts["method"].(string)
    83  	u := headerOpts["url"].(string)
    84  	stringToSign := buildStringToSign(method, u, q.Query())
    85  	signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
    86  
    87  	authHeader := buildAuthHeader(q.Query(), signature)
    88  
    89  	headers := map[string]string{
    90  		"Authorization": authHeader,
    91  		"X-Auth-Token":  "",
    92  	}
    93  
    94  	return headers, nil
    95  }
    96  
    97  // ToTokenV3ScopeMap allows AuthOptions to satisfy the tokens.AuthOptionsBuilder
    98  // interface.
    99  func (opts AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
   100  	return nil, nil
   101  }
   102  
   103  // CanReauth allows AuthOptions to satisfy the tokens.AuthOptionsBuilder
   104  // interface.
   105  func (opts AuthOptions) CanReauth() bool {
   106  	return opts.AllowReauth
   107  }
   108  
   109  // ToTokenV3CreateMap builds a create request body.
   110  func (opts AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
   111  	// identityReq defines the "identity" portion of an OAuth1-based authentication
   112  	// create request body.
   113  	type identityReq struct {
   114  		Methods []string `json:"methods"`
   115  		OAuth1  struct{} `json:"oauth1"`
   116  	}
   117  
   118  	// authReq defines the "auth" portion of an OAuth1-based authentication
   119  	// create request body.
   120  	type authReq struct {
   121  		Identity identityReq `json:"identity"`
   122  	}
   123  
   124  	// oauth1Request defines how  an OAuth1-based authentication create
   125  	// request body looks.
   126  	type oauth1Request struct {
   127  		Auth authReq `json:"auth"`
   128  	}
   129  
   130  	var req oauth1Request
   131  
   132  	req.Auth.Identity.Methods = []string{"oauth1"}
   133  	return gophercloud.BuildRequestBody(req, "")
   134  }
   135  
   136  // Create authenticates and either generates a new OpenStack token from an
   137  // OAuth1 token.
   138  func Create(client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
   139  	b, err := opts.ToTokenV3CreateMap(nil)
   140  	if err != nil {
   141  		r.Err = err
   142  		return
   143  	}
   144  
   145  	headerOpts := map[string]interface{}{
   146  		"method": "POST",
   147  		"url":    authURL(client),
   148  	}
   149  
   150  	h, err := opts.ToTokenV3HeadersMap(headerOpts)
   151  	if err != nil {
   152  		r.Err = err
   153  		return
   154  	}
   155  
   156  	resp, err := client.Post(authURL(client), b, &r.Body, &gophercloud.RequestOpts{
   157  		MoreHeaders: h,
   158  		OkCodes:     []int{201},
   159  	})
   160  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   161  	return
   162  }
   163  
   164  // CreateConsumerOptsBuilder allows extensions to add additional parameters to
   165  // the CreateConsumer request.
   166  type CreateConsumerOptsBuilder interface {
   167  	ToOAuth1CreateConsumerMap() (map[string]interface{}, error)
   168  }
   169  
   170  // CreateConsumerOpts provides options used to create a new Consumer.
   171  type CreateConsumerOpts struct {
   172  	// Description is the consumer description.
   173  	Description string `json:"description"`
   174  }
   175  
   176  // ToOAuth1CreateConsumerMap formats a CreateConsumerOpts into a create request.
   177  func (opts CreateConsumerOpts) ToOAuth1CreateConsumerMap() (map[string]interface{}, error) {
   178  	return gophercloud.BuildRequestBody(opts, "consumer")
   179  }
   180  
   181  // Create creates a new Consumer.
   182  func CreateConsumer(client *gophercloud.ServiceClient, opts CreateConsumerOptsBuilder) (r CreateConsumerResult) {
   183  	b, err := opts.ToOAuth1CreateConsumerMap()
   184  	if err != nil {
   185  		r.Err = err
   186  		return
   187  	}
   188  	resp, err := client.Post(consumersURL(client), b, &r.Body, &gophercloud.RequestOpts{
   189  		OkCodes: []int{201},
   190  	})
   191  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   192  	return
   193  }
   194  
   195  // Delete deletes a Consumer.
   196  func DeleteConsumer(client *gophercloud.ServiceClient, id string) (r DeleteConsumerResult) {
   197  	resp, err := client.Delete(consumerURL(client, id), nil)
   198  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   199  	return
   200  }
   201  
   202  // List enumerates Consumers.
   203  func ListConsumers(client *gophercloud.ServiceClient) pagination.Pager {
   204  	return pagination.NewPager(client, consumersURL(client), func(r pagination.PageResult) pagination.Page {
   205  		return ConsumersPage{pagination.LinkedPageBase{PageResult: r}}
   206  	})
   207  }
   208  
   209  // GetConsumer retrieves details on a single Consumer by ID.
   210  func GetConsumer(client *gophercloud.ServiceClient, id string) (r GetConsumerResult) {
   211  	resp, err := client.Get(consumerURL(client, id), &r.Body, nil)
   212  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   213  	return
   214  }
   215  
   216  // UpdateConsumerOpts provides options used to update a consumer.
   217  type UpdateConsumerOpts struct {
   218  	// Description is the consumer description.
   219  	Description string `json:"description"`
   220  }
   221  
   222  // ToOAuth1UpdateConsumerMap formats an UpdateConsumerOpts into a consumer update
   223  // request.
   224  func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]interface{}, error) {
   225  	return gophercloud.BuildRequestBody(opts, "consumer")
   226  }
   227  
   228  // UpdateConsumer updates an existing Consumer.
   229  func UpdateConsumer(client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) {
   230  	b, err := opts.ToOAuth1UpdateConsumerMap()
   231  	if err != nil {
   232  		r.Err = err
   233  		return
   234  	}
   235  	resp, err := client.Patch(consumerURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
   236  		OkCodes: []int{200},
   237  	})
   238  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   239  	return
   240  }
   241  
   242  // RequestTokenOptsBuilder allows extensions to add additional parameters to the
   243  // RequestToken request.
   244  type RequestTokenOptsBuilder interface {
   245  	ToOAuth1RequestTokenHeaders(string, string) (map[string]string, error)
   246  }
   247  
   248  // RequestTokenOpts provides options used to get a consumer unauthorized
   249  // request token.
   250  type RequestTokenOpts struct {
   251  	// OAuthConsumerKey is the OAuth1 Consumer Key.
   252  	OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`
   253  
   254  	// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
   255  	// an OAuth1 request signature.
   256  	OAuthConsumerSecret string `required:"true"`
   257  
   258  	// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
   259  	// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
   260  	// "PLAINTEXT" is not recommended for production usage.
   261  	OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`
   262  
   263  	// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
   264  	// timestamp will be used.
   265  	OAuthTimestamp *time.Time
   266  
   267  	// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
   268  	// uniquely generated for each request. Will be generated automatically
   269  	// when it is not set.
   270  	OAuthNonce string `q:"oauth_nonce"`
   271  
   272  	// RequestedProjectID is a Project ID a consumer user requested an
   273  	// access to.
   274  	RequestedProjectID string `h:"Requested-Project-Id"`
   275  }
   276  
   277  // ToOAuth1RequestTokenHeaders formats a RequestTokenOpts into a map of request
   278  // headers.
   279  func (opts RequestTokenOpts) ToOAuth1RequestTokenHeaders(method, u string) (map[string]string, error) {
   280  	q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "oob")
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	h, err := gophercloud.BuildHeaders(opts)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	signatureKeys := []string{opts.OAuthConsumerSecret}
   291  	stringToSign := buildStringToSign(method, u, q.Query())
   292  	signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
   293  	authHeader := buildAuthHeader(q.Query(), signature)
   294  
   295  	h["Authorization"] = authHeader
   296  
   297  	return h, nil
   298  }
   299  
   300  // RequestToken requests an unauthorized OAuth1 Token.
   301  func RequestToken(client *gophercloud.ServiceClient, opts RequestTokenOptsBuilder) (r TokenResult) {
   302  	h, err := opts.ToOAuth1RequestTokenHeaders("POST", requestTokenURL(client))
   303  	if err != nil {
   304  		r.Err = err
   305  		return
   306  	}
   307  
   308  	resp, err := client.Post(requestTokenURL(client), nil, nil, &gophercloud.RequestOpts{
   309  		MoreHeaders:      h,
   310  		OkCodes:          []int{201},
   311  		KeepResponseBody: true,
   312  	})
   313  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   314  	if r.Err != nil {
   315  		return
   316  	}
   317  	defer resp.Body.Close()
   318  	if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType {
   319  		r.Err = fmt.Errorf("unsupported Content-Type: %q", v)
   320  		return
   321  	}
   322  	r.Body, r.Err = ioutil.ReadAll(resp.Body)
   323  	return
   324  }
   325  
   326  // AuthorizeTokenOptsBuilder allows extensions to add additional parameters to
   327  // the AuthorizeToken request.
   328  type AuthorizeTokenOptsBuilder interface {
   329  	ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error)
   330  }
   331  
   332  // AuthorizeTokenOpts provides options used to authorize a request token.
   333  type AuthorizeTokenOpts struct {
   334  	Roles []Role `json:"roles"`
   335  }
   336  
   337  // Role is a struct representing a role object in a AuthorizeTokenOpts struct.
   338  type Role struct {
   339  	ID   string `json:"id,omitempty"`
   340  	Name string `json:"name,omitempty"`
   341  }
   342  
   343  // ToOAuth1AuthorizeTokenMap formats an AuthorizeTokenOpts into an authorize token
   344  // request.
   345  func (opts AuthorizeTokenOpts) ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) {
   346  	for _, r := range opts.Roles {
   347  		if r == (Role{}) {
   348  			return nil, fmt.Errorf("role must not be empty")
   349  		}
   350  	}
   351  	return gophercloud.BuildRequestBody(opts, "")
   352  }
   353  
   354  // AuthorizeToken authorizes an unauthorized consumer token.
   355  func AuthorizeToken(client *gophercloud.ServiceClient, id string, opts AuthorizeTokenOptsBuilder) (r AuthorizeTokenResult) {
   356  	b, err := opts.ToOAuth1AuthorizeTokenMap()
   357  	if err != nil {
   358  		r.Err = err
   359  		return
   360  	}
   361  	resp, err := client.Put(authorizeTokenURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
   362  		OkCodes: []int{200},
   363  	})
   364  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   365  	return
   366  }
   367  
   368  // CreateAccessTokenOptsBuilder allows extensions to add additional parameters
   369  // to the CreateAccessToken request.
   370  type CreateAccessTokenOptsBuilder interface {
   371  	ToOAuth1CreateAccessTokenHeaders(string, string) (map[string]string, error)
   372  }
   373  
   374  // CreateAccessTokenOpts provides options used to create an OAuth1 token.
   375  type CreateAccessTokenOpts struct {
   376  	// OAuthConsumerKey is the OAuth1 Consumer Key.
   377  	OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`
   378  
   379  	// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
   380  	// an OAuth1 request signature.
   381  	OAuthConsumerSecret string `required:"true"`
   382  
   383  	// OAuthToken is the OAuth1 Request Token.
   384  	OAuthToken string `q:"oauth_token" required:"true"`
   385  
   386  	// OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate
   387  	// an OAuth1 request signature.
   388  	OAuthTokenSecret string `required:"true"`
   389  
   390  	// OAuthVerifier is the OAuth1 verification code.
   391  	OAuthVerifier string `q:"oauth_verifier" required:"true"`
   392  
   393  	// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
   394  	// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
   395  	// "PLAINTEXT" is not recommended for production usage.
   396  	OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`
   397  
   398  	// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
   399  	// timestamp will be used.
   400  	OAuthTimestamp *time.Time
   401  
   402  	// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
   403  	// uniquely generated for each request. Will be generated automatically
   404  	// when it is not set.
   405  	OAuthNonce string `q:"oauth_nonce"`
   406  }
   407  
   408  // ToOAuth1CreateAccessTokenHeaders formats a CreateAccessTokenOpts into a map of
   409  // request headers.
   410  func (opts CreateAccessTokenOpts) ToOAuth1CreateAccessTokenHeaders(method, u string) (map[string]string, error) {
   411  	q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "")
   412  	if err != nil {
   413  		return nil, err
   414  	}
   415  
   416  	signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret}
   417  	stringToSign := buildStringToSign(method, u, q.Query())
   418  	signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
   419  	authHeader := buildAuthHeader(q.Query(), signature)
   420  
   421  	headers := map[string]string{
   422  		"Authorization": authHeader,
   423  	}
   424  
   425  	return headers, nil
   426  }
   427  
   428  // CreateAccessToken creates a new OAuth1 Access Token
   429  func CreateAccessToken(client *gophercloud.ServiceClient, opts CreateAccessTokenOptsBuilder) (r TokenResult) {
   430  	h, err := opts.ToOAuth1CreateAccessTokenHeaders("POST", createAccessTokenURL(client))
   431  	if err != nil {
   432  		r.Err = err
   433  		return
   434  	}
   435  
   436  	resp, err := client.Post(createAccessTokenURL(client), nil, nil, &gophercloud.RequestOpts{
   437  		MoreHeaders:      h,
   438  		OkCodes:          []int{201},
   439  		KeepResponseBody: true,
   440  	})
   441  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   442  	if r.Err != nil {
   443  		return
   444  	}
   445  	defer resp.Body.Close()
   446  	if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType {
   447  		r.Err = fmt.Errorf("unsupported Content-Type: %q", v)
   448  		return
   449  	}
   450  	r.Body, r.Err = ioutil.ReadAll(resp.Body)
   451  	return
   452  }
   453  
   454  // GetAccessToken retrieves details on a single OAuth1 access token by an ID.
   455  func GetAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r GetAccessTokenResult) {
   456  	resp, err := client.Get(userAccessTokenURL(client, userID, id), &r.Body, nil)
   457  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   458  	return
   459  }
   460  
   461  // RevokeAccessToken revokes an OAuth1 access token.
   462  func RevokeAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r RevokeAccessTokenResult) {
   463  	resp, err := client.Delete(userAccessTokenURL(client, userID, id), nil)
   464  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   465  	return
   466  }
   467  
   468  // ListAccessTokens enumerates authorized access tokens.
   469  func ListAccessTokens(client *gophercloud.ServiceClient, userID string) pagination.Pager {
   470  	url := userAccessTokensURL(client, userID)
   471  	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
   472  		return AccessTokensPage{pagination.LinkedPageBase{PageResult: r}}
   473  	})
   474  }
   475  
   476  // ListAccessTokenRoles enumerates authorized access token roles.
   477  func ListAccessTokenRoles(client *gophercloud.ServiceClient, userID string, id string) pagination.Pager {
   478  	url := userAccessTokenRolesURL(client, userID, id)
   479  	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
   480  		return AccessTokenRolesPage{pagination.LinkedPageBase{PageResult: r}}
   481  	})
   482  }
   483  
   484  // GetAccessTokenRole retrieves details on a single OAuth1 access token role by
   485  // an ID.
   486  func GetAccessTokenRole(client *gophercloud.ServiceClient, userID string, id string, roleID string) (r GetAccessTokenRoleResult) {
   487  	resp, err := client.Get(userAccessTokenRoleURL(client, userID, id, roleID), &r.Body, nil)
   488  	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
   489  	return
   490  }
   491  
   492  // The following are small helper functions used to help build the signature.
   493  
   494  // buildOAuth1QueryString builds a URLEncoded parameters string specific for
   495  // OAuth1-based requests.
   496  func buildOAuth1QueryString(opts interface{}, timestamp *time.Time, callback string) (*url.URL, error) {
   497  	q, err := gophercloud.BuildQueryString(opts)
   498  	if err != nil {
   499  		return nil, err
   500  	}
   501  
   502  	query := q.Query()
   503  
   504  	if timestamp != nil {
   505  		// use provided timestamp
   506  		query.Set("oauth_timestamp", strconv.FormatInt(timestamp.Unix(), 10))
   507  	} else {
   508  		// use current timestamp
   509  		query.Set("oauth_timestamp", strconv.FormatInt(time.Now().UTC().Unix(), 10))
   510  	}
   511  
   512  	if query.Get("oauth_nonce") == "" {
   513  		// when nonce is not set, generate a random one
   514  		query.Set("oauth_nonce", strconv.FormatInt(rand.Int63(), 10)+query.Get("oauth_timestamp"))
   515  	}
   516  
   517  	if callback != "" {
   518  		query.Set("oauth_callback", callback)
   519  	}
   520  	query.Set("oauth_version", "1.0")
   521  
   522  	return &url.URL{RawQuery: query.Encode()}, nil
   523  }
   524  
   525  // buildStringToSign builds a string to be signed.
   526  func buildStringToSign(method string, u string, query url.Values) []byte {
   527  	parsedURL, _ := url.Parse(u)
   528  	p := parsedURL.Port()
   529  	s := parsedURL.Scheme
   530  
   531  	// Default scheme port must be stripped
   532  	if s == "http" && p == "80" || s == "https" && p == "443" {
   533  		parsedURL.Host = strings.TrimSuffix(parsedURL.Host, ":"+p)
   534  	}
   535  
   536  	// Ensure that URL doesn't contain queries
   537  	parsedURL.RawQuery = ""
   538  
   539  	v := strings.Join(
   540  		[]string{method, url.QueryEscape(parsedURL.String()), url.QueryEscape(query.Encode())}, "&")
   541  
   542  	return []byte(v)
   543  }
   544  
   545  // signString signs a string using an OAuth1 signature method.
   546  func signString(signatureMethod SignatureMethod, strToSign []byte, signatureKeys []string) string {
   547  	var key []byte
   548  	for i, k := range signatureKeys {
   549  		key = append(key, []byte(url.QueryEscape(k))...)
   550  		if i == 0 {
   551  			key = append(key, '&')
   552  		}
   553  	}
   554  
   555  	var signedString string
   556  	switch signatureMethod {
   557  	case PLAINTEXT:
   558  		signedString = string(key)
   559  	default:
   560  		h := hmac.New(sha1.New, key)
   561  		h.Write(strToSign)
   562  		signedString = base64.StdEncoding.EncodeToString(h.Sum(nil))
   563  	}
   564  
   565  	return signedString
   566  }
   567  
   568  // buildAuthHeader generates an OAuth1 Authorization header with a signature
   569  // calculated using an OAuth1 signature method.
   570  func buildAuthHeader(query url.Values, signature string) string {
   571  	var authHeader []string
   572  	var keys []string
   573  	for k := range query {
   574  		keys = append(keys, k)
   575  	}
   576  	sort.Strings(keys)
   577  
   578  	for _, k := range keys {
   579  		for _, v := range query[k] {
   580  			authHeader = append(authHeader, fmt.Sprintf("%s=%q", k, url.QueryEscape(v)))
   581  		}
   582  	}
   583  
   584  	authHeader = append(authHeader, fmt.Sprintf("oauth_signature=%q", signature))
   585  
   586  	return "OAuth " + strings.Join(authHeader, ", ")
   587  }