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, ", ") }