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.go

358 lines
10 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 (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
)
const (
// TypeOIDC is the config type to specify if the OIDC authorization code
// workflow is desired. The Authenticator methods GetAuthCodeURL and
// ClaimsFromAuthCode are activated with the type.
TypeOIDC = "oidc"
// TypeJWT is the config type to specify if simple JWT decoding (via static
// keys, JWKS, and OIDC discovery) is desired. The Authenticator method
// ClaimsFromJWT is activated with this type.
TypeJWT = "jwt"
)
// Config is the collection of all settings that pertain to doing OIDC-based
// authentication and direct JWT-based authentication processes.
type Config struct {
// Type defines which kind of authentication will be happening, OIDC-based
// or JWT-based. Allowed values are either 'oidc' or 'jwt'.
//
// Defaults to 'oidc' if unset.
Type string
// -------
// common for type=oidc and type=jwt
// -------
// JWTSupportedAlgs is a list of supported signing algorithms. Defaults to
// RS256.
JWTSupportedAlgs []string
// Comma-separated list of 'aud' claims that are valid for login; any match
// is sufficient
BoundAudiences []string
// Mappings of claims (key) that will be copied to a metadata field
// (value). Use this if the claim you are capturing is singular (such as an
// attribute).
//
// When mapped, the values can be any of a number, string, or boolean and
// will all be stringified when returned.
ClaimMappings map[string]string
// Mappings of claims (key) that will be copied to a metadata field
// (value). Use this if the claim you are capturing is list-like (such as
// groups).
//
// When mapped, the values in each list can be any of a number, string, or
// boolean and will all be stringified when returned.
ListClaimMappings map[string]string
// OIDCDiscoveryURL is the OIDC Discovery URL, without any .well-known
// component (base path). Cannot be used with "JWKSURL" or
// "JWTValidationPubKeys".
OIDCDiscoveryURL string
// OIDCDiscoveryCACert is the CA certificate or chain of certificates, in
// PEM format, to use to validate connections to the OIDC Discovery URL. If
// not set, system certificates are used.
OIDCDiscoveryCACert string
// -------
// just for type=oidc
// -------
// OIDCClientID is the OAuth Client ID configured with your OIDC provider.
//
// Valid only if Type=oidc
OIDCClientID string
// The OAuth Client Secret configured with your OIDC provider.
//
// Valid only if Type=oidc
OIDCClientSecret string
// Comma-separated list of OIDC scopes
//
// Valid only if Type=oidc
OIDCScopes []string
// Space-separated list of OIDC Authorization Context Class Reference values
//
// Valid only if Type=oidc
OIDCACRValues []string
// Comma-separated list of allowed values for redirect_uri
//
// Valid only if Type=oidc
AllowedRedirectURIs []string
// Log received OIDC tokens and claims when debug-level logging is active.
// Not recommended in production since sensitive information may be present
// in OIDC responses.
//
// Valid only if Type=oidc
VerboseOIDCLogging bool
// -------
// just for type=jwt
// -------
// JWKSURL is the JWKS URL to use to authenticate signatures. Cannot be
// used with "OIDCDiscoveryURL" or "JWTValidationPubKeys".
//
// Valid only if Type=jwt
JWKSURL string
// JWKSCACert is the CA certificate or chain of certificates, in PEM
// format, to use to validate connections to the JWKS URL. If not set,
// system certificates are used.
//
// Valid only if Type=jwt
JWKSCACert string
// JWTValidationPubKeys is a list of PEM-encoded public keys to use to
// authenticate signatures locally. Cannot be used with "JWKSURL" or
// "OIDCDiscoveryURL".
//
// Valid only if Type=jwt
JWTValidationPubKeys []string
// BoundIssuer is the value against which to match the 'iss' claim in a
// JWT. Optional.
//
// Valid only if Type=jwt
BoundIssuer string
// Duration in seconds of leeway when validating expiration of
// a token to account for clock skew.
//
// Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to -1.`,
//
// Valid only if Type=jwt
ExpirationLeeway time.Duration
// Duration in seconds of leeway when validating not before values of a
// token to account for clock skew.
//
// Defaults to 150 (2.5 minutes) if set to 0 and can be disabled if set to
// -1.`,
//
// Valid only if Type=jwt
NotBeforeLeeway time.Duration
// Duration in seconds of leeway when validating all claims to account for
// clock skew.
//
// Defaults to 60 (1 minute) if set to 0 and can be disabled if set to
// -1.`,
//
// Valid only if Type=jwt
ClockSkewLeeway time.Duration
}
// Validate returns an error if the config is not valid.
func (c *Config) Validate() error {
validateCtx, validateCtxCancel := context.WithCancel(context.Background())
defer validateCtxCancel()
switch c.Type {
case TypeOIDC, "":
// required
switch {
case c.OIDCDiscoveryURL == "":
return fmt.Errorf("'OIDCDiscoveryURL' must be set for type %q", c.Type)
case c.OIDCClientID == "":
return fmt.Errorf("'OIDCClientID' must be set for type %q", c.Type)
case c.OIDCClientSecret == "":
return fmt.Errorf("'OIDCClientSecret' must be set for type %q", c.Type)
case len(c.AllowedRedirectURIs) == 0:
return fmt.Errorf("'AllowedRedirectURIs' must be set for type %q", c.Type)
}
// not allowed
switch {
case c.JWKSURL != "":
return fmt.Errorf("'JWKSURL' must not be set for type %q", c.Type)
case c.JWKSCACert != "":
return fmt.Errorf("'JWKSCACert' must not be set for type %q", c.Type)
case len(c.JWTValidationPubKeys) != 0:
return fmt.Errorf("'JWTValidationPubKeys' must not be set for type %q", c.Type)
case c.BoundIssuer != "":
return fmt.Errorf("'BoundIssuer' must not be set for type %q", c.Type)
case c.ExpirationLeeway != 0:
return fmt.Errorf("'ExpirationLeeway' must not be set for type %q", c.Type)
case c.NotBeforeLeeway != 0:
return fmt.Errorf("'NotBeforeLeeway' must not be set for type %q", c.Type)
case c.ClockSkewLeeway != 0:
return fmt.Errorf("'ClockSkewLeeway' must not be set for type %q", c.Type)
}
var bad []string
for _, allowed := range c.AllowedRedirectURIs {
if _, err := url.Parse(allowed); err != nil {
bad = append(bad, allowed)
}
}
if len(bad) > 0 {
return fmt.Errorf("Invalid AllowedRedirectURIs provided: %v", bad)
}
case TypeJWT:
// not allowed
switch {
case c.OIDCClientID != "":
return fmt.Errorf("'OIDCClientID' must not be set for type %q", c.Type)
case c.OIDCClientSecret != "":
return fmt.Errorf("'OIDCClientSecret' must not be set for type %q", c.Type)
case len(c.OIDCScopes) != 0:
return fmt.Errorf("'OIDCScopes' must not be set for type %q", c.Type)
case len(c.OIDCACRValues) != 0:
return fmt.Errorf("'OIDCACRValues' must not be set for type %q", c.Type)
case len(c.AllowedRedirectURIs) != 0:
return fmt.Errorf("'AllowedRedirectURIs' must not be set for type %q", c.Type)
case c.VerboseOIDCLogging:
return fmt.Errorf("'VerboseOIDCLogging' must not be set for type %q", c.Type)
}
methodCount := 0
if c.OIDCDiscoveryURL != "" {
methodCount++
}
if len(c.JWTValidationPubKeys) != 0 {
methodCount++
}
if c.JWKSURL != "" {
methodCount++
}
if methodCount != 1 {
return fmt.Errorf("exactly one of 'JWTValidationPubKeys', 'JWKSURL', or 'OIDCDiscoveryURL' must be set for type %q", c.Type)
}
if c.JWKSURL != "" {
httpClient, err := createHTTPClient(c.JWKSCACert)
if err != nil {
return fmt.Errorf("error checking JWKSCACert: %v", err)
}
ctx := contextWithHttpClient(validateCtx, httpClient)
keyset := oidc.NewRemoteKeySet(ctx, c.JWKSURL)
// Try to verify a correctly formatted JWT. The signature will fail
// to match, but other errors with fetching the remote keyset
// should be reported.
_, err = keyset.VerifySignature(ctx, testJWT)
if err == nil {
err = errors.New("unexpected verification of JWT")
}
if !strings.Contains(err.Error(), "failed to verify id token signature") {
return fmt.Errorf("error checking JWKSURL: %v", err)
}
} else if c.JWKSCACert != "" {
return fmt.Errorf("'JWKSCACert' should not be set unless 'JWKSURL' is set")
}
if len(c.JWTValidationPubKeys) != 0 {
for i, v := range c.JWTValidationPubKeys {
if _, err := parsePublicKeyPEM([]byte(v)); err != nil {
return fmt.Errorf("error parsing public key JWTValidationPubKeys[%d]: %v", i, err)
}
}
}
default:
return fmt.Errorf("authenticator type should be %q or %q", TypeOIDC, TypeJWT)
}
if c.OIDCDiscoveryURL != "" {
httpClient, err := createHTTPClient(c.OIDCDiscoveryCACert)
if err != nil {
return fmt.Errorf("error checking OIDCDiscoveryCACert: %v", err)
}
ctx := contextWithHttpClient(validateCtx, httpClient)
if _, err := oidc.NewProvider(ctx, c.OIDCDiscoveryURL); err != nil {
return fmt.Errorf("error checking OIDCDiscoveryURL: %v", err)
}
} else if c.OIDCDiscoveryCACert != "" {
return fmt.Errorf("'OIDCDiscoveryCACert' should not be set unless 'OIDCDiscoveryURL' is set")
}
for _, a := range c.JWTSupportedAlgs {
switch a {
case oidc.RS256, oidc.RS384, oidc.RS512,
oidc.ES256, oidc.ES384, oidc.ES512,
oidc.PS256, oidc.PS384, oidc.PS512:
default:
return fmt.Errorf("Invalid supported algorithm: %s", a)
}
}
if len(c.ClaimMappings) > 0 {
targets := make(map[string]bool)
for _, mappedKey := range c.ClaimMappings {
if targets[mappedKey] {
return fmt.Errorf("ClaimMappings contains multiple mappings for key %q", mappedKey)
}
targets[mappedKey] = true
}
}
if len(c.ListClaimMappings) > 0 {
targets := make(map[string]bool)
for _, mappedKey := range c.ListClaimMappings {
if targets[mappedKey] {
return fmt.Errorf("ListClaimMappings contains multiple mappings for key %q", mappedKey)
}
targets[mappedKey] = true
}
}
return nil
}
const (
authUnconfigured = iota
authStaticKeys
authJWKS
authOIDCDiscovery
authOIDCFlow
)
// authType classifies the authorization type/flow based on config parameters.
// It is only valid to invoke if Validate() returns a nil error.
func (c *Config) authType() int {
switch {
case len(c.JWTValidationPubKeys) > 0:
return authStaticKeys
case c.JWKSURL != "":
return authJWKS
case c.OIDCDiscoveryURL != "":
if c.OIDCClientID != "" && c.OIDCClientSecret != "" {
return authOIDCFlow
}
return authOIDCDiscovery
default:
return authUnconfigured
}
}
const testJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.Hf3E3iCHzqC5QIQ0nCqS1kw78IiQTRVzsLTuKoDIpdk"