forked from TrueCloudLab/certificates
Add tests for OIDC and complete some JWK tests.
This commit is contained in:
parent
dce3100cfb
commit
4ceb88fbae
4 changed files with 354 additions and 22 deletions
|
@ -111,11 +111,16 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
assert.FatalError(t, err)
|
||||
t2, err := generateSimpleToken(p2.Name, testAudiences[1], key2)
|
||||
assert.FatalError(t, err)
|
||||
t3, err := generateToken("test.smallstep.com", p1.Name, testAudiences[0], []string{}, key1)
|
||||
t3, err := generateToken("test.smallstep.com", p1.Name, testAudiences[0], []string{}, time.Now(), key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Invalid tokens
|
||||
parts := strings.Split(t1, ".")
|
||||
key3, err := generateJSONWebKey()
|
||||
assert.FatalError(t, err)
|
||||
// missing key
|
||||
failKey, err := generateSimpleToken(p1.Name, testAudiences[0], key3)
|
||||
assert.FatalError(t, err)
|
||||
// invalid token
|
||||
failTok := "foo." + parts[1] + "." + parts[2]
|
||||
// invalid claims
|
||||
|
@ -129,7 +134,13 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
// invalid signature
|
||||
failSig := t1[0 : len(t1)-2]
|
||||
// no subject
|
||||
failSub, err := generateToken("", p1.Name, testAudiences[0], []string{"test.smallstep.com"}, key1)
|
||||
failSub, err := generateToken("", p1.Name, testAudiences[0], []string{"test.smallstep.com"}, time.Now(), key1)
|
||||
assert.FatalError(t, err)
|
||||
// expired
|
||||
failExp, err := generateToken("subject", p1.Name, testAudiences[0], []string{"test.smallstep.com"}, time.Now().Add(-360*time.Second), key1)
|
||||
assert.FatalError(t, err)
|
||||
// not before
|
||||
failNbf, err := generateToken("subject", p1.Name, testAudiences[0], []string{"test.smallstep.com"}, time.Now().Add(360*time.Second), key1)
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Remove encrypted key for p2
|
||||
|
@ -147,12 +158,15 @@ func TestJWK_Authorize(t *testing.T) {
|
|||
{"ok", p1, args{t1}, false},
|
||||
{"ok-no-encrypted-key", p2, args{t2}, false},
|
||||
{"ok-no-sans", p1, args{t3}, false},
|
||||
{"fail-key", p1, args{failKey}, true},
|
||||
{"fail-token", p1, args{failTok}, true},
|
||||
{"fail-claims", p1, args{failClaims}, true},
|
||||
{"fail-issuer", p1, args{failIss}, true},
|
||||
{"fail-audience", p1, args{failAud}, true},
|
||||
{"fail-signature", p1, args{failSig}, true},
|
||||
{"fail-subject", p1, args{failSub}, true},
|
||||
{"fail-expired", p1, args{failExp}, true},
|
||||
{"fail-not-before", p1, args{failNbf}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
|
@ -17,6 +17,18 @@ type openIDConfiguration struct {
|
|||
JWKSetURI string `json:"jwks_uri"`
|
||||
}
|
||||
|
||||
// Validate validates the values in a well-known OpenID configuration endpoint.
|
||||
func (c openIDConfiguration) Validate() error {
|
||||
switch {
|
||||
case c.Issuer == "":
|
||||
return errors.New("issuer cannot be empty")
|
||||
case c.JWKSetURI == "":
|
||||
return errors.New("jwks_uri cannot be empty")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// openIDPayload represents the fields on the id_token JWT payload.
|
||||
type openIDPayload struct {
|
||||
jose.Claims
|
||||
|
@ -87,12 +99,12 @@ func (o *OIDC) Init(config Config) (err error) {
|
|||
if o.Claims, err = o.Claims.Init(&config.Claims); err != nil {
|
||||
return err
|
||||
}
|
||||
// Decode openid-configuration endpoint
|
||||
// Decode and validate openid-configuration endpoint
|
||||
if err := getAndDecode(o.ConfigurationEndpoint, &o.configuration); err != nil {
|
||||
return err
|
||||
}
|
||||
if o.configuration.JWKSetURI == "" {
|
||||
return errors.Errorf("error parsing %s: jwks_uri cannot be empty", o.ConfigurationEndpoint)
|
||||
if err := o.configuration.Validate(); err != nil {
|
||||
return errors.Wrapf(err, "error parsing %s", o.ConfigurationEndpoint)
|
||||
}
|
||||
// Get JWK key set
|
||||
o.keyStore, err = newKeyStore(o.configuration.JWKSetURI)
|
||||
|
@ -103,8 +115,6 @@ func (o *OIDC) Init(config Config) (err error) {
|
|||
}
|
||||
|
||||
// ValidatePayload validates the given token payload.
|
||||
//
|
||||
// TODO(mariano): avoid reply attacks validating nonce.
|
||||
func (o *OIDC) ValidatePayload(p openIDPayload) error {
|
||||
// According to "rfc7519 JSON Web Token" acceptable skew should be no more
|
||||
// than a few minutes.
|
||||
|
@ -151,8 +161,13 @@ func (o *OIDC) Authorize(token string) ([]SignOption, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Admins should be able to authorize any SAN
|
||||
if o.IsAdmin(claims.Email) {
|
||||
return []SignOption{}, nil
|
||||
return []SignOption{
|
||||
profileDefaultDuration(o.Claims.DefaultTLSCertDuration()),
|
||||
newProvisionerExtensionOption(TypeOIDC, o.Name, o.ClientID),
|
||||
newValidityValidator(o.Claims.MinTLSCertDuration(), o.Claims.MaxTLSCertDuration()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return []SignOption{
|
||||
|
|
287
authority/provisioner/oidc_test.go
Normal file
287
authority/provisioner/oidc_test.go
Normal file
|
@ -0,0 +1,287 @@
|
|||
package provisioner
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/assert"
|
||||
"github.com/smallstep/cli/jose"
|
||||
)
|
||||
|
||||
func Test_openIDConfiguration_Validate(t *testing.T) {
|
||||
type fields struct {
|
||||
Issuer string
|
||||
JWKSetURI string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{"the-issuer", "the-jwks-uri"}, false},
|
||||
{"no-issuer", fields{"", "the-jwks-uri"}, true},
|
||||
{"no-jwks-uri", fields{"the-issuer", ""}, true},
|
||||
{"empty", fields{"", ""}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := openIDConfiguration{
|
||||
Issuer: tt.fields.Issuer,
|
||||
JWKSetURI: tt.fields.JWKSetURI,
|
||||
}
|
||||
if err := c.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("openIDConfiguration.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_Getters(t *testing.T) {
|
||||
p, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
if got := p.GetID(); got != p.ClientID {
|
||||
t.Errorf("OIDC.GetID() = %v, want %v", got, p.ClientID)
|
||||
}
|
||||
if got := p.GetName(); got != p.Name {
|
||||
t.Errorf("OIDC.GetName() = %v, want %v", got, p.Name)
|
||||
}
|
||||
if got := p.GetType(); got != TypeOIDC {
|
||||
t.Errorf("OIDC.GetType() = %v, want %v", got, TypeOIDC)
|
||||
}
|
||||
kid, key, ok := p.GetEncryptedKey()
|
||||
if kid != "" || key != "" || ok == true {
|
||||
t.Errorf("OIDC.GetEncryptedKey() = (%v, %v, %v), want (%v, %v, %v)",
|
||||
kid, key, ok, "", "", false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_Init(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
config := Config{
|
||||
Claims: globalProvisionerClaims,
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
Type string
|
||||
Name string
|
||||
ClientID string
|
||||
ConfigurationEndpoint string
|
||||
Claims *Claims
|
||||
Admins []string
|
||||
}
|
||||
type args struct {
|
||||
config Config
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", fields{"oidc", "name", "client-id", srv.URL + "/openid-configuration", nil, nil}, args{config}, false},
|
||||
{"ok-admins", fields{"oidc", "name", "client-id", srv.URL + "/openid-configuration", nil, []string{"foo@smallstep.com"}}, args{config}, false},
|
||||
{"no-name", fields{"oidc", "", "client-id", srv.URL + "/openid-configuration", nil, nil}, args{config}, true},
|
||||
{"no-client-id", fields{"oidc", "name", "", srv.URL + "/openid-configuration", nil, nil}, args{config}, true},
|
||||
{"no-configuration", fields{"oidc", "name", "client-id", "", nil, nil}, args{config}, true},
|
||||
{"bad-configuration", fields{"oidc", "name", "client-id", srv.URL, nil, nil}, args{config}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := &OIDC{
|
||||
Type: tt.fields.Type,
|
||||
Name: tt.fields.Name,
|
||||
ClientID: tt.fields.ClientID,
|
||||
ConfigurationEndpoint: tt.fields.ConfigurationEndpoint,
|
||||
Claims: tt.fields.Claims,
|
||||
Admins: tt.fields.Admins,
|
||||
}
|
||||
if err := p.Init(tt.args.config); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OIDC.Init() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr == false {
|
||||
assert.Len(t, 2, p.keyStore.keySet.Keys)
|
||||
assert.Equals(t, openIDConfiguration{
|
||||
Issuer: "the-issuer",
|
||||
JWKSetURI: srv.URL + "/jwks_uri",
|
||||
}, p.configuration)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_Authorize(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
|
||||
var keys jose.JSONWebKeySet
|
||||
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
|
||||
|
||||
// Create test provisioners
|
||||
p1, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p2, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p3, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
// Admin
|
||||
p3.Admins = []string{"name@smallstep.com"}
|
||||
|
||||
// Update configuration endpoints and initialize
|
||||
config := Config{Claims: globalProvisionerClaims}
|
||||
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
p2.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
p3.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
assert.FatalError(t, p1.Init(config))
|
||||
assert.FatalError(t, p2.Init(config))
|
||||
assert.FatalError(t, p3.Init(config))
|
||||
|
||||
t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
t2, err := generateSimpleToken("the-issuer", p2.ClientID, &keys.Keys[1])
|
||||
assert.FatalError(t, err)
|
||||
t3, err := generateSimpleToken("the-issuer", p3.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Invalid tokens
|
||||
parts := strings.Split(t1, ".")
|
||||
key, err := generateJSONWebKey()
|
||||
assert.FatalError(t, err)
|
||||
// missing key
|
||||
failKey, err := generateSimpleToken("the-issuer", p1.ClientID, key)
|
||||
assert.FatalError(t, err)
|
||||
// invalid token
|
||||
failTok := "foo." + parts[1] + "." + parts[2]
|
||||
// invalid claims
|
||||
failClaims := parts[0] + ".foo." + parts[1]
|
||||
// invalid issuer
|
||||
failIss, err := generateSimpleToken("bad-issuer", p1.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// invalid audience
|
||||
failAud, err := generateSimpleToken("the-issuer", "foobar", &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// invalid signature
|
||||
failSig := t1[0 : len(t1)-2]
|
||||
// expired
|
||||
failExp, err := generateToken("subject", "the-issuer", p1.ClientID, []string{}, time.Now().Add(-360*time.Second), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
// not before
|
||||
failNbf, err := generateToken("subject", "the-issuer", p1.ClientID, []string{}, time.Now().Add(360*time.Second), &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *OIDC
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok1", p1, args{t1}, false},
|
||||
{"ok2", p2, args{t2}, false},
|
||||
{"admin", p3, args{t3}, false},
|
||||
{"fail-key", p1, args{failKey}, true},
|
||||
{"fail-token", p1, args{failTok}, true},
|
||||
{"fail-claims", p1, args{failClaims}, true},
|
||||
{"fail-issuer", p1, args{failIss}, true},
|
||||
{"fail-audience", p1, args{failAud}, true},
|
||||
{"fail-signature", p1, args{failSig}, true},
|
||||
{"fail-expired", p1, args{failExp}, true},
|
||||
{"fail-not-before", p1, args{failNbf}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.prov.Authorize(tt.args.token)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("OIDC.Authorize() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
assert.Nil(t, got)
|
||||
} else {
|
||||
assert.NotNil(t, got)
|
||||
if tt.name == "admin" {
|
||||
assert.Len(t, 3, got)
|
||||
} else {
|
||||
assert.Len(t, 4, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_AuthorizeRenewal(t *testing.T) {
|
||||
p1, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
p2, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// disable renewal
|
||||
disable := true
|
||||
p2.Claims = &Claims{
|
||||
globalClaims: &globalProvisionerClaims,
|
||||
DisableRenewal: &disable,
|
||||
}
|
||||
|
||||
type args struct {
|
||||
cert *x509.Certificate
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *OIDC
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"ok", p1, args{nil}, false},
|
||||
{"fail", p2, args{nil}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.prov.AuthorizeRenewal(tt.args.cert); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OIDC.AuthorizeRenewal() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDC_AuthorizeRevoke(t *testing.T) {
|
||||
srv := generateJWKServer(2)
|
||||
defer srv.Close()
|
||||
|
||||
var keys jose.JSONWebKeySet
|
||||
assert.FatalError(t, getAndDecode(srv.URL+"/private", &keys))
|
||||
|
||||
// Create test provisioners
|
||||
p1, err := generateOIDC()
|
||||
assert.FatalError(t, err)
|
||||
|
||||
// Update configuration endpoints and initialize
|
||||
config := Config{Claims: globalProvisionerClaims}
|
||||
p1.ConfigurationEndpoint = srv.URL + "/.well-known/openid-configuration"
|
||||
assert.FatalError(t, p1.Init(config))
|
||||
|
||||
t1, err := generateSimpleToken("the-issuer", p1.ClientID, &keys.Keys[0])
|
||||
assert.FatalError(t, err)
|
||||
|
||||
type args struct {
|
||||
token string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
prov *OIDC
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{"disabled", p1, args{t1}, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.prov.AuthorizeRevoke(tt.args.token); (err != nil) != tt.wantErr {
|
||||
t.Errorf("OIDC.AuthorizeRevoke() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ func generateJSONWebKeySet(n int) (jose.JSONWebKeySet, error) {
|
|||
if err != nil {
|
||||
return jose.JSONWebKeySet{}, err
|
||||
}
|
||||
keySet.Keys = append(keySet.Keys, key.Public())
|
||||
keySet.Keys = append(keySet.Keys, *key)
|
||||
}
|
||||
return keySet, nil
|
||||
}
|
||||
|
@ -173,11 +173,10 @@ func generateCollection(nJWK, nOIDC int) (*Collection, error) {
|
|||
}
|
||||
|
||||
func generateSimpleToken(iss, aud string, jwk *jose.JSONWebKey) (string, error) {
|
||||
return generateToken("subject", iss, aud, []string{"test.smallstep.com"}, jwk)
|
||||
// return generateToken("the-sub", []string{"test.smallstep.com"}, jwk.KeyID, iss, aud, "testdata/root_ca.crt", now, now.Add(5*time.Minute), jwk)
|
||||
return generateToken("subject", iss, aud, []string{"test.smallstep.com"}, time.Now(), jwk)
|
||||
}
|
||||
|
||||
func generateToken(sub, iss, aud string, sans []string, jwk *jose.JSONWebKey) (string, error) {
|
||||
func generateToken(sub, iss, aud string, sans []string, iat time.Time, jwk *jose.JSONWebKey) (string, error) {
|
||||
sig, err := jose.NewSigner(
|
||||
jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key},
|
||||
new(jose.SignerOptions).WithType("JWT").WithHeader("kid", jwk.KeyID),
|
||||
|
@ -191,20 +190,22 @@ func generateToken(sub, iss, aud string, sans []string, jwk *jose.JSONWebKey) (s
|
|||
return "", err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
claims := struct {
|
||||
jose.Claims
|
||||
Email string `json:"email"`
|
||||
SANS []string `json:"sans"`
|
||||
}{
|
||||
Claims: jose.Claims{
|
||||
ID: id,
|
||||
Subject: sub,
|
||||
Issuer: iss,
|
||||
NotBefore: jose.NewNumericDate(now),
|
||||
Expiry: jose.NewNumericDate(now.Add(5 * time.Minute)),
|
||||
IssuedAt: jose.NewNumericDate(iat),
|
||||
NotBefore: jose.NewNumericDate(iat),
|
||||
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
|
||||
Audience: []string{aud},
|
||||
},
|
||||
SANS: sans,
|
||||
Email: "name@smallstep.com",
|
||||
}
|
||||
return jose.Signed(sig).Claims(claims).CompactSerialize()
|
||||
}
|
||||
|
@ -235,22 +236,37 @@ func generateJWKServer(n int) *httptest.Server {
|
|||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(b)
|
||||
}
|
||||
// keySet, err := generateJSONWebKeySet(n)
|
||||
getPublic := func(ks jose.JSONWebKeySet) jose.JSONWebKeySet {
|
||||
var ret jose.JSONWebKeySet
|
||||
for _, k := range ks.Keys {
|
||||
ret.Keys = append(ret.Keys, k.Public())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
defaultKeySet := must(generateJSONWebKeySet(2))[0].(jose.JSONWebKeySet)
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
srv := httptest.NewUnstartedServer(nil)
|
||||
srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
hits.Hits++
|
||||
switch r.RequestURI {
|
||||
case "/error":
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
case "/hits":
|
||||
writeJSON(w, hits)
|
||||
case "/openid-configuration", "/.well-known/openid-configuration":
|
||||
writeJSON(w, openIDConfiguration{Issuer: "the-issuer", JWKSetURI: srv.URL + "/jwks_uri"})
|
||||
case "/random":
|
||||
keySet := must(generateJSONWebKeySet(2))[0].(jose.JSONWebKeySet)
|
||||
w.Header().Add("Cache-Control", "max-age=5")
|
||||
writeJSON(w, keySet)
|
||||
writeJSON(w, getPublic(keySet))
|
||||
case "/private":
|
||||
writeJSON(w, defaultKeySet)
|
||||
default:
|
||||
w.Header().Add("Cache-Control", "max-age=5")
|
||||
writeJSON(w, defaultKeySet)
|
||||
writeJSON(w, getPublic(defaultKeySet))
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
||||
srv.Start()
|
||||
return srv
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue