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 }