apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go

3432 lines
96 KiB
Go

/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oidc
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"text/template"
"time"
"gopkg.in/square/go-jose.v2"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/component-base/metrics/testutil"
"k8s.io/klog/v2"
"k8s.io/utils/pointer"
)
// utilities for loading JOSE keys.
func loadRSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
key, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
return nil, err
}
return key.Public(), nil
})
}
func loadRSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
return x509.ParsePKCS1PrivateKey(b)
})
}
func loadECDSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
key, err := x509.ParseECPrivateKey(b)
if err != nil {
return nil, err
}
return key.Public(), nil
})
}
func loadECDSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
return x509.ParseECPrivateKey(b)
})
}
func loadKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm, unmarshal func([]byte) (interface{}, error)) *jose.JSONWebKey {
data, err := os.ReadFile(filepath)
if err != nil {
t.Fatalf("load file: %v", err)
}
block, _ := pem.Decode(data)
if block == nil {
t.Fatalf("file contained no PEM encoded data: %s", filepath)
}
priv, err := unmarshal(block.Bytes)
if err != nil {
t.Fatalf("unmarshal key: %v", err)
}
key := &jose.JSONWebKey{Key: priv, Use: "sig", Algorithm: string(alg)}
thumbprint, err := key.Thumbprint(crypto.SHA256)
if err != nil {
t.Fatalf("computing thumbprint: %v", err)
}
key.KeyID = hex.EncodeToString(thumbprint)
return key
}
// staticKeySet implements oidc.KeySet.
type staticKeySet struct {
keys []*jose.JSONWebKey
}
func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
jws, err := jose.ParseSigned(jwt)
if err != nil {
return nil, err
}
if len(jws.Signatures) == 0 {
return nil, fmt.Errorf("jwt contained no signatures")
}
kid := jws.Signatures[0].Header.KeyID
for _, key := range s.keys {
if key.KeyID == kid {
return jws.Verify(key)
}
}
return nil, fmt.Errorf("no keys matches jwk keyid")
}
var (
expired, _ = time.Parse(time.RFC3339Nano, "2009-11-10T22:00:00Z")
now, _ = time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z")
valid, _ = time.Parse(time.RFC3339Nano, "2009-11-11T00:00:00Z")
)
type claimsTest struct {
name string
options Options
optsFunc func(*Options)
signingKey *jose.JSONWebKey
pubKeys []*jose.JSONWebKey
claims string
want *user.DefaultInfo
wantSkip bool
wantErr string
wantInitErr string
claimToResponseMap map[string]string
openIDConfig string
fetchKeysFromRemote bool
}
// Replace formats the contents of v into the provided template.
func replace(tmpl string, v interface{}) string {
t := template.Must(template.New("test").Parse(tmpl))
buf := bytes.NewBuffer(nil)
t.Execute(buf, &v)
ret := buf.String()
klog.V(4).Infof("Replaced: %v into: %v", tmpl, ret)
return ret
}
// newClaimServer returns a new test HTTPS server, which is rigged to return
// OIDC responses to requests that resolve distributed claims. signer is the
// signer used for the served JWT tokens. claimToResponseMap is a map of
// responses that the server will return for each claim it is given.
func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *httptest.Server {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
klog.V(5).Infof("request: %+v", *r)
switch r.URL.Path {
case "/.testing/keys":
w.Header().Set("Content-Type", "application/json")
keyBytes, err := json.Marshal(keys)
if err != nil {
t.Fatalf("unexpected error while marshaling keys: %v", err)
}
klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
w.Write(keyBytes)
// /c/d/bar/.well-known/openid-configuration is used to test issuer url and discovery url with a path
case "/.well-known/openid-configuration", "/c/d/bar/.well-known/openid-configuration":
w.Header().Set("Content-Type", "application/json")
klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
w.Write([]byte(*openIDConfig))
// These claims are tested in the unit tests.
case "/groups":
fallthrough
case "/rabbits":
if claimToResponseMap == nil {
t.Errorf("no claims specified in response")
}
claim := r.URL.Path[1:] // "/groups" -> "groups"
expectedAuth := fmt.Sprintf("Bearer %v_token", claim)
auth := r.Header.Get("Authorization")
if auth != expectedAuth {
t.Errorf("bearer token expected: %q, was %q", expectedAuth, auth)
}
jws, err := signer.Sign([]byte(claimToResponseMap[claim]))
if err != nil {
t.Errorf("while signing response token: %v", err)
}
token, err := jws.CompactSerialize()
if err != nil {
t.Errorf("while serializing response token: %v", err)
}
w.Write([]byte(token))
default:
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "unexpected URL: %v", r.URL)
}
}))
klog.V(4).Infof("Serving OIDC at: %v", ts.URL)
return ts
}
func toKeySet(keys []*jose.JSONWebKey) jose.JSONWebKeySet {
ret := jose.JSONWebKeySet{}
for _, k := range keys {
ret.Keys = append(ret.Keys, *k)
}
return ret
}
func (c *claimsTest) run(t *testing.T) {
var (
signer jose.Signer
err error
)
if c.signingKey != nil {
// Initialize the signer only in the tests that make use of it. We can
// not defer this initialization because the test server uses it too.
signer, err = jose.NewSigner(jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(c.signingKey.Algorithm),
Key: c.signingKey,
}, nil)
if err != nil {
t.Fatalf("initialize signer: %v", err)
}
}
// The HTTPS server used for requesting distributed groups claims.
ts := newClaimServer(t, toKeySet(c.pubKeys), signer, c.claimToResponseMap, &c.openIDConfig)
defer ts.Close()
// Make the certificate of the helper server available to the authenticator
caBundle := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: ts.Certificate().Raw,
})
caContent, err := dynamiccertificates.NewStaticCAContent("oidc-authenticator", caBundle)
if err != nil {
t.Fatalf("initialize ca: %v", err)
}
c.options.CAContentProvider = caContent
// Allow claims to refer to the serving URL of the test server. For this,
// substitute all references to {{.URL}} in appropriate places.
// Use {{.Expired}} to handle the token expiry date string with correct timezone handling.
v := struct {
URL string
Expired string
}{
URL: ts.URL,
Expired: fmt.Sprintf("%v", time.Unix(expired.Unix(), 0)),
}
c.claims = replace(c.claims, &v)
c.openIDConfig = replace(c.openIDConfig, &v)
c.options.JWTAuthenticator.Issuer.URL = replace(c.options.JWTAuthenticator.Issuer.URL, &v)
c.options.JWTAuthenticator.Issuer.DiscoveryURL = replace(c.options.JWTAuthenticator.Issuer.DiscoveryURL, &v)
for claim, response := range c.claimToResponseMap {
c.claimToResponseMap[claim] = replace(response, &v)
}
c.wantErr = replace(c.wantErr, &v)
c.wantInitErr = replace(c.wantInitErr, &v)
if !c.fetchKeysFromRemote {
// Set the verifier to use the public key set instead of reading from a remote.
c.options.KeySet = &staticKeySet{keys: c.pubKeys}
}
if c.optsFunc != nil {
c.optsFunc(&c.options)
}
expectInitErr := len(c.wantInitErr) > 0
// Initialize the authenticator.
a, err := New(c.options)
if err != nil {
if !expectInitErr {
t.Fatalf("initialize authenticator: %v", err)
}
if got := err.Error(); c.wantInitErr != got {
t.Fatalf("expected initialization error %q but got %q", c.wantInitErr, got)
}
return
}
if expectInitErr {
t.Fatalf("wanted initialization error %q but got none", c.wantInitErr)
}
claims := struct{}{}
if err := json.Unmarshal([]byte(c.claims), &claims); err != nil {
t.Fatalf("failed to unmarshal claims: %v", err)
}
// Sign and serialize the claims in a JWT.
jws, err := signer.Sign([]byte(c.claims))
if err != nil {
t.Fatalf("sign claims: %v", err)
}
token, err := jws.CompactSerialize()
if err != nil {
t.Fatalf("serialize token: %v", err)
}
ia, ok := a.(*instrumentedAuthenticator)
if !ok {
t.Fatalf("expected authenticator to be instrumented")
}
authenticator, ok := ia.delegate.(*Authenticator)
if !ok {
t.Fatalf("expected delegate to be Authenticator")
}
ctx := testContext(t)
// wait for the authenticator to be initialized
err = wait.PollUntilContextCancel(ctx, time.Millisecond, true, func(context.Context) (bool, error) {
if v, _ := authenticator.idTokenVerifier(); v == nil {
return false, nil
}
return true, nil
})
if err != nil {
t.Fatalf("failed to initialize the authenticator: %v", err)
}
got, ok, err := a.AuthenticateToken(ctx, token)
expectErr := len(c.wantErr) > 0
if err != nil {
if !expectErr {
t.Fatalf("authenticate token: %v", err)
}
if got := err.Error(); c.wantErr != got {
t.Fatalf("expected error %q when authenticating token but got %q", c.wantErr, got)
}
return
}
if expectErr {
t.Fatalf("expected error %q when authenticating token but got none", c.wantErr)
}
if !ok {
if !c.wantSkip {
// We don't have any cases where we return (nil, false, nil)
t.Fatalf("no error but token not authenticated")
}
return
}
if c.wantSkip {
t.Fatalf("expected authenticator to skip token")
}
gotUser := got.User.(*user.DefaultInfo)
if !reflect.DeepEqual(gotUser, c.want) {
t.Fatalf("wanted user=%#v, got=%#v", c.want, gotUser)
}
}
func TestToken(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
synchronizeTokenIDVerifierForTest = true
tests := []claimsTest{
{
name: "token",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "no-username",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
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": "my-client",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: parse username claims "username": claim not present`,
},
{
name: "email",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "email",
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": "my-client",
"email": "jane@example.com",
"email_verified": true,
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane@example.com",
},
},
{
name: "email-not-verified",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "email",
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": "my-client",
"email": "jane@example.com",
"email_verified": false,
"exp": %d
}`, valid.Unix()),
wantErr: "oidc: email not verified",
},
{
// If "email_verified" isn't present, assume true
name: "no-email-verified-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "email",
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": "my-client",
"email": "jane@example.com",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane@example.com",
},
},
{
name: "invalid-email-verified-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "email",
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),
},
// string value for "email_verified"
claims: fmt.Sprintf(`{
"iss": "https://auth.example.com",
"aud": "my-client",
"email": "jane@example.com",
"email_verified": "false",
"exp": %d
}`, valid.Unix()),
wantErr: "oidc: parse 'email_verified' claim: json: cannot unmarshal string into Go value of type bool",
},
{
name: "groups",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
},
},
{
name: "groups-distributed",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
},
},
{
name: "groups-distributed invalid client",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String(""),
},
},
},
Client: &http.Client{Transport: errTransport("some unexpected oidc error")}, // return an error that we can assert against
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
optsFunc: func(opts *Options) {
opts.CAContentProvider = nil // unset CA automatically set by the test to allow us to use a custom client
},
wantErr: `oidc: could not expand distributed claims: while getting distributed claim "groups": Get "{{.URL}}/groups": some unexpected oidc error`,
},
{
name: "groups-distributed-malformed-claim-names",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "nonexistent-claim-source"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
wantErr: "oidc: verify token: oidc: source does not exist",
},
{
name: "groups-distributed-malformed-names-and-sources",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
wantErr: "oidc: verify token: oidc: source does not exist",
},
{
name: "groups-distributed-malformed-distributed-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
// Doesn't contain the "groups" claim as it promises.
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
wantErr: `oidc: could not expand distributed claims: jwt returned by distributed claim endpoint "{{.URL}}/groups" did not contain claim: groups`,
},
{
name: "groups-distributed-unusual-name",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "rabbits",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"rabbits": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/rabbits",
"access_token": "rabbits_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"rabbits": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"rabbits": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
},
},
{
name: "groups-distributed-wrong-audience",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
// Note mismatching "aud"
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "your-client",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
wantErr: `oidc: could not expand distributed claims: verify distributed claim token: oidc: expected audience "my-client" got ["your-client"]`,
},
{
name: "groups-distributed-expired-token",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
// Note expired timestamp.
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team1", "team2"],
"exp": %d
}`, expired.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
wantErr: "oidc: could not expand distributed claims: verify distributed claim token: oidc: token is expired (Token Expiry: {{.Expired}})",
},
{
// Specs are unclear about this behavior. We adopt a behavior where
// normal claim wins over a distributed claim by the same name.
name: "groups-distributed-normal-claim-wins",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"groups": "team1",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
want: &user.DefaultInfo{
Name: "jane",
// "team1" is from the normal "groups" claim.
Groups: []string{"team1"},
},
},
{
// Groups should be able to be a single string, not just a slice.
name: "group-string-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "my-client",
"username": "jane",
"groups": "team1",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1"},
},
},
{
// Groups should be able to be a single string, not just a slice.
name: "group-string-claim-distributed",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": "team1",
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1"},
},
},
{
name: "group-string-claim-aggregated-not-supported",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"JWT": "some.jwt.token"
}
},
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
// if the groups claim isn't provided, this shouldn't error out
name: "no-groups-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "invalid-groups-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
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": "my-client",
"username": "jane",
"groups": 42,
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: parse groups claim "groups": json: cannot unmarshal number into Go value of type string`,
},
{
name: "required-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Claim: "hd",
RequiredValue: "example.com",
},
{
Claim: "sub",
RequiredValue: "test",
},
},
},
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": "my-client",
"username": "jane",
"hd": "example.com",
"sub": "test",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "no-required-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Claim: "hd",
RequiredValue: "example.com",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: "oidc: required claim hd not present in ID token",
},
{
name: "invalid-required-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Claim: "hd",
RequiredValue: "example.com",
},
},
},
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": "my-client",
"username": "jane",
"hd": "example.org",
"exp": %d
}`, valid.Unix()),
wantErr: "oidc: required claim hd value does not match. Got = example.org, want = example.com",
},
{
name: "invalid-signature",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
now: func() time.Time { return now },
},
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_2.pem", jose.RS256),
},
claims: fmt.Sprintf(`{
"iss": "https://auth.example.com",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: "oidc: verify token: failed to verify signature: no keys matches jwk keyid",
},
{
name: "expired",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, expired.Unix()),
wantErr: `oidc: verify token: oidc: token is expired (Token Expiry: {{.Expired}})`,
},
{
name: "invalid-aud",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
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",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: verify token: oidc: expected audience "my-client" got ["not-my-client"]`,
},
{
// ID tokens may contain multiple audiences:
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
name: "multiple-audiences",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
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",
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, multiple matches",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"random-client", "my-client", "other-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", "other-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: "nuanced audience validation using claim validation rules",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"bar", "foo", "baz"},
AudienceMatchPolicy: "MatchAny",
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Expression: `sets.equivalent(claims.aud, ["bar", "foo", "baz"])`,
Message: "audience must exactly contain [bar, foo, baz]",
},
},
},
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": ["foo", "bar", "baz"],
"azp": "not-my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "audience validation using claim validation rules fails",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"bar", "foo", "baz"},
AudienceMatchPolicy: "MatchAny",
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Expression: `sets.equivalent(claims.aud, ["bar", "foo", "baz"])`,
Message: "audience must exactly contain [bar, foo, baz]",
},
},
},
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": ["foo", "baz"],
"azp": "not-my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: error evaluating claim validation expression: validation expression 'sets.equivalent(claims.aud, ["bar", "foo", "baz"])' failed: audience must exactly contain [bar, foo, baz]`,
},
{
name: "invalid-issuer",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
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://example.com",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantSkip: true,
},
{
name: "username-prefix",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("oidc:"),
},
},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "oidc:jane",
},
},
{
name: "groups-prefix",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("oidc:"),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String("groups:"),
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "oidc:jane",
Groups: []string{"groups:team1", "groups:team2"},
},
},
{
name: "groups-prefix-distributed",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "{{.URL}}",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("oidc:"),
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String("groups:"),
},
},
},
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": "{{.URL}}",
"aud": "my-client",
"username": "jane",
"_claim_names": {
"groups": "src1"
},
"_claim_sources": {
"src1": {
"endpoint": "{{.URL}}/groups",
"access_token": "groups_token"
}
},
"exp": %d
}`, valid.Unix()),
claimToResponseMap: map[string]string{
"groups": fmt.Sprintf(`{
"iss": "{{.URL}}",
"aud": "my-client",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
},
openIDConfig: `{
"issuer": "{{.URL}}",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
want: &user.DefaultInfo{
Name: "oidc:jane",
Groups: []string{"groups:team1", "groups:team2"},
},
},
{
name: "invalid-signing-alg",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
now: func() time.Time { return now },
},
// Correct key but invalid signature algorithm "PS256"
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256),
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
},
claims: fmt.Sprintf(`{
"iss": "https://auth.example.com",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: verify token: oidc: id token signed with unsupported algorithm, expected ["RS256"] got "PS256"`,
},
{
name: "ps256",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
},
SupportedSigningAlgs: []string{"PS256"},
now: func() time.Time { return now },
},
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256),
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.PS256),
},
claims: fmt.Sprintf(`{
"iss": "https://auth.example.com",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "es512",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
},
SupportedSigningAlgs: []string{"ES512"},
now: func() time.Time { return now },
},
signingKey: loadECDSAPrivKey(t, "testdata/ecdsa_2.pem", jose.ES512),
pubKeys: []*jose.JSONWebKey{
loadECDSAKey(t, "testdata/ecdsa_1.pem", jose.ES512),
loadECDSAKey(t, "testdata/ecdsa_2.pem", jose.ES512),
},
claims: fmt.Sprintf(`{
"iss": "https://auth.example.com",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "not-https",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "http://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
now: func() time.Time { return now },
},
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
},
wantInitErr: `issuer.url: Invalid value: "http://auth.example.com": URL scheme must be https`,
},
{
name: "no-username-claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Prefix: pointer.String(""),
},
},
},
now: func() time.Time { return now },
},
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
},
wantInitErr: `claimMappings.username: Required value: claim or expression is required`,
},
{
name: "invalid-sig-alg",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
SupportedSigningAlgs: []string{"HS256"},
now: func() time.Time { return now },
},
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
},
wantInitErr: `oidc: unsupported signing alg: "HS256"`,
},
{
name: "client and ca mutually exclusive",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
SupportedSigningAlgs: []string{"RS256"},
now: func() time.Time { return now },
Client: http.DefaultClient, // test automatically sets CAContentProvider
},
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
},
wantInitErr: "oidc: Client and CAContentProvider are mutually exclusive",
},
{
name: "accounts.google.com issuer",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://accounts.google.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "email",
Prefix: pointer.String(""),
},
},
},
now: func() time.Time { return now },
},
claims: fmt.Sprintf(`{
"iss": "accounts.google.com",
"email": "thomas.jefferson@gmail.com",
"aud": "my-client",
"exp": %d
}`, valid.Unix()),
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
pubKeys: []*jose.JSONWebKey{
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
},
want: &user.DefaultInfo{
Name: "thomas.jefferson@gmail.com",
},
},
{
name: "good token with bad client id",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("prefix:"),
},
},
},
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": "my-wrong-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: verify token: oidc: expected audience "my-client" got ["my-wrong-client"]`,
},
{
name: "user validation rule fails for user.username",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("system:"),
},
},
UserValidationRules: []apiserver.UserValidationRule{
{
Expression: "!user.username.startsWith('system:')",
Message: "username cannot used reserved system: prefix",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: error evaluating user info validation rule: validation expression '!user.username.startsWith('system:')' failed: username cannot used reserved system: prefix`,
},
{
name: "user validation rule fails for user.groups",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Claim: "groups",
Prefix: pointer.String("system:"),
},
},
UserValidationRules: []apiserver.UserValidationRule{
{
Expression: "user.groups.all(group, !group.startsWith('system:'))",
Message: "groups cannot used reserved system: prefix",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"groups": ["team1", "team2"]
}`, valid.Unix()),
wantErr: `oidc: error evaluating user info validation rule: validation expression 'user.groups.all(group, !group.startsWith('system:'))' failed: groups cannot used reserved system: prefix`,
},
{
name: "claim validation rule with expression fails",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Expression: `claims.hd == "example.com"`,
Message: "hd claim must be example.com",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
wantErr: `oidc: error evaluating claim validation expression: expression 'claims.hd == "example.com"' resulted in error: no such key: hd`,
},
{
name: "claim validation rule with expression",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Expression: `claims.hd == "example.com"`,
Message: "hd claim must be example.com",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"hd": "example.com"
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "claim validation rule with expression and nested claims",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Expression: `claims.foo.bar == "baz"`,
Message: "foo.bar claim must be baz",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"hd": "example.com",
"foo": {
"bar": "baz"
}
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "claim validation rule with mix of expression and claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
},
ClaimValidationRules: []apiserver.ClaimValidationRule{
{
Expression: `claims.foo.bar == "baz"`,
Message: "foo.bar claim must be baz",
},
{
Claim: "hd",
RequiredValue: "example.com",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"hd": "example.com",
"foo": {
"bar": "baz"
}
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "username claim mapping with expression",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "username claim mapping with expression and nested claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.foo.username",
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"foo": {
"username": "jane"
}
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "groups claim mapping with expression",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
},
},
{
name: "groups claim with expression",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String("oidc:"),
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: `(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "groups:" + role)`,
},
},
},
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": "my-client",
"username": "jane",
"roles": "foo,bar",
"other_roles": "baz,qux",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "oidc:jane",
Groups: []string{"groups:foo", "groups:bar", "groups:baz", "groups:qux"},
},
},
{
name: "uid claim mapping with expression",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Expression: "claims.uid",
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234"
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
UID: "1234",
},
},
{
name: "uid claim mapping with claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Claim: "uid",
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234"
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
UID: "1234",
},
},
{
name: "extra claim mapping with expression",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Expression: "claims.uid",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/foo",
ValueExpression: "claims.foo",
},
{
Key: "example.org/bar",
ValueExpression: "claims.bar",
},
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234",
"foo": "bar",
"bar": [
"baz",
"qux"
]
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
UID: "1234",
Extra: map[string][]string{
"example.org/foo": {"bar"},
"example.org/bar": {"baz", "qux"},
},
},
},
{
name: "extra claim mapping, value derived from claim value",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/admin",
ValueExpression: `(has(claims.is_admin) && claims.is_admin) ? "true":""`,
},
{
Key: "example.org/admin_1",
ValueExpression: `claims.?is_admin.orValue(false) == true ? "true":""`,
},
{
Key: "example.org/non_existent",
ValueExpression: `claims.?non_existent.orValue("default") == "default" ? "true":""`,
},
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"is_admin": true
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Extra: map[string][]string{
"example.org/admin": {"true"},
"example.org/admin_1": {"true"},
"example.org/non_existent": {"true"},
},
},
},
{
name: "hardcoded extra claim mapping",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/admin",
ValueExpression: `"true"`,
},
},
},
},
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": "my-client",
"username": "jane",
"exp": %d,
"is_admin": true
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Extra: map[string][]string{
"example.org/admin": {"true"},
},
},
},
{
name: "extra claim mapping, multiple expressions for same key",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Expression: "claims.uid",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/foo",
ValueExpression: "claims.foo",
},
{
Key: "example.org/bar",
ValueExpression: "claims.bar",
},
{
Key: "example.org/foo",
ValueExpression: "claims.bar",
},
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234",
"foo": "bar",
"bar": [
"baz",
"qux"
]
}`, valid.Unix()),
wantInitErr: `claimMappings.extra[2].key: Duplicate value: "example.org/foo"`,
},
{
name: "disallowed issuer via configured value",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Expression: "claims.uid",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/foo",
ValueExpression: "claims.foo",
},
{
Key: "example.org/bar",
ValueExpression: "claims.bar",
},
},
},
},
DisallowedIssuers: []string{"https://auth.example.com"},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234",
"foo": "bar",
"bar": [
"baz",
"qux"
]
}`, valid.Unix()),
wantInitErr: `issuer.url: Invalid value: "https://auth.example.com": URL must not overlap with disallowed issuers: [https://auth.example.com]`,
},
{
name: "extra claim mapping, empty string value for key",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Expression: "claims.uid",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/foo",
ValueExpression: "claims.foo",
},
{
Key: "example.org/bar",
ValueExpression: "claims.bar",
},
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234",
"foo": "",
"bar": [
"baz",
"qux"
]
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
UID: "1234",
Extra: map[string][]string{
"example.org/bar": {"baz", "qux"},
},
},
},
{
name: "extra claim mapping with user validation rule succeeds",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
UID: apiserver.ClaimOrExpression{
Expression: "claims.uid",
},
Extra: []apiserver.ExtraMapping{
{
Key: "example.org/foo",
ValueExpression: "'bar'",
},
{
Key: "example.org/baz",
ValueExpression: "claims.baz",
},
},
},
UserValidationRules: []apiserver.UserValidationRule{
{
Expression: "'bar' in user.extra['example.org/foo'] && 'qux' in user.extra['example.org/baz']",
Message: "example.org/foo must be bar and example.org/baz must be qux",
},
},
},
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": "my-client",
"username": "jane",
"groups": ["team1", "team2"],
"exp": %d,
"uid": "1234",
"baz": "qux"
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
Groups: []string{"team1", "team2"},
UID: "1234",
Extra: map[string][]string{
"example.org/foo": {"bar"},
"example.org/baz": {"qux"},
},
},
},
{
name: "groups expression returns null",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
},
},
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": "my-client",
"username": "jane",
"groups": null,
"exp": %d,
"uid": "1234",
"baz": "qux"
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
// test to ensure omitempty fields not included in user info
// are set and accessible for CEL evaluation.
{
name: "test user validation rule doesn't fail when user info is empty except username",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.username",
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
},
UserValidationRules: []apiserver.UserValidationRule{
{
Expression: `user.username == " "`,
Message: "username must be single space",
},
{
Expression: `user.uid == ""`,
Message: "uid must be empty string",
},
{
Expression: `!('bar' in user.groups)`,
Message: "groups must not contain bar",
},
{
Expression: `!('bar' in user.extra)`,
Message: "extra must not contain bar",
},
},
},
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": "my-client",
"username": " ",
"groups": null,
"exp": %d,
"baz": "qux"
}`, valid.Unix()),
want: &user.DefaultInfo{Name: " "},
},
{
name: "empty username is allowed via claim",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Claim: "username",
Prefix: pointer.String(""),
},
Groups: apiserver.PrefixedClaimOrExpression{
Expression: "claims.groups",
},
},
UserValidationRules: []apiserver.UserValidationRule{
{
Expression: `user.username == ""`,
Message: "username must be empty string",
},
{
Expression: `user.uid == ""`,
Message: "uid must be empty string",
},
{
Expression: `!('bar' in user.groups)`,
Message: "groups must not contain bar",
},
{
Expression: `!('bar' in user.extra)`,
Message: "extra must not contain bar",
},
},
},
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": "my-client",
"username": "",
"groups": null,
"exp": %d,
"baz": "qux"
}`, valid.Unix()),
want: &user.DefaultInfo{},
},
// test to assert the minimum valid jwt payload
// the required claims are iss, aud, exp and <claimMappings.Username> (in this case user).
{
name: "minimum valid jwt payload",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
Audiences: []string{"my-client"},
},
ClaimMappings: apiserver.ClaimMappings{
Username: apiserver.PrefixedClaimOrExpression{
Expression: "claims.user",
},
},
},
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": "my-client",
"user": "jane",
"exp": %d
}`, valid.Unix()),
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "discovery-url",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
DiscoveryURL: "{{.URL}}/.well-known/openid-configuration",
Audiences: []string{"my-client"},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
openIDConfig: `{
"issuer": "https://auth.example.com",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
fetchKeysFromRemote: true,
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "discovery url, issuer has a path",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com/a/b/foo",
DiscoveryURL: "{{.URL}}/.well-known/openid-configuration",
Audiences: []string{"my-client"},
},
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/a/b/foo",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
openIDConfig: `{
"issuer": "https://auth.example.com/a/b/foo",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
fetchKeysFromRemote: true,
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "discovery url has a path, issuer url has no path",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com",
DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
Audiences: []string{"my-client"},
},
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": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
openIDConfig: `{
"issuer": "https://auth.example.com",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
fetchKeysFromRemote: true,
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "discovery url and issuer url have paths",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com/a/b/foo",
DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
Audiences: []string{"my-client"},
},
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/a/b/foo",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
openIDConfig: `{
"issuer": "https://auth.example.com/a/b/foo",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
fetchKeysFromRemote: true,
want: &user.DefaultInfo{
Name: "jane",
},
},
{
name: "discovery url and issuer url have paths, issuer url has trailing slash",
options: Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
URL: "https://auth.example.com/a/b/foo/",
DiscoveryURL: "{{.URL}}/c/d/bar/.well-known/openid-configuration",
Audiences: []string{"my-client"},
},
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/a/b/foo/",
"aud": "my-client",
"username": "jane",
"exp": %d
}`, valid.Unix()),
openIDConfig: `{
"issuer": "https://auth.example.com/a/b/foo/",
"jwks_uri": "{{.URL}}/.testing/keys"
}`,
fetchKeysFromRemote: true,
want: &user.DefaultInfo{
Name: "jane",
},
},
}
var successTestCount, failureTestCount int
for _, test := range tests {
t.Run(test.name, test.run)
if test.wantSkip || test.wantInitErr != "" {
continue
}
// check metrics for success and failure
if test.wantErr == "" {
successTestCount++
testutil.AssertHistogramTotalCount(t, "apiserver_authentication_jwt_authenticator_latency_seconds", map[string]string{"result": "success"}, successTestCount)
} else {
failureTestCount++
testutil.AssertHistogramTotalCount(t, "apiserver_authentication_jwt_authenticator_latency_seconds", map[string]string{"result": "failure"}, failureTestCount)
}
}
}
func TestUnmarshalClaimError(t *testing.T) {
// Ensure error strings returned by unmarshaling claims don't include the claim.
const token = "96bb299a-02e9-11e8-8673-54ee7553240e" // Fake token for testing.
payload := fmt.Sprintf(`{
"token": "%s"
}`, token)
var c claims
if err := json.Unmarshal([]byte(payload), &c); err != nil {
t.Fatal(err)
}
var n int
err := c.unmarshalClaim("token", &n)
if err == nil {
t.Fatal("expected error")
}
if strings.Contains(err.Error(), token) {
t.Fatalf("unmarshal error included token")
}
}
func TestUnmarshalClaim(t *testing.T) {
tests := []struct {
name string
claims string
do func(claims) (interface{}, error)
want interface{}
wantErr bool
}{
{
name: "string claim",
claims: `{"aud":"foo"}`,
do: func(c claims) (interface{}, error) {
var s string
err := c.unmarshalClaim("aud", &s)
return s, err
},
want: "foo",
},
{
name: "mismatched types",
claims: `{"aud":"foo"}`,
do: func(c claims) (interface{}, error) {
var n int
err := c.unmarshalClaim("aud", &n)
return n, err
},
wantErr: true,
},
{
name: "bool claim",
claims: `{"email":"foo@coreos.com","email_verified":true}`,
do: func(c claims) (interface{}, error) {
var verified bool
err := c.unmarshalClaim("email_verified", &verified)
return verified, err
},
want: true,
},
{
name: "strings claim",
claims: `{"groups":["a","b","c"]}`,
do: func(c claims) (interface{}, error) {
var groups []string
err := c.unmarshalClaim("groups", &groups)
return groups, err
},
want: []string{"a", "b", "c"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var c claims
if err := json.Unmarshal([]byte(test.claims), &c); err != nil {
t.Fatal(err)
}
got, err := test.do(c)
if err != nil {
if test.wantErr {
return
}
t.Fatalf("unexpected error: %v", err)
}
if test.wantErr {
t.Fatalf("expected error")
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("wanted=%#v, got=%#v", test.want, got)
}
})
}
}
type errTransport string
func (e errTransport) RoundTrip(_ *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("%s", e)
}
func testContext(t *testing.T) context.Context {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
return ctx
}