-
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
3 changed files
with
292 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
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.Credentials{ | ||
Access: "a7f1e798b7c2417cba4a02de97dc3cdc", | ||
Secret: "18f4f6761ada4e3795fa5273c30349b9", | ||
} | ||
token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken() | ||
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,263 @@ | ||
package ec2tokens | ||
|
||
import ( | ||
"crypto/hmac" | ||
"crypto/sha1" | ||
"crypto/sha256" | ||
"encoding/hex" | ||
"encoding/json" | ||
"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" | ||
) | ||
|
||
// Credentials represents options for authenticating a user using EC2 credentials. | ||
type Credentials struct { | ||
// EC2 access ID | ||
Access string `json:"access" required:"true"` | ||
// EC2 access secret, used to calculate signature only | ||
Secret string `json:"-" required:"true"` | ||
// Optional parameters | ||
Host string `json:"host"` | ||
Path string `json:"path"` | ||
Verb string `json:"verb"` | ||
Headers *map[string]string `json:"headers,omitempty"` | ||
Region string `json:"-"` | ||
Service string `json:"-"` | ||
Params Params `json:"params"` | ||
AllowReauth bool `json:"-"` | ||
signature | ||
} | ||
|
||
type signature struct { | ||
// can be either a []byte (encoded to base64 automatically) or a string | ||
Signature interface{} `json:"signature"` | ||
BodyHash *string `json:"body_hash,omitempty"` | ||
timestamp time.Time `json:"-"` | ||
} | ||
|
||
type unexported struct { | ||
Date string `json:"X-Amz-Date,omitempty"` | ||
Algorithm string `json:"X-Amz-Algorithm,omitempty"` | ||
Credential string `json:"X-Amz-Credential,omitempty"` | ||
} | ||
|
||
type Params struct { | ||
unexported | ||
SignedHeaders *string `json:"X-Amz-SignedHeaders,omitempty"` | ||
// signature v2 | ||
SignatureMethod string `json:"SignatureMethod,omitempty"` | ||
SignatureVersion string `json:"SignatureVersion,omitempty"` | ||
} | ||
|
||
func canonicalQs(opts *Credentials) string { | ||
var params map[string]string | ||
s, _ := json.Marshal(opts.Params) | ||
json.Unmarshal(s, ¶ms) | ||
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 paramsToQs(opts *Credentials) string { | ||
strToSign := strings.Join([]string{ | ||
opts.Verb, | ||
opts.Host, | ||
opts.Path, | ||
}, "\n") | ||
|
||
return strings.Join([]string{ | ||
strToSign, | ||
canonicalQs(opts), | ||
}, "\n") | ||
} | ||
|
||
func paramsToQs4(opts *Credentials) string { | ||
if opts.Verb == "POST" { | ||
return "" | ||
} | ||
return canonicalQs(opts) | ||
} | ||
|
||
func canonicalHeaders(opts *Credentials) string { | ||
headersLower := make(map[string]string, len(*opts.Headers)) | ||
for k, v := range *opts.Headers { | ||
headersLower[strings.ToLower(k)] = v | ||
} | ||
|
||
var headersList []string | ||
for _, h := range strings.Split(*opts.Params.SignedHeaders, ";") { | ||
if v, ok := headersLower[h]; !ok { | ||
continue | ||
} else { | ||
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 v4SignatureKey(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 v4Signature(opts *Credentials) string { | ||
scope := strings.Join([]string{ | ||
opts.timestamp.Format(yyyymmdd), | ||
opts.Region, | ||
opts.Service, | ||
aws4Request, | ||
}, "/") | ||
|
||
canonicalRequest := strings.Join([]string{ | ||
opts.Verb, | ||
opts.Path, | ||
paramsToQs4(opts), | ||
canonicalHeaders(opts), | ||
*opts.Params.SignedHeaders, | ||
*opts.BodyHash, | ||
}, "\n") | ||
t := sha256.Sum256([]byte(canonicalRequest)) | ||
|
||
strToSign := strings.Join([]string{ | ||
v4Algo, | ||
opts.Params.Date, | ||
scope, | ||
hex.EncodeToString(t[:]), | ||
}, "\n") | ||
|
||
key := v4SignatureKey(opts.Secret, opts.timestamp.Format(yyyymmdd), opts.Region, opts.Service) | ||
|
||
return hex.EncodeToString(sumHMAC256(key, []byte(strToSign))) | ||
} | ||
|
||
func generateBodyHash() *string { | ||
h := make([]byte, 64) | ||
rand.Read(h) | ||
bodyHash := hex.EncodeToString(h) | ||
return &bodyHash | ||
} | ||
|
||
// ToTokenV3CreateMap builds a scope request body from AuthOptions. | ||
func (opts *Credentials) ToTokenV3ScopeMap() (map[string]interface{}, error) { | ||
return nil, nil | ||
} | ||
|
||
func (opts *Credentials) CanReauth() bool { | ||
return opts.AllowReauth | ||
} | ||
|
||
func CalculateSignature(opts *Credentials) error { | ||
switch opts.Params.SignatureVersion { | ||
case "2": // signature v2 | ||
if opts.Headers != nil { | ||
opts.Headers = nil | ||
} | ||
if opts.Params.SignedHeaders != nil { | ||
opts.Params.SignedHeaders = nil | ||
} | ||
strToSign := paramsToQs(opts) | ||
switch opts.Params.SignatureMethod { | ||
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 | ||
opts.Signature = sumHMAC1([]byte(opts.Secret), []byte(strToSign)) | ||
case v2AlgoSha256: | ||
opts.Signature = sumHMAC256([]byte(opts.Secret), []byte(strToSign)) | ||
default: | ||
return fmt.Errorf("unsupported signature method: %s", opts.Params.SignatureMethod) | ||
} | ||
case "": // signature v4 | ||
opts.timestamp = time.Now().UTC() | ||
opts.BodyHash = generateBodyHash() | ||
opts.Params.Date = opts.timestamp.Format(iso8601utc) | ||
opts.Params.Algorithm = v4Algo | ||
opts.Params.Credential = strings.Join([]string{ | ||
opts.Access, | ||
opts.timestamp.Format(yyyymmdd), | ||
opts.Region, | ||
opts.Service, | ||
aws4Request, | ||
}, "/") | ||
if opts.Headers == nil { | ||
h := make(map[string]string) | ||
opts.Headers = &h | ||
} | ||
if opts.Params.SignedHeaders == nil { | ||
opts.Params.SignedHeaders = new(string) | ||
} | ||
opts.Signature = v4Signature(opts) | ||
default: | ||
return fmt.Errorf("unsupported signature version: %s", opts.Params.SignatureVersion) | ||
} | ||
return nil | ||
} | ||
|
||
// ToCredentialsCreateMap formats a Credentials into a create request. | ||
func (opts Credentials) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { | ||
err := CalculateSignature(&opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return gophercloud.BuildRequestBody(opts, "credentials") | ||
} | ||
|
||
// 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 | ||
} |
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,7 @@ | ||
package ec2tokens | ||
|
||
import "github.com/gophercloud/gophercloud" | ||
|
||
func ec2tokensURL(c *gophercloud.ServiceClient) string { | ||
return c.ServiceURL("ec2tokens") | ||
} |