Add test for oidc with preferred usernames.

This commit is contained in:
Mariano Cano 2021-04-29 18:30:26 -07:00 committed by Cristian Le
parent f730c0bec4
commit aafac179a5
5 changed files with 92 additions and 20 deletions

View file

@ -388,7 +388,7 @@ func (o *OIDC) AuthorizeSSHSign(ctx context.Context, token string) ([]SignOption
}
// Get the identity using either the default identityFunc or one injected
// externally.
// externally. Note that the PreferredUsername might be empty.
iden, err := o.getIdentityFunc(ctx, o, claims.Email, claims.PreferredUsername)
if err != nil {
return nil, errs.Wrap(http.StatusInternalServerError, err, "oidc.AuthorizeSSHSign")

View file

@ -514,8 +514,10 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
assert.FatalError(t, err)
failGetIdentityToken, err := generateSimpleToken("the-issuer", p5.ClientID, &keys.Keys[0])
assert.FatalError(t, err)
okPreferredUsername, err := generateOIDCToken("subject", "the-issuer", p1.ClientID, "name@smallstep.com", "lecris", time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Admin email not in domains
okAdmin, err := generateToken("subject", "the-issuer", p3.ClientID, "root@example.com", []string{}, time.Now(), &keys.Keys[0])
okAdmin, err := generateOIDCToken("subject", "the-issuer", p3.ClientID, "root@example.com", "", time.Now(), &keys.Keys[0])
assert.FatalError(t, err)
// Empty email
failEmail, err := generateToken("subject", "the-issuer", p3.ClientID, "", []string{}, time.Now(), &keys.Keys[0])
@ -574,14 +576,17 @@ func TestOIDC_AuthorizeSSHSign(t *testing.T) {
{"ok-emptyPrincipals-getIdentity", p4, args{okGetIdentityToken, SignSSHOptions{}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"max", "mariano"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-preferred-username", p1, args{okPreferredUsername, SignSSHOptions{CertType: "user", KeyID: "name@smallstep.com", Principals: []string{"lecris"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"lecris", "name", "name@smallstep.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"ok-options", p1, args{t1, SignSSHOptions{CertType: "user", Principals: []string{"name"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"name", "name@smallstep.com"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"admin-user", p3, args{okAdmin, SignSSHOptions{CertType: "user", KeyID: "root@example.com", Principals: []string{"root", "root@example.com"}}, pub},
{"ok-admin-user", p3, args{okAdmin, SignSSHOptions{CertType: "user", KeyID: "root@example.com", Principals: []string{"root", "root@example.com"}}, pub},
expectedAdminOptions, http.StatusOK, false, false},
{"admin-host", p3, args{okAdmin, SignSSHOptions{CertType: "host", KeyID: "smallstep.com", Principals: []string{"smallstep.com"}}, pub},
{"ok-admin-host", p3, args{okAdmin, SignSSHOptions{CertType: "host", KeyID: "smallstep.com", Principals: []string{"smallstep.com"}}, pub},
expectedHostOptions, http.StatusOK, false, false},
{"admin-options", p3, args{okAdmin, SignSSHOptions{CertType: "user", KeyID: "name", Principals: []string{"name"}}, pub},
{"ok-admin-options", p3, args{okAdmin, SignSSHOptions{CertType: "user", KeyID: "name", Principals: []string{"name"}}, pub},
&SignSSHOptions{CertType: "user", Principals: []string{"name"},
ValidAfter: NewTimeDuration(tm), ValidBefore: NewTimeDuration(tm.Add(userDuration))}, http.StatusOK, false, false},
{"fail-rsa1024", p1, args{t1, SignSSHOptions{}, rsa1024.Public()}, expectedUserOptions, http.StatusOK, false, true},

View file

@ -339,34 +339,35 @@ type Permissions struct {
// GetIdentityFunc is a function that returns an identity.
type GetIdentityFunc func(ctx context.Context, p Interface, email string, usernames ...string) (*Identity, error)
// DefaultIdentityFunc return a default identity depending on the provisioner type.
// DefaultIdentityFunc return a default identity depending on the provisioner
// type. For OIDC email is always present and the usernames might
// contain empty strings.
func DefaultIdentityFunc(ctx context.Context, p Interface, email string, usernames ...string) (*Identity, error) {
switch k := p.(type) {
case *OIDC:
// OIDC principals would be:
// 1. Sanitized local.
// 2. Raw local (if different).
// 3. Email address.
// 1. Preferred usernames.
// 2. Sanitized local.
// 3. Raw local (if different).
// 4. Email address.
name := SanitizeSSHUserPrincipal(email)
if !sshUserRegex.MatchString(name) {
return nil, errors.Errorf("invalid principal '%s' from email '%s'", name, email)
}
usernames := append(usernames, name)
if i := strings.LastIndex(email, "@"); i >= 0 {
if local := email[:i]; !strings.EqualFold(local, name) {
usernames = append(usernames, local)
}
usernames = append(usernames, email[:i])
}
usernames = append(usernames, email)
usernames = SanitizeStringSlices(usernames)
return &Identity{
Usernames: usernames,
Usernames: SanitizeStringSlices(usernames),
}, nil
default:
return nil, errors.Errorf("provisioner type '%T' not supported by identity function", k)
}
}
// SanitizeStringSlices removes duplicated an empty strings.
func SanitizeStringSlices(original []string) []string {
output := []string{}
seen := make(map[string]bool)

View file

@ -62,10 +62,11 @@ func TestSanitizeSSHUserPrincipal(t *testing.T) {
func TestDefaultIdentityFunc(t *testing.T) {
type test struct {
p Interface
email string
err error
identity *Identity
p Interface
email string
usernames []string
err error
identity *Identity
}
tests := map[string]func(*testing.T) test{
"fail/unsupported-provisioner": func(t *testing.T) test {
@ -106,7 +107,7 @@ func TestDefaultIdentityFunc(t *testing.T) {
return test{
p: &OIDC{},
email: "John@smallstep.com",
identity: &Identity{Usernames: []string{"john", "John@smallstep.com"}},
identity: &Identity{Usernames: []string{"john", "John", "John@smallstep.com"}},
}
},
"ok symbol": func(t *testing.T) test {
@ -116,11 +117,35 @@ func TestDefaultIdentityFunc(t *testing.T) {
identity: &Identity{Usernames: []string{"john_doe", "John+Doe", "John+Doe@smallstep.com"}},
}
},
"ok username": func(t *testing.T) test {
return test{
p: &OIDC{},
email: "john@smallstep.com",
usernames: []string{"johnny"},
identity: &Identity{Usernames: []string{"johnny", "john", "john@smallstep.com"}},
}
},
"ok usernames": func(t *testing.T) test {
return test{
p: &OIDC{},
email: "john@smallstep.com",
usernames: []string{"johnny", "js", ""},
identity: &Identity{Usernames: []string{"johnny", "js", "john", "john@smallstep.com"}},
}
},
"ok empty username": func(t *testing.T) test {
return test{
p: &OIDC{},
email: "john@smallstep.com",
usernames: []string{""},
identity: &Identity{Usernames: []string{"john", "john@smallstep.com"}},
}
},
}
for name, get := range tests {
t.Run(name, func(t *testing.T) {
tc := get(t)
identity, err := DefaultIdentityFunc(context.Background(), tc.p, tc.email)
identity, err := DefaultIdentityFunc(context.Background(), tc.p, tc.email, tc.usernames...)
if err != nil {
if assert.NotNil(t, tc.err) {
assert.Equals(t, tc.err.Error(), err.Error())

View file

@ -773,6 +773,47 @@ func generateToken(sub, iss, aud string, email string, sans []string, iat time.T
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateOIDCToken(sub, iss, aud string, email string, preferredUsername string, iat time.Time, jwk *jose.JSONWebKey, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")
so.WithHeader("kid", jwk.KeyID)
for _, o := range tokOpts {
if err := o(so); err != nil {
return "", err
}
}
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.ES256, Key: jwk.Key}, so)
if err != nil {
return "", err
}
id, err := randutil.ASCII(64)
if err != nil {
return "", err
}
claims := struct {
jose.Claims
Email string `json:"email"`
PreferredUsername string `json:"preferred_username,omitempty"`
}{
Claims: jose.Claims{
ID: id,
Subject: sub,
Issuer: iss,
IssuedAt: jose.NewNumericDate(iat),
NotBefore: jose.NewNumericDate(iat),
Expiry: jose.NewNumericDate(iat.Add(5 * time.Minute)),
Audience: []string{aud},
},
Email: email,
PreferredUsername: preferredUsername,
}
return jose.Signed(sig).Claims(claims).CompactSerialize()
}
func generateX5CSSHToken(jwk *jose.JSONWebKey, claims *x5cPayload, tokOpts ...tokOption) (string, error) {
so := new(jose.SignerOptions)
so.WithType("JWT")