[NET-3090] Add new JWT provider config entry (#17036)
* [NET-3090] Add new JWT provider config entry * Add initial test cases * update validations for jwt-provider config entry fields * more validation * start improving tests * more tests * Normalize * Improve tests and move validate fns * usage test update * Add split between ent and oss for partitions * fix lint issues * Added retry backoff, fixed tests, removed unused defaults * take into account default partitions * use countTrue and add aliases * omit audiences if empty * fix failing tests * add omit-entry * update copyright headers ids --------- Co-authored-by: Ronald Ekambi <ronekambi@gmail.com> Co-authored-by: Ronald <roncodingenthusiast@users.noreply.github.com>
This commit is contained in:
parent
d8d89d4b59
commit
91ca3b012c
|
@ -600,6 +600,7 @@ func validateProposedConfigEntryInGraph(
|
|||
case structs.HTTPRoute:
|
||||
case structs.TCPRoute:
|
||||
case structs.RateLimitIPConfig:
|
||||
case structs.JWTProvider:
|
||||
default:
|
||||
return fmt.Errorf("unhandled kind %q during validation of %q", kindName.Kind, kindName.Name)
|
||||
}
|
||||
|
|
|
@ -456,6 +456,22 @@ var baseCases = map[string]testCase{
|
|||
{Name: "kind", Value: "tcp-route"},
|
||||
},
|
||||
},
|
||||
"consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=jwt-provider": { // Legacy
|
||||
Name: "consul.usage.test.consul.state.config_entries",
|
||||
Value: 0,
|
||||
Labels: []metrics.Label{
|
||||
{Name: "datacenter", Value: "dc1"},
|
||||
{Name: "kind", Value: "jwt-provider"},
|
||||
},
|
||||
},
|
||||
"consul.usage.test.state.config_entries;datacenter=dc1;kind=jwt-provider": {
|
||||
Name: "consul.usage.test.state.config_entries",
|
||||
Value: 0,
|
||||
Labels: []metrics.Label{
|
||||
{Name: "datacenter", Value: "dc1"},
|
||||
{Name: "kind", Value: "jwt-provider"},
|
||||
},
|
||||
},
|
||||
"consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=control-plane-request-limit": {
|
||||
Name: "consul.usage.test.consul.state.config_entries",
|
||||
Value: 0,
|
||||
|
@ -915,6 +931,22 @@ var baseCases = map[string]testCase{
|
|||
{Name: "kind", Value: "tcp-route"},
|
||||
},
|
||||
},
|
||||
"consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=jwt-provider": { // Legacy
|
||||
Name: "consul.usage.test.consul.state.config_entries",
|
||||
Value: 0,
|
||||
Labels: []metrics.Label{
|
||||
{Name: "datacenter", Value: "dc1"},
|
||||
{Name: "kind", Value: "jwt-provider"},
|
||||
},
|
||||
},
|
||||
"consul.usage.test.state.config_entries;datacenter=dc1;kind=jwt-provider": {
|
||||
Name: "consul.usage.test.state.config_entries",
|
||||
Value: 0,
|
||||
Labels: []metrics.Label{
|
||||
{Name: "datacenter", Value: "dc1"},
|
||||
{Name: "kind", Value: "jwt-provider"},
|
||||
},
|
||||
},
|
||||
"consul.usage.test.consul.state.config_entries;datacenter=dc1;kind=control-plane-request-limit": {
|
||||
Name: "consul.usage.test.consul.state.config_entries",
|
||||
Value: 0,
|
||||
|
|
|
@ -45,6 +45,7 @@ const (
|
|||
TCPRoute string = "tcp-route"
|
||||
// TODO: decide if we want to highlight 'ip' keyword in the name of RateLimitIPConfig
|
||||
RateLimitIPConfig string = "control-plane-request-limit"
|
||||
JWTProvider string = "jwt-provider"
|
||||
|
||||
ProxyConfigGlobal string = "global"
|
||||
MeshConfigMesh string = "mesh"
|
||||
|
@ -72,6 +73,7 @@ var AllConfigEntryKinds = []string{
|
|||
TCPRoute,
|
||||
InlineCertificate,
|
||||
RateLimitIPConfig,
|
||||
JWTProvider,
|
||||
}
|
||||
|
||||
// ConfigEntry is the interface for centralized configuration stored in Raft.
|
||||
|
@ -730,6 +732,8 @@ func MakeConfigEntry(kind, name string) (ConfigEntry, error) {
|
|||
return &HTTPRouteConfigEntry{Name: name}, nil
|
||||
case TCPRoute:
|
||||
return &TCPRouteConfigEntry{Name: name}, nil
|
||||
case JWTProvider:
|
||||
return &JWTProviderConfigEntry{Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultClockSkewSeconds = 30
|
||||
)
|
||||
|
||||
type JWTProviderConfigEntry struct {
|
||||
// Kind is the kind of configuration entry and must be "jwt-provider".
|
||||
Kind string `json:",omitempty"`
|
||||
|
||||
// Name is the name of the provider being configured.
|
||||
Name string `json:",omitempty"`
|
||||
|
||||
// JSONWebKeySet defines a JSON Web Key Set, its location on disk, or the
|
||||
// means with which to fetch a key set from a remote server.
|
||||
JSONWebKeySet *JSONWebKeySet `json:",omitempty" alias:"json_web_key_set"`
|
||||
|
||||
// Issuer is the entity that must have issued the JWT.
|
||||
// This value must match the "iss" claim of the token.
|
||||
Issuer string `json:",omitempty"`
|
||||
|
||||
// Audiences is the set of audiences the JWT is allowed to access.
|
||||
// If specified, all JWTs verified with this provider must address
|
||||
// at least one of these to be considered valid.
|
||||
Audiences []string `json:",omitempty"`
|
||||
|
||||
// Locations where the JWT will be present in requests.
|
||||
// Envoy will check all of these locations to extract a JWT.
|
||||
// If no locations are specified Envoy will default to:
|
||||
// 1. Authorization header with Bearer schema:
|
||||
// "Authorization: Bearer <token>"
|
||||
// 2. access_token query parameter.
|
||||
Locations []*JWTLocation `json:",omitempty"`
|
||||
|
||||
// Forwarding defines rules for forwarding verified JWTs to the backend.
|
||||
Forwarding *JWTForwardingConfig `json:",omitempty"`
|
||||
|
||||
// ClockSkewSeconds specifies the maximum allowable time difference
|
||||
// from clock skew when validating the "exp" (Expiration) and "nbf"
|
||||
// (Not Before) claims.
|
||||
//
|
||||
// Default value is 30 seconds.
|
||||
ClockSkewSeconds int `json:",omitempty" alias:"clock_skew_seconds"`
|
||||
|
||||
// CacheConfig defines configuration for caching the validation
|
||||
// result for previously seen JWTs. Caching results can speed up
|
||||
// verification when individual tokens are expected to be handled
|
||||
// multiple times.
|
||||
CacheConfig *JWTCacheConfig `json:",omitempty" alias:"cache_config"`
|
||||
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
acl.EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
|
||||
RaftIndex
|
||||
}
|
||||
|
||||
// JWTLocation is a location where the JWT could be present in requests.
|
||||
//
|
||||
// Only one of Header, QueryParam, or Cookie can be specified.
|
||||
type JWTLocation struct {
|
||||
// Header defines how to extract a JWT from an HTTP request header.
|
||||
Header *JWTLocationHeader `json:",omitempty"`
|
||||
|
||||
// QueryParam defines how to extract a JWT from an HTTP request
|
||||
// query parameter.
|
||||
QueryParam *JWTLocationQueryParam `json:",omitempty" alias:"query_param"`
|
||||
|
||||
// Cookie defines how to extract a JWT from an HTTP request cookie.
|
||||
Cookie *JWTLocationCookie `json:",omitempty"`
|
||||
}
|
||||
|
||||
func countTrue(vals ...bool) int {
|
||||
var result int
|
||||
for _, v := range vals {
|
||||
if v {
|
||||
result++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (location *JWTLocation) Validate() error {
|
||||
hasHeader := location.Header != nil
|
||||
hasQueryParam := location.QueryParam != nil
|
||||
hasCookie := location.Cookie != nil
|
||||
|
||||
if countTrue(hasHeader, hasQueryParam, hasCookie) != 1 {
|
||||
return fmt.Errorf("Must set exactly one of: JWT location header, query param or cookie")
|
||||
}
|
||||
|
||||
if hasHeader {
|
||||
return location.Header.Validate()
|
||||
}
|
||||
|
||||
if hasCookie {
|
||||
return location.Cookie.Validate()
|
||||
}
|
||||
|
||||
return location.QueryParam.Validate()
|
||||
}
|
||||
|
||||
// JWTLocationHeader defines how to extract a JWT from an HTTP
|
||||
// request header.
|
||||
type JWTLocationHeader struct {
|
||||
// Name is the name of the header containing the token.
|
||||
Name string `json:",omitempty"`
|
||||
|
||||
// ValuePrefix is an optional prefix that precedes the token in the
|
||||
// header value.
|
||||
// For example, "Bearer " is a standard value prefix for a header named
|
||||
// "Authorization", but the prefix is not part of the token itself:
|
||||
// "Authorization: Bearer <token>"
|
||||
ValuePrefix string `json:",omitempty" alias:"value_prefix"`
|
||||
|
||||
// Forward defines whether the header with the JWT should be
|
||||
// forwarded after the token has been verified. If false, the
|
||||
// header will not be forwarded to the backend.
|
||||
//
|
||||
// Default value is false.
|
||||
Forward bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// JWTLocationQueryParam defines how to extract a JWT from an HTTP request query parameter.
|
||||
type JWTLocationQueryParam struct {
|
||||
// Name is the name of the query param containing the token.
|
||||
Name string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (qp *JWTLocationQueryParam) Validate() error {
|
||||
if qp.Name == "" {
|
||||
return fmt.Errorf("JWT location query param name must be specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// JWTLocationCookie defines how to extract a JWT from an HTTP request cookie.
|
||||
type JWTLocationCookie struct {
|
||||
// Name is the name of the cookie containing the token.
|
||||
Name string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type JWTForwardingConfig struct {
|
||||
// HeaderName is a header name to use when forwarding a verified
|
||||
// JWT to the backend. The verified JWT could have been extracted
|
||||
// from any location (query param, header, or cookie).
|
||||
//
|
||||
// The header value will be base64-URL-encoded, and will not be
|
||||
// padded unless PadForwardPayloadHeader is true.
|
||||
HeaderName string `json:",omitempty" alias:"header_name"`
|
||||
|
||||
// PadForwardPayloadHeader determines whether padding should be added
|
||||
// to the base64 encoded token forwarded with ForwardPayloadHeader.
|
||||
//
|
||||
// Default value is false.
|
||||
PadForwardPayloadHeader bool `alias:"pad_forward_payload_header"`
|
||||
}
|
||||
|
||||
func (fc *JWTForwardingConfig) Validate() error {
|
||||
if fc.HeaderName == "" {
|
||||
return fmt.Errorf("Header name required for forwarding config")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONWebKeySet defines a key set, its location on disk, or the
|
||||
// means with which to fetch a key set from a remote server.
|
||||
//
|
||||
// Exactly one of Local or Remote must be specified.
|
||||
type JSONWebKeySet struct {
|
||||
// Local specifies a local source for the key set.
|
||||
Local *LocalJWKS `json:",omitempty"`
|
||||
|
||||
// Remote specifies how to fetch a key set from a remote server.
|
||||
Remote *RemoteJWKS `json:",omitempty"`
|
||||
}
|
||||
|
||||
// LocalJWKS specifies a location for a local JWKS.
|
||||
//
|
||||
// Only one of String and Filename can be specified.
|
||||
type LocalJWKS struct {
|
||||
// JWKS contains a base64 encoded JWKS.
|
||||
JWKS string `json:",omitempty"`
|
||||
|
||||
// Filename configures a location on disk where the JWKS can be
|
||||
// found. If specified, the file must be present on the disk of ALL
|
||||
// proxies with intentions referencing this provider.
|
||||
Filename string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (ks *LocalJWKS) Validate() error {
|
||||
hasFilename := ks.Filename != ""
|
||||
hasJWKS := ks.JWKS != ""
|
||||
|
||||
if countTrue(hasFilename, hasJWKS) != 1 {
|
||||
return fmt.Errorf("Must specify exactly one of String or filename for local keyset")
|
||||
}
|
||||
|
||||
if hasJWKS {
|
||||
if _, err := base64.StdEncoding.DecodeString(ks.JWKS); err != nil {
|
||||
return fmt.Errorf("JWKS must be valid base64 encoded string")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteJWKS specifies how to fetch a JWKS from a remote server.
|
||||
type RemoteJWKS struct {
|
||||
// URI is the URI of the server to query for the JWKS.
|
||||
URI string `json:",omitempty"`
|
||||
|
||||
// RequestTimeoutMs is the number of milliseconds to
|
||||
// time out when making a request for the JWKS.
|
||||
RequestTimeoutMs int `json:",omitempty" alias:"request_timeout_ms"`
|
||||
|
||||
// CacheDuration is the duration after which cached keys
|
||||
// should be expired.
|
||||
//
|
||||
// Default value from envoy is 10 minutes.
|
||||
CacheDuration time.Duration `json:",omitempty" alias:"cache_duration"`
|
||||
|
||||
// FetchAsynchronously indicates that the JWKS should be fetched
|
||||
// when a client request arrives. Client requests will be paused
|
||||
// until the JWKS is fetched.
|
||||
// If false, the proxy listener will wait for the JWKS to be
|
||||
// fetched before being activated.
|
||||
//
|
||||
// Default value is false.
|
||||
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"`
|
||||
|
||||
// RetryPolicy defines a retry policy for fetching JWKS.
|
||||
//
|
||||
// There is no retry by default.
|
||||
RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"`
|
||||
}
|
||||
|
||||
func (ks *RemoteJWKS) Validate() error {
|
||||
if ks.URI == "" {
|
||||
return fmt.Errorf("Remote JWKS URI is required")
|
||||
}
|
||||
|
||||
if _, err := url.ParseRequestURI(ks.URI); err != nil {
|
||||
return fmt.Errorf("Remote JWKS URI is invalid: %w, uri: %s", err, ks.URI)
|
||||
}
|
||||
|
||||
if ks.RetryPolicy != nil && ks.RetryPolicy.RetryPolicyBackOff != nil {
|
||||
return ks.RetryPolicy.RetryPolicyBackOff.Validate()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type JWKSRetryPolicy struct {
|
||||
// NumRetries is the number of times to retry fetching the JWKS.
|
||||
// The retry strategy uses jittered exponential backoff with
|
||||
// a base interval of 1s and max of 10s.
|
||||
//
|
||||
// Default value is 0.
|
||||
NumRetries int `json:",omitempty" alias:"num_retries"`
|
||||
|
||||
// Backoff policy
|
||||
//
|
||||
// Defaults to envoy's backoff policy
|
||||
RetryPolicyBackOff *RetryPolicyBackOff `json:",omitempty" alias:"retry_policy_back_off"`
|
||||
}
|
||||
|
||||
type RetryPolicyBackOff struct {
|
||||
// BaseInterval to be used for the next back off computation
|
||||
//
|
||||
// The default value from envoy is 1s
|
||||
BaseInterval time.Duration `json:",omitempty" alias:"base_interval"`
|
||||
|
||||
// MaxInternal to be used to specify the maximum interval between retries.
|
||||
// Optional but should be greater or equal to BaseInterval.
|
||||
//
|
||||
// Defaults to 10 times BaseInterval
|
||||
MaxInterval time.Duration `json:",omitempty" alias:"max_interval"`
|
||||
}
|
||||
|
||||
func (r *RetryPolicyBackOff) Validate() error {
|
||||
|
||||
if (r.MaxInterval != 0) && (r.BaseInterval > r.MaxInterval) {
|
||||
return fmt.Errorf("Retry policy backoff's MaxInterval should be greater or equal to BaseInterval")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type JWTCacheConfig struct {
|
||||
// Size specifies the maximum number of JWT verification
|
||||
// results to cache.
|
||||
//
|
||||
// Defaults to 0, meaning that JWT caching is disabled.
|
||||
Size int `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (e *JWTProviderConfigEntry) GetKind() string { return JWTProvider }
|
||||
func (e *JWTProviderConfigEntry) GetName() string { return e.Name }
|
||||
func (e *JWTProviderConfigEntry) GetMeta() map[string]string { return e.Meta }
|
||||
func (e *JWTProviderConfigEntry) GetEnterpriseMeta() *acl.EnterpriseMeta { return &e.EnterpriseMeta }
|
||||
func (e *JWTProviderConfigEntry) GetRaftIndex() *RaftIndex { return &e.RaftIndex }
|
||||
|
||||
func (e *JWTProviderConfigEntry) CanRead(authz acl.Authorizer) error {
|
||||
var authzContext acl.AuthorizerContext
|
||||
e.FillAuthzContext(&authzContext)
|
||||
return authz.ToAllowAuthorizer().MeshReadAllowed(&authzContext)
|
||||
}
|
||||
|
||||
func (e *JWTProviderConfigEntry) CanWrite(authz acl.Authorizer) error {
|
||||
var authzContext acl.AuthorizerContext
|
||||
e.FillAuthzContext(&authzContext)
|
||||
return authz.ToAllowAuthorizer().MeshWriteAllowed(&authzContext)
|
||||
}
|
||||
|
||||
func (jwks *JSONWebKeySet) Validate() error {
|
||||
hasLocalKeySet := jwks.Local != nil
|
||||
hasRemoteKeySet := jwks.Remote != nil
|
||||
|
||||
if countTrue(hasLocalKeySet, hasRemoteKeySet) != 1 {
|
||||
return fmt.Errorf("Must specify exactly one of Local or Remote JSON Web key set")
|
||||
}
|
||||
|
||||
if hasRemoteKeySet {
|
||||
return jwks.Remote.Validate()
|
||||
}
|
||||
|
||||
return jwks.Local.Validate()
|
||||
}
|
||||
|
||||
func (lh *JWTLocationHeader) Validate() error {
|
||||
if lh.Name == "" {
|
||||
return fmt.Errorf("JWT location header name must be specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (lc *JWTLocationCookie) Validate() error {
|
||||
if lc.Name == "" {
|
||||
return fmt.Errorf("JWT location cookie name must be specified")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateLocations(locations []*JWTLocation) error {
|
||||
var result error
|
||||
for _, location := range locations {
|
||||
if err := location.Validate(); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (e *JWTProviderConfigEntry) Validate() error {
|
||||
if e.Name == "" {
|
||||
return fmt.Errorf("Name is required")
|
||||
}
|
||||
|
||||
if err := validateConfigEntryMeta(e.Meta); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := e.validatePartition(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.JSONWebKeySet == nil {
|
||||
return fmt.Errorf("JSONWebKeySet is required")
|
||||
}
|
||||
|
||||
if err := e.JSONWebKeySet.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateLocations(e.Locations); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.Forwarding != nil {
|
||||
if err := e.Forwarding.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *JWTProviderConfigEntry) Normalize() error {
|
||||
if e == nil {
|
||||
return fmt.Errorf("Config entry is nil")
|
||||
}
|
||||
|
||||
e.Kind = JWTProvider
|
||||
e.EnterpriseMeta.Normalize()
|
||||
|
||||
if e.ClockSkewSeconds == 0 {
|
||||
e.ClockSkewSeconds = DefaultClockSkewSeconds
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//go:build !consulent
|
||||
// +build !consulent
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
)
|
||||
|
||||
func (e *JWTProviderConfigEntry) validatePartition() error {
|
||||
if !acl.IsDefaultPartition(e.PartitionOrDefault()) {
|
||||
return fmt.Errorf("Partitions are an enterprise only feature")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,369 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package structs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/acl"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func newTestAuthz(t *testing.T, src string) acl.Authorizer {
|
||||
policy, err := acl.NewPolicyFromSource(src, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
authorizer, err := acl.NewPolicyAuthorizerWithDefaults(acl.DenyAll(), []*acl.Policy{policy}, nil)
|
||||
require.NoError(t, err)
|
||||
return authorizer
|
||||
}
|
||||
|
||||
var tenSeconds time.Duration = 10 * time.Second
|
||||
var hundredSeconds time.Duration = 100 * time.Second
|
||||
|
||||
func TestJWTProviderConfigEntry_ValidateAndNormalize(t *testing.T) {
|
||||
defaultMeta := DefaultEnterpriseMetaInDefaultPartition()
|
||||
|
||||
cases := map[string]configEntryTestcase{
|
||||
"valid jwt-provider - local jwks": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-jwt-provider",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
Filename: "jwks.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-jwt-provider",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
Filename: "jwks.txt",
|
||||
},
|
||||
},
|
||||
ClockSkewSeconds: DefaultClockSkewSeconds,
|
||||
EnterpriseMeta: *defaultMeta,
|
||||
},
|
||||
},
|
||||
"valid jwt-provider - remote jwks defaults": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-jwt-provider",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
},
|
||||
},
|
||||
Locations: []*JWTLocation{
|
||||
{
|
||||
Header: &JWTLocationHeader{
|
||||
Name: "Authorization",
|
||||
},
|
||||
},
|
||||
},
|
||||
Forwarding: &JWTForwardingConfig{
|
||||
HeaderName: "Some-Header",
|
||||
},
|
||||
},
|
||||
expected: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-jwt-provider",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
},
|
||||
},
|
||||
Forwarding: &JWTForwardingConfig{
|
||||
HeaderName: "Some-Header",
|
||||
},
|
||||
Locations: []*JWTLocation{
|
||||
{
|
||||
Header: &JWTLocationHeader{
|
||||
Name: "Authorization",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClockSkewSeconds: DefaultClockSkewSeconds,
|
||||
EnterpriseMeta: *defaultMeta,
|
||||
},
|
||||
},
|
||||
"invalid jwt-provider - no name": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "",
|
||||
},
|
||||
validateErr: "Name is required",
|
||||
},
|
||||
"invalid jwt-provider - no jwks": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
},
|
||||
validateErr: "JSONWebKeySet is required",
|
||||
},
|
||||
"invalid jwt-provider - no jwks local or remote set": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{},
|
||||
},
|
||||
validateErr: "Must specify exactly one of Local or Remote JSON Web key set",
|
||||
},
|
||||
"invalid jwt-provider - local jwks with non-encoded base64 jwks": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
JWKS: "not base64 encoded",
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "JWKS must be valid base64 encoded string",
|
||||
},
|
||||
"invalid jwt-provider - both jwks local and remote set": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
Filename: "jwks.txt",
|
||||
},
|
||||
Remote: &RemoteJWKS{},
|
||||
},
|
||||
},
|
||||
validateErr: "Must specify exactly one of Local or Remote JSON Web key set",
|
||||
},
|
||||
"invalid jwt-provider - local jwks string and filename both set": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
Filename: "jwks.txt",
|
||||
JWKS: "d2VhcmV0ZXN0aW5n",
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "Must specify exactly one of String or filename for local keyset",
|
||||
},
|
||||
"invalid jwt-provider - remote jwks missing uri": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "Remote JWKS URI is required",
|
||||
},
|
||||
"invalid jwt-provider - remote jwks invalid uri": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "jibberishUrl",
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "Remote JWKS URI is invalid",
|
||||
},
|
||||
"invalid jwt-provider - JWT location with all fields": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
},
|
||||
},
|
||||
Locations: []*JWTLocation{
|
||||
{
|
||||
Header: &JWTLocationHeader{
|
||||
Name: "Authorization",
|
||||
},
|
||||
QueryParam: &JWTLocationQueryParam{
|
||||
Name: "TOKEN-QUERY",
|
||||
},
|
||||
Cookie: &JWTLocationCookie{
|
||||
Name: "SomeCookie",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "Must set exactly one of: JWT location header, query param or cookie",
|
||||
},
|
||||
"invalid jwt-provider - Remote JWKS retry policy maxinterval < baseInterval": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
RetryPolicy: &JWKSRetryPolicy{
|
||||
RetryPolicyBackOff: &RetryPolicyBackOff{
|
||||
BaseInterval: hundredSeconds,
|
||||
MaxInterval: tenSeconds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "Retry policy backoff's MaxInterval should be greater or equal to BaseInterval",
|
||||
},
|
||||
"invalid jwt-provider - JWT location with 2 fields": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "okta",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
},
|
||||
},
|
||||
Locations: []*JWTLocation{
|
||||
{
|
||||
Header: &JWTLocationHeader{
|
||||
Name: "Authorization",
|
||||
},
|
||||
QueryParam: &JWTLocationQueryParam{
|
||||
Name: "TOKEN-QUERY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
validateErr: "Must set exactly one of: JWT location header, query param or cookie",
|
||||
},
|
||||
"valid jwt-provider - with all possible fields": {
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-jwt-provider",
|
||||
Issuer: "iss",
|
||||
Audiences: []string{"api", "web"},
|
||||
CacheConfig: &JWTCacheConfig{
|
||||
Size: 30,
|
||||
},
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
RetryPolicy: &JWKSRetryPolicy{
|
||||
RetryPolicyBackOff: &RetryPolicyBackOff{
|
||||
BaseInterval: tenSeconds,
|
||||
MaxInterval: hundredSeconds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Forwarding: &JWTForwardingConfig{
|
||||
HeaderName: "Some-Header",
|
||||
},
|
||||
Locations: []*JWTLocation{
|
||||
{
|
||||
Cookie: &JWTLocationCookie{
|
||||
Name: "SomeCookie",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClockSkewSeconds: 20,
|
||||
},
|
||||
expected: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-jwt-provider",
|
||||
Issuer: "iss",
|
||||
Audiences: []string{"api", "web"},
|
||||
CacheConfig: &JWTCacheConfig{
|
||||
Size: 30,
|
||||
},
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Remote: &RemoteJWKS{
|
||||
FetchAsynchronously: true,
|
||||
URI: "https://example.com/.well-known/jwks.json",
|
||||
RetryPolicy: &JWKSRetryPolicy{
|
||||
RetryPolicyBackOff: &RetryPolicyBackOff{
|
||||
BaseInterval: tenSeconds,
|
||||
MaxInterval: hundredSeconds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Forwarding: &JWTForwardingConfig{
|
||||
HeaderName: "Some-Header",
|
||||
},
|
||||
Locations: []*JWTLocation{
|
||||
{
|
||||
Cookie: &JWTLocationCookie{
|
||||
Name: "SomeCookie",
|
||||
},
|
||||
},
|
||||
},
|
||||
ClockSkewSeconds: 20,
|
||||
EnterpriseMeta: *defaultMeta,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testConfigEntryNormalizeAndValidate(t, cases)
|
||||
}
|
||||
|
||||
func TestJWTProviderConfigEntry_ACLs(t *testing.T) {
|
||||
cases := []configEntryACLTestCase{
|
||||
{
|
||||
name: "jwt-provider",
|
||||
entry: &JWTProviderConfigEntry{
|
||||
Kind: JWTProvider,
|
||||
Name: "test-provider",
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
Filename: "jwks.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectACLs: []configEntryTestACL{
|
||||
{
|
||||
name: "no-authz",
|
||||
authorizer: newTestAuthz(t, ``),
|
||||
canRead: false,
|
||||
canWrite: false,
|
||||
},
|
||||
{
|
||||
name: "jwt-provider: mesh read",
|
||||
authorizer: newTestAuthz(t, `mesh = "read"`),
|
||||
canRead: true,
|
||||
canWrite: false,
|
||||
},
|
||||
{
|
||||
name: "jwt-provider: mesh write",
|
||||
authorizer: newTestAuthz(t, `mesh = "write"`),
|
||||
canRead: true,
|
||||
canWrite: true,
|
||||
},
|
||||
{
|
||||
name: "jwt-provider: operator read",
|
||||
authorizer: newTestAuthz(t, `operator = "read"`),
|
||||
canRead: true,
|
||||
canWrite: false,
|
||||
},
|
||||
{
|
||||
name: "jwt-provider: operator write",
|
||||
authorizer: newTestAuthz(t, `operator = "write"`),
|
||||
canRead: true,
|
||||
canWrite: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testConfigEntries_ListRelatedServices_AndACLs(t, cases)
|
||||
}
|
|
@ -35,6 +35,7 @@ const (
|
|||
TCPRoute string = "tcp-route"
|
||||
InlineCertificate string = "inline-certificate"
|
||||
HTTPRoute string = "http-route"
|
||||
JWTProvider string = "jwt-provider"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -392,6 +393,8 @@ func makeConfigEntry(kind, name string) (ConfigEntry, error) {
|
|||
return &HTTPRouteConfigEntry{Kind: kind, Name: name}, nil
|
||||
case RateLimitIPConfig:
|
||||
return &RateLimitIPConfigEntry{Kind: kind, Name: name}, nil
|
||||
case JWTProvider:
|
||||
return &JWTProviderConfigEntry{Kind: kind, Name: name}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid config entry kind: %s", kind)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type JWTProviderConfigEntry struct {
|
||||
// Kind is the kind of configuration entry and must be "jwt-provider".
|
||||
Kind string `json:",omitempty"`
|
||||
|
||||
// Name is the name of the provider being configured.
|
||||
Name string `json:",omitempty"`
|
||||
|
||||
// JSONWebKeySet defines a JSON Web Key Set, its location on disk, or the
|
||||
// means with which to fetch a key set from a remote server.
|
||||
JSONWebKeySet *JSONWebKeySet `json:",omitempty" alias:"json_web_key_set"`
|
||||
|
||||
// Issuer is the entity that must have issued the JWT.
|
||||
// This value must match the "iss" claim of the token.
|
||||
Issuer string `json:",omitempty"`
|
||||
|
||||
// Audiences is the set of audiences the JWT is allowed to access.
|
||||
// If specified, all JWTs verified with this provider must address
|
||||
// at least one of these to be considered valid.
|
||||
Audiences []string `json:",omitempty"`
|
||||
|
||||
// Locations where the JWT will be present in requests.
|
||||
// Envoy will check all of these locations to extract a JWT.
|
||||
// If no locations are specified Envoy will default to:
|
||||
// 1. Authorization header with Bearer schema:
|
||||
// "Authorization: Bearer <token>"
|
||||
// 2. access_token query parameter.
|
||||
Locations []*JWTLocation `json:",omitempty"`
|
||||
|
||||
// Forwarding defines rules for forwarding verified JWTs to the backend.
|
||||
Forwarding *JWTForwardingConfig `json:",omitempty"`
|
||||
|
||||
// ClockSkewSeconds specifies the maximum allowable time difference
|
||||
// from clock skew when validating the "exp" (Expiration) and "nbf"
|
||||
// (Not Before) claims.
|
||||
//
|
||||
// Default value is 30 seconds.
|
||||
ClockSkewSeconds int `json:",omitempty" alias:"clock_skew_seconds"`
|
||||
|
||||
// CacheConfig defines configuration for caching the validation
|
||||
// result for previously seen JWTs. Caching results can speed up
|
||||
// verification when individual tokens are expected to be handled
|
||||
// multiple times.
|
||||
CacheConfig *JWTCacheConfig `json:",omitempty" alias:"cache_config"`
|
||||
|
||||
Meta map[string]string `json:",omitempty"`
|
||||
|
||||
// CreateIndex is the Raft index this entry was created at. This is a
|
||||
// read-only field.
|
||||
CreateIndex uint64 `json:",omitempty"`
|
||||
|
||||
// ModifyIndex is used for the Check-And-Set operations and can also be fed
|
||||
// back into the WaitIndex of the QueryOptions in order to perform blocking
|
||||
// queries.
|
||||
ModifyIndex uint64 `json:",omitempty"`
|
||||
|
||||
// Partition is the partition the JWTProviderConfigEntry applies to.
|
||||
// Partitioning is a Consul Enterprise feature.
|
||||
Partition string `json:",omitempty"`
|
||||
|
||||
// Namespace is the namespace the JWTProviderConfigEntry applies to.
|
||||
// Namespacing is a Consul Enterprise feature.
|
||||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// JWTLocation is a location where the JWT could be present in requests.
|
||||
//
|
||||
// Only one of Header, QueryParam, or Cookie can be specified.
|
||||
type JWTLocation struct {
|
||||
// Header defines how to extract a JWT from an HTTP request header.
|
||||
Header *JWTLocationHeader `json:",omitempty"`
|
||||
|
||||
// QueryParam defines how to extract a JWT from an HTTP request
|
||||
// query parameter.
|
||||
QueryParam *JWTLocationQueryParam `json:",omitempty" alias:"query_param"`
|
||||
|
||||
// Cookie defines how to extract a JWT from an HTTP request cookie.
|
||||
Cookie *JWTLocationCookie `json:",omitempty"`
|
||||
}
|
||||
|
||||
// JWTLocationHeader defines how to extract a JWT from an HTTP
|
||||
// request header.
|
||||
type JWTLocationHeader struct {
|
||||
// Name is the name of the header containing the token.
|
||||
Name string `json:",omitempty"`
|
||||
|
||||
// ValuePrefix is an optional prefix that precedes the token in the
|
||||
// header value.
|
||||
// For example, "Bearer " is a standard value prefix for a header named
|
||||
// "Authorization", but the prefix is not part of the token itself:
|
||||
// "Authorization: Bearer <token>"
|
||||
ValuePrefix string `json:",omitempty" alias:"value_prefix"`
|
||||
|
||||
// Forward defines whether the header with the JWT should be
|
||||
// forwarded after the token has been verified. If false, the
|
||||
// header will not be forwarded to the backend.
|
||||
//
|
||||
// Default value is false.
|
||||
Forward bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// JWTLocationQueryParam defines how to extract a JWT from an HTTP request query parameter.
|
||||
type JWTLocationQueryParam struct {
|
||||
// Name is the name of the query param containing the token.
|
||||
Name string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// JWTLocationCookie defines how to extract a JWT from an HTTP request cookie.
|
||||
type JWTLocationCookie struct {
|
||||
// Name is the name of the cookie containing the token.
|
||||
Name string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type JWTForwardingConfig struct {
|
||||
// HeaderName is a header name to use when forwarding a verified
|
||||
// JWT to the backend. The verified JWT could have been extracted
|
||||
// from any location (query param, header, or cookie).
|
||||
//
|
||||
// The header value will be base64-URL-encoded, and will not be
|
||||
// padded unless PadForwardPayloadHeader is true.
|
||||
HeaderName string `json:",omitempty" alias:"header_name"`
|
||||
|
||||
// PadForwardPayloadHeader determines whether padding should be added
|
||||
// to the base64 encoded token forwarded with ForwardPayloadHeader.
|
||||
//
|
||||
// Default value is false.
|
||||
PadForwardPayloadHeader bool `json:",omitempty" alias:"pad_forward_payload_header"`
|
||||
}
|
||||
|
||||
// JSONWebKeySet defines a key set, its location on disk, or the
|
||||
// means with which to fetch a key set from a remote server.
|
||||
//
|
||||
// Exactly one of Local or Remote must be specified.
|
||||
type JSONWebKeySet struct {
|
||||
// Local specifies a local source for the key set.
|
||||
Local *LocalJWKS `json:",omitempty"`
|
||||
|
||||
// Remote specifies how to fetch a key set from a remote server.
|
||||
Remote *RemoteJWKS `json:",omitempty"`
|
||||
}
|
||||
|
||||
// LocalJWKS specifies a location for a local JWKS.
|
||||
//
|
||||
// Only one of String and Filename can be specified.
|
||||
type LocalJWKS struct {
|
||||
// JWKS contains a base64 encoded JWKS.
|
||||
JWKS string `json:",omitempty"`
|
||||
|
||||
// Filename configures a location on disk where the JWKS can be
|
||||
// found. If specified, the file must be present on the disk of ALL
|
||||
// proxies with intentions referencing this provider.
|
||||
Filename string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// RemoteJWKS specifies how to fetch a JWKS from a remote server.
|
||||
type RemoteJWKS struct {
|
||||
// URI is the URI of the server to query for the JWKS.
|
||||
URI string `json:",omitempty"`
|
||||
|
||||
// RequestTimeoutMs is the number of milliseconds to
|
||||
// time out when making a request for the JWKS.
|
||||
RequestTimeoutMs int `json:",omitempty" alias:"request_timeout_ms"`
|
||||
|
||||
// CacheDuration is the duration after which cached keys
|
||||
// should be expired.
|
||||
//
|
||||
// Default value is 5 minutes.
|
||||
CacheDuration time.Duration `json:",omitempty" alias:"cache_duration"`
|
||||
|
||||
// FetchAsynchronously indicates that the JWKS should be fetched
|
||||
// when a client request arrives. Client requests will be paused
|
||||
// until the JWKS is fetched.
|
||||
// If false, the proxy listener will wait for the JWKS to be
|
||||
// fetched before being activated.
|
||||
//
|
||||
// Default value is false.
|
||||
FetchAsynchronously bool `json:",omitempty" alias:"fetch_asynchronously"`
|
||||
|
||||
// RetryPolicy defines a retry policy for fetching JWKS.
|
||||
//
|
||||
// There is no retry by default.
|
||||
RetryPolicy *JWKSRetryPolicy `json:",omitempty" alias:"retry_policy"`
|
||||
}
|
||||
|
||||
type JWKSRetryPolicy struct {
|
||||
// NumRetries is the number of times to retry fetching the JWKS.
|
||||
// The retry strategy uses jittered exponential backoff with
|
||||
// a base interval of 1s and max of 10s.
|
||||
//
|
||||
// Default value is 0.
|
||||
NumRetries int `json:",omitempty" alias:"num_retries"`
|
||||
|
||||
// Backoff policy
|
||||
//
|
||||
// Defaults to Envoy's backoff policy
|
||||
RetryPolicyBackOff *RetryPolicyBackOff `json:",omitempty" alias:"retry_policy_back_off"`
|
||||
}
|
||||
|
||||
type RetryPolicyBackOff struct {
|
||||
// BaseInterval to be used for the next back off computation
|
||||
//
|
||||
// The default value from envoy is 1s
|
||||
BaseInterval time.Duration `json:",omitempty" alias:"base_interval"`
|
||||
|
||||
// MaxInternal to be used to specify the maximum interval between retries.
|
||||
// Optional but should be greater or equal to BaseInterval.
|
||||
//
|
||||
// Defaults to 10 times BaseInterval
|
||||
MaxInterval time.Duration `json:",omitempty" alias:"max_interval"`
|
||||
}
|
||||
|
||||
type JWTCacheConfig struct {
|
||||
// Size specifies the maximum number of JWT verification
|
||||
// results to cache.
|
||||
//
|
||||
// Defaults to 0, meaning that JWT caching is disabled.
|
||||
Size int `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (e *JWTProviderConfigEntry) GetKind() string {
|
||||
return JWTProvider
|
||||
}
|
||||
|
||||
func (e *JWTProviderConfigEntry) GetName() string { return e.Name }
|
||||
func (e *JWTProviderConfigEntry) GetMeta() map[string]string { return e.Meta }
|
||||
func (e *JWTProviderConfigEntry) GetCreateIndex() uint64 { return e.CreateIndex }
|
||||
func (e *JWTProviderConfigEntry) GetModifyIndex() uint64 { return e.ModifyIndex }
|
||||
func (e *JWTProviderConfigEntry) GetPartition() string { return e.Partition }
|
||||
func (e *JWTProviderConfigEntry) GetNamespace() string { return e.Namespace }
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) HashiCorp, Inc.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/consul/sdk/testutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPI_ConfigEntries_JWTProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
c, s := makeClient(t)
|
||||
defer s.Stop()
|
||||
|
||||
entries := c.ConfigEntries()
|
||||
|
||||
testutil.RunStep(t, "set and get", func(t *testing.T) {
|
||||
jwtProvider := &JWTProviderConfigEntry{
|
||||
Name: "okta",
|
||||
Kind: JWTProvider,
|
||||
JSONWebKeySet: &JSONWebKeySet{
|
||||
Local: &LocalJWKS{
|
||||
Filename: "test.txt",
|
||||
},
|
||||
},
|
||||
Meta: map[string]string{
|
||||
"gir": "zim",
|
||||
},
|
||||
}
|
||||
|
||||
_, wm, err := entries.Set(jwtProvider, nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, wm)
|
||||
require.NotEqual(t, 0, wm.RequestTime)
|
||||
|
||||
entry, qm, err := entries.Get(JWTProvider, "okta", nil)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, qm)
|
||||
require.NotEqual(t, 0, qm.RequestTime)
|
||||
|
||||
result, ok := entry.(*JWTProviderConfigEntry)
|
||||
require.True(t, ok)
|
||||
|
||||
require.Equal(t, jwtProvider.Name, result.Name)
|
||||
require.Equal(t, jwtProvider.JSONWebKeySet, result.JSONWebKeySet)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue