From cb58182900a32fa015ccf8fa999ebe766ab375db Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 31 Oct 2018 16:04:39 -0400 Subject: [PATCH] Update jwt to pull in groups claim delimiter pattern --- .../vault-plugin-auth-jwt/path_config.go | 8 +- .../vault-plugin-auth-jwt/path_login.go | 31 +++++- .../vault-plugin-auth-jwt/path_role.go | 96 ++++++++++++++----- vendor/vendor.json | 6 +- website/source/api/auth/jwt/index.html.md | 7 ++ 5 files changed, 112 insertions(+), 36 deletions(-) diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go index 83ce60d618..5019cbd603 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_config.go @@ -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.", }, diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go index 595de420c5..868fe9d094 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_login.go @@ -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 } diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go index e54424f78a..9ca1fec415 100644 --- a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go +++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_role.go @@ -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": { diff --git a/vendor/vendor.json b/vendor/vendor.json index 2edc888e97..4cd5f7a5b6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -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=", diff --git a/website/source/api/auth/jwt/index.html.md b/website/source/api/auth/jwt/index.html.md index 45a1553e85..febcfcf774 100644 --- a/website/source/api/auth/jwt/index.html.md +++ b/website/source/api/auth/jwt/index.html.md @@ -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