-
Notifications
You must be signed in to change notification settings - Fork 515
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auth: introduce ec2 credentials auth support
- Loading branch information
Showing
5 changed files
with
559 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
Package tokens provides information and interaction with the EC2 token API | ||
resource for the OpenStack Identity service. | ||
For more information, see: | ||
https://docs.openstack.org/api-ref/identity/v2-ext/ | ||
Example to Create a Token From an EC2 access and secret keys | ||
var authOptions tokens.AuthOptionsBuilder | ||
authOptions = &ec2tokens.AuthOptions{ | ||
Access: "a7f1e798b7c2417cba4a02de97dc3cdc", | ||
Secret: "18f4f6761ada4e3795fa5273c30349b9", | ||
} | ||
token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken() | ||
if err != nil { | ||
panic(err) | ||
} | ||
Example to auth a client using EC2 access and secret keys | ||
client, err := openstack.NewClient("http://localhost:5000/v3") | ||
if err != nil { | ||
panic(err) | ||
} | ||
var authOptions tokens.AuthOptionsBuilder | ||
authOptions = &ec2tokens.AuthOptions{ | ||
Access: "a7f1e798b7c2417cba4a02de97dc3cdc", | ||
Secret: "18f4f6761ada4e3795fa5273c30349b9", | ||
AllowReauth: true, | ||
} | ||
err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
*/ | ||
package ec2tokens |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,279 @@ | ||
package ec2tokens | ||
|
||
import ( | ||
"crypto/hmac" | ||
"crypto/sha1" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"fmt" | ||
"math/rand" | ||
"net/url" | ||
"sort" | ||
"strings" | ||
"time" | ||
|
||
"github.com/gophercloud/gophercloud" | ||
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" | ||
) | ||
|
||
const ( | ||
aws4Request = "aws4_request" | ||
v2AlgoSha1 = "HmacSHA1" | ||
v2AlgoSha256 = "HmacSHA256" | ||
v4Algo = "AWS4-HMAC-SHA256" | ||
iso8601utc = "20060102T150405Z" | ||
rfc3339utc = "2006-01-02T15:04:05Z" | ||
yyyymmdd = "20060102" | ||
) | ||
|
||
// AuthOptions represents options for authenticating a user using EC2 credentials. | ||
type AuthOptions struct { | ||
// EC2 access ID. | ||
Access string `json:"access" required:"true"` | ||
// EC2 access secret, used to calculate signature only. | ||
// Not used, when a Signature is provided. | ||
Secret string `json:"-"` | ||
// Optional parameters. | ||
Host string `json:"host"` | ||
Path string `json:"path"` | ||
Verb string `json:"verb"` | ||
Headers map[string]string `json:"headers"` | ||
Region string `json:"-"` | ||
Service string `json:"-"` | ||
Params map[string]string `json:"params"` | ||
// Allows Gophercloud to re-authenticate automatically if/when your | ||
// token expires. | ||
AllowReauth bool `json:"-"` | ||
// Can be either a []byte (encoded to base64 automatically) or a | ||
// string. You can set the singature explicitly, when you already know | ||
// it. | ||
Signature interface{} `json:"signature"` | ||
// Optional random hash, when signature is nil. | ||
BodyHash *string `json:"body_hash"` | ||
// Optional timestamp to calculate a V4 signature. | ||
Timestamp *time.Time `json:"-"` | ||
} | ||
|
||
func canonicalQsV2(params map[string]string) string { | ||
var keys []string | ||
for k := range params { | ||
keys = append(keys, k) | ||
} | ||
sort.Strings(keys) | ||
|
||
var pairs []string | ||
for _, k := range keys { | ||
pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k]))) | ||
} | ||
|
||
return strings.Join(pairs, "&") | ||
} | ||
|
||
func stringToSignV2(opts AuthOptions, params map[string]string) string { | ||
stringToSign := strings.Join([]string{ | ||
opts.Verb, | ||
opts.Host, | ||
opts.Path, | ||
}, "\n") | ||
|
||
return strings.Join([]string{ | ||
stringToSign, | ||
canonicalQsV2(params), | ||
}, "\n") | ||
} | ||
|
||
func canonicalQsV4(verb string, params map[string]string) string { | ||
if verb == "POST" { | ||
return "" | ||
} | ||
return canonicalQsV2(params) | ||
} | ||
|
||
func canonicalHeadersV4(headers map[string]string, signedHeaders string) string { | ||
headersLower := make(map[string]string, len(headers)) | ||
for k, v := range headers { | ||
headersLower[strings.ToLower(k)] = v | ||
} | ||
|
||
var headersList []string | ||
for _, h := range strings.Split(signedHeaders, ";") { | ||
if v, ok := headersLower[h]; ok { | ||
headersList = append(headersList, h+":"+v) | ||
} | ||
} | ||
|
||
return strings.Join(headersList, "\n") + "\n" | ||
} | ||
|
||
func sumHMAC1(key []byte, data []byte) []byte { | ||
hash := hmac.New(sha1.New, key) | ||
hash.Write(data) | ||
return hash.Sum(nil) | ||
} | ||
|
||
func sumHMAC256(key []byte, data []byte) []byte { | ||
hash := hmac.New(sha256.New, key) | ||
hash.Write(data) | ||
return hash.Sum(nil) | ||
} | ||
|
||
func signatureKeyV4(secret, date, region, service string) []byte { | ||
kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date)) | ||
kRegion := sumHMAC256(kDate, []byte(region)) | ||
kService := sumHMAC256(kRegion, []byte(service)) | ||
return sumHMAC256(kService, []byte(aws4Request)) | ||
} | ||
|
||
func signatureV4(opts AuthOptions, params map[string]string, date time.Time, bodyHash string) string { | ||
scope := strings.Join([]string{ | ||
date.Format(yyyymmdd), | ||
opts.Region, | ||
opts.Service, | ||
aws4Request, | ||
}, "/") | ||
|
||
canonicalRequest := strings.Join([]string{ | ||
opts.Verb, | ||
opts.Path, | ||
canonicalQsV4(opts.Verb, params), | ||
canonicalHeadersV4(opts.Headers, params["X-Amz-SignedHeaders"]), | ||
params["X-Amz-SignedHeaders"], | ||
bodyHash, | ||
}, "\n") | ||
hash := sha256.Sum256([]byte(canonicalRequest)) | ||
|
||
strToSign := strings.Join([]string{ | ||
v4Algo, | ||
date.Format(iso8601utc), | ||
scope, | ||
hex.EncodeToString(hash[:]), | ||
}, "\n") | ||
|
||
key := signatureKeyV4(opts.Secret, date.Format(yyyymmdd), opts.Region, opts.Service) | ||
|
||
return hex.EncodeToString(sumHMAC256(key, []byte(strToSign))) | ||
} | ||
|
||
func randomBodyHash() string { | ||
h := make([]byte, 64) | ||
rand.Read(h) | ||
return hex.EncodeToString(h) | ||
} | ||
|
||
func paramsToMap(c map[string]interface{}) map[string]string { | ||
// convert map[string]interface{} to map[string]string | ||
p := make(map[string]string) | ||
if v, _ := c["params"].(map[string]interface{}); v != nil { | ||
for k, v := range v { | ||
p[k] = v.(string) | ||
} | ||
} | ||
|
||
c["params"] = p | ||
|
||
return p | ||
} | ||
|
||
// ToTokenV3CreateMap is a dummy method to satisfy tokens.AuthOptionsBuilder interface | ||
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { | ||
return nil, nil | ||
} | ||
|
||
// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface | ||
func (opts *AuthOptions) CanReauth() bool { | ||
return opts.AllowReauth | ||
} | ||
|
||
// ToAuthOptionsCreateMap formats an AuthOptions into a create request. | ||
func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { | ||
b, err := gophercloud.BuildRequestBody(opts, "credentials") | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if opts.Signature != nil { | ||
return b, nil | ||
} | ||
|
||
// calculate signature, when it is not set | ||
c, _ := b["credentials"].(map[string]interface{}) | ||
p := paramsToMap(c) | ||
|
||
if v, ok := p["SignatureVersion"]; ok && v == "2" { | ||
// signature v2 | ||
if _, ok := c["body_hash"]; ok { | ||
delete(c, "body_hash") | ||
} | ||
if _, ok := c["headers"]; ok { | ||
delete(c, "headers") | ||
} | ||
if v, ok := p["SignatureMethod"]; ok { | ||
// params is a map of strings | ||
strToSign := stringToSignV2(*opts, p) | ||
switch v { | ||
case v2AlgoSha1: | ||
// keystone uses this method only when HmacSHA256 is not available on the server side | ||
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156 | ||
c["signature"] = sumHMAC1([]byte(opts.Secret), []byte(strToSign)) | ||
return b, nil | ||
case v2AlgoSha256: | ||
c["signature"] = sumHMAC256([]byte(opts.Secret), []byte(strToSign)) | ||
return b, nil | ||
} | ||
return nil, fmt.Errorf("unsupported signature method: %s", v) | ||
} | ||
return nil, fmt.Errorf("signature method must be provided") | ||
} else if ok { | ||
return nil, fmt.Errorf("unsupported signature version: %s", v) | ||
} | ||
|
||
// signature v4 | ||
date := time.Now().UTC() | ||
if opts.Timestamp != nil { | ||
date = *opts.Timestamp | ||
} | ||
if v, _ := c["body_hash"]; v == nil { | ||
// when body_hash is not set, generate a random one | ||
c["body_hash"] = randomBodyHash() | ||
} | ||
if v, _ := c["headers"]; v == nil { | ||
// when headers is not set, make an empty map | ||
h := make(map[string]string) | ||
c["headers"] = &h | ||
} | ||
if _, ok := p["X-Amz-SignedHeaders"]; !ok { | ||
// when X-Amz-SignedHeaders is not set, make an empty string | ||
p["X-Amz-SignedHeaders"] = "" | ||
} | ||
p["X-Amz-Date"] = date.Format(iso8601utc) | ||
p["X-Amz-Algorithm"] = v4Algo | ||
p["X-Amz-Credential"] = strings.Join([]string{ | ||
opts.Access, | ||
date.Format(yyyymmdd), | ||
opts.Region, | ||
opts.Service, | ||
aws4Request, | ||
}, "/") | ||
c["signature"] = signatureV4(*opts, p, date, c["body_hash"].(string)) | ||
|
||
return b, nil | ||
} | ||
|
||
// Create authenticates and either generates a new token from EC2 credentials | ||
func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { | ||
b, err := opts.ToTokenV3CreateMap(nil) | ||
if err != nil { | ||
r.Err = err | ||
return | ||
} | ||
|
||
resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ | ||
MoreHeaders: map[string]string{"X-Auth-Token": ""}, | ||
OkCodes: []int{200}, | ||
}) | ||
r.Err = err | ||
if resp != nil { | ||
r.Header = resp.Header | ||
} | ||
return | ||
} |
Oops, something went wrong.