open-consul/internal/go-sso/oidcauth/config.go

349 lines
10 KiB
Go

package oidcauth
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/coreos/go-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
// TODO(sso): actually just send these down as string claims?
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
// 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.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"