mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Add AWS KMS support for OAuth2 Client Credentials JWT authentication
This implementaion adds new configuration properties to "oauth2" aws_kms: AWS KMS key details aws_signing: Infomation for signing AWS requestion, similar to s3_signing References: 1) https://github.com/go-jose/go-jose/blob/v3/asymmetric.go#L501 2) https://github.com/codelittinc/gobitauth/blob/master/sign.go#L101 Signed-off-by: Prasanth Ullattil <prasanth.ullattil@dnb.no>
This commit is contained in:
committed by
Ashutosh Narkar
parent
f74a5f61d8
commit
db2f8ae7bb
+200
-15
@@ -8,15 +8,20 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -169,6 +174,97 @@ type tokenEndpointResponse struct {
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
type awsKmsKeyConfig struct {
|
||||
Name string `json:"name"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
func convertSignatureToBase64(alg string, der []byte) (string, error) {
|
||||
r, s, derErr := pointsFromDER(der)
|
||||
if derErr != nil {
|
||||
return "", fmt.Errorf("failed to read points from der %v", derErr)
|
||||
}
|
||||
|
||||
signatureData, err := convertPointsToBase64(alg, r.Bytes(), s.Bytes())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return signatureData, nil
|
||||
}
|
||||
|
||||
func pointsFromDER(der []byte) (R, S *big.Int, err error) {
|
||||
R, S = &big.Int{}, &big.Int{}
|
||||
data := asn1.RawValue{}
|
||||
if _, err := asn1.Unmarshal(der, &data); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshall the signature from DER format %v", err)
|
||||
|
||||
}
|
||||
// https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_ResponseSyntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3
|
||||
// The format of our DER string is 0x02 + rlen + r + 0x02 + slen + s
|
||||
rLen := data.Bytes[1] // The entire length of R + offset of 2 for 0x02 and rlen
|
||||
r := data.Bytes[2 : rLen+2]
|
||||
// Ignore the next 0x02 and slen bytes and just take the start of S to the end of the byte array
|
||||
s := data.Bytes[rLen+4:]
|
||||
R.SetBytes(r)
|
||||
S.SetBytes(s)
|
||||
return
|
||||
}
|
||||
|
||||
func convertPointsToBase64(alg string, r, s []byte) (string, error) {
|
||||
curveBits, err := retrieveCurveBits(alg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keyBytes := curveBits / 8
|
||||
if curveBits%8 > 0 {
|
||||
keyBytes++
|
||||
}
|
||||
// We serialize the outputs (r and s) into big-endian byte arrays and pad
|
||||
// them with zeros on the left to make sure the sizes work out. Both arrays
|
||||
// must be keyBytes long, and the output must be 2*keyBytes long.
|
||||
rBytesPadded := make([]byte, keyBytes)
|
||||
copy(rBytesPadded[keyBytes-len(r):], r)
|
||||
sBytesPadded := make([]byte, keyBytes)
|
||||
copy(sBytesPadded[keyBytes-len(s):], s)
|
||||
signatureEnc := append(rBytesPadded, sBytesPadded...)
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(signatureEnc), nil
|
||||
}
|
||||
|
||||
func retrieveCurveBits(alg string) (int, error) {
|
||||
var curveBits int
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
curveBits = 256
|
||||
case "ECDSA_SHA_384":
|
||||
curveBits = 384
|
||||
case "ECDSA_SHA_512":
|
||||
curveBits = 512
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
return curveBits, nil
|
||||
}
|
||||
|
||||
func messageDigest(message []byte, alg string) ([]byte, error) {
|
||||
var digest hash.Hash
|
||||
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
digest = sha256.New()
|
||||
case "ECDSA_SHA_384":
|
||||
digest = sha512.New384()
|
||||
case "ECDSA_SHA_512":
|
||||
digest = sha512.New()
|
||||
default:
|
||||
return []byte{}, fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
|
||||
digest.Write(message)
|
||||
return digest.Sum(nil), nil
|
||||
}
|
||||
|
||||
// oauth2ClientCredentialsAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
|
||||
// obtained through the OAuth2 client credentials flow
|
||||
type oauth2ClientCredentialsAuthPlugin struct {
|
||||
@@ -183,6 +279,8 @@ type oauth2ClientCredentialsAuthPlugin struct {
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
AdditionalHeaders map[string]string `json:"additional_headers,omitempty"`
|
||||
AdditionalParameters map[string]string `json:"additional_parameters,omitempty"`
|
||||
AWSKmsKey *awsKmsKeyConfig `json:"aws_kms,omitempty"`
|
||||
AWSSigningPlugin *awsSigningAuthPlugin `json:"aws_signing,omitempty"`
|
||||
|
||||
signingKey *keys.Config
|
||||
signingKeyParsed interface{}
|
||||
@@ -196,7 +294,7 @@ type oauth2Token struct {
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(claims map[string]interface{}, signingKey interface{}) (*string, error) {
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(ctx context.Context, claims map[string]interface{}, signingKey interface{}) (*string, error) {
|
||||
now := time.Now()
|
||||
baseClaims := map[string]interface{}{
|
||||
"iat": now.Unix(),
|
||||
@@ -227,22 +325,35 @@ func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(claims map[string]int
|
||||
}
|
||||
|
||||
var jwsHeaders []byte
|
||||
var signatureAlg string
|
||||
if ap.AWSKmsKey == nil {
|
||||
signatureAlg = ap.signingKey.Algorithm
|
||||
} else {
|
||||
signatureAlg, err = ap.mapKMSAlgToSign(ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if ap.Thumbprint != "" {
|
||||
bytes, err := hex.DecodeString(ap.Thumbprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x5t := base64.URLEncoding.EncodeToString(bytes)
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s","x5t":"%s"}`, ap.signingKey.Algorithm, x5t))
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s","x5t":"%s"}`, signatureAlg, x5t))
|
||||
} else {
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s"}`, ap.signingKey.Algorithm))
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s"}`, signatureAlg))
|
||||
}
|
||||
var jwsCompact []byte
|
||||
if ap.AWSKmsKey == nil {
|
||||
jwsCompact, err = jws.SignLiteral(payload,
|
||||
jwa.SignatureAlgorithm(signatureAlg),
|
||||
signingKey,
|
||||
jwsHeaders,
|
||||
rand.Reader)
|
||||
} else {
|
||||
jwsCompact, err = ap.SignWithKMS(ctx, payload, jwsHeaders)
|
||||
}
|
||||
|
||||
jwsCompact, err := jws.SignLiteral(payload,
|
||||
jwa.SignatureAlgorithm(ap.signingKey.Algorithm),
|
||||
signingKey,
|
||||
jwsHeaders,
|
||||
rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -251,6 +362,55 @@ func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(claims map[string]int
|
||||
return &jwt, nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) mapKMSAlgToSign(alg string) (string, error) {
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
return "ES256", nil
|
||||
case "ECDSA_SHA_384":
|
||||
return "ES384", nil
|
||||
case "ECDSA_SHA_512":
|
||||
return "ES512", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
}
|
||||
|
||||
// SignWithKMS will sign the JWT in AWS using the key stored in the supplied kmsArn
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) SignWithKMS(ctx context.Context, payload []byte, hdrBuf []byte) ([]byte, error) {
|
||||
|
||||
encodedHdr := base64.RawURLEncoding.EncodeToString(hdrBuf)
|
||||
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
|
||||
input := strings.Join(
|
||||
[]string{
|
||||
encodedHdr,
|
||||
encodedPayload,
|
||||
}, ".",
|
||||
)
|
||||
digest, err := messageDigest([]byte(input), ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ap.AWSSigningPlugin != nil {
|
||||
signature, err := ap.AWSSigningPlugin.SignDigest(ctx, digest, ap.AWSKmsKey.Name, ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatureData, err := convertSignatureToBase64(ap.AWSKmsKey.Algorithm, der)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedAssertion := input + "." + signatureData
|
||||
|
||||
return []byte(signedAssertion), nil
|
||||
}
|
||||
return nil, errors.New("missing AWS credentials, failed to sign the assertion with kms")
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) parseSigningKey(c Config) (err error) {
|
||||
if ap.SigningKeyID == "" {
|
||||
return errors.New("signing_key required for jwt_bearer grant type")
|
||||
@@ -302,12 +462,23 @@ func (ap *oauth2ClientCredentialsAuthPlugin) NewClient(c Config) (*http.Client,
|
||||
return nil, errors.New("token_url required to use https scheme")
|
||||
}
|
||||
if ap.GrantType == grantTypeClientCredentials {
|
||||
if ap.ClientSecret != "" && ap.SigningKeyID != "" {
|
||||
return nil, errors.New("can only use one of client_secret and signing_key for client_credentials")
|
||||
if ap.AWSKmsKey != nil && (ap.ClientSecret != "" || ap.SigningKeyID != "") ||
|
||||
(ap.ClientSecret != "" && ap.SigningKeyID != "") {
|
||||
return nil, errors.New("can only use one of client_secret, signing_key or signing_kms_key for client_credentials")
|
||||
}
|
||||
if ap.SigningKeyID == "" && (ap.ClientID == "" || ap.ClientSecret == "") {
|
||||
if ap.SigningKeyID == "" && ap.AWSKmsKey == nil && (ap.ClientID == "" || ap.ClientSecret == "") {
|
||||
return nil, errors.New("client_id and client_secret required")
|
||||
}
|
||||
if ap.AWSKmsKey != nil {
|
||||
if ap.AWSSigningPlugin == nil {
|
||||
return nil, errors.New("aws_kms and aws_signing required")
|
||||
}
|
||||
// initialize the awsSigningAuthPlugin
|
||||
_, err = ap.AWSSigningPlugin.NewClient(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
@@ -320,7 +491,7 @@ func (ap *oauth2ClientCredentialsAuthPlugin) NewClient(c Config) (*http.Client,
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) requestToken(ctx context.Context) (*oauth2Token, error) {
|
||||
body := url.Values{}
|
||||
if ap.GrantType == grantTypeJwtBearer {
|
||||
authJwt, err := ap.createAuthJWT(ap.Claims, ap.signingKeyParsed)
|
||||
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -329,8 +500,8 @@ func (ap *oauth2ClientCredentialsAuthPlugin) requestToken(ctx context.Context) (
|
||||
} else {
|
||||
body.Add("grant_type", grantTypeClientCredentials)
|
||||
|
||||
if ap.SigningKeyID != "" {
|
||||
authJwt, err := ap.createAuthJWT(ap.Claims, ap.signingKeyParsed)
|
||||
if ap.SigningKeyID != "" || ap.AWSKmsKey != nil {
|
||||
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -536,6 +707,7 @@ type awsSigningAuthPlugin struct {
|
||||
AWSSignatureVersion string `json:"signature_version,omitempty"`
|
||||
|
||||
ecrAuthPlugin *ecrAuthPlugin
|
||||
kmsSignPlugin *awsKMSSignPlugin
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
@@ -680,6 +852,10 @@ func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error
|
||||
if ap.AWSService == "ecr" {
|
||||
return errors.New(`aws service "ecr" must be used with service type "oci"`)
|
||||
}
|
||||
if ap.AWSService == "kms" && ap.kmsSignPlugin == nil {
|
||||
// We need a special plugin for KMS.
|
||||
ap.kmsSignPlugin = newKMSSignPlugin(ap)
|
||||
}
|
||||
if ap.AWSService == "" {
|
||||
ap.AWSService = awsSigv4SigningDefaultService
|
||||
}
|
||||
@@ -691,3 +867,12 @@ func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
|
||||
switch ap.AWSService {
|
||||
case "kms":
|
||||
return ap.kmsSignPlugin.SignDigest(ctx, digest, keyID, signingAlgorithm)
|
||||
default:
|
||||
return "", fmt.Errorf(`cannot use SignDigest with aws service %q`, ap.AWSService)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,3 +91,47 @@ func TestECRWithoutOCIFails(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOauth2WithAWSKMS(t *testing.T) {
|
||||
conf := `{
|
||||
"name": "foo",
|
||||
"url": "http://localhost",
|
||||
"credentials": {
|
||||
"oauth2": {
|
||||
"grant_type": "client_credentials",
|
||||
"aws_kms": {
|
||||
"name": "arn:aws:kms:eu-west-1:account_no:key/key_id",
|
||||
"algorithm": "ECDSA_SHA_256"
|
||||
},
|
||||
"aws_signing": {
|
||||
"service": "kms",
|
||||
"environment_credentials": {
|
||||
"aws_default_region": "eu-west-1"
|
||||
}
|
||||
},
|
||||
"token_url": "https://localhost",
|
||||
"scopes": ["profile", "opa"],
|
||||
"additional_claims": {
|
||||
"aud": "some audience"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
client, err := New([]byte(conf), map[string]*keys.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("New() = %v", err)
|
||||
}
|
||||
|
||||
if _, err := client.config.Credentials.OAuth2.NewClient(client.config); err != nil {
|
||||
t.Fatalf("OAuth2.NewClient() = %q", err)
|
||||
}
|
||||
|
||||
if client.config.Credentials.OAuth2.AWSKmsKey.Name != "arn:aws:kms:eu-west-1:account_no:key/key_id" {
|
||||
t.Errorf("OAuth2.AWSKmsKey.Name = %v, want = %v", client.config.Credentials.OAuth2.AWSKmsKey.Name, "arn:aws:kms:eu-west-1:account_no:key/key_id")
|
||||
}
|
||||
|
||||
if client.config.Credentials.OAuth2.AWSSigningPlugin.kmsSignPlugin == nil {
|
||||
t.Errorf("OAuth2.AWSSigningPlugin.kmsSignPlugin isn't setup")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,3 +517,41 @@ func (ap *ecrAuthPlugin) refreshAuthorizationToken(ctx context.Context) error {
|
||||
ap.token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// awsKMSSignPlugin signs digests using AWS KMS.
|
||||
type awsKMSSignPlugin struct {
|
||||
|
||||
// awsAuthPlugin is used to sign kms sign requests.
|
||||
awsAuthPlugin *awsSigningAuthPlugin
|
||||
|
||||
// kms represents the service for signing digests.
|
||||
kms awskms
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type awskms interface {
|
||||
SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string, creds aws.Credentials, signatureVersion string) (string, error)
|
||||
}
|
||||
|
||||
func newKMSSignPlugin(ap *awsSigningAuthPlugin) *awsKMSSignPlugin {
|
||||
return &awsKMSSignPlugin{
|
||||
awsAuthPlugin: ap,
|
||||
kms: aws.NewKMS(ap.logger),
|
||||
logger: ap.logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *awsKMSSignPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
|
||||
creds, err := ap.awsAuthPlugin.awsCredentialService().credentials(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
signature, err := ap.kms.SignDigest(ctx, digest, keyID, signingAlgorithm, creds, ap.awsAuthPlugin.AWSSignatureVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("kms: failed to sign digest: %w", err)
|
||||
}
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
+175
-4
@@ -34,6 +34,7 @@ import (
|
||||
"github.com/open-policy-agent/opa/bundle"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jwa"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jws"
|
||||
"github.com/open-policy-agent/opa/internal/providers/aws"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
"github.com/open-policy-agent/opa/tracing"
|
||||
@@ -543,6 +544,55 @@ func TestNew(t *testing.T) {
|
||||
}
|
||||
}`, grantTypeClientCredentials),
|
||||
},
|
||||
{
|
||||
name: "Oauth2ClientCredentialsJWTAuthentication_with_AWS_KMS",
|
||||
input: fmt.Sprintf(`{
|
||||
"name": "foo",
|
||||
"url": "http://localhost",
|
||||
"credentials": {
|
||||
"oauth2": {
|
||||
"grant_type": %q,
|
||||
"aws_kms": {
|
||||
"name": "arn:aws:kms:eu-west-1:account_no:key/key_id",
|
||||
"algorithm": "ECDSA_SHA_256"
|
||||
},
|
||||
"aws_signing": {
|
||||
"service": "kms",
|
||||
"environment_credentials": {
|
||||
"aws_default_region": "eu-west-1"
|
||||
}
|
||||
},
|
||||
"token_url": "https://localhost",
|
||||
"scopes": ["profile", "opa"],
|
||||
"additional_claims": {
|
||||
"aud": "some audience"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, grantTypeClientCredentials),
|
||||
},
|
||||
{
|
||||
name: "Oauth2ClientCredentialsJWTAuthentication_with_AWS_KMS_missing_credentials",
|
||||
input: fmt.Sprintf(`{
|
||||
"name": "foo",
|
||||
"url": "http://localhost",
|
||||
"credentials": {
|
||||
"oauth2": {
|
||||
"grant_type": %q,
|
||||
"aws_kms": {
|
||||
"name": "arn:aws:kms:eu-west-1:account_no:key/key_id",
|
||||
"algorithm": "ECDSA_SHA_256"
|
||||
},
|
||||
"token_url": "https://localhost",
|
||||
"scopes": ["profile", "opa"],
|
||||
"additional_claims": {
|
||||
"aud": "some audience"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, grantTypeClientCredentials),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "S3WebIdentityMissingEnvVars",
|
||||
input: `{
|
||||
@@ -1915,6 +1965,7 @@ type oauth2TestServer struct {
|
||||
expScope *[]string
|
||||
expAlgorithm jwa.SignatureAlgorithm
|
||||
expX5t string
|
||||
expSignature string
|
||||
tokenType string
|
||||
tokenTTL int64
|
||||
invocations int32
|
||||
@@ -2110,11 +2161,17 @@ func (t *oauth2TestServer) handle(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
token = r.Form["client_assertion"][0]
|
||||
}
|
||||
_, err := jws.Verify([]byte(token), t.expAlgorithm, t.verificationKey)
|
||||
if err != nil {
|
||||
t.t.Fatalf("Unexpected signature verification error %v", err)
|
||||
if t.expSignature != "" {
|
||||
signature := strings.Split(token, ".")[2]
|
||||
if t.expSignature != signature {
|
||||
t.t.Errorf("Expected expSignature %v, got %v", t.expSignature, signature)
|
||||
}
|
||||
} else {
|
||||
_, err := jws.Verify([]byte(token), t.expAlgorithm, t.verificationKey)
|
||||
if err != nil {
|
||||
t.t.Fatalf("Unexpected signature verification error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if t.expX5t != "" {
|
||||
headerRaw, _ := base64.RawURLEncoding.DecodeString(strings.Split(token, ".")[0])
|
||||
var headers map[string]string
|
||||
@@ -2344,3 +2401,117 @@ func (m *myPluginMock) NewClient(c Config) (*http.Client, error) {
|
||||
func (*myPluginMock) Prepare(*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestOauth2ClientCredentialsGrantTypeWithKms(t *testing.T) {
|
||||
|
||||
// DER-encoded object from KMS as explained here: https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_ResponseSyntax
|
||||
derEncodeSignature := []byte{48, 68, 2, 32, 84, 124, 17, 255, 68, 181, 189, 159, 77, 235, 242, 88, 85, 139, 84, 111, 204, 108, 235, 90, 128, 220, 247, 176, 215, 28, 188, 110, 19, 158, 137, 30, 2, 32, 88, 17, 176, 72, 157, 42, 1, 223, 69, 41, 225, 77, 121, 13, 117, 132, 146, 243, 45, 208, 207, 119, 233, 156, 96, 94, 192, 174, 136, 218, 206, 84}
|
||||
// The signature representing the above object
|
||||
jwtSignature := "VHwR_0S1vZ9N6_JYVYtUb8xs61qA3Pew1xy8bhOeiR5YEbBInSoB30Up4U15DXWEkvMt0M936ZxgXsCuiNrOVA"
|
||||
|
||||
ts := testServer{t: t, expBearerToken: "token_1"}
|
||||
ts.start()
|
||||
defer ts.stop()
|
||||
|
||||
ots := oauth2TestServer{
|
||||
t: t,
|
||||
tokenTTL: 300,
|
||||
expScope: &[]string{"scope1", "scope2"},
|
||||
expJwtCredential: true,
|
||||
expAlgorithm: jwa.ES256,
|
||||
expGrantType: grantTypeClientCredentials,
|
||||
expSignature: jwtSignature,
|
||||
}
|
||||
ots.start()
|
||||
defer ots.stop()
|
||||
|
||||
kmsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var signRequest = &aws.KMSSignRequest{}
|
||||
if r.Body != nil {
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read kms sign request = %v", err)
|
||||
}
|
||||
defer r.Body.Close()
|
||||
err = json.Unmarshal(bodyBytes, signRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to unmarshall kms sign request = %v", err)
|
||||
}
|
||||
}
|
||||
responseFmt := `{"KeyId": "%s", "Signature": "%s", "SigningAlgorithm": "%s"}`
|
||||
responsePayload := fmt.Sprintf(responseFmt, signRequest.KeyID, base64.StdEncoding.EncodeToString(derEncodeSignature), signRequest.SigningAlgorithm)
|
||||
if _, err := io.WriteString(w, responsePayload); err != nil {
|
||||
t.Fatalf("io.WriteString(w, payload) = %v", err)
|
||||
}
|
||||
}))
|
||||
defer kmsServer.Close()
|
||||
|
||||
logger := logging.New()
|
||||
logger.SetLevel(logging.Debug)
|
||||
|
||||
kms := aws.NewKMSWithURLClient(kmsServer.URL, kmsServer.Client(), logger)
|
||||
client := newOauth2KmsClientCredentialsTestClient(t, &ts, &ots, kms)
|
||||
ctx := context.Background()
|
||||
_, err := client.Do(ctx, "GET", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
_, err = client.Do(ctx, "GET", "test")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create client to test ClientCredentials grant using KMS
|
||||
func newOauth2KmsClientCredentialsTestClient(t *testing.T, ts *testServer, ots *oauth2TestServer, kms *aws.KMS) *Client {
|
||||
config := fmt.Sprintf(`{
|
||||
"name": "foo",
|
||||
"url": %q,
|
||||
"allow_insecure_tls": true,
|
||||
"credentials": {
|
||||
"oauth2": {
|
||||
"token_url": "%v/token",
|
||||
"grant_type": %q,
|
||||
"scopes": ["scope1", "scope2"],
|
||||
"additional_claims": {
|
||||
"aud": "test-audience",
|
||||
"iss": "client-one"
|
||||
},
|
||||
"aws_kms": {
|
||||
"name": "arn:aws:kms:eu-west-1:account_no:key/key_id",
|
||||
"algorithm": "ECDSA_SHA_256"
|
||||
},
|
||||
"aws_signing": {
|
||||
"service": "kms",
|
||||
"environment_credentials": {
|
||||
"aws_default_region": "eu-west-1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`, ts.server.URL, ots.server.URL, grantTypeClientCredentials)
|
||||
|
||||
// Setup variables for environment_credentials{}
|
||||
t.Setenv(accessKeyEnvVar, accessKeyEnvVar)
|
||||
t.Setenv(secretKeyEnvVar, secretKeyEnvVar)
|
||||
t.Setenv(awsRegionEnvVar, awsRegionEnvVar)
|
||||
|
||||
client, err := New([]byte(config), map[string]*keys.Config{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if _, err := client.config.Credentials.OAuth2.NewClient(client.config); err != nil {
|
||||
t.Fatalf("OAuth2.NewClient() = %q", err)
|
||||
}
|
||||
|
||||
if client.config.Credentials.OAuth2.AWSSigningPlugin.kmsSignPlugin == nil {
|
||||
t.Errorf("OAuth2.AWSSigningPlugin.kmsSignPlugin isn't setup")
|
||||
}
|
||||
|
||||
// setup fake KMS signer
|
||||
client.config.Credentials.OAuth2.AWSSigningPlugin.kmsSignPlugin.kms = kms
|
||||
return &client
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user