Blob Blame History Raw
package oauth1

import (
	"crypto/hmac"
	"crypto/sha1"
	"encoding/base64"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/url"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/gophercloud/gophercloud"
	"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
	"github.com/gophercloud/gophercloud/pagination"
)

// Type SignatureMethod is a OAuth1 SignatureMethod type.
type SignatureMethod string

const (
	// HMACSHA1 is a recommended OAuth1 signature method.
	HMACSHA1 SignatureMethod = "HMAC-SHA1"

	// PLAINTEXT signature method is not recommended to be used in
	// production environment.
	PLAINTEXT SignatureMethod = "PLAINTEXT"

	// OAuth1TokenContentType is a supported content type for an OAuth1
	// token.
	OAuth1TokenContentType = "application/x-www-form-urlencoded"
)

// AuthOptions represents options for authenticating a user using OAuth1 tokens.
type AuthOptions struct {
	// OAuthConsumerKey is the OAuth1 Consumer Key.
	OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`

	// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
	// an OAuth1 request signature.
	OAuthConsumerSecret string `required:"true"`

	// OAuthToken is the OAuth1 Request Token.
	OAuthToken string `q:"oauth_token" required:"true"`

	// OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate
	// an OAuth1 request signature.
	OAuthTokenSecret string `required:"true"`

	// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
	// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
	// "PLAINTEXT" is not recommended for production usage.
	OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`

	// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
	// timestamp will be used.
	OAuthTimestamp *time.Time

	// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
	// uniquely generated for each request. Will be generated automatically
	// when it is not set.
	OAuthNonce string `q:"oauth_nonce"`

	// AllowReauth allows Gophercloud to re-authenticate automatically
	// if/when your token expires.
	AllowReauth bool
}

// ToTokenV3HeadersMap builds the headers required for an OAuth1-based create
// request.
func (opts AuthOptions) ToTokenV3HeadersMap(headerOpts map[string]interface{}) (map[string]string, error) {
	q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "")
	if err != nil {
		return nil, err
	}

	signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret}

	method := headerOpts["method"].(string)
	u := headerOpts["url"].(string)
	stringToSign := buildStringToSign(method, u, q.Query())
	signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))

	authHeader := buildAuthHeader(q.Query(), signature)

	headers := map[string]string{
		"Authorization": authHeader,
		"X-Auth-Token":  "",
	}

	return headers, nil
}

// ToTokenV3ScopeMap allows AuthOptions to satisfy the tokens.AuthOptionsBuilder
// interface.
func (opts AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
	return nil, nil
}

// CanReauth allows AuthOptions to satisfy the tokens.AuthOptionsBuilder
// interface.
func (opts AuthOptions) CanReauth() bool {
	return opts.AllowReauth
}

// ToTokenV3CreateMap builds a create request body.
func (opts AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
	// identityReq defines the "identity" portion of an OAuth1-based authentication
	// create request body.
	type identityReq struct {
		Methods []string `json:"methods"`
		OAuth1  struct{} `json:"oauth1"`
	}

	// authReq defines the "auth" portion of an OAuth1-based authentication
	// create request body.
	type authReq struct {
		Identity identityReq `json:"identity"`
	}

	// oauth1Request defines how  an OAuth1-based authentication create
	// request body looks.
	type oauth1Request struct {
		Auth authReq `json:"auth"`
	}

	var req oauth1Request

	req.Auth.Identity.Methods = []string{"oauth1"}
	return gophercloud.BuildRequestBody(req, "")
}

// Create authenticates and either generates a new OpenStack token from an
// OAuth1 token.
func Create(client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
	b, err := opts.ToTokenV3CreateMap(nil)
	if err != nil {
		r.Err = err
		return
	}

	headerOpts := map[string]interface{}{
		"method": "POST",
		"url":    authURL(client),
	}

	h, err := opts.ToTokenV3HeadersMap(headerOpts)
	if err != nil {
		r.Err = err
		return
	}

	resp, err := client.Post(authURL(client), b, &r.Body, &gophercloud.RequestOpts{
		MoreHeaders: h,
		OkCodes:     []int{201},
	})
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// CreateConsumerOptsBuilder allows extensions to add additional parameters to
// the CreateConsumer request.
type CreateConsumerOptsBuilder interface {
	ToOAuth1CreateConsumerMap() (map[string]interface{}, error)
}

// CreateConsumerOpts provides options used to create a new Consumer.
type CreateConsumerOpts struct {
	// Description is the consumer description.
	Description string `json:"description"`
}

// ToOAuth1CreateConsumerMap formats a CreateConsumerOpts into a create request.
func (opts CreateConsumerOpts) ToOAuth1CreateConsumerMap() (map[string]interface{}, error) {
	return gophercloud.BuildRequestBody(opts, "consumer")
}

// Create creates a new Consumer.
func CreateConsumer(client *gophercloud.ServiceClient, opts CreateConsumerOptsBuilder) (r CreateConsumerResult) {
	b, err := opts.ToOAuth1CreateConsumerMap()
	if err != nil {
		r.Err = err
		return
	}
	resp, err := client.Post(consumersURL(client), b, &r.Body, &gophercloud.RequestOpts{
		OkCodes: []int{201},
	})
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// Delete deletes a Consumer.
func DeleteConsumer(client *gophercloud.ServiceClient, id string) (r DeleteConsumerResult) {
	resp, err := client.Delete(consumerURL(client, id), nil)
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// List enumerates Consumers.
func ListConsumers(client *gophercloud.ServiceClient) pagination.Pager {
	return pagination.NewPager(client, consumersURL(client), func(r pagination.PageResult) pagination.Page {
		return ConsumersPage{pagination.LinkedPageBase{PageResult: r}}
	})
}

// GetConsumer retrieves details on a single Consumer by ID.
func GetConsumer(client *gophercloud.ServiceClient, id string) (r GetConsumerResult) {
	resp, err := client.Get(consumerURL(client, id), &r.Body, nil)
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// UpdateConsumerOpts provides options used to update a consumer.
type UpdateConsumerOpts struct {
	// Description is the consumer description.
	Description string `json:"description"`
}

// ToOAuth1UpdateConsumerMap formats an UpdateConsumerOpts into a consumer update
// request.
func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]interface{}, error) {
	return gophercloud.BuildRequestBody(opts, "consumer")
}

// UpdateConsumer updates an existing Consumer.
func UpdateConsumer(client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) {
	b, err := opts.ToOAuth1UpdateConsumerMap()
	if err != nil {
		r.Err = err
		return
	}
	resp, err := client.Patch(consumerURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
		OkCodes: []int{200},
	})
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// RequestTokenOptsBuilder allows extensions to add additional parameters to the
// RequestToken request.
type RequestTokenOptsBuilder interface {
	ToOAuth1RequestTokenHeaders(string, string) (map[string]string, error)
}

// RequestTokenOpts provides options used to get a consumer unauthorized
// request token.
type RequestTokenOpts struct {
	// OAuthConsumerKey is the OAuth1 Consumer Key.
	OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`

	// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
	// an OAuth1 request signature.
	OAuthConsumerSecret string `required:"true"`

	// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
	// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
	// "PLAINTEXT" is not recommended for production usage.
	OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`

	// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
	// timestamp will be used.
	OAuthTimestamp *time.Time

	// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
	// uniquely generated for each request. Will be generated automatically
	// when it is not set.
	OAuthNonce string `q:"oauth_nonce"`

	// RequestedProjectID is a Project ID a consumer user requested an
	// access to.
	RequestedProjectID string `h:"Requested-Project-Id"`
}

// ToOAuth1RequestTokenHeaders formats a RequestTokenOpts into a map of request
// headers.
func (opts RequestTokenOpts) ToOAuth1RequestTokenHeaders(method, u string) (map[string]string, error) {
	q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "oob")
	if err != nil {
		return nil, err
	}

	h, err := gophercloud.BuildHeaders(opts)
	if err != nil {
		return nil, err
	}

	signatureKeys := []string{opts.OAuthConsumerSecret}
	stringToSign := buildStringToSign(method, u, q.Query())
	signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
	authHeader := buildAuthHeader(q.Query(), signature)

	h["Authorization"] = authHeader

	return h, nil
}

// RequestToken requests an unauthorized OAuth1 Token.
func RequestToken(client *gophercloud.ServiceClient, opts RequestTokenOptsBuilder) (r TokenResult) {
	h, err := opts.ToOAuth1RequestTokenHeaders("POST", requestTokenURL(client))
	if err != nil {
		r.Err = err
		return
	}

	resp, err := client.Post(requestTokenURL(client), nil, nil, &gophercloud.RequestOpts{
		MoreHeaders:      h,
		OkCodes:          []int{201},
		KeepResponseBody: true,
	})
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	if r.Err != nil {
		return
	}
	defer resp.Body.Close()
	if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType {
		r.Err = fmt.Errorf("unsupported Content-Type: %q", v)
		return
	}
	r.Body, r.Err = ioutil.ReadAll(resp.Body)
	return
}

// AuthorizeTokenOptsBuilder allows extensions to add additional parameters to
// the AuthorizeToken request.
type AuthorizeTokenOptsBuilder interface {
	ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error)
}

// AuthorizeTokenOpts provides options used to authorize a request token.
type AuthorizeTokenOpts struct {
	Roles []Role `json:"roles"`
}

// Role is a struct representing a role object in a AuthorizeTokenOpts struct.
type Role struct {
	ID   string `json:"id,omitempty"`
	Name string `json:"name,omitempty"`
}

// ToOAuth1AuthorizeTokenMap formats an AuthorizeTokenOpts into an authorize token
// request.
func (opts AuthorizeTokenOpts) ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) {
	for _, r := range opts.Roles {
		if r == (Role{}) {
			return nil, fmt.Errorf("role must not be empty")
		}
	}
	return gophercloud.BuildRequestBody(opts, "")
}

// AuthorizeToken authorizes an unauthorized consumer token.
func AuthorizeToken(client *gophercloud.ServiceClient, id string, opts AuthorizeTokenOptsBuilder) (r AuthorizeTokenResult) {
	b, err := opts.ToOAuth1AuthorizeTokenMap()
	if err != nil {
		r.Err = err
		return
	}
	resp, err := client.Put(authorizeTokenURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
		OkCodes: []int{200},
	})
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// CreateAccessTokenOptsBuilder allows extensions to add additional parameters
// to the CreateAccessToken request.
type CreateAccessTokenOptsBuilder interface {
	ToOAuth1CreateAccessTokenHeaders(string, string) (map[string]string, error)
}

// CreateAccessTokenOpts provides options used to create an OAuth1 token.
type CreateAccessTokenOpts struct {
	// OAuthConsumerKey is the OAuth1 Consumer Key.
	OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`

	// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
	// an OAuth1 request signature.
	OAuthConsumerSecret string `required:"true"`

	// OAuthToken is the OAuth1 Request Token.
	OAuthToken string `q:"oauth_token" required:"true"`

	// OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate
	// an OAuth1 request signature.
	OAuthTokenSecret string `required:"true"`

	// OAuthVerifier is the OAuth1 verification code.
	OAuthVerifier string `q:"oauth_verifier" required:"true"`

	// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
	// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
	// "PLAINTEXT" is not recommended for production usage.
	OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`

	// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
	// timestamp will be used.
	OAuthTimestamp *time.Time

	// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
	// uniquely generated for each request. Will be generated automatically
	// when it is not set.
	OAuthNonce string `q:"oauth_nonce"`
}

// ToOAuth1CreateAccessTokenHeaders formats a CreateAccessTokenOpts into a map of
// request headers.
func (opts CreateAccessTokenOpts) ToOAuth1CreateAccessTokenHeaders(method, u string) (map[string]string, error) {
	q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "")
	if err != nil {
		return nil, err
	}

	signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret}
	stringToSign := buildStringToSign(method, u, q.Query())
	signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
	authHeader := buildAuthHeader(q.Query(), signature)

	headers := map[string]string{
		"Authorization": authHeader,
	}

	return headers, nil
}

// CreateAccessToken creates a new OAuth1 Access Token
func CreateAccessToken(client *gophercloud.ServiceClient, opts CreateAccessTokenOptsBuilder) (r TokenResult) {
	h, err := opts.ToOAuth1CreateAccessTokenHeaders("POST", createAccessTokenURL(client))
	if err != nil {
		r.Err = err
		return
	}

	resp, err := client.Post(createAccessTokenURL(client), nil, nil, &gophercloud.RequestOpts{
		MoreHeaders:      h,
		OkCodes:          []int{201},
		KeepResponseBody: true,
	})
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	if r.Err != nil {
		return
	}
	defer resp.Body.Close()
	if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType {
		r.Err = fmt.Errorf("unsupported Content-Type: %q", v)
		return
	}
	r.Body, r.Err = ioutil.ReadAll(resp.Body)
	return
}

// GetAccessToken retrieves details on a single OAuth1 access token by an ID.
func GetAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r GetAccessTokenResult) {
	resp, err := client.Get(userAccessTokenURL(client, userID, id), &r.Body, nil)
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// RevokeAccessToken revokes an OAuth1 access token.
func RevokeAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r RevokeAccessTokenResult) {
	resp, err := client.Delete(userAccessTokenURL(client, userID, id), nil)
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// ListAccessTokens enumerates authorized access tokens.
func ListAccessTokens(client *gophercloud.ServiceClient, userID string) pagination.Pager {
	url := userAccessTokensURL(client, userID)
	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
		return AccessTokensPage{pagination.LinkedPageBase{PageResult: r}}
	})
}

// ListAccessTokenRoles enumerates authorized access token roles.
func ListAccessTokenRoles(client *gophercloud.ServiceClient, userID string, id string) pagination.Pager {
	url := userAccessTokenRolesURL(client, userID, id)
	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
		return AccessTokenRolesPage{pagination.LinkedPageBase{PageResult: r}}
	})
}

// GetAccessTokenRole retrieves details on a single OAuth1 access token role by
// an ID.
func GetAccessTokenRole(client *gophercloud.ServiceClient, userID string, id string, roleID string) (r GetAccessTokenRoleResult) {
	resp, err := client.Get(userAccessTokenRoleURL(client, userID, id, roleID), &r.Body, nil)
	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
	return
}

// The following are small helper functions used to help build the signature.

// buildOAuth1QueryString builds a URLEncoded parameters string specific for
// OAuth1-based requests.
func buildOAuth1QueryString(opts interface{}, timestamp *time.Time, callback string) (*url.URL, error) {
	q, err := gophercloud.BuildQueryString(opts)
	if err != nil {
		return nil, err
	}

	query := q.Query()

	if timestamp != nil {
		// use provided timestamp
		query.Set("oauth_timestamp", strconv.FormatInt(timestamp.Unix(), 10))
	} else {
		// use current timestamp
		query.Set("oauth_timestamp", strconv.FormatInt(time.Now().UTC().Unix(), 10))
	}

	if query.Get("oauth_nonce") == "" {
		// when nonce is not set, generate a random one
		query.Set("oauth_nonce", strconv.FormatInt(rand.Int63(), 10)+query.Get("oauth_timestamp"))
	}

	if callback != "" {
		query.Set("oauth_callback", callback)
	}
	query.Set("oauth_version", "1.0")

	return &url.URL{RawQuery: query.Encode()}, nil
}

// buildStringToSign builds a string to be signed.
func buildStringToSign(method string, u string, query url.Values) []byte {
	parsedURL, _ := url.Parse(u)
	p := parsedURL.Port()
	s := parsedURL.Scheme

	// Default scheme port must be stripped
	if s == "http" && p == "80" || s == "https" && p == "443" {
		parsedURL.Host = strings.TrimSuffix(parsedURL.Host, ":"+p)
	}

	// Ensure that URL doesn't contain queries
	parsedURL.RawQuery = ""

	v := strings.Join(
		[]string{method, url.QueryEscape(parsedURL.String()), url.QueryEscape(query.Encode())}, "&")

	return []byte(v)
}

// signString signs a string using an OAuth1 signature method.
func signString(signatureMethod SignatureMethod, strToSign []byte, signatureKeys []string) string {
	var key []byte
	for i, k := range signatureKeys {
		key = append(key, []byte(url.QueryEscape(k))...)
		if i == 0 {
			key = append(key, '&')
		}
	}

	var signedString string
	switch signatureMethod {
	case PLAINTEXT:
		signedString = string(key)
	default:
		h := hmac.New(sha1.New, key)
		h.Write(strToSign)
		signedString = base64.StdEncoding.EncodeToString(h.Sum(nil))
	}

	return signedString
}

// buildAuthHeader generates an OAuth1 Authorization header with a signature
// calculated using an OAuth1 signature method.
func buildAuthHeader(query url.Values, signature string) string {
	var authHeader []string
	var keys []string
	for k := range query {
		keys = append(keys, k)
	}
	sort.Strings(keys)

	for _, k := range keys {
		for _, v := range query[k] {
			authHeader = append(authHeader, fmt.Sprintf("%s=%q", k, url.QueryEscape(v)))
		}
	}

	authHeader = append(authHeader, fmt.Sprintf("oauth_signature=%q", signature))

	return "OAuth " + strings.Join(authHeader, ", ")
}