mirror of https://github.com/hashicorp/consul
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.
358 lines
11 KiB
358 lines
11 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: MPL-2.0 |
|
|
|
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 |
|
|
|
// 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"
|
|
|