mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
bd5ceb5142
Signed-off-by: Anders Eknert <anders@styra.com>
2686 lines
64 KiB
Go
2686 lines
64 KiB
Go
// Copyright 2018 The OPA Authors. All rights reserved.
|
|
// Use of this source code is governed by an Apache2
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package rest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/v1/bundle"
|
|
"github.com/open-policy-agent/opa/v1/keys"
|
|
"github.com/open-policy-agent/opa/v1/logging"
|
|
"github.com/open-policy-agent/opa/v1/tracing"
|
|
|
|
"github.com/open-policy-agent/opa/internal/version"
|
|
"github.com/open-policy-agent/opa/v1/util/test"
|
|
|
|
testlogger "github.com/open-policy-agent/opa/v1/logging/test"
|
|
)
|
|
|
|
const keyID = "key1"
|
|
|
|
func TestAuthPluginWithNoAuthPluginLookup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
authPlugin := "anything"
|
|
cfg := Config{
|
|
Credentials: struct {
|
|
Bearer *bearerAuthPlugin `json:"bearer,omitempty"`
|
|
OAuth2 *oauth2ClientCredentialsAuthPlugin `json:"oauth2,omitempty"`
|
|
ClientTLS *clientTLSAuthPlugin `json:"client_tls,omitempty"`
|
|
S3Signing *awsSigningAuthPlugin `json:"s3_signing,omitempty"`
|
|
GCPMetadata *gcpMetadataAuthPlugin `json:"gcp_metadata,omitempty"`
|
|
AzureManagedIdentity *azureManagedIdentitiesAuthPlugin `json:"azure_managed_identity,omitempty"`
|
|
Plugin *string `json:"plugin,omitempty"`
|
|
}{
|
|
Plugin: &authPlugin,
|
|
},
|
|
}
|
|
_, err := cfg.AuthPlugin(nil)
|
|
if err == nil {
|
|
t.Error("Expected error but got nil")
|
|
}
|
|
if want, have := "missing auth plugin lookup function", err.Error(); want != have {
|
|
t.Errorf("Unexpected error, want %q, have %q", want, have)
|
|
}
|
|
}
|
|
|
|
// Note(philipc): Cannot run this test in parallel, due to the t.Setenv calls
|
|
// from one of its helper methods.
|
|
func TestNew(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr bool
|
|
env map[string]string
|
|
}{
|
|
{
|
|
name: "BadScheme",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "bad scheme://authority",
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidUrl",
|
|
input: `{
|
|
"name": "foo",
|
|
"url", "http://localhost/some/path",
|
|
}`,
|
|
},
|
|
{
|
|
name: "Token",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"bearer": {
|
|
"token": "secret",
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "TokenWithScheme",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"bearer": {
|
|
"scheme": "Acmecorp-Token",
|
|
"token": "secret"
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "MissingTlsOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"client_tls": {}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "IncompleteTlsOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"client_tls": {
|
|
"cert": "cert.pem"
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EmptyS3Options",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "ValidS3EnvCreds",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"environment_credentials": {}
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "ValidApiGatewayEnvCreds",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"service": "execute-api",
|
|
"environment_credentials": {}
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "ValidS3MetadataCredsWithRole",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"metadata_credentials": {
|
|
"aws_region": "us-east-1",
|
|
"iam_role": "my_iam_role"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "ValidS3MetadataCreds",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"metadata_credentials": {
|
|
"aws_region": "us-east-1",
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "MissingS3MetadataCredOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"metadata_credentials": {}
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "MultipleS3CredOptions/metadata+environment",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"metadata_credentials": {
|
|
"aws_region": "us-east-1",
|
|
"iam_role": "my_iam_role"
|
|
},
|
|
"environment_credentials": {}
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "MultipleS3CredOptions/metadata+profile+environment+webidentity",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"profile_credentials": {},
|
|
"environment_credentials": {},
|
|
"web_identity_credentials": {},
|
|
"metadata_credentials": {
|
|
"aws_region": "us-east-1",
|
|
"iam_role": "my_iam_role"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
env: map[string]string{
|
|
awsRoleArnEnvVar: "TEST",
|
|
awsWebIdentityTokenFileEnvVar: "TEST",
|
|
awsRegionEnvVar: "us-west-2",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "MultipleCredentialsOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"environment_credentials": {}
|
|
},
|
|
"bearer": {
|
|
"scheme": "Acmecorp-Token",
|
|
"token": "secret"
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2NoTokenUrl",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": ""
|
|
}
|
|
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2MissingScopes",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "https://localhost",
|
|
"client_id": "client_one",
|
|
"client_secret": "super_secret"
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "Oauth2MissingClientId",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "https://localhost",
|
|
"client_id": ""
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2MissingSecret",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "https://localhost",
|
|
"client_id": "client_one"
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2Creds",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "https://localhost",
|
|
"client_id": "client_one",
|
|
"client_secret": "super_secret"
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "Oauth2GetCredScopes",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "https://localhost",
|
|
"client_id": "client_one",
|
|
"client_secret": "super_secret",
|
|
"scopes": ["profile", "opa"]
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "Oauth2JwtBearerMissingSigningKey",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, grantTypeJwtBearer),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2JwtBearerSigningKeyWithoutCorrespondingKey",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "key2",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, grantTypeJwtBearer),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2JwtBearerSigningKeyWithCorrespondingKey",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "key1",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, grantTypeJwtBearer),
|
|
},
|
|
{
|
|
name: "Oauth2JwtBearerSigningKeyPublicKeyReference",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "pub_key",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, grantTypeJwtBearer),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2WrongGrantType",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": "authorization_code",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2ClientCredentialsMissingCredentials",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, grantTypeClientCredentials),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2ClientCredentialsJwtNoAdditionalClaims",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "key1",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"]
|
|
}
|
|
}
|
|
}`, grantTypeClientCredentials),
|
|
},
|
|
{
|
|
name: "Oauth2ClientCredentialsJwtThumbprint",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "key1",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"thumbprint": "8F1BDDDE9982299E62749C20EDDBAAC57F619D04"
|
|
}
|
|
}
|
|
}`, grantTypeClientCredentials),
|
|
},
|
|
{
|
|
name: "Oauth2ClientCredentialsTooManyCredentials",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "key1",
|
|
"client_id": "client-one",
|
|
"client_secret": "supersecret",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, grantTypeClientCredentials),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2ClientCredentialsJWTAuthentication",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"signing_key": "key1",
|
|
"token_url": "https://localhost",
|
|
"scopes": ["profile", "opa"],
|
|
"additional_claims": {
|
|
"aud": "some audience"
|
|
}
|
|
}
|
|
}
|
|
}`, 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: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"web_identity_credentials": {}
|
|
},
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "S3WebIdentityCreds",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"web_identity_credentials": {}
|
|
},
|
|
}
|
|
}`,
|
|
env: map[string]string{
|
|
awsRoleArnEnvVar: "TEST",
|
|
awsWebIdentityTokenFileEnvVar: "TEST",
|
|
awsRegionEnvVar: "us-west-1",
|
|
},
|
|
},
|
|
{
|
|
name: "S3AssumeRoleMissingEnvVars",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"assume_role_credentials": {}
|
|
},
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "S3AssumeRoleCredsMissingSigningPlugin",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"assume_role_credentials": {}
|
|
},
|
|
}
|
|
}`,
|
|
env: map[string]string{
|
|
awsRoleArnEnvVar: "TEST",
|
|
accessKeyEnvVar: "TEST",
|
|
secretKeyEnvVar: "TEST",
|
|
awsRegionEnvVar: "us-west-1",
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "S3AssumeRoleCreds",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"assume_role_credentials": {"aws_signing": {"environment_credentials": {}}}
|
|
},
|
|
}
|
|
}`,
|
|
env: map[string]string{
|
|
awsRoleArnEnvVar: "TEST",
|
|
accessKeyEnvVar: "TEST",
|
|
secretKeyEnvVar: "TEST",
|
|
awsRegionEnvVar: "us-west-1",
|
|
},
|
|
},
|
|
{
|
|
name: "ValidGCPMetadataIDTokenOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://localhost",
|
|
"credentials": {
|
|
"gcp_metadata": {
|
|
"audience": "https://localhost"
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "ValidGCPMetadataAccessTokenOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://localhost",
|
|
"credentials": {
|
|
"gcp_metadata": {
|
|
"scopes": ["storage.read_only"]
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "EmptyGCPMetadataOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"gcp_metadata": {
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EmptyGCPMetadataIDTokenAudienceOption",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://localhost",
|
|
"credentials": {
|
|
"gcp_metadata": {
|
|
"audience": ""
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "EmptyGCPMetadataAccessTokenScopesOption",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://localhost",
|
|
"credentials": {
|
|
"gcp_metadata": {
|
|
"scopes": []
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "InvalidGCPMetadataOptions",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://localhost",
|
|
"credentials": {
|
|
"gcp_metadata": {
|
|
"audience": "https://localhost",
|
|
"scopes": ["storage.read_only"]
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Plugin",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"plugin": "my_plugin"
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
name: "Unknown plugin",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"plugin": "unknown_plugin"
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "Oauth2CredsClientAssertionPath",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"token_url": "https://localhost",
|
|
"client_id": "client_one",
|
|
"client_assertion_path": "/some/file",
|
|
"scopes": ["profile", "opa"]
|
|
}
|
|
}
|
|
}`, grantTypeClientCredentials),
|
|
},
|
|
{
|
|
name: "Oauth2CredsClientAssertion",
|
|
input: fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"credentials": {
|
|
"oauth2": {
|
|
"grant_type": %q,
|
|
"token_url": "https://localhost",
|
|
"client_id": "client_one",
|
|
"client_assertion": "assertive",
|
|
"scopes": ["profile", "opa"]
|
|
}
|
|
}
|
|
}`, grantTypeClientCredentials),
|
|
},
|
|
}
|
|
|
|
var results []Client
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
keyPem := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
})
|
|
pubKeyPem := pem.EncodeToMemory(&pem.Block{
|
|
Type: "RSA PUBLIC KEY",
|
|
Bytes: x509.MarshalPKCS1PublicKey(&key.PublicKey),
|
|
})
|
|
|
|
ks := map[string]*keys.Config{
|
|
keyID: {
|
|
PrivateKey: string(keyPem),
|
|
Algorithm: "RS256",
|
|
},
|
|
"pub_key": {
|
|
Key: string(pubKeyPem),
|
|
Algorithm: "RS256",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for key, val := range tc.env {
|
|
t.Setenv(key, val)
|
|
}
|
|
|
|
client, err := New([]byte(tc.input), ks, AuthPluginLookup(mockAuthPluginLookup))
|
|
if err != nil {
|
|
// We never want an error here and cannot proceed if there is one.
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
plugin, err := client.config.AuthPlugin(mockAuthPluginLookup)
|
|
if err != nil {
|
|
if tc.wantErr {
|
|
return
|
|
}
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = plugin.NewClient(client.config)
|
|
if err != nil && !tc.wantErr {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
} else if err == nil && tc.wantErr {
|
|
t.Fatalf("Expected error for input %v", tc.input)
|
|
}
|
|
|
|
if *client.config.ResponseHeaderTimeoutSeconds != defaultResponseHeaderTimeoutSeconds {
|
|
t.Fatalf("Expected default response header timeout but got %v seconds", *client.config.ResponseHeaderTimeoutSeconds)
|
|
}
|
|
|
|
results = append(results, client)
|
|
})
|
|
}
|
|
|
|
if results[3].config.Credentials.Bearer.Scheme != "Acmecorp-Token" {
|
|
t.Fatalf("Expected custom token but got: %v", results[3].config.Credentials.Bearer.Scheme)
|
|
}
|
|
}
|
|
|
|
func TestNewWithResponseHeaderTimeout(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
input := `{
|
|
"name": "foo",
|
|
"url": "http://localhost",
|
|
"response_header_timeout_seconds": 20
|
|
}`
|
|
|
|
client, err := New([]byte(input), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatal("Unexpected error")
|
|
}
|
|
|
|
if *client.config.ResponseHeaderTimeoutSeconds != 20 {
|
|
t.Fatalf("Expected response header timeout %v seconds but got %v seconds", 20, *client.config.ResponseHeaderTimeoutSeconds)
|
|
}
|
|
}
|
|
|
|
func TestDoWithResponseHeaderTimeout(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("too slow for testing.Short")
|
|
}
|
|
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
tests := map[string]struct {
|
|
d time.Duration
|
|
responseHeaderTimeout string
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
"response_headers_timeout_not_met": {1, "2", false, ""},
|
|
"response_headers_timeout_met": {2, "1", true, "net/http: timeout awaiting response headers"},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
baseURL, teardown := getTestServerWithTimeout(tc.d)
|
|
defer teardown()
|
|
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"response_header_timeout_seconds": %v,
|
|
}`, baseURL, tc.responseHeaderTimeout)
|
|
ks := map[string]*keys.Config{}
|
|
client, err := New([]byte(config), ks)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = client.Do(ctx, "GET", "/v1/test")
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatal("Expected error but got nil")
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), tc.errMsg) {
|
|
t.Fatalf("Expected error %v but got %v", tc.errMsg, err.Error())
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type tracemock struct {
|
|
called int
|
|
}
|
|
|
|
func (m *tracemock) NewTransport(rt http.RoundTripper, _ tracing.Options) http.RoundTripper {
|
|
m.called++
|
|
return rt
|
|
}
|
|
|
|
func (*tracemock) NewHandler(http.Handler, string, tracing.Options) http.Handler {
|
|
panic("unreachable")
|
|
}
|
|
|
|
func TestDoWithDistributedTracingOpts(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
mock := tracemock{}
|
|
tracing.RegisterHTTPTracing(&mock)
|
|
|
|
body := "Some Bad Request was received"
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintln(w, body)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
buf := bytes.Buffer{}
|
|
logger := logging.New()
|
|
logger.SetOutput(&buf)
|
|
logger.SetLevel(logging.Debug)
|
|
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
}`, ts.URL)
|
|
ks := map[string]*keys.Config{}
|
|
client, err := New([]byte(config), ks, DistributedTracingOpts(tracing.Options{"testoption"}))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = client.Do(ctx, "GET", ts.URL)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if exp, act := 1, mock.called; exp != act {
|
|
t.Errorf("calls to NewTransport: expected %d, got %d", exp, act)
|
|
}
|
|
}
|
|
|
|
func TestDoWithResponseInClientLog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
body := "Some Bad Request was received"
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintln(w, body)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
buf := bytes.Buffer{}
|
|
logger := logging.New()
|
|
logger.SetOutput(&buf)
|
|
logger.SetLevel(logging.Debug)
|
|
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
}`, ts.URL)
|
|
ks := map[string]*keys.Config{}
|
|
client, err := New([]byte(config), ks, Logger(logger))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = client.Do(ctx, "GET", ts.URL)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(buf.String(), body) {
|
|
t.Errorf("expected string %q not found in client logs", body)
|
|
}
|
|
}
|
|
|
|
func TestDoWithTruncatedResponseInClientLog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintln(w, strings.Repeat("Some Bad Request was received", 50))
|
|
}))
|
|
defer ts.Close()
|
|
|
|
buf := bytes.Buffer{}
|
|
logger := logging.New()
|
|
logger.SetOutput(&buf)
|
|
logger.SetLevel(logging.Debug)
|
|
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
}`, ts.URL)
|
|
ks := map[string]*keys.Config{}
|
|
client, err := New([]byte(config), ks, Logger(logger))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
_, err = client.Do(ctx, "GET", ts.URL)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
exp := "Some Bad Request was recei..."
|
|
if !strings.Contains(buf.String(), exp) {
|
|
t.Errorf("expected string %q not found in client logs", exp)
|
|
}
|
|
}
|
|
|
|
func TestValidUrl(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expMethod: "GET",
|
|
expPath: "/test",
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
}`, ts.server.URL)
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func testBearerToken(t *testing.T, scheme, token string) {
|
|
ts := testServer{
|
|
t: t,
|
|
expBearerScheme: scheme,
|
|
expBearerToken: token,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"credentials": {
|
|
"bearer": {
|
|
"scheme": %q,
|
|
"token": %q
|
|
}
|
|
}
|
|
}`, ts.server.URL, scheme, token)
|
|
ks := map[string]*keys.Config{}
|
|
client, err := New([]byte(config), ks)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBearerTokenDefaultScheme(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testBearerToken(t, "", "secret")
|
|
}
|
|
|
|
func TestBearerTokenCustomScheme(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
testBearerToken(t, "Acmecorp-Token", "secret")
|
|
}
|
|
|
|
func TestBearerTokenPath(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expBearerScheme: "",
|
|
expBearerToken: "secret",
|
|
expBearerTokenPath: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"token.txt": "secret",
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
tokenPath := filepath.Join(path, "token.txt")
|
|
|
|
client := newTestBearerClient(t, &ts, tokenPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Stop server and update the token
|
|
ts.stop()
|
|
ts.expBearerToken = "newsecret"
|
|
ts.start()
|
|
|
|
// check client cannot access the server
|
|
client = newTestBearerClient(t, &ts, tokenPath)
|
|
|
|
if resp, err := client.Do(ctx, "GET", "test"); err == nil {
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("Expected http status %v but got %v", http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
expectedErrMsg := "Expected bearer token \"newsecret\", got authorization header \"Bearer secret\""
|
|
|
|
if string(bodyBytes) != expectedErrMsg {
|
|
t.Fatalf("Expected error message %v but got %v", expectedErrMsg, string(bodyBytes))
|
|
}
|
|
} else {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Update the token file and try again
|
|
if err := os.WriteFile(filepath.Join(path, "token.txt"), []byte("newsecret"), 0600); err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBearerWithCustomCACert(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
tls: true,
|
|
expBearerScheme: "",
|
|
expBearerToken: "secret",
|
|
expBearerTokenPath: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"token.txt": "secret",
|
|
"ca.pem": string(ts.rootCertPEM),
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
tokenPath := filepath.Join(path, "token.txt")
|
|
ts.caCert = filepath.Join(path, "ca.pem")
|
|
|
|
client := newTestBearerClient(t, &ts, tokenPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBearerWithCustomCACertAndSystemCA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
tls: true,
|
|
expBearerScheme: "",
|
|
expBearerToken: "secret",
|
|
expBearerTokenPath: true,
|
|
expectSystemCA: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"token.txt": "secret",
|
|
"ca.pem": string(ts.rootCertPEM),
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
tokenPath := filepath.Join(path, "token.txt")
|
|
ts.caCert = filepath.Join(path, "ca.pem")
|
|
|
|
client := newTestBearerClient(t, &ts, tokenPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestBearerTokenInvalidConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expBearerScheme: "",
|
|
expBearerToken: "secret",
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"credentials": {
|
|
"bearer": {
|
|
"token_path": %q,
|
|
"token": %q
|
|
}
|
|
}
|
|
}`, ts.server.URL, "token.txt", "secret")
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
ctx := context.Background()
|
|
|
|
_, err = client.Do(ctx, "GET", "test")
|
|
|
|
if err == nil {
|
|
t.Fatalf("Expected error but got nil")
|
|
}
|
|
|
|
if !strings.HasPrefix(err.Error(), "invalid config") {
|
|
t.Fatalf("Unexpected error message %v\n", err)
|
|
}
|
|
}
|
|
|
|
func TestBearerTokenIsEncodedForOCI(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
config := `{
|
|
"name": "foo",
|
|
"type": "oci",
|
|
"credentials": {
|
|
"bearer": {
|
|
"token": "secret",
|
|
"scheme": "Bearer"
|
|
}
|
|
}
|
|
}`
|
|
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("New() = %q", err)
|
|
}
|
|
|
|
if _, err := client.config.Credentials.Bearer.NewClient(client.config); err != nil {
|
|
t.Errorf("Bearer.NewClient() = %q", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("", "http://somewhere.com", nil)
|
|
if err := client.config.Credentials.Bearer.Prepare(req); err != nil {
|
|
t.Errorf("Bearer.Prepare() = %q", err)
|
|
}
|
|
|
|
token := base64.StdEncoding.EncodeToString([]byte("secret"))
|
|
|
|
want := "Bearer " + token
|
|
got := req.Header.Get("Authorization")
|
|
if got != want {
|
|
t.Errorf("req.Header.Get(\"Authorization\") = %q, want = %q", got, want)
|
|
}
|
|
}
|
|
|
|
func newTestBearerClient(t *testing.T, ts *testServer, tokenPath string) *Client {
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"tls": {"ca_cert": %q, system_ca_required: %v},
|
|
"credentials": {
|
|
"bearer": {
|
|
"token_path": %q
|
|
}
|
|
}
|
|
}`, ts.server.URL, ts.caCert, ts.expectSystemCA, tokenPath)
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
return &client
|
|
}
|
|
|
|
func TestClientCert(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
tls: true,
|
|
expectClientCert: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"client.pem": string(ts.clientCertPem),
|
|
"client.key": string(ts.clientCertKey),
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
certPath := filepath.Join(path, "client.pem")
|
|
keyPath := filepath.Join(path, "client.key")
|
|
|
|
client := newTestClient(t, &ts, certPath, keyPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Scramble the keys in the server
|
|
ts.stop()
|
|
ts.start()
|
|
|
|
// Ensure the keys don't work anymore, make a new client as the url will have changed
|
|
client = newTestClient(t, &ts, certPath, keyPath)
|
|
_, err := client.Do(ctx, "GET", "test")
|
|
expectedErrMsg := func(s string) bool {
|
|
switch {
|
|
case strings.Contains(s, "tls: unknown certificate authority"):
|
|
case strings.Contains(s, "tls: bad certificate"):
|
|
default:
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
if err == nil || !expectedErrMsg(err.Error()) {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
|
|
// Update the key files and try again..
|
|
if err := os.WriteFile(filepath.Join(path, "client.pem"), ts.clientCertPem, 0600); err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(path, "client.key"), ts.clientCertKey, 0600); err != nil {
|
|
t.Fatalf("Unexpected error: %s", err)
|
|
}
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClientCertPassword(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
tls: true,
|
|
expectClientCert: true,
|
|
clientCertPassword: "password",
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"client.pem": string(ts.clientCertPem),
|
|
"client.key": string(ts.clientCertKey),
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
certPath := filepath.Join(path, "client.pem")
|
|
keyPath := filepath.Join(path, "client.key")
|
|
|
|
client := newTestClient(t, &ts, certPath, keyPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClientTLSWithCustomCACert(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
tls: true,
|
|
expectClientCert: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"client.pem": string(ts.clientCertPem),
|
|
"client.key": string(ts.clientCertKey),
|
|
"ca.pem": string(ts.rootCertPEM),
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
certPath := filepath.Join(path, "client.pem")
|
|
keyPath := filepath.Join(path, "client.key")
|
|
ts.caCert = filepath.Join(path, "ca.pem")
|
|
|
|
client := newTestClient(t, &ts, certPath, keyPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestClientTLSWithCustomCACertAndSystemCA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
tls: true,
|
|
expectClientCert: true,
|
|
expectSystemCA: true,
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
files := map[string]string{
|
|
"client.pem": string(ts.clientCertPem),
|
|
"client.key": string(ts.clientCertKey),
|
|
"ca.pem": string(ts.rootCertPEM),
|
|
}
|
|
|
|
test.WithTempFS(files, func(path string) {
|
|
certPath := filepath.Join(path, "client.pem")
|
|
keyPath := filepath.Join(path, "client.key")
|
|
ts.caCert = filepath.Join(path, "ca.pem")
|
|
|
|
client := newTestClient(t, &ts, certPath, keyPath)
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOauth2ClientCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
ts *testServer
|
|
ots *oauth2TestServer
|
|
options testPluginCustomizer
|
|
wantErr bool
|
|
}{
|
|
{
|
|
ts: &testServer{t: t, expBearerToken: "token_1"},
|
|
ots: &oauth2TestServer{t: t},
|
|
},
|
|
{
|
|
ts: &testServer{t: t, expBearerToken: "token_1"},
|
|
ots: &oauth2TestServer{t: t, tokenType: "unknown"},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
ts: &testServer{t: t},
|
|
ots: &oauth2TestServer{t: t},
|
|
options: func(c *Config) {
|
|
c.Credentials.OAuth2.ClientSecret = "not_super_secret"
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
ts: &testServer{t: t},
|
|
ots: &oauth2TestServer{t: t, expScope: &[]string{"read", "opa"}},
|
|
options: func(c *Config) {
|
|
c.Credentials.OAuth2.Scopes = []string{"read", "opa"}
|
|
},
|
|
},
|
|
{
|
|
ts: &testServer{t: t},
|
|
ots: &oauth2TestServer{t: t, expHeaders: map[string]string{"x-custom-header": "custom-value"}},
|
|
options: func(c *Config) {
|
|
c.Credentials.OAuth2.AdditionalHeaders = map[string]string{"x-custom-header": "custom-value"}
|
|
},
|
|
},
|
|
{
|
|
ts: &testServer{t: t},
|
|
ots: &oauth2TestServer{t: t, expBody: map[string]string{"custom_field": "custom-value"}},
|
|
options: func(c *Config) {
|
|
c.Credentials.OAuth2.AdditionalParameters = map[string]string{"custom_field": "custom-value"}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
func() {
|
|
tc.ts.start()
|
|
defer tc.ts.stop()
|
|
tc.ots.start()
|
|
defer tc.ots.stop()
|
|
|
|
if tc.options == nil {
|
|
tc.options = func(_ *Config) {}
|
|
}
|
|
|
|
client := newOauth2TestClient(t, tc.ts, tc.ots, tc.options)
|
|
ctx := context.Background()
|
|
_, err := client.Do(ctx, "GET", "test")
|
|
if err != nil && !tc.wantErr {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
} else if err == nil && tc.wantErr {
|
|
t.Fatalf("Expected error: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func TestOauth2ClientCredentialsExpiringTokenIsRefreshed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expBearerToken: "token_1",
|
|
}
|
|
ts.start()
|
|
ots := oauth2TestServer{
|
|
t: t,
|
|
// Issue tokens with a TTL below our considered minimum - this should force the client to fetch a new one the
|
|
// second time the credentials are used rather than reusing the token it has
|
|
tokenTTL: 9,
|
|
}
|
|
ots.start()
|
|
defer ots.stop()
|
|
|
|
client := newOauth2TestClient(t, &ts, &ots)
|
|
ctx := context.Background()
|
|
_, err := client.Do(ctx, "GET", "test")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
|
|
ts.stop()
|
|
ts = testServer{
|
|
t: t,
|
|
expBearerToken: "token_2",
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
client = newOauth2TestClient(t, &ts, &ots)
|
|
ctx = context.Background()
|
|
_, err = client.Do(ctx, "GET", "test")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOauth2ClientCredentialsNonExpiringTokenIsReused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ts := testServer{
|
|
t: t,
|
|
expBearerToken: "token_1",
|
|
}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
ots := oauth2TestServer{
|
|
t: t,
|
|
tokenTTL: 300,
|
|
}
|
|
ots.start()
|
|
defer ots.stop()
|
|
|
|
client := newOauth2TestClient(t, &ts, &ots)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestOauth2JwtBearerGrantType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
|
ks := map[string]*keys.Config{
|
|
keyID: {
|
|
PrivateKey: string(keyPem),
|
|
Algorithm: "RS256",
|
|
},
|
|
}
|
|
|
|
ts := testServer{t: t, expBearerToken: "token_1"}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
ots := oauth2TestServer{
|
|
t: t,
|
|
tokenTTL: 300,
|
|
expGrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
expScope: &[]string{"scope1", "scope2"},
|
|
expJwtCredential: true,
|
|
expAlgorithm: jwa.RS256,
|
|
verificationKey: &key.PublicKey,
|
|
}
|
|
ots.start()
|
|
defer ots.stop()
|
|
|
|
client := newOauth2JwtBearerTestClient(t, ks, &ts, &ots, func(c *Config) {
|
|
c.Credentials.OAuth2.SigningKeyID = keyID
|
|
})
|
|
ctx := context.Background()
|
|
_, err = client.Do(ctx, "GET", "test")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOauth2JwtBearerGrantTypePKCS8EncodedPrivateKey(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
|
|
privateKey, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
|
|
keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privateKey})
|
|
ks := map[string]*keys.Config{
|
|
keyID: {
|
|
PrivateKey: string(keyPem),
|
|
Algorithm: "RS256",
|
|
},
|
|
}
|
|
|
|
ts := testServer{t: t, expBearerToken: "token_1"}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
ots := oauth2TestServer{
|
|
t: t,
|
|
tokenTTL: 300,
|
|
expGrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
expScope: &[]string{"scope1", "scope2"},
|
|
expJwtCredential: true,
|
|
expAlgorithm: jwa.RS256,
|
|
verificationKey: &key.PublicKey,
|
|
}
|
|
ots.start()
|
|
defer ots.stop()
|
|
|
|
client := newOauth2JwtBearerTestClient(t, ks, &ts, &ots, func(c *Config) {
|
|
c.Credentials.OAuth2.SigningKeyID = keyID
|
|
})
|
|
ctx := context.Background()
|
|
_, err = client.Do(ctx, "GET", "test")
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOauth2JwtBearerGrantTypeEllipticCurveAlgorithm(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
privateKey, err := x509.MarshalECPrivateKey(key)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
|
|
keyPem := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKey})
|
|
ks := map[string]*keys.Config{
|
|
keyID: {
|
|
PrivateKey: string(keyPem),
|
|
Algorithm: "ES256",
|
|
},
|
|
}
|
|
|
|
ts := testServer{t: t, expBearerToken: "token_1"}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
ots := oauth2TestServer{
|
|
t: t,
|
|
tokenTTL: 300,
|
|
expGrantType: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
expScope: &[]string{"scope1", "scope2"},
|
|
expJwtCredential: true,
|
|
expAlgorithm: jwa.ES256,
|
|
verificationKey: &key.PublicKey,
|
|
}
|
|
ots.start()
|
|
defer ots.stop()
|
|
|
|
client := newOauth2JwtBearerTestClient(t, ks, &ts, &ots, func(c *Config) {
|
|
c.Credentials.OAuth2.SigningKeyID = keyID
|
|
c.Credentials.OAuth2.IncludeJti = true
|
|
})
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestOauth2ClientCredentialsJwtAuthentication(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error %v", err)
|
|
}
|
|
keyPem := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
|
ks := map[string]*keys.Config{
|
|
keyID: {
|
|
PrivateKey: string(keyPem),
|
|
Algorithm: "RS256",
|
|
},
|
|
}
|
|
|
|
ts := testServer{t: t, expBearerToken: "token_1"}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
ots := oauth2TestServer{
|
|
t: t,
|
|
tokenTTL: 300,
|
|
expGrantType: grantTypeClientCredentials,
|
|
expScope: &[]string{"scope1", "scope2"},
|
|
expX5t: "jxvd3pmCKZ5idJwg7duqxX9hnQQ=",
|
|
expJwtCredential: true,
|
|
expAlgorithm: jwa.RS256,
|
|
verificationKey: &key.PublicKey,
|
|
}
|
|
ots.start()
|
|
defer ots.stop()
|
|
|
|
client := newOauth2ClientCredentialsJwtAuthClient(t, ks, &ts, &ots, func(c *Config) {
|
|
c.Credentials.OAuth2.SigningKeyID = keyID
|
|
})
|
|
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)
|
|
}
|
|
}
|
|
|
|
// https://github.com/open-policy-agent/opa/issues/3255
|
|
func TestS3SigningInstantiationInitializesLogger(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
config := `{
|
|
"name": "foo",
|
|
"url": "https://bundles.example.com",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"environment_credentials": {}
|
|
}
|
|
}
|
|
}`
|
|
|
|
authPlugin := &awsSigningAuthPlugin{
|
|
AWSEnvironmentCredentials: &awsEnvironmentCredentialService{},
|
|
}
|
|
client, err := New([]byte(config), map[string]*keys.Config{}, AuthPluginLookup(func(_ string) HTTPAuthPlugin {
|
|
return authPlugin
|
|
}))
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
plugin := client.authPluginLookup("s3_signing")
|
|
if _, err = plugin.NewClient(client.config); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if authPlugin.logger == nil {
|
|
t.Errorf("Expected logger to be initialized")
|
|
}
|
|
}
|
|
|
|
func TestS3SigningMultiCredentialProvider(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
credentialProviderCount := 4
|
|
config := `{
|
|
"name": "foo",
|
|
"url": "https://bundles.example.com",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"environment_credentials": {},
|
|
"profile_credentials": {},
|
|
"metadata_credentials": {},
|
|
"web_identity_credentials": {}
|
|
}
|
|
}
|
|
}`
|
|
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
awsPlugin := client.config.Credentials.S3Signing
|
|
if awsPlugin == nil {
|
|
t.Fatalf("Client config S3 signing credentials setup unexpected")
|
|
}
|
|
|
|
awsCredentialServiceChain, ok := awsPlugin.awsCredentialService().(*awsCredentialServiceChain)
|
|
if !ok {
|
|
t.Fatalf("Unexpected AWS credential service:%v is not a chain",
|
|
reflect.TypeOf(awsCredentialServiceChain))
|
|
}
|
|
|
|
if len(awsCredentialServiceChain.awsCredentialServices) != credentialProviderCount {
|
|
t.Fatalf("Credential provider count mismatch %d != %d", credentialProviderCount,
|
|
len(awsCredentialServiceChain.awsCredentialServices))
|
|
}
|
|
|
|
expectedOrder := []awsCredentialService{
|
|
&awsEnvironmentCredentialService{},
|
|
&awsWebIdentityCredentialService{},
|
|
&awsProfileCredentialService{},
|
|
&awsMetadataCredentialService{},
|
|
}
|
|
|
|
if !reflect.DeepEqual(awsCredentialServiceChain.awsCredentialServices,
|
|
expectedOrder) {
|
|
t.Fatalf("Ordering is unexpected")
|
|
}
|
|
}
|
|
|
|
func TestAWSCredentialServiceChain(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
wantErr bool
|
|
env map[string]string
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Fallback to Environment Credential",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://bundles.example.com",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"web_identity_credentials": {},
|
|
"environment_credentials": {},
|
|
"profile_credentials": {},
|
|
"metadata_credentials": {}
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: false,
|
|
env: map[string]string{
|
|
accessKeyEnvVar: "a",
|
|
secretKeyEnvVar: "a",
|
|
awsRegionEnvVar: "us-east-1",
|
|
},
|
|
},
|
|
{
|
|
name: "No provider is successful",
|
|
input: `{
|
|
"name": "foo",
|
|
"url": "https://bundles.example.com",
|
|
"credentials": {
|
|
"s3_signing": {
|
|
"web_identity_credentials": {},
|
|
"environment_credentials": {},
|
|
"profile_credentials": {},
|
|
"metadata_credentials": {}
|
|
}
|
|
}
|
|
}`,
|
|
wantErr: true,
|
|
errMsg: "all AWS credential providers failed: 4 errors occurred",
|
|
env: map[string]string{},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
for key, val := range tc.env {
|
|
t.Setenv(key, val)
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
for key := range tc.env {
|
|
_ = os.Unsetenv(key)
|
|
}
|
|
})
|
|
|
|
client, err := New([]byte(tc.input), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
awsPlugin := client.config.Credentials.S3Signing
|
|
if awsPlugin == nil {
|
|
t.Fatalf("Client config S3 signing credentials setup unexpected")
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", "/example/bundle.tar.gz", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create HTTP request: %v", err)
|
|
}
|
|
|
|
awsPlugin.logger = client.logger
|
|
err = awsPlugin.Prepare(req)
|
|
|
|
if tc.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("Expected error for input %v", tc.input)
|
|
}
|
|
|
|
if !strings.Contains(err.Error(), tc.errMsg) {
|
|
t.Fatalf("Expected error message %v but got %v", tc.errMsg, err.Error())
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDebugLoggingRequestMaskAuthorizationHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
token := "secret"
|
|
plaintext := "plaintext"
|
|
ts := testServer{t: t, expBearerToken: token}
|
|
ts.start()
|
|
defer ts.stop()
|
|
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"credentials": {
|
|
"bearer": {
|
|
"token": %q
|
|
}
|
|
},
|
|
"headers": {
|
|
"X-AMZ-SECURITY-TOKEN": %q,
|
|
"remains-unmasked": %q
|
|
}
|
|
}`, ts.server.URL, token, token, plaintext)
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
logger := testlogger.New()
|
|
logger.SetLevel(logging.Debug)
|
|
client.logger = logger
|
|
|
|
ctx := context.Background()
|
|
if _, err := client.Do(ctx, "GET", "test"); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
entries := logger.Entries()
|
|
if len(entries) != 2 {
|
|
t.Fatalf("Expected 2 log entries, got %d", len(entries))
|
|
}
|
|
|
|
requestEntry := entries[0]
|
|
headers := requestEntry.Fields["headers"].(http.Header)
|
|
for k := range headers {
|
|
v := headers.Get(k)
|
|
if _, ok := maskedHeaderKeys[k]; ok {
|
|
if v != "REDACTED" {
|
|
t.Errorf("Expected redacted %q header value, got %v", k, v)
|
|
}
|
|
} else if k == "Remains-Unmasked" && v != plaintext {
|
|
t.Errorf("Expected %q header to have value %q, got %v", k, plaintext, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newTestClient(t *testing.T, ts *testServer, certPath string, keypath string) *Client {
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"allow_insecure_tls": true,
|
|
"tls": {"ca_cert": %q, system_ca_required: %v},
|
|
"credentials": {
|
|
"client_tls": {
|
|
"cert": %q,
|
|
"private_key": %q
|
|
}
|
|
}
|
|
}`, ts.server.URL, ts.caCert, ts.expectSystemCA, certPath, keypath)
|
|
client, err := New([]byte(config), map[string]*keys.Config{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if ts.clientCertPassword != "" {
|
|
client.Config().Credentials.ClientTLS.PrivateKeyPassphrase = ts.clientCertPassword
|
|
}
|
|
|
|
return &client
|
|
}
|
|
|
|
type testPluginCustomizer func(c *Config)
|
|
|
|
type testServer struct {
|
|
t *testing.T
|
|
server *httptest.Server
|
|
expPath string
|
|
expMethod string
|
|
expBearerToken string
|
|
expBearerScheme string
|
|
expBearerTokenPath bool
|
|
tls bool
|
|
clientCertPem []byte
|
|
clientCertKey []byte
|
|
clientCertPassword string
|
|
expectClientCert bool
|
|
rootCertPEM []byte
|
|
caCert string
|
|
expectSystemCA bool
|
|
serverCertPool *x509.CertPool
|
|
certificates []tls.Certificate
|
|
}
|
|
|
|
type oauth2TestServer struct {
|
|
t *testing.T
|
|
server *httptest.Server
|
|
expGrantType string
|
|
expClientID string
|
|
expClientSecret string
|
|
expHeaders map[string]string
|
|
expBody map[string]string
|
|
expJwtCredential bool
|
|
expScope *[]string
|
|
expAlgorithm jwa.SignatureAlgorithm
|
|
expX5t string
|
|
expSignature string
|
|
tokenType string
|
|
tokenTTL int64
|
|
invocations int32
|
|
verificationKey interface{}
|
|
}
|
|
|
|
func newOauth2TestClient(t *testing.T, ts *testServer, ots *oauth2TestServer, options ...testPluginCustomizer) *Client {
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"allow_insecure_tls": true,
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "%v/token",
|
|
"client_id": "client_one",
|
|
"client_secret": "super_secret"
|
|
}
|
|
}
|
|
}`, ts.server.URL, ots.server.URL)
|
|
client, err := New([]byte(config), map[string]*bundle.KeyConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(client.Config())
|
|
}
|
|
|
|
return &client
|
|
}
|
|
|
|
// Create client to test JWT authorization grant as described in https://tools.ietf.org/html/rfc7523
|
|
func newOauth2JwtBearerTestClient(t *testing.T, keys map[string]*keys.Config, ts *testServer, ots *oauth2TestServer, options ...testPluginCustomizer) *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"
|
|
}
|
|
}
|
|
}
|
|
}`, ts.server.URL, ots.server.URL, grantTypeJwtBearer)
|
|
|
|
client, err := New([]byte(config), keys)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(client.Config())
|
|
}
|
|
|
|
return &client
|
|
}
|
|
|
|
// Create client to test JWT client authentication as described in https://tools.ietf.org/html/rfc7523
|
|
func newOauth2ClientCredentialsJwtAuthClient(t *testing.T, keys map[string]*keys.Config, ts *testServer, ots *oauth2TestServer, options ...testPluginCustomizer) *Client {
|
|
config := fmt.Sprintf(`{
|
|
"name": "foo",
|
|
"url": %q,
|
|
"allow_insecure_tls": true,
|
|
"credentials": {
|
|
"oauth2": {
|
|
"token_url": "%v/token",
|
|
"grant_type": %q,
|
|
"signing_key": "key1",
|
|
"client_id": "client-one",
|
|
"scopes": ["scope1", "scope2"],
|
|
"thumbprint": "8F1BDDDE9982299E62749C20EDDBAAC57F619D04",
|
|
"additional_claims": {
|
|
"aud": "test-audience",
|
|
"iss": "client-one"
|
|
}
|
|
}
|
|
}
|
|
}`, ts.server.URL, ots.server.URL, grantTypeClientCredentials)
|
|
|
|
client, err := New([]byte(config), keys)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
for _, option := range options {
|
|
option(client.Config())
|
|
}
|
|
|
|
return &client
|
|
}
|
|
|
|
func (t *oauth2TestServer) start() {
|
|
if t.tokenTTL == 0 {
|
|
t.tokenTTL = 3600
|
|
}
|
|
if t.expScope == nil {
|
|
t.expScope = &[]string{}
|
|
}
|
|
if t.tokenType == "" {
|
|
t.tokenType = "bearer"
|
|
}
|
|
t.expClientID = "client_one"
|
|
t.expClientSecret = "super_secret"
|
|
|
|
t.server = httptest.NewUnstartedServer(http.HandlerFunc(t.handle))
|
|
|
|
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.t.Fatalf("generating random key: %v", err)
|
|
}
|
|
_, rootCertPem, err := createRootCert(rootKey)
|
|
if err != nil {
|
|
t.t.Fatalf("creating root cert: %v", err)
|
|
}
|
|
|
|
serverCertPool := x509.NewCertPool()
|
|
serverCertPool.AppendCertsFromPEM(rootCertPem)
|
|
t.server.TLS = &tls.Config{
|
|
RootCAs: serverCertPool,
|
|
}
|
|
t.server.StartTLS()
|
|
}
|
|
|
|
func (t *oauth2TestServer) stop() {
|
|
t.server.Close()
|
|
}
|
|
|
|
func (t *oauth2TestServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
t.t.Fatalf("Expected method POST, got %v", r.Method)
|
|
}
|
|
if r.URL.Path != "/token" {
|
|
t.t.Fatalf("Expected path /token got %q", r.URL.Path)
|
|
}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
t.t.Fatal(err)
|
|
}
|
|
|
|
if t.expGrantType == "" {
|
|
t.expGrantType = grantTypeClientCredentials
|
|
}
|
|
if r.Form["grant_type"][0] != t.expGrantType {
|
|
t.t.Fatalf("Expected grant_type=%v", t.expGrantType)
|
|
}
|
|
|
|
for k, v := range t.expBody {
|
|
if r.Form[k][0] != v {
|
|
t.t.Fatalf("Expected header %s=%s got %s", k, v, r.Form[k][0])
|
|
}
|
|
}
|
|
|
|
for k, v := range t.expHeaders {
|
|
if r.Header.Get(k) != v {
|
|
t.t.Fatalf("Expected header %s=%s got %s", k, v, r.Header.Get(k))
|
|
}
|
|
}
|
|
|
|
if len(r.Form["scope"]) > 0 {
|
|
scope := strings.Split(r.Form["scope"][0], " ")
|
|
if !slices.Equal(*t.expScope, scope) {
|
|
t.t.Fatalf("Expected scope %v, got %v", *t.expScope, scope)
|
|
}
|
|
} else if t.expScope != nil && len(*t.expScope) > 0 {
|
|
t.t.Fatal("Expected scope to be provided")
|
|
}
|
|
|
|
if !t.expJwtCredential {
|
|
authHeader := strings.TrimSpace(r.Header.Get("Authorization"))
|
|
split := strings.Split(authHeader, " ")
|
|
credentials := split[len(split)-1]
|
|
|
|
decoded, err := base64.StdEncoding.DecodeString(credentials)
|
|
if err != nil {
|
|
t.t.Fatal(err)
|
|
}
|
|
|
|
pair := strings.SplitN(string(decoded), ":", 2)
|
|
if len(pair) != 2 || pair[0] != t.expClientID || pair[1] != t.expClientSecret {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_, _ = w.Write([]byte(`{"error"": "invalid_client"}`))
|
|
return
|
|
}
|
|
} else {
|
|
var token string
|
|
if t.expGrantType == "urn:ietf:params:oauth:grant-type:jwt-bearer" {
|
|
token = r.Form["assertion"][0]
|
|
} else {
|
|
token = r.Form["client_assertion"][0]
|
|
}
|
|
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
|
|
_ = json.Unmarshal(headerRaw, &headers)
|
|
x5t := headers["x5t"]
|
|
|
|
if t.expX5t != x5t {
|
|
t.t.Errorf("Expected expX5t %v, got %v", t.expX5t, x5t)
|
|
}
|
|
}
|
|
}
|
|
|
|
t.invocations++
|
|
token := fmt.Sprintf("token_%v", t.invocations)
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
body := fmt.Sprintf(`{"token_type": "%v", "access_token": "%v", "expires_in": %v}`, t.tokenType, token, t.tokenTTL)
|
|
_, _ = w.Write([]byte(body))
|
|
}
|
|
|
|
func (t *testServer) handle(w http.ResponseWriter, r *http.Request) {
|
|
if t.expMethod != "" && t.expMethod != r.Method {
|
|
t.t.Fatalf("Expected method %v, got %v", t.expMethod, r.Method)
|
|
}
|
|
if t.expPath != "" && t.expPath != r.URL.Path {
|
|
t.t.Fatalf("Expected path %q, got %q", t.expPath, r.URL.Path)
|
|
}
|
|
if (t.expBearerToken != "" || t.expBearerScheme != "") && len(r.Header["Authorization"]) == 0 {
|
|
t.t.Fatal("Expected bearer token, but didn't get any")
|
|
}
|
|
if len(r.Header["Authorization"]) > 0 {
|
|
auth := r.Header["Authorization"][0]
|
|
if t.expBearerScheme != "" && !strings.HasPrefix(auth, t.expBearerScheme) {
|
|
errMsg := fmt.Sprintf("Expected bearer scheme %q, got authorization header %q", t.expBearerScheme, auth)
|
|
if t.expBearerTokenPath {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(errMsg))
|
|
return
|
|
}
|
|
t.t.Fatal(errMsg)
|
|
}
|
|
if t.expBearerToken != "" && !strings.HasSuffix(auth, t.expBearerToken) {
|
|
errMsg := fmt.Sprintf("Expected bearer token %q, got authorization header %q", t.expBearerToken, auth)
|
|
if t.expBearerTokenPath {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = w.Write([]byte(errMsg))
|
|
return
|
|
}
|
|
t.t.Fatal(errMsg)
|
|
}
|
|
}
|
|
if t.expectClientCert {
|
|
if len(r.TLS.PeerCertificates) == 0 {
|
|
t.t.Fatal("Expected client certificate but didn't get any")
|
|
}
|
|
}
|
|
ua := r.Header.Get("user-Agent")
|
|
if ua != version.UserAgent {
|
|
t.t.Errorf("Unexpected User-Agent string: %s", ua)
|
|
}
|
|
|
|
w.WriteHeader(200)
|
|
}
|
|
|
|
func (t *testServer) generateClientKeys() {
|
|
// generate a new set of root key+cert objects
|
|
rootKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.t.Fatalf("generating random key: %v", err)
|
|
}
|
|
rootCert, rootCertPEM, err := createRootCert(rootKey)
|
|
if err != nil {
|
|
t.t.Fatalf("error creating cert: %v", err)
|
|
}
|
|
|
|
keyPEMBlock := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rootKey)})
|
|
cert, err := tls.X509KeyPair(rootCertPEM, keyPEMBlock)
|
|
if err != nil {
|
|
t.t.Fatalf("error creating tls.X509KeyPair: %v", err)
|
|
}
|
|
|
|
// save a copy of the root certificate for clients to use
|
|
t.serverCertPool = x509.NewCertPool()
|
|
t.serverCertPool.AppendCertsFromPEM(rootCertPEM)
|
|
t.rootCertPEM = rootCertPEM
|
|
t.certificates = []tls.Certificate{cert}
|
|
|
|
// create a key-pair for the client
|
|
clientKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
t.t.Fatalf("generating random key: %v", err)
|
|
}
|
|
|
|
// create a template for the client
|
|
clientCertTmpl, err := certTemplate()
|
|
if err != nil {
|
|
t.t.Fatalf("creating cert template: %v", err)
|
|
}
|
|
clientCertTmpl.KeyUsage = x509.KeyUsageDigitalSignature
|
|
clientCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
|
|
|
|
// the root cert signs the client cert
|
|
_, t.clientCertPem, err = createCert(clientCertTmpl, rootCert, &clientKey.PublicKey, rootKey)
|
|
if err != nil {
|
|
t.t.Fatalf("error creating cert: %v", err)
|
|
}
|
|
|
|
var pemBlock *pem.Block
|
|
if t.clientCertPassword != "" {
|
|
// nolint: staticcheck // We don't want to forbid users from using this encryption.
|
|
pemBlock, err = x509.EncryptPEMBlock(rand.Reader, "RSA PRIVATE KEY", x509.MarshalPKCS1PrivateKey(clientKey),
|
|
[]byte(t.clientCertPassword), x509.PEMCipherAES128)
|
|
if err != nil {
|
|
t.t.Fatalf("error encrypting pem block: %v", err)
|
|
}
|
|
} else {
|
|
pemBlock = &pem.Block{
|
|
Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(clientKey),
|
|
}
|
|
}
|
|
|
|
// encode and load the cert and private key for the client
|
|
t.clientCertKey = pem.EncodeToMemory(pemBlock)
|
|
}
|
|
|
|
func (t *testServer) start() {
|
|
t.server = httptest.NewUnstartedServer(http.HandlerFunc(t.handle))
|
|
|
|
if t.tls {
|
|
t.generateClientKeys()
|
|
t.server.TLS = &tls.Config{
|
|
ClientAuth: tls.VerifyClientCertIfGiven,
|
|
ClientCAs: t.serverCertPool,
|
|
Certificates: t.certificates,
|
|
}
|
|
t.server.StartTLS()
|
|
} else {
|
|
t.server.Start()
|
|
}
|
|
}
|
|
|
|
func (t *testServer) stop() {
|
|
t.server.Close()
|
|
}
|
|
|
|
// helper function to create a cert template with a serial number and other required fields
|
|
func certTemplate() (*x509.Certificate, error) {
|
|
// generate a random serial number (a real cert authority would have some logic behind this)
|
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
if err != nil {
|
|
return nil, errors.New("failed to generate serial number: " + err.Error())
|
|
}
|
|
|
|
tmpl := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{Organization: []string{"OPA"}},
|
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().Add(time.Hour), // valid for an hour
|
|
BasicConstraintsValid: true,
|
|
}
|
|
return &tmpl, nil
|
|
}
|
|
|
|
func createCert(template, parent *x509.Certificate, pub interface{}, parentPriv interface{}) (
|
|
cert *x509.Certificate, certPEM []byte, err error) {
|
|
|
|
certDER, err := x509.CreateCertificate(rand.Reader, template, parent, pub, parentPriv)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// parse the resulting certificate so we can use it again
|
|
cert, err = x509.ParseCertificate(certDER)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// PEM encode the certificate (this is a standard TLS encoding)
|
|
b := pem.Block{Type: "CERTIFICATE", Bytes: certDER}
|
|
certPEM = pem.EncodeToMemory(&b)
|
|
return
|
|
}
|
|
|
|
func createRootCert(rootKey *rsa.PrivateKey) (cert *x509.Certificate, certPEM []byte, err error) {
|
|
rootCertTmpl, err := certTemplate()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
rootCertTmpl.IsCA = true
|
|
rootCertTmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature
|
|
rootCertTmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
|
|
rootCertTmpl.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
|
|
|
|
return createCert(rootCertTmpl, rootCertTmpl, &rootKey.PublicKey, rootKey)
|
|
}
|
|
|
|
func getTestServerWithTimeout(d time.Duration) (baseURL string, teardownFn func()) {
|
|
mux := http.NewServeMux()
|
|
ts := httptest.NewServer(mux)
|
|
|
|
mux.HandleFunc("/v1/test", func(w http.ResponseWriter, _ *http.Request) {
|
|
time.Sleep(d * time.Second)
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
return ts.URL, ts.Close
|
|
}
|
|
|
|
func mockAuthPluginLookup(name string) HTTPAuthPlugin {
|
|
if name == "my_plugin" {
|
|
return &myPluginMock{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type myPluginMock struct{}
|
|
|
|
func (*myPluginMock) NewClient(c Config) (*http.Client, error) {
|
|
tlsConfig, err := DefaultTLSConfig(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return DefaultRoundTripperClient(
|
|
tlsConfig,
|
|
defaultResponseHeaderTimeoutSeconds,
|
|
), nil
|
|
}
|
|
func (*myPluginMock) Prepare(*http.Request) error {
|
|
return nil
|
|
}
|
|
|
|
// Note(philipc): Cannot run this test in parallel, due to the t.Setenv calls
|
|
// from one of its helper methods.
|
|
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
|
|
}
|