You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/internal/go-sso/oidcauth/config_test.go

667 lines
19 KiB

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
1 year ago
// SPDX-License-Identifier: BUSL-1.1
package oidcauth
import (
"strings"
"testing"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/hashicorp/consul/internal/go-sso/oidcauth/oidcauthtest"
"github.com/stretchr/testify/require"
)
func TestConfigValidate(t *testing.T) {
type testcase struct {
config Config
expectAuthType int
expectErr string
}
srv := oidcauthtest.Start(t)
oidcCases := map[string]testcase{
"all required": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectAuthType: authOIDCFlow,
},
"missing required OIDCDiscoveryURL": {
config: Config{
Type: TypeOIDC,
// OIDCDiscoveryURL: srv.Addr(),
// OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectErr: "must be set for type",
},
"missing required OIDCClientID": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
// OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectErr: "must be set for type",
},
"missing required OIDCClientSecret": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
// OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectErr: "must be set for type",
},
"missing required AllowedRedirectURIs": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{},
},
expectErr: "must be set for type",
},
"incompatible with JWKSURL": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
JWKSURL: srv.Addr() + "/certs",
},
expectErr: "must not be set for type",
},
"incompatible with JWKSCACert": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
JWKSCACert: srv.CACert(),
},
expectErr: "must not be set for type",
},
"incompatible with JWTValidationPubKeys": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
JWTValidationPubKeys: []string{testJWTPubKey},
},
expectErr: "must not be set for type",
},
"incompatible with BoundIssuer": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
BoundIssuer: "foo",
},
expectErr: "must not be set for type",
},
"incompatible with ExpirationLeeway": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
ExpirationLeeway: 1 * time.Second,
},
expectErr: "must not be set for type",
},
"incompatible with NotBeforeLeeway": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
NotBeforeLeeway: 1 * time.Second,
},
expectErr: "must not be set for type",
},
"incompatible with ClockSkewLeeway": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
ClockSkewLeeway: 1 * time.Second,
},
expectErr: "must not be set for type",
},
"bad discovery cert": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: oidcBadCACerts,
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectErr: "certificate signed by unknown authority",
},
"garbage discovery cert": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: garbageCACert,
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectErr: "could not parse CA PEM value successfully",
},
"good discovery cert": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectAuthType: authOIDCFlow,
},
"valid redirect uris": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{
"http://foo.test",
"https://example.com",
"https://evilcorp.com:8443",
},
},
expectAuthType: authOIDCFlow,
},
"invalid redirect uris": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{
"%%%%",
"http://foo.test",
"https://example.com",
"https://evilcorp.com:8443",
},
},
expectErr: "Invalid AllowedRedirectURIs provided: [%%%%]",
},
"valid algorithm": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
JWTSupportedAlgs: []string{
oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.PS384, oidc.PS512,
},
},
expectAuthType: authOIDCFlow,
},
"invalid algorithm": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
JWTSupportedAlgs: []string{
oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.PS384, oidc.PS512,
"foo",
},
},
expectErr: "Invalid supported algorithm",
},
"valid claim mappings": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
ClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
ListClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
},
expectAuthType: authOIDCFlow,
},
"invalid repeated value claim mappings": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
ClaimMappings: map[string]string{
"foo": "bar",
"bling": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
ListClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
},
expectErr: "ClaimMappings contains multiple mappings for key",
},
"invalid repeated list claim mappings": {
config: Config{
Type: TypeOIDC,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
OIDCClientID: "abc",
OIDCClientSecret: "def",
AllowedRedirectURIs: []string{"http://foo.test"},
ClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
ListClaimMappings: map[string]string{
"foo": "bar",
"bling": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
},
expectErr: "ListClaimMappings contains multiple mappings for key",
},
}
jwtCases := map[string]testcase{
"all required for oidc discovery": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
},
expectAuthType: authOIDCDiscovery,
},
"all required for jwks": {
config: Config{
Type: TypeJWT,
JWKSURL: srv.Addr() + "/certs",
JWKSCACert: srv.CACert(), // needed to avoid self signed cert issue
},
expectAuthType: authJWKS,
},
"all required for public keys": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
},
expectAuthType: authStaticKeys,
},
"incompatible with OIDCClientID": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
OIDCClientID: "abc",
},
expectErr: "must not be set for type",
},
"incompatible with OIDCClientSecret": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
OIDCClientSecret: "abc",
},
expectErr: "must not be set for type",
},
"incompatible with OIDCScopes": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
OIDCScopes: []string{"blah"},
},
expectErr: "must not be set for type",
},
"incompatible with OIDCACRValues": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
OIDCACRValues: []string{"acr1"},
},
expectErr: "must not be set for type",
},
"incompatible with AllowedRedirectURIs": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
AllowedRedirectURIs: []string{"http://foo.test"},
},
expectErr: "must not be set for type",
},
"incompatible with VerboseOIDCLogging": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
VerboseOIDCLogging: true,
},
expectErr: "must not be set for type",
},
"too many methods (discovery + jwks)": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
JWKSURL: srv.Addr() + "/certs",
JWKSCACert: srv.CACert(),
// JWTValidationPubKeys: []string{testJWTPubKey},
},
expectErr: "exactly one of",
},
"too many methods (discovery + pubkeys)": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
// JWKSURL: srv.Addr() + "/certs",
// JWKSCACert: srv.CACert(),
JWTValidationPubKeys: []string{testJWTPubKey},
},
expectErr: "exactly one of",
},
"too many methods (jwks + pubkeys)": {
config: Config{
Type: TypeJWT,
// OIDCDiscoveryURL: srv.Addr(),
// OIDCDiscoveryCACert: srv.CACert(),
JWKSURL: srv.Addr() + "/certs",
JWKSCACert: srv.CACert(),
JWTValidationPubKeys: []string{testJWTPubKey},
},
expectErr: "exactly one of",
},
"too many methods (discovery + jwks + pubkeys)": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
JWKSURL: srv.Addr() + "/certs",
JWKSCACert: srv.CACert(),
JWTValidationPubKeys: []string{testJWTPubKey},
},
expectErr: "exactly one of",
},
"incompatible with JWKSCACert": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
JWKSCACert: srv.CACert(),
},
expectErr: "should not be set unless",
},
"invalid pubkey": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKeyBad},
},
expectErr: "error parsing public key",
},
"incompatible with OIDCDiscoveryCACert": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
OIDCDiscoveryCACert: srv.CACert(),
},
expectErr: "should not be set unless",
},
"bad discovery cert": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: oidcBadCACerts,
},
expectErr: "certificate signed by unknown authority",
},
"good discovery cert": {
config: Config{
Type: TypeJWT,
OIDCDiscoveryURL: srv.Addr(),
OIDCDiscoveryCACert: srv.CACert(),
},
expectAuthType: authOIDCDiscovery,
},
"jwks invalid 404": {
config: Config{
Type: TypeJWT,
JWKSURL: srv.Addr() + "/certs_missing",
JWKSCACert: srv.CACert(),
},
expectErr: "get keys failed",
},
"jwks mismatched certs": {
config: Config{
Type: TypeJWT,
JWKSURL: srv.Addr() + "/certs_invalid",
JWKSCACert: srv.CACert(),
},
expectErr: "failed to decode keys",
},
"jwks bad certs": {
config: Config{
Type: TypeJWT,
JWKSURL: srv.Addr() + "/certs_invalid",
JWKSCACert: garbageCACert,
},
expectErr: "could not parse CA PEM value successfully",
},
"valid algorithm": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
JWTSupportedAlgs: []string{
oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.PS384, oidc.PS512,
},
},
expectAuthType: authStaticKeys,
},
"invalid algorithm": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
JWTSupportedAlgs: []string{
oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.PS384, oidc.PS512,
"foo",
},
},
expectErr: "Invalid supported algorithm",
},
"valid claim mappings": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
ClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
ListClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
},
expectAuthType: authStaticKeys,
},
"invalid repeated value claim mappings": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
ClaimMappings: map[string]string{
"foo": "bar",
"bling": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
ListClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
},
expectErr: "ClaimMappings contains multiple mappings for key",
},
"invalid repeated list claim mappings": {
config: Config{
Type: TypeJWT,
JWTValidationPubKeys: []string{testJWTPubKey},
ClaimMappings: map[string]string{
"foo": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
ListClaimMappings: map[string]string{
"foo": "bar",
"bling": "bar",
"peanutbutter": "jelly",
"wd40": "ducttape",
},
},
expectErr: "ListClaimMappings contains multiple mappings for key",
},
}
cases := map[string]testcase{
"bad type": {
config: Config{Type: "invalid"},
expectErr: "authenticator type should be",
},
}
for k, v := range oidcCases {
cases["type=oidc/"+k] = v
v2 := v
v2.config.Type = ""
cases["type=inferred_oidc/"+k] = v2
}
for k, v := range jwtCases {
cases["type=jwt/"+k] = v
}
for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
err := tc.config.Validate()
if tc.expectErr != "" {
require.Error(t, err)
requireErrorContains(t, err, tc.expectErr)
} else {
require.NoError(t, err)
require.Equal(t, tc.expectAuthType, tc.config.authType())
}
})
}
}
func requireErrorContains(t *testing.T, err error, expectedErrorMessage string) {
t.Helper()
if err == nil {
t.Fatal("An error is expected but got nil.")
}
if !strings.Contains(err.Error(), expectedErrorMessage) {
t.Fatalf("unexpected error: %v", err)
}
}
const (
testJWTPubKey = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----`
testJWTPubKeyBad = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIrollingyourricksEVs/o5+uQbTjL3chynL4wXgUg2R9
q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==
-----END PUBLIC KEY-----`
garbageCACert = `this is not a key`
oidcBadCACerts = `-----BEGIN CERTIFICATE-----
MIIDYDCCAkigAwIBAgIJAK8uAVsPxWKGMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgwNzA5MTgwODI5WhcNMjgwNzA2MTgwODI5WjBF
MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA1eaEmIHKQqDlSadCtg6YY332qIMoeSb2iZTRhBRYBXRhMIKF3HoLXlI8
/3veheMnBQM7zxIeLwtJ4VuZVZcpJlqHdsXQVj6A8+8MlAzNh3+Xnv0tjZ83QLwZ
D6FWvMEzihxATD9uTCu2qRgeKnMYQFq4EG72AGb5094zfsXTAiwCfiRPVumiNbs4
Mr75vf+2DEhqZuyP7GR2n3BKzrWo62yAmgLQQ07zfd1u1buv8R72HCYXYpFul5qx
slZHU3yR+tLiBKOYB+C/VuB7hJZfVx25InIL1HTpIwWvmdk3QzpSpAGIAxWMXSzS
oRmBYGnsgR6WTymfXuokD4ZhHOpFZQIDAQABo1MwUTAdBgNVHQ4EFgQURh/QFJBn
hMXcgB1bWbGiU9B2VBQwHwYDVR0jBBgwFoAURh/QFJBnhMXcgB1bWbGiU9B2VBQw
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAr8CZLA3MQjMDWweS
ax9S1fRb8ifxZ4RqDcLj3dw5KZqnjEo8ggczR66T7vVXet/2TFBKYJAM0np26Z4A
WjZfrDT7/bHXseWQAUhw/k2d39o+Um4aXkGpg1Paky9D+ddMdbx1hFkYxDq6kYGd
PlBYSEiYQvVxDx7s7H0Yj9FWKO8WIO6BRUEvLlG7k/Xpp1OI6dV3nqwJ9CbcbqKt
ff4hAtoAmN0/x6yFclFFWX8s7bRGqmnoj39/r98kzeGFb/lPKgQjSVcBJuE7UO4k
8HP6vsnr/ruSlzUMv6XvHtT68kGC1qO3MfqiPhdSa4nxf9g/1xyBmAw/Uf90BJrm
sj9DpQ==
-----END CERTIFICATE-----`
)