mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-04 18:00:08 +00:00
support multiple audiences with jwt authenticator
Signed-off-by: Anish Ramasekar <anish.ramasekar@gmail.com>
This commit is contained in:
parent
19da90d639
commit
18c563546a
@ -143,21 +143,23 @@ func validateAudiences(audiences []string, audienceMatchPolicy api.AudienceMatch
|
|||||||
allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath)))
|
allErrs = append(allErrs, field.Required(fldPath, fmt.Sprintf(atLeastOneRequiredErrFmt, fldPath)))
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
// This stricter validation is because the --oidc-client-id flag option is singular.
|
|
||||||
// This will be removed when we support multiple audiences with the StructuredAuthenticationConfiguration feature gate.
|
|
||||||
if len(audiences) > 1 {
|
|
||||||
allErrs = append(allErrs, field.TooMany(fldPath, len(audiences), 1))
|
|
||||||
return allErrs
|
|
||||||
}
|
|
||||||
|
|
||||||
|
seenAudiences := sets.NewString()
|
||||||
for i, audience := range audiences {
|
for i, audience := range audiences {
|
||||||
fldPath := fldPath.Index(i)
|
fldPath := fldPath.Index(i)
|
||||||
if len(audience) == 0 {
|
if len(audience) == 0 {
|
||||||
allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty"))
|
allErrs = append(allErrs, field.Required(fldPath, "audience can't be empty"))
|
||||||
}
|
}
|
||||||
|
if seenAudiences.Has(audience) {
|
||||||
|
allErrs = append(allErrs, field.Duplicate(fldPath, audience))
|
||||||
|
}
|
||||||
|
seenAudiences.Insert(audience)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(audienceMatchPolicy) > 0 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny {
|
if len(audiences) > 1 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny {
|
||||||
|
allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be MatchAny for multiple audiences"))
|
||||||
|
}
|
||||||
|
if len(audiences) == 1 && (len(audienceMatchPolicy) > 0 && audienceMatchPolicy != api.AudienceMatchPolicyMatchAny) {
|
||||||
allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be empty or MatchAny for single audience"))
|
allErrs = append(allErrs, field.Invalid(audienceMatchPolicyFldPath, audienceMatchPolicy, "audienceMatchPolicy must be empty or MatchAny for single audience"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,6 +304,23 @@ func TestValidateAudiences(t *testing.T) {
|
|||||||
matchPolicy: "MatchAny",
|
matchPolicy: "MatchAny",
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate audience",
|
||||||
|
in: []string{"audience", "audience"},
|
||||||
|
matchPolicy: "MatchAny",
|
||||||
|
want: `issuer.audiences[1]: Duplicate value: "audience"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match policy not set with multiple audiences",
|
||||||
|
in: []string{"audience1", "audience2"},
|
||||||
|
want: `issuer.audienceMatchPolicy: Invalid value: "": audienceMatchPolicy must be MatchAny for multiple audiences`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid multiple audiences",
|
||||||
|
in: []string{"audience1", "audience2"},
|
||||||
|
matchPolicy: "MatchAny",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range testCases {
|
for _, tt := range testCases {
|
||||||
|
@ -49,6 +49,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/net"
|
"k8s.io/apimachinery/pkg/util/net"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"k8s.io/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||||
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
||||||
@ -99,12 +100,12 @@ type CAContentProvider interface {
|
|||||||
|
|
||||||
// initVerifier creates a new ID token verifier for the given configuration and issuer URL. On success, calls setVerifier with the
|
// initVerifier creates a new ID token verifier for the given configuration and issuer URL. On success, calls setVerifier with the
|
||||||
// resulting verifier.
|
// resulting verifier.
|
||||||
func initVerifier(ctx context.Context, config *oidc.Config, iss string) (*oidc.IDTokenVerifier, error) {
|
func initVerifier(ctx context.Context, config *oidc.Config, iss string, audiences sets.Set[string]) (*idTokenVerifier, error) {
|
||||||
provider, err := oidc.NewProvider(ctx, iss)
|
provider, err := oidc.NewProvider(ctx, iss)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("init verifier failed: %v", err)
|
return nil, fmt.Errorf("init verifier failed: %v", err)
|
||||||
}
|
}
|
||||||
return provider.Verifier(config), nil
|
return &idTokenVerifier{provider.Verifier(config), audiences}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// asyncIDTokenVerifier is an ID token verifier that allows async initialization
|
// asyncIDTokenVerifier is an ID token verifier that allows async initialization
|
||||||
@ -115,13 +116,13 @@ type asyncIDTokenVerifier struct {
|
|||||||
// v is the ID token verifier initialized asynchronously. It remains nil
|
// v is the ID token verifier initialized asynchronously. It remains nil
|
||||||
// up until it is eventually initialized.
|
// up until it is eventually initialized.
|
||||||
// Guarded by m
|
// Guarded by m
|
||||||
v *oidc.IDTokenVerifier
|
v *idTokenVerifier
|
||||||
}
|
}
|
||||||
|
|
||||||
// newAsyncIDTokenVerifier creates a new asynchronous token verifier. The
|
// newAsyncIDTokenVerifier creates a new asynchronous token verifier. The
|
||||||
// verifier is available immediately, but may remain uninitialized for some time
|
// verifier is available immediately, but may remain uninitialized for some time
|
||||||
// after creation.
|
// after creation.
|
||||||
func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *asyncIDTokenVerifier {
|
func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string, audiences sets.Set[string]) *asyncIDTokenVerifier {
|
||||||
t := &asyncIDTokenVerifier{}
|
t := &asyncIDTokenVerifier{}
|
||||||
|
|
||||||
sync := make(chan struct{})
|
sync := make(chan struct{})
|
||||||
@ -129,7 +130,7 @@ func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *a
|
|||||||
// verifier, or until context canceled.
|
// verifier, or until context canceled.
|
||||||
initFn := func() (done bool, err error) {
|
initFn := func() (done bool, err error) {
|
||||||
klog.V(4).Infof("oidc authenticator: attempting init: iss=%v", iss)
|
klog.V(4).Infof("oidc authenticator: attempting init: iss=%v", iss)
|
||||||
v, err := initVerifier(ctx, c, iss)
|
v, err := initVerifier(ctx, c, iss, audiences)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Errorf("oidc authenticator: async token verifier for issuer: %q: %v", iss, err)
|
klog.Errorf("oidc authenticator: async token verifier for issuer: %q: %v", iss, err)
|
||||||
return false, nil
|
return false, nil
|
||||||
@ -155,7 +156,7 @@ func newAsyncIDTokenVerifier(ctx context.Context, c *oidc.Config, iss string) *a
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verifier returns the underlying ID token verifier, or nil if one is not yet initialized.
|
// verifier returns the underlying ID token verifier, or nil if one is not yet initialized.
|
||||||
func (a *asyncIDTokenVerifier) verifier() *oidc.IDTokenVerifier {
|
func (a *asyncIDTokenVerifier) verifier() *idTokenVerifier {
|
||||||
a.m.Lock()
|
a.m.Lock()
|
||||||
defer a.m.Unlock()
|
defer a.m.Unlock()
|
||||||
return a.v
|
return a.v
|
||||||
@ -181,13 +182,20 @@ type Authenticator struct {
|
|||||||
requiredClaims map[string]string
|
requiredClaims map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) setVerifier(v *oidc.IDTokenVerifier) {
|
// idTokenVerifier is a wrapper around oidc.IDTokenVerifier. It uses the oidc.IDTokenVerifier
|
||||||
|
// to verify the raw ID token and then performs audience validation locally.
|
||||||
|
type idTokenVerifier struct {
|
||||||
|
verifier *oidc.IDTokenVerifier
|
||||||
|
audiences sets.Set[string]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Authenticator) setVerifier(v *idTokenVerifier) {
|
||||||
a.verifier.Store(v)
|
a.verifier.Store(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) idTokenVerifier() (*oidc.IDTokenVerifier, bool) {
|
func (a *Authenticator) idTokenVerifier() (*idTokenVerifier, bool) {
|
||||||
if v := a.verifier.Load(); v != nil {
|
if v := a.verifier.Load(); v != nil {
|
||||||
return v.(*oidc.IDTokenVerifier), true
|
return v.(*idTokenVerifier), true
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -265,16 +273,26 @@ func New(opts Options) (*Authenticator, error) {
|
|||||||
now = time.Now
|
now = time.Now
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audiences := sets.New[string](opts.JWTAuthenticator.Issuer.Audiences...)
|
||||||
verifierConfig := &oidc.Config{
|
verifierConfig := &oidc.Config{
|
||||||
ClientID: opts.JWTAuthenticator.Issuer.Audiences[0],
|
ClientID: opts.JWTAuthenticator.Issuer.Audiences[0],
|
||||||
SupportedSigningAlgs: supportedSigningAlgs,
|
SupportedSigningAlgs: supportedSigningAlgs,
|
||||||
Now: now,
|
Now: now,
|
||||||
}
|
}
|
||||||
|
if audiences.Len() > 1 {
|
||||||
|
verifierConfig.ClientID = ""
|
||||||
|
// SkipClientIDCheck is set to true because we want to support multiple audiences
|
||||||
|
// in the authentication configuration.
|
||||||
|
// The go oidc library does not support validating
|
||||||
|
// multiple audiences, so we have to skip the client ID check and do it ourselves.
|
||||||
|
// xref: https://github.com/coreos/go-oidc/issues/397
|
||||||
|
verifierConfig.SkipClientIDCheck = true
|
||||||
|
}
|
||||||
|
|
||||||
var resolver *claimResolver
|
var resolver *claimResolver
|
||||||
groupsClaim := opts.JWTAuthenticator.ClaimMappings.Groups.Claim
|
groupsClaim := opts.JWTAuthenticator.ClaimMappings.Groups.Claim
|
||||||
if groupsClaim != "" {
|
if groupsClaim != "" {
|
||||||
resolver = newClaimResolver(groupsClaim, client, verifierConfig)
|
resolver = newClaimResolver(groupsClaim, client, verifierConfig, audiences)
|
||||||
}
|
}
|
||||||
|
|
||||||
requiredClaims := make(map[string]string)
|
requiredClaims := make(map[string]string)
|
||||||
@ -294,7 +312,10 @@ func New(opts Options) (*Authenticator, error) {
|
|||||||
|
|
||||||
if opts.KeySet != nil {
|
if opts.KeySet != nil {
|
||||||
// We already have a key set, synchronously initialize the verifier.
|
// We already have a key set, synchronously initialize the verifier.
|
||||||
authenticator.setVerifier(oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig))
|
authenticator.setVerifier(&idTokenVerifier{
|
||||||
|
oidc.NewVerifier(opts.JWTAuthenticator.Issuer.URL, opts.KeySet, verifierConfig),
|
||||||
|
audiences,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Asynchronously attempt to initialize the authenticator. This enables
|
// Asynchronously attempt to initialize the authenticator. This enables
|
||||||
// self-hosted providers, providers that run on top of Kubernetes itself.
|
// self-hosted providers, providers that run on top of Kubernetes itself.
|
||||||
@ -306,7 +327,7 @@ func New(opts Options) (*Authenticator, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
verifier := provider.Verifier(verifierConfig)
|
verifier := provider.Verifier(verifierConfig)
|
||||||
authenticator.setVerifier(verifier)
|
authenticator.setVerifier(&idTokenVerifier{verifier, audiences})
|
||||||
return true, nil
|
return true, nil
|
||||||
}, ctx.Done())
|
}, ctx.Done())
|
||||||
}
|
}
|
||||||
@ -374,6 +395,10 @@ type claimResolver struct {
|
|||||||
// claim is the distributed claim that may be resolved.
|
// claim is the distributed claim that may be resolved.
|
||||||
claim string
|
claim string
|
||||||
|
|
||||||
|
// audiences is the set of acceptable audiences the JWT must be issued to.
|
||||||
|
// At least one of the entries must match the "aud" claim in presented JWTs.
|
||||||
|
audiences sets.Set[string]
|
||||||
|
|
||||||
// client is the to use for resolving distributed claims
|
// client is the to use for resolving distributed claims
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
|
||||||
@ -390,19 +415,25 @@ type claimResolver struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newClaimResolver creates a new resolver for distributed claims.
|
// newClaimResolver creates a new resolver for distributed claims.
|
||||||
func newClaimResolver(claim string, client *http.Client, config *oidc.Config) *claimResolver {
|
func newClaimResolver(claim string, client *http.Client, config *oidc.Config, audiences sets.Set[string]) *claimResolver {
|
||||||
return &claimResolver{claim: claim, client: client, config: config, verifierPerIssuer: map[string]*asyncIDTokenVerifier{}}
|
return &claimResolver{
|
||||||
|
claim: claim,
|
||||||
|
audiences: audiences,
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
verifierPerIssuer: map[string]*asyncIDTokenVerifier{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verifier returns either the verifier for the specified issuer, or error.
|
// Verifier returns either the verifier for the specified issuer, or error.
|
||||||
func (r *claimResolver) Verifier(iss string) (*oidc.IDTokenVerifier, error) {
|
func (r *claimResolver) Verifier(iss string) (*idTokenVerifier, error) {
|
||||||
r.m.Lock()
|
r.m.Lock()
|
||||||
av := r.verifierPerIssuer[iss]
|
av := r.verifierPerIssuer[iss]
|
||||||
if av == nil {
|
if av == nil {
|
||||||
// This lazy init should normally be very quick.
|
// This lazy init should normally be very quick.
|
||||||
// TODO: Make this context cancelable.
|
// TODO: Make this context cancelable.
|
||||||
ctx := oidc.ClientContext(context.Background(), r.client)
|
ctx := oidc.ClientContext(context.Background(), r.client)
|
||||||
av = newAsyncIDTokenVerifier(ctx, r.config, iss)
|
av = newAsyncIDTokenVerifier(ctx, r.config, iss, r.audiences)
|
||||||
r.verifierPerIssuer[iss] = av
|
r.verifierPerIssuer[iss] = av
|
||||||
}
|
}
|
||||||
r.m.Unlock()
|
r.m.Unlock()
|
||||||
@ -520,6 +551,39 @@ func (r *claimResolver) resolve(ctx context.Context, endpoint endpoint, allClaim
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *idTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*oidc.IDToken, error) {
|
||||||
|
t, err := v.verifier.Verify(ctx, rawIDToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := v.verifyAudience(t); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyAudience verifies the audience field in the ID token matches the expected audience.
|
||||||
|
// This is added based on https://github.com/coreos/go-oidc/blob/b203e58c24394ddf5e816706a7645f01280245c7/oidc/verify.go#L275-L281
|
||||||
|
// with the difference that we allow multiple audiences.
|
||||||
|
//
|
||||||
|
// AuthenticationConfiguration has a audienceMatchPolicy field, but the only supported value now is "MatchAny".
|
||||||
|
// So, The default match behavior is to match at least one of the audiences in the ID token.
|
||||||
|
func (v *idTokenVerifier) verifyAudience(t *oidc.IDToken) error {
|
||||||
|
// We validate audience field is not empty in the authentication configuration.
|
||||||
|
// This check ensures callers of "Verify" using idTokenVerifier are not passing
|
||||||
|
// an empty audience.
|
||||||
|
if v.audiences.Len() == 0 {
|
||||||
|
return fmt.Errorf("oidc: invalid configuration, audiences cannot be empty")
|
||||||
|
}
|
||||||
|
for _, aud := range t.Audience {
|
||||||
|
if v.audiences.Has(aud) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("oidc: expected audience in %q got %q", sets.List(v.audiences), t.Audience)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
func (a *Authenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||||
if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) {
|
if !hasCorrectIssuer(a.jwtAuthenticator.Issuer.URL, token) {
|
||||||
return nil, false, nil
|
return nil, false, nil
|
||||||
|
@ -1521,6 +1521,70 @@ func TestToken(t *testing.T) {
|
|||||||
Name: "jane",
|
Name: "jane",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "multiple-audiences in authentication config",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com",
|
||||||
|
Audiences: []string{"random-client", "my-client"},
|
||||||
|
AudienceMatchPolicy: "MatchAny",
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com",
|
||||||
|
"aud": ["not-my-client", "my-client"],
|
||||||
|
"azp": "not-my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
want: &user.DefaultInfo{
|
||||||
|
Name: "jane",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple-audiences in authentication config, no match",
|
||||||
|
options: Options{
|
||||||
|
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||||
|
Issuer: apiserver.Issuer{
|
||||||
|
URL: "https://auth.example.com",
|
||||||
|
Audiences: []string{"random-client", "my-client"},
|
||||||
|
AudienceMatchPolicy: "MatchAny",
|
||||||
|
},
|
||||||
|
ClaimMappings: apiserver.ClaimMappings{
|
||||||
|
Username: apiserver.PrefixedClaimOrExpression{
|
||||||
|
Claim: "username",
|
||||||
|
Prefix: pointer.String(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
now: func() time.Time { return now },
|
||||||
|
},
|
||||||
|
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
pubKeys: []*jose.JSONWebKey{
|
||||||
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
||||||
|
},
|
||||||
|
claims: fmt.Sprintf(`{
|
||||||
|
"iss": "https://auth.example.com",
|
||||||
|
"aud": ["not-my-client"],
|
||||||
|
"azp": "not-my-client",
|
||||||
|
"username": "jane",
|
||||||
|
"exp": %d
|
||||||
|
}`, valid.Unix()),
|
||||||
|
wantErr: `oidc: verify token: oidc: expected audience in ["my-client" "random-client"] got ["not-my-client"]`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid-issuer",
|
name: "invalid-issuer",
|
||||||
options: Options{
|
options: Options{
|
||||||
|
@ -432,6 +432,8 @@ jwt:
|
|||||||
url: %s
|
url: %s
|
||||||
audiences:
|
audiences:
|
||||||
- %s
|
- %s
|
||||||
|
- another-audience
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
certificateAuthority: |
|
certificateAuthority: |
|
||||||
%s
|
%s
|
||||||
claimMappings:
|
claimMappings:
|
||||||
@ -475,6 +477,8 @@ jwt:
|
|||||||
url: %s
|
url: %s
|
||||||
audiences:
|
audiences:
|
||||||
- %s
|
- %s
|
||||||
|
- another-audience
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
certificateAuthority: |
|
certificateAuthority: |
|
||||||
%s
|
%s
|
||||||
claimMappings:
|
claimMappings:
|
||||||
@ -522,6 +526,8 @@ jwt:
|
|||||||
url: %s
|
url: %s
|
||||||
audiences:
|
audiences:
|
||||||
- %s
|
- %s
|
||||||
|
- another-audience
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
certificateAuthority: |
|
certificateAuthority: |
|
||||||
%s
|
%s
|
||||||
claimMappings:
|
claimMappings:
|
||||||
@ -565,6 +571,8 @@ jwt:
|
|||||||
url: %s
|
url: %s
|
||||||
audiences:
|
audiences:
|
||||||
- %s
|
- %s
|
||||||
|
- another-audience
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
certificateAuthority: |
|
certificateAuthority: |
|
||||||
%s
|
%s
|
||||||
claimMappings:
|
claimMappings:
|
||||||
@ -621,6 +629,8 @@ jwt:
|
|||||||
url: %s
|
url: %s
|
||||||
audiences:
|
audiences:
|
||||||
- %s
|
- %s
|
||||||
|
- another-audience
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
certificateAuthority: |
|
certificateAuthority: |
|
||||||
%s
|
%s
|
||||||
claimMappings:
|
claimMappings:
|
||||||
@ -668,6 +678,8 @@ jwt:
|
|||||||
url: %s
|
url: %s
|
||||||
audiences:
|
audiences:
|
||||||
- %s
|
- %s
|
||||||
|
- another-audience
|
||||||
|
audienceMatchPolicy: MatchAny
|
||||||
certificateAuthority: |
|
certificateAuthority: |
|
||||||
%s
|
%s
|
||||||
claimMappings:
|
claimMappings:
|
||||||
|
Loading…
Reference in New Issue
Block a user