1644 lines
44 KiB
Go
1644 lines
44 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"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
"time"
|
|
|
|
oidc "github.com/coreos/go-oidc"
|
|
jose "gopkg.in/square/go-jose.v2"
|
|
|
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
"k8s.io/klog"
|
|
)
|
|
|
|
// 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 := ioutil.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
|
|
now time.Time
|
|
signingKey *jose.JSONWebKey
|
|
pubKeys []*jose.JSONWebKey
|
|
claims string
|
|
want *user.DefaultInfo
|
|
wantSkip bool
|
|
wantErr bool
|
|
wantInitErr bool
|
|
claimToResponseMap map[string]string
|
|
openIDConfig string
|
|
reqAudiences authenticator.Audiences
|
|
}
|
|
|
|
// 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)
|
|
|
|
case "/.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
|
|
}
|
|
|
|
// writeTempCert writes out the supplied certificate into a temporary file in
|
|
// PEM-encoded format. Returns the name of the temporary file used. The caller
|
|
// is responsible for cleaning the file up.
|
|
func writeTempCert(t *testing.T, cert []byte) string {
|
|
tempFile, err := ioutil.TempFile("", "ca.crt")
|
|
if err != nil {
|
|
t.Fatalf("could not open temp file: %v", err)
|
|
}
|
|
block := &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cert,
|
|
}
|
|
if err := pem.Encode(tempFile, block); err != nil {
|
|
t.Fatalf("could not write to temp file %v: %v", tempFile.Name(), err)
|
|
}
|
|
tempFile.Close()
|
|
return tempFile.Name()
|
|
}
|
|
|
|
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
|
|
// by writing its root CA certificate into a temporary file.
|
|
tempFileName := writeTempCert(t, ts.TLS.Certificates[0].Certificate[0])
|
|
defer os.Remove(tempFileName)
|
|
c.options.CAFile = tempFileName
|
|
|
|
// Allow claims to refer to the serving URL of the test server. For this,
|
|
// substitute all references to {{.URL}} in appropriate places.
|
|
v := struct{ URL string }{URL: ts.URL}
|
|
c.claims = replace(c.claims, &v)
|
|
c.openIDConfig = replace(c.openIDConfig, &v)
|
|
c.options.IssuerURL = replace(c.options.IssuerURL, &v)
|
|
for claim, response := range c.claimToResponseMap {
|
|
c.claimToResponseMap[claim] = replace(response, &v)
|
|
}
|
|
|
|
// Initialize the authenticator.
|
|
a, err := newAuthenticator(c.options, func(ctx context.Context, a *Authenticator, config *oidc.Config) {
|
|
// Set the verifier to use the public key set instead of reading from a remote.
|
|
a.setVerifier(oidc.NewVerifier(
|
|
c.options.IssuerURL,
|
|
&staticKeySet{keys: c.pubKeys},
|
|
config,
|
|
))
|
|
})
|
|
if err != nil {
|
|
if !c.wantInitErr {
|
|
t.Fatalf("initialize authenticator: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if c.wantInitErr {
|
|
t.Fatalf("wanted initialization error")
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if c.reqAudiences != nil {
|
|
ctx = authenticator.WithAudiences(ctx, c.reqAudiences)
|
|
}
|
|
|
|
got, ok, err := a.AuthenticateToken(ctx, token)
|
|
|
|
if err != nil {
|
|
if !c.wantErr {
|
|
t.Fatalf("authenticate token: %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if c.wantErr {
|
|
t.Fatalf("expected error authenticating token")
|
|
}
|
|
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) {
|
|
synchronizeTokenIDVerifierForTest = true
|
|
tests := []claimsTest{
|
|
{
|
|
name: "token",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "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: "no-username",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "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",
|
|
"exp": %d
|
|
}`, valid.Unix()),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "email",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "email",
|
|
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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "email",
|
|
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: true,
|
|
},
|
|
{
|
|
// If "email_verified" isn't present, assume true
|
|
name: "no-email-verified-claim",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "email",
|
|
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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "email",
|
|
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: true,
|
|
},
|
|
{
|
|
name: "groups",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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-distributed",
|
|
options: Options{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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: "jane",
|
|
Groups: []string{"team1", "team2"},
|
|
},
|
|
},
|
|
{
|
|
name: "groups-distributed-malformed-claim-names",
|
|
options: Options{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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": "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: true,
|
|
},
|
|
{
|
|
name: "groups-distributed-malformed-names-and-sources",
|
|
options: Options{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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"
|
|
},
|
|
"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: true,
|
|
},
|
|
{
|
|
name: "groups-distributed-malformed-distributed-claim",
|
|
options: Options{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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{
|
|
// 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: true,
|
|
},
|
|
{
|
|
name: "groups-distributed-unusual-name",
|
|
options: Options{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "rabbits",
|
|
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{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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{
|
|
// 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"
|
|
}`,
|
|
// "aud" was "your-client", not "my-client"
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "groups-distributed-wrong-audience",
|
|
options: Options{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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{
|
|
// 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"
|
|
}`,
|
|
// The distributed token is expired.
|
|
wantErr: true,
|
|
},
|
|
{
|
|
// 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{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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",
|
|
"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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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",
|
|
"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{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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",
|
|
"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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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",
|
|
"_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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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",
|
|
"exp": %d
|
|
}`, valid.Unix()),
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid-groups-claim",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "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": 42,
|
|
"exp": %d
|
|
}`, valid.Unix()),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "required-claim",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "groups",
|
|
RequiredClaims: map[string]string{
|
|
"hd": "example.com",
|
|
"sub": "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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "groups",
|
|
RequiredClaims: map[string]string{
|
|
"hd": "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: true,
|
|
},
|
|
{
|
|
name: "invalid-required-claim",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
GroupsClaim: "groups",
|
|
RequiredClaims: map[string]string{
|
|
"hd": "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: true,
|
|
},
|
|
{
|
|
name: "invalid-signature",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
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: true,
|
|
},
|
|
{
|
|
name: "expired",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "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
|
|
}`, expired.Unix()),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid-aud",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "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": "not-my-client",
|
|
"username": "jane",
|
|
"exp": %d
|
|
}`, valid.Unix()),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
// ID tokens may contain multiple audiences:
|
|
// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
|
|
name: "multiple-audiences",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "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": ["not-my-client", "my-client"],
|
|
"azp": "not-my-client",
|
|
"username": "jane",
|
|
"exp": %d
|
|
}`, valid.Unix()),
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid-issuer",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "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://example.com",
|
|
"aud": "my-client",
|
|
"username": "jane",
|
|
"exp": %d
|
|
}`, valid.Unix()),
|
|
wantSkip: true,
|
|
},
|
|
{
|
|
name: "username-prefix",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
UsernamePrefix: "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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
UsernamePrefix: "oidc:",
|
|
GroupsClaim: "groups",
|
|
GroupsPrefix: "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{
|
|
IssuerURL: "{{.URL}}",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
UsernamePrefix: "oidc:",
|
|
GroupsClaim: "groups",
|
|
GroupsPrefix: "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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
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: true,
|
|
},
|
|
{
|
|
name: "ps256",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
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{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
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{
|
|
IssuerURL: "http://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
now: func() time.Time { return now },
|
|
},
|
|
pubKeys: []*jose.JSONWebKey{
|
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
|
},
|
|
wantInitErr: true,
|
|
},
|
|
{
|
|
name: "no-username-claim",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
now: func() time.Time { return now },
|
|
},
|
|
pubKeys: []*jose.JSONWebKey{
|
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
|
},
|
|
wantInitErr: true,
|
|
},
|
|
{
|
|
name: "invalid-sig-alg",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "username",
|
|
SupportedSigningAlgs: []string{"HS256"},
|
|
now: func() time.Time { return now },
|
|
},
|
|
pubKeys: []*jose.JSONWebKey{
|
|
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
|
|
},
|
|
wantInitErr: true,
|
|
},
|
|
{
|
|
name: "accounts.google.com issuer",
|
|
options: Options{
|
|
IssuerURL: "https://accounts.google.com",
|
|
ClientID: "my-client",
|
|
UsernameClaim: "email",
|
|
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 api req audience",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
APIAudiences: authenticator.Audiences{"api"},
|
|
UsernameClaim: "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()),
|
|
reqAudiences: authenticator.Audiences{"api"},
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "good token with multiple api req audience",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
APIAudiences: authenticator.Audiences{"api", "other"},
|
|
UsernameClaim: "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()),
|
|
reqAudiences: authenticator.Audiences{"api"},
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "good token with client_id req audience",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
APIAudiences: authenticator.Audiences{"api"},
|
|
UsernameClaim: "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()),
|
|
reqAudiences: authenticator.Audiences{"my-client"},
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "good token with client_id and api req audience",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
APIAudiences: authenticator.Audiences{"api"},
|
|
UsernameClaim: "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()),
|
|
reqAudiences: authenticator.Audiences{"my-client", "api"},
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "good token with client_id and api req audience",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
APIAudiences: authenticator.Audiences{"api"},
|
|
UsernameClaim: "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()),
|
|
reqAudiences: authenticator.Audiences{"my-client", "api"},
|
|
want: &user.DefaultInfo{
|
|
Name: "jane",
|
|
},
|
|
},
|
|
{
|
|
name: "good token with client_id and bad req audience",
|
|
options: Options{
|
|
IssuerURL: "https://auth.example.com",
|
|
ClientID: "my-client",
|
|
APIAudiences: authenticator.Audiences{"api"},
|
|
UsernameClaim: "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()),
|
|
reqAudiences: authenticator.Audiences{"other"},
|
|
wantSkip: true,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, test.run)
|
|
}
|
|
}
|
|
|
|
func TestUnmarshalClaimError(t *testing.T) {
|
|
// Ensure error strings returned by unmarshaling claims don't include the claim.
|
|
const token = "96bb299a-02e9-11e8-8673-54ee7553240e"
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|