Update jwt to pull in groups claim delimiter pattern

This commit is contained in:
Jeff Mitchell 2018-10-31 16:04:39 -04:00
parent b2ead22689
commit cb58182900
5 changed files with 112 additions and 36 deletions

View File

@ -21,19 +21,19 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: `config`,
Fields: map[string]*framework.FieldSchema{
"oidc_discovery_url": &framework.FieldSchema{
"oidc_discovery_url": {
Type: framework.TypeString,
Description: `OIDC Discovery URL, without any .well-known component (base path). Cannot be used with "jwt_validation_pubkeys".`,
},
"oidc_discovery_ca_pem": &framework.FieldSchema{
"oidc_discovery_ca_pem": {
Type: framework.TypeString,
Description: "The CA certificate or chain of certificates, in PEM format, to use to validate conections to the OIDC Discovery URL. If not set, system certificates are used.",
},
"jwt_validation_pubkeys": &framework.FieldSchema{
"jwt_validation_pubkeys": {
Type: framework.TypeCommaStringSlice,
Description: `A list of PEM-encoded public keys to use to authenticate signatures locally. Cannot be used with "oidc_discovery_url".`,
},
"bound_issuer": &framework.FieldSchema{
"bound_issuer": {
Type: framework.TypeString,
Description: "The value against which to match the 'iss' claim in a JWT. Optional.",
},

View File

@ -19,11 +19,11 @@ func pathLogin(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: `login$`,
Fields: map[string]*framework.FieldSchema{
"role": &framework.FieldSchema{
"role": {
Type: framework.TypeLowerCaseString,
Description: "The role to log in against.",
},
"jwt": &framework.FieldSchema{
"jwt": {
Type: framework.TypeString,
Description: "The signed JWT to validate.",
},
@ -179,7 +179,32 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
var groupAliases []*logical.Alias
if role.GroupsClaim != "" {
groupsClaimRaw, ok := allClaims[role.GroupsClaim]
mapPath, err := parseClaimWithDelimiters(role.GroupsClaim, role.GroupsClaimDelimiterPattern)
if err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error parsing delimiters for groups claim: {{err}}", err).Error()), nil
}
if len(mapPath) < 1 {
return logical.ErrorResponse("unexpected length 0 of claims path after parsing groups claim against delimiters"), nil
}
var claimKey string
claimMap := allClaims
for i, key := range mapPath {
if i == len(mapPath)-1 {
claimKey = key
break
}
nextMapRaw, ok := claimMap[key]
if !ok {
return logical.ErrorResponse(fmt.Sprintf("map via key %q not found while navigating group claim delimiters", key)), nil
}
nextMap, ok := nextMapRaw.(map[string]interface{})
if !ok {
return logical.ErrorResponse(fmt.Sprintf("key %q does not reference a map while navigating group claim delimiters", key)), nil
}
claimMap = nextMap
}
groupsClaimRaw, ok := claimMap[claimKey]
if !ok {
return logical.ErrorResponse(fmt.Sprintf("%q claim not found in token", role.GroupsClaim)), nil
}

View File

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/hashicorp/errwrap"
sockaddr "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/helper/parseutil"
"github.com/hashicorp/vault/helper/policyutil"
@ -30,52 +31,56 @@ func pathRole(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: "role/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
"name": {
Type: framework.TypeLowerCaseString,
Description: "Name of the role.",
},
"policies": &framework.FieldSchema{
"policies": {
Type: framework.TypeCommaStringSlice,
Description: "List of policies on the role.",
},
"num_uses": &framework.FieldSchema{
"num_uses": {
Type: framework.TypeInt,
Description: `Number of times issued tokens can be used`,
},
"ttl": &framework.FieldSchema{
"ttl": {
Type: framework.TypeDurationSecond,
Description: `Duration in seconds after which the issued token should expire. Defaults
to 0, in which case the value will fall back to the system/mount defaults.`,
},
"max_ttl": &framework.FieldSchema{
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: `Duration in seconds after which the issued token should not be allowed to
be renewed. Defaults to 0, in which case the value will fall back to the system/mount defaults.`,
},
"period": &framework.FieldSchema{
"period": {
Type: framework.TypeDurationSecond,
Description: `If set, indicates that the token generated using this role
should never expire. The token should be renewed within the
duration specified by this value. At each renewal, the token's
TTL will be set to the value of this parameter.`,
},
"bound_subject": &framework.FieldSchema{
"bound_subject": {
Type: framework.TypeString,
Description: `The 'sub' claim that is valid for login. Optional.`,
},
"bound_audiences": &framework.FieldSchema{
"bound_audiences": {
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of 'aud' claims that are valid for login; any match is sufficient`,
},
"user_claim": &framework.FieldSchema{
"user_claim": {
Type: framework.TypeString,
Description: `The claim to use for the Identity entity alias name`,
},
"groups_claim": &framework.FieldSchema{
"groups_claim": {
Type: framework.TypeString,
Description: `The claim to use for the Identity group alias names`,
},
"bound_cidrs": &framework.FieldSchema{
"groups_claim_delimiter_pattern": {
Type: framework.TypeString,
Description: `A pattern of delimiters used to allow the groups_claim to live outside of the top-level JWT structure. For instance, a "groups_claim" of "meta/user.name/groups" with this field set to "//" will expect nested structures named "meta", "user.name", and "groups". If this field was set to "/./" the groups information would expect to be via nested structures of "meta", "user", "name", and "groups".`,
},
"bound_cidrs": {
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of IP CIDRS that are allowed to
authenticate against this role`,
@ -114,11 +119,12 @@ type jwtRole struct {
Period time.Duration `json:"period"`
// Role binding properties
BoundAudiences []string `json:"bound_audiences"`
BoundSubject string `json:"bound_subject"`
BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"`
UserClaim string `json:"user_claim"`
GroupsClaim string `json:"groups_claim"`
BoundAudiences []string `json:"bound_audiences"`
BoundSubject string `json:"bound_subject"`
BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"`
UserClaim string `json:"user_claim"`
GroupsClaim string `json:"groups_claim"`
GroupsClaimDelimiterPattern string `json:"groups_claim_delimiter_pattern"`
}
// role takes a storage backend and the name and returns the role's storage
@ -176,16 +182,17 @@ func (b *jwtAuthBackend) pathRoleRead(ctx context.Context, req *logical.Request,
// Create a map of data to be returned
resp := &logical.Response{
Data: map[string]interface{}{
"policies": role.Policies,
"num_uses": role.NumUses,
"period": int64(role.Period.Seconds()),
"ttl": int64(role.TTL.Seconds()),
"max_ttl": int64(role.MaxTTL.Seconds()),
"bound_audiences": role.BoundAudiences,
"bound_subject": role.BoundSubject,
"bound_cidrs": role.BoundCIDRs,
"user_claim": role.UserClaim,
"groups_claim": role.GroupsClaim,
"policies": role.Policies,
"num_uses": role.NumUses,
"period": int64(role.Period.Seconds()),
"ttl": int64(role.TTL.Seconds()),
"max_ttl": int64(role.MaxTTL.Seconds()),
"bound_audiences": role.BoundAudiences,
"bound_subject": role.BoundSubject,
"bound_cidrs": role.BoundCIDRs,
"user_claim": role.UserClaim,
"groups_claim": role.GroupsClaim,
"groups_claim_delimiter_pattern": role.GroupsClaimDelimiterPattern,
},
}
@ -291,6 +298,17 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role.GroupsClaim = groupsClaim.(string)
}
if groupsClaimDelimiterPattern, ok := data.GetOk("groups_claim_delimiter_pattern"); ok {
role.GroupsClaimDelimiterPattern = groupsClaimDelimiterPattern.(string)
}
// Validate claim/delims
if role.GroupsClaim != "" {
if _, err := parseClaimWithDelimiters(role.GroupsClaim, role.GroupsClaimDelimiterPattern); err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error validating delimiters for groups claim: {{err}}", err).Error()), nil
}
}
if len(role.BoundAudiences) == 0 &&
len(role.BoundCIDRs) == 0 &&
role.BoundSubject == "" {
@ -322,6 +340,32 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
return resp, nil
}
// parseClaimWithDelimiters parses a given claim string and ensures that we can
// separate it out into a "map path"
func parseClaimWithDelimiters(claim, delimiters string) ([]string, error) {
if delimiters == "" {
return []string{claim}, nil
}
var ret []string
for _, runeVal := range delimiters {
idx := strings.IndexRune(claim, runeVal)
switch idx {
case -1:
return nil, fmt.Errorf("could not find instance of %q delimiter in claim", string(runeVal))
case 0:
return nil, fmt.Errorf("instance of %q delimiter in claim is at beginning of claim string", string(runeVal))
case len(claim) - 1:
return nil, fmt.Errorf("instance of %q delimiter in claim is at end of claim string", string(runeVal))
default:
ret = append(ret, claim[:idx])
claim = claim[idx+1:]
}
}
ret = append(ret, claim)
return ret, nil
}
// roleStorageEntry stores all the options that are set on an role
var roleHelp = map[string][2]string{
"role-list": {

6
vendor/vendor.json vendored
View File

@ -1431,10 +1431,10 @@
"revisionTime": "2018-10-12T20:41:23Z"
},
{
"checksumSHA1": "nfHZ5lzZ2BUM97WnQ7acdnSEPQo=",
"checksumSHA1": "tt3FtyjXgdBI9Mb43UL4LtOZmAk=",
"path": "github.com/hashicorp/vault-plugin-auth-jwt",
"revision": "bf8970c9734c5d1e9fbab23255456c8272ff354a",
"revisionTime": "2018-10-15T15:58:27Z"
"revision": "f428c77917331c1b87dae2dd37016bd1dd4c55da",
"revisionTime": "2018-10-31T19:59:42Z"
},
{
"checksumSHA1": "hrJZzU9iG2ixRu2hOdPgN7wa48c=",

View File

@ -122,6 +122,13 @@ entities attempting to login. At least one of the bound values must be set.
the set of groups to which the user belongs; this will be used as the names
for the Identity group aliases created due to a successful login. The claim
value must be a list of strings.
- `groups_claim_delimiter_pattern` `(string: optional)` - A pattern of
delimiters used to allow the `groups_claim` to live outside of the top-level
JWT structure. For instance, a `groups_claim` of `meta/user.name/groups` with
this field set to `//` will expect nested structures named `meta`,
`user.name`, and `groups`. If this field was set to `/./` the groups
information would expect to be via nested structures of `meta`, `user`,
`name`, and `groups`.
### Sample Payload