consul/internal/go-sso/oidcauth/config_test.go

667 lines
19 KiB
Go

// Copyright (c) HashiCorp, Inc.
// 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-----`
)