open-vault/vendor/github.com/hashicorp/vault-plugin-auth-jwt/claims.go
2019-06-20 23:43:02 -04:00

138 lines
3.8 KiB
Go

package jwtauth
import (
"errors"
"fmt"
"strings"
"github.com/hashicorp/vault/sdk/helper/strutil"
log "github.com/hashicorp/go-hclog"
"github.com/mitchellh/pointerstructure"
)
// getClaim returns a claim value from allClaims given a provided claim string.
// If this string is a valid JSONPointer, it will be interpreted as such to locate
// the claim. Otherwise, the claim string will be used directly.
func getClaim(logger log.Logger, allClaims map[string]interface{}, claim string) interface{} {
var val interface{}
var err error
if !strings.HasPrefix(claim, "/") {
val = allClaims[claim]
} else {
val, err = pointerstructure.Get(allClaims, claim)
if err != nil {
logger.Warn(fmt.Sprintf("unable to locate %s in claims: %s", claim, err.Error()))
return nil
}
}
// The claims unmarshalled by go-oidc don't use UseNumber, so there will
// be mismatches if they're coming in as float64 since Vault's config will
// be represented as json.Number. If the operator can coerce claims data to
// be in string form, there is no problem. Alternatively, we could try to
// intelligently convert float64 to json.Number, e.g.:
//
// switch v := val.(type) {
// case float64:
// val = json.Number(strconv.Itoa(int(v)))
// }
//
// Or we fork and/or PR go-oidc.
return val
}
// extractMetadata builds a metadata map from a set of claims and claims mappings.
// The referenced claims must be strings and the claims mappings must be of the structure:
//
// {
// "/some/claim/pointer": "metadata_key1",
// "another_claim": "metadata_key2",
// ...
// }
func extractMetadata(logger log.Logger, allClaims map[string]interface{}, claimMappings map[string]string) (map[string]string, error) {
metadata := make(map[string]string)
for source, target := range claimMappings {
if value := getClaim(logger, allClaims, source); value != nil {
strValue, ok := value.(string)
if !ok {
return nil, fmt.Errorf("error converting claim '%s' to string", source)
}
metadata[target] = strValue
}
}
return metadata, nil
}
// validateAudience checks whether any of the audiences in audClaim match those
// in boundAudiences. If strict is true and there are no bound audiences, then the
// presence of any audience in the received claim is considered an error.
func validateAudience(boundAudiences, audClaim []string, strict bool) error {
if strict && len(boundAudiences) == 0 && len(audClaim) > 0 {
return errors.New("audience claim found in JWT but no audiences bound to the role")
}
if len(boundAudiences) > 0 {
for _, v := range boundAudiences {
if strutil.StrListContains(audClaim, v) {
return nil
}
}
return errors.New("aud claim does not match any bound audience")
}
return nil
}
// validateBoundClaims checks that all of the claim:value requirements in boundClaims are
// met in allClaims.
func validateBoundClaims(logger log.Logger, boundClaims, allClaims map[string]interface{}) error {
for claim, expValue := range boundClaims {
actValue := getClaim(logger, allClaims, claim)
if actValue == nil {
return fmt.Errorf("claim %q is missing", claim)
}
var actVals, expVals []interface{}
switch v := actValue.(type) {
case []interface{}:
actVals = v
case string:
actVals = []interface{}{v}
default:
return fmt.Errorf("received claim is not a string or list: %v", actValue)
}
switch v := expValue.(type) {
case []interface{}:
expVals = v
case string:
expVals = []interface{}{v}
default:
return fmt.Errorf("bound claim is not a string or list: %v", expValue)
}
found := false
scan:
for _, v := range expVals {
for _, av := range actVals {
if av == v {
found = true
break scan
}
}
}
if !found {
return fmt.Errorf("claim %q does not match any associated bound claim values", claim)
}
}
return nil
}