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 }