github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/openstack/identity/v3/oauth1/requests.go (about)

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