+
+
+
+`
+ return fmt.Sprintf(html, summary, detail)
+}
+
+// Help method for OIDC cli
+func (h *CLIHandler) Help() string {
+ help := `
+Usage: vault login -method=oidc [CONFIG K=V...]
+
+ The OIDC auth method allows users to authenticate using an OIDC provider.
+ The provider must be configured as part of a role by the operator.
+
+ Authenticate using role "engineering":
+
+ $ vault login -method=oidc role=engineering
+ Complete the login via your OIDC provider. Launching browser to:
+
+ https://accounts.google.com/o/oauth2/v2/...
+
+ The default browser will be opened for the user to complete the login. Alternatively,
+ the user may visit the provided URL directly.
+
+Configuration:
+
+ role=
+ Vault role of type "OIDC" to use for authentication.
+
+ port=
+ Optional localhost port to use for OIDC callback (default: 8300).
+`
+
+ return strings.TrimSpace(help)
+}
+
+const successHTML = `
+
+
+
+
+
+ Vault Authentication Succeeded
+
+
+
+
+
+
+
+
+
+
+ Signed in via your OIDC provider
+
+
+ You can now close this window and start using Vault.
+
+
+
+
+
Not sure how to get started?
+
+ Check out beginner and advanced guides on HashiCorp Vault at the HashiCorp Learn site or read more in the official documentation.
+
+
+
+`
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 5019cbd603de..3fc200df4cc2 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
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"crypto/x509"
"errors"
+ "fmt"
"net/http"
"context"
@@ -29,19 +30,44 @@ func pathConfig(b *jwtAuthBackend) *framework.Path {
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.",
},
+ "oidc_client_id": {
+ Type: framework.TypeString,
+ Description: "The OAuth Client ID configured with your OIDC provider.",
+ },
+ "oidc_client_secret": {
+ Type: framework.TypeString,
+ Description: "The OAuth Client Secret configured with your OIDC provider.",
+ DisplaySensitive: true,
+ },
+ "default_role": {
+ Type: framework.TypeString,
+ Description: "The default role to use if none is provided during login. If not set, a role is required during login.",
+ },
"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".`,
},
+ "jwt_supported_algs": {
+ Type: framework.TypeCommaStringSlice,
+ Description: `A list of supported signing algorithms. Defaults to RS256.`,
+ },
"bound_issuer": {
Type: framework.TypeString,
Description: "The value against which to match the 'iss' claim in a JWT. Optional.",
},
},
- Callbacks: map[logical.Operation]framework.OperationFunc{
- logical.ReadOperation: b.pathConfigRead,
- logical.UpdateOperation: b.pathConfigWrite,
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.ReadOperation: &framework.PathOperation{
+ Callback: b.pathConfigRead,
+ Summary: "Read the current JWT authentication backend configuration.",
+ },
+
+ logical.UpdateOperation: &framework.PathOperation{
+ Callback: b.pathConfigWrite,
+ Summary: "Configure the JWT authentication backend.",
+ Description: confHelpDesc,
+ },
},
HelpSynopsis: confHelpSyn,
@@ -98,7 +124,11 @@ func (b *jwtAuthBackend) pathConfigRead(ctx context.Context, req *logical.Reques
Data: map[string]interface{}{
"oidc_discovery_url": config.OIDCDiscoveryURL,
"oidc_discovery_ca_pem": config.OIDCDiscoveryCAPEM,
+ "oidc_client_id": config.OIDCClientID,
+ "oidc_client_secret": config.OIDCClientSecret,
+ "default_role": config.DefaultRole,
"jwt_validation_pubkeys": config.JWTValidationPubKeys,
+ "jwt_supported_algs": config.JWTSupportedAlgs,
"bound_issuer": config.BoundIssuer,
},
}
@@ -110,7 +140,11 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
config := &jwtConfig{
OIDCDiscoveryURL: d.Get("oidc_discovery_url").(string),
OIDCDiscoveryCAPEM: d.Get("oidc_discovery_ca_pem").(string),
+ OIDCClientID: d.Get("oidc_client_id").(string),
+ OIDCClientSecret: d.Get("oidc_client_secret").(string),
+ DefaultRole: d.Get("default_role").(string),
JWTValidationPubKeys: d.Get("jwt_validation_pubkeys").([]string),
+ JWTSupportedAlgs: d.Get("jwt_supported_algs").([]string),
BoundIssuer: d.Get("bound_issuer").(string),
}
@@ -120,12 +154,19 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
config.OIDCDiscoveryURL != "" && len(config.JWTValidationPubKeys) != 0:
return logical.ErrorResponse("exactly one of 'oidc_discovery_url' and 'jwt_validation_pubkeys' must be set"), nil
+ case config.OIDCClientID != "" && config.OIDCClientSecret == "",
+ config.OIDCClientID == "" && config.OIDCClientSecret != "":
+ return logical.ErrorResponse("both 'oidc_client_id' and 'oidc_client_secret' must be set for OIDC"), nil
+
case config.OIDCDiscoveryURL != "":
_, err := b.createProvider(config)
if err != nil {
return logical.ErrorResponse(errwrap.Wrapf("error checking discovery URL: {{err}}", err).Error()), nil
}
+ case config.OIDCClientID != "" && config.OIDCDiscoveryURL == "":
+ return logical.ErrorResponse("'oidc_discovery_url' must be set for OIDC"), nil
+
case len(config.JWTValidationPubKeys) != 0:
for _, v := range config.JWTValidationPubKeys {
if _, err := certutil.ParsePublicKeyPEM([]byte(v)); err != nil {
@@ -137,6 +178,14 @@ func (b *jwtAuthBackend) pathConfigWrite(ctx context.Context, req *logical.Reque
return nil, errors.New("unknown condition")
}
+ for _, a := range config.JWTSupportedAlgs {
+ switch a {
+ case oidc.RS256, oidc.RS384, oidc.RS512, oidc.ES256, oidc.ES384, oidc.ES512, oidc.PS256, oidc.PS384, oidc.PS512:
+ default:
+ return logical.ErrorResponse(fmt.Sprintf("Invalid supported algorithm: %s", a)), nil
+ }
+ }
+
entry, err := logical.StorageEntryJSON(configPath, config)
if err != nil {
return nil, err
@@ -181,8 +230,12 @@ func (b *jwtAuthBackend) createProvider(config *jwtConfig) (*oidc.Provider, erro
type jwtConfig struct {
OIDCDiscoveryURL string `json:"oidc_discovery_url"`
OIDCDiscoveryCAPEM string `json:"oidc_discovery_ca_pem"`
+ OIDCClientID string `json:"oidc_client_id"`
+ OIDCClientSecret string `json:"oidc_client_secret"`
JWTValidationPubKeys []string `json:"jwt_validation_pubkeys"`
+ JWTSupportedAlgs []string `json:"jwt_supported_algs"`
BoundIssuer string `json:"bound_issuer"`
+ DefaultRole string `json:"default_role"`
ParsedJWTPubKeys []interface{} `json:"-"`
}
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 868fe9d094e4..70d7e943ad30 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
@@ -29,9 +29,14 @@ func pathLogin(b *jwtAuthBackend) *framework.Path {
},
},
- Callbacks: map[logical.Operation]framework.OperationFunc{
- logical.UpdateOperation: b.pathLogin,
- logical.AliasLookaheadOperation: b.pathLogin,
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.UpdateOperation: &framework.PathOperation{
+ Callback: b.pathLogin,
+ Summary: pathLoginHelpSyn,
+ },
+ logical.AliasLookaheadOperation: &framework.PathOperation{
+ Callback: b.pathLogin,
+ },
},
HelpSynopsis: pathLoginHelpSyn,
@@ -40,13 +45,19 @@ func pathLogin(b *jwtAuthBackend) *framework.Path {
}
func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
- token := d.Get("jwt").(string)
- if len(token) == 0 {
- return logical.ErrorResponse("missing token"), nil
+ config, err := b.config(ctx, req.Storage)
+ if err != nil {
+ return nil, err
+ }
+ if config == nil {
+ return logical.ErrorResponse("could not load configuration"), nil
}
roleName := d.Get("role").(string)
- if len(roleName) == 0 {
+ if roleName == "" {
+ roleName = config.DefaultRole
+ }
+ if roleName == "" {
return logical.ErrorResponse("missing role"), nil
}
@@ -55,19 +66,16 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
return nil, err
}
if role == nil {
- return logical.ErrorResponse("role could not be found"), nil
+ return logical.ErrorResponse("role %q could not be found", roleName), nil
}
- if req.Connection != nil && !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, role.BoundCIDRs) {
- return logical.ErrorResponse("request originated from invalid CIDR"), nil
+ token := d.Get("jwt").(string)
+ if len(token) == 0 {
+ return logical.ErrorResponse("missing token"), nil
}
- config, err := b.config(ctx, req.Storage)
- if err != nil {
- return nil, err
- }
- if config == nil {
- return logical.ErrorResponse("could not load configuration"), nil
+ if req.Connection != nil && !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, role.BoundCIDRs) {
+ return logical.ErrorResponse("request originated from invalid CIDR"), nil
}
// Here is where things diverge. If it is using OIDC Discovery, validate
@@ -130,118 +138,37 @@ func (b *jwtAuthBackend) pathLogin(ctx context.Context, req *logical.Request, d
}
case config.OIDCDiscoveryURL != "":
- provider, err := b.getProvider(ctx, config)
+ allClaims, err = b.verifyToken(ctx, config, role, token)
if err != nil {
- return nil, errwrap.Wrapf("error getting provider for login operation: {{err}}", err)
- }
-
- verifier := provider.Verifier(&oidc.Config{
- SkipClientIDCheck: true,
- })
-
- idToken, err := verifier.Verify(ctx, token)
- if err != nil {
- return logical.ErrorResponse(errwrap.Wrapf("error validating signature: {{err}}", err).Error()), nil
- }
-
- if err := idToken.Claims(&allClaims); err != nil {
- return logical.ErrorResponse(errwrap.Wrapf("unable to successfully parse all claims from token: {{err}}", err).Error()), nil
- }
-
- if role.BoundSubject != "" && role.BoundSubject != idToken.Subject {
- return logical.ErrorResponse("sub claim does not match bound subject"), nil
- }
- if len(role.BoundAudiences) != 0 {
- var found bool
- for _, v := range role.BoundAudiences {
- if strutil.StrListContains(idToken.Audience, v) {
- found = true
- break
- }
- }
- if !found {
- return logical.ErrorResponse("aud claim does not match any bound audience"), nil
- }
+ return logical.ErrorResponse(err.Error()), nil
}
default:
return nil, errors.New("unhandled case during login")
}
- userClaimRaw, ok := allClaims[role.UserClaim]
- if !ok {
- return logical.ErrorResponse(fmt.Sprintf("%q claim not found in token", role.UserClaim)), nil
- }
- userName, ok := userClaimRaw.(string)
- if !ok {
- return logical.ErrorResponse(fmt.Sprintf("%q claim could not be converted to string", role.UserClaim)), nil
+ alias, groupAliases, err := b.createIdentity(allClaims, role)
+ if err != nil {
+ return logical.ErrorResponse(err.Error()), nil
}
- var groupAliases []*logical.Alias
- if 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
- }
- groups, ok := groupsClaimRaw.([]interface{})
- if !ok {
- return logical.ErrorResponse(fmt.Sprintf("%q claim could not be converted to string list", role.GroupsClaim)), nil
- }
- for _, groupRaw := range groups {
- group, ok := groupRaw.(string)
- if !ok {
- return logical.ErrorResponse(fmt.Sprintf("value %v in groups claim could not be parsed as string", groupRaw)), nil
- }
- if group == "" {
- continue
- }
- groupAliases = append(groupAliases, &logical.Alias{
- Name: group,
- })
- }
+ tokenMetadata := map[string]string{"role": roleName}
+ for k, v := range alias.Metadata {
+ tokenMetadata[k] = v
}
resp := &logical.Response{
Auth: &logical.Auth{
- Policies: role.Policies,
- DisplayName: userName,
- Period: role.Period,
- NumUses: role.NumUses,
- Alias: &logical.Alias{
- Name: userName,
- },
+ Policies: role.Policies,
+ DisplayName: alias.Name,
+ Period: role.Period,
+ NumUses: role.NumUses,
+ Alias: alias,
GroupAliases: groupAliases,
InternalData: map[string]interface{}{
"role": roleName,
},
- Metadata: map[string]string{
- "role": roleName,
- },
+ Metadata: tokenMetadata,
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: role.TTL,
@@ -276,6 +203,120 @@ func (b *jwtAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques
return resp, nil
}
+func (b *jwtAuthBackend) verifyToken(ctx context.Context, config *jwtConfig, role *jwtRole, rawToken string) (map[string]interface{}, error) {
+ allClaims := make(map[string]interface{})
+
+ provider, err := b.getProvider(ctx, config)
+ if err != nil {
+ return nil, errwrap.Wrapf("error getting provider for login operation: {{err}}", err)
+ }
+
+ oidcConfig := &oidc.Config{
+ SupportedSigningAlgs: config.JWTSupportedAlgs,
+ }
+
+ if role.RoleType == "oidc" {
+ oidcConfig.ClientID = config.OIDCClientID
+ } else {
+ oidcConfig.SkipClientIDCheck = true
+ }
+ verifier := provider.Verifier(oidcConfig)
+
+ idToken, err := verifier.Verify(ctx, rawToken)
+ if err != nil {
+ return nil, errwrap.Wrapf("error validating signature: {{err}}", err)
+ }
+
+ if err := idToken.Claims(&allClaims); err != nil {
+ return nil, errwrap.Wrapf("unable to successfully parse all claims from token: {{err}}", err)
+ }
+
+ if role.BoundSubject != "" && role.BoundSubject != idToken.Subject {
+ return nil, errors.New("sub claim does not match bound subject")
+ }
+ if len(role.BoundAudiences) > 0 {
+ var found bool
+ for _, v := range role.BoundAudiences {
+ if strutil.StrListContains(idToken.Audience, v) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, errors.New("aud claim does not match any bound audience")
+ }
+ }
+
+ if len(role.BoundClaims) > 0 {
+ for claim, expValue := range role.BoundClaims {
+ actValue := getClaim(b.Logger(), allClaims, claim)
+ if actValue == nil {
+ return nil, fmt.Errorf("claim is missing: %s", claim)
+ }
+
+ if expValue != actValue {
+ return nil, fmt.Errorf("claim '%s' does not match associated bound claim", claim)
+ }
+ }
+ }
+
+ return allClaims, nil
+}
+
+// createIdentity creates an alias and set of groups aliass based on the role
+// definition and received claims.
+func (b *jwtAuthBackend) createIdentity(allClaims map[string]interface{}, role *jwtRole) (*logical.Alias, []*logical.Alias, error) {
+ userClaimRaw, ok := allClaims[role.UserClaim]
+ if !ok {
+ return nil, nil, fmt.Errorf("claim %q not found in token", role.UserClaim)
+ }
+ userName, ok := userClaimRaw.(string)
+ if !ok {
+ return nil, nil, fmt.Errorf("claim %q could not be converted to string", role.UserClaim)
+ }
+
+ metadata, err := extractMetadata(b.Logger(), allClaims, role.ClaimMappings)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ alias := &logical.Alias{
+ Name: userName,
+ Metadata: metadata,
+ }
+
+ var groupAliases []*logical.Alias
+
+ if role.GroupsClaim == "" {
+ return alias, groupAliases, nil
+ }
+
+ groupsClaimRaw := getClaim(b.Logger(), allClaims, role.GroupsClaim)
+
+ if groupsClaimRaw == nil {
+ return nil, nil, fmt.Errorf("%q claim not found in token", role.GroupsClaim)
+ }
+ groups, ok := groupsClaimRaw.([]interface{})
+
+ if !ok {
+ return nil, nil, fmt.Errorf("%q claim could not be converted to string list", role.GroupsClaim)
+ }
+ for _, groupRaw := range groups {
+ group, ok := groupRaw.(string)
+ if !ok {
+ return nil, nil, fmt.Errorf("value %v in groups claim could not be parsed as string", groupRaw)
+ }
+ if group == "" {
+ continue
+ }
+ groupAliases = append(groupAliases, &logical.Alias{
+ Name: group,
+ })
+ }
+
+ return alias, groupAliases, nil
+}
+
const (
pathLoginHelpSyn = `
Authenticates to Vault using a JWT (or OIDC) token.
diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_oidc.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_oidc.go
new file mode 100644
index 000000000000..b437ed6e5474
--- /dev/null
+++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_oidc.go
@@ -0,0 +1,303 @@
+package jwtauth
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ oidc "github.com/coreos/go-oidc"
+ "github.com/hashicorp/errwrap"
+ uuid "github.com/hashicorp/go-uuid"
+ "github.com/hashicorp/vault/helper/strutil"
+ "github.com/hashicorp/vault/logical"
+ "github.com/hashicorp/vault/logical/framework"
+ "golang.org/x/oauth2"
+)
+
+var oidcStateTimeout = 10 * time.Minute
+
+// OIDC error prefixes. This are searched for specifically by the UI, so any
+// changes to them must be aligned with a UI change.
+const errLoginFailed = "Vault login failed."
+const errNoResponse = "No response from provider."
+const errTokenVerification = "Token verification failed."
+
+// oidcState is created when an authURL is requested. The state identifier is
+// passed throughout the OAuth process.
+type oidcState struct {
+ rolename string
+ nonce string
+ redirectURI string
+}
+
+func pathOIDC(b *jwtAuthBackend) []*framework.Path {
+ return []*framework.Path{
+ {
+ Pattern: `oidc/callback`,
+ Fields: map[string]*framework.FieldSchema{
+ "state": {
+ Type: framework.TypeString,
+ },
+ "code": {
+ Type: framework.TypeString,
+ },
+ },
+
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.ReadOperation: &framework.PathOperation{
+ Callback: b.pathCallback,
+ Summary: "Callback endpoint to complete an OIDC login.",
+ },
+ },
+ },
+ {
+ Pattern: `oidc/auth_url`,
+ Fields: map[string]*framework.FieldSchema{
+ "role": {
+ Type: framework.TypeLowerCaseString,
+ Description: "The role to issue an OIDC authorization URL against.",
+ },
+ "redirect_uri": {
+ Type: framework.TypeString,
+ Description: "The OAuth redirect_uri to use in the authorization URL.",
+ },
+ },
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.UpdateOperation: &framework.PathOperation{
+ Callback: b.authURL,
+ Summary: "Request an authorization URL to start an OIDC login flow.",
+ },
+ },
+ },
+ }
+}
+
+func (b *jwtAuthBackend) pathCallback(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ state := b.verifyState(d.Get("state").(string))
+ if state == nil {
+ return logical.ErrorResponse(errLoginFailed + " Expired or missing OAuth state."), nil
+ }
+
+ roleName := state.rolename
+ role, err := b.role(ctx, req.Storage, roleName)
+ if err != nil {
+ return nil, err
+ }
+ if role == nil {
+ return logical.ErrorResponse(errLoginFailed + " Role could not be found"), nil
+ }
+
+ config, err := b.config(ctx, req.Storage)
+ if err != nil {
+ return nil, err
+ }
+ if config == nil {
+ return logical.ErrorResponse(errLoginFailed + " Could not load configuration"), nil
+ }
+
+ provider, err := b.getProvider(ctx, config)
+ if err != nil {
+ return nil, errwrap.Wrapf(errLoginFailed+" Error getting provider for login operation: {{err}}", err)
+ }
+
+ var oauth2Config = oauth2.Config{
+ ClientID: config.OIDCClientID,
+ ClientSecret: config.OIDCClientSecret,
+ RedirectURL: state.redirectURI,
+ Endpoint: provider.Endpoint(),
+ Scopes: []string{oidc.ScopeOpenID},
+ }
+
+ code := d.Get("code").(string)
+ if code == "" {
+ return logical.ErrorResponse(errLoginFailed + " OAuth code parameter not provided"), nil
+ }
+
+ oauth2Token, err := oauth2Config.Exchange(ctx, code)
+ if err != nil {
+ return logical.ErrorResponse(errLoginFailed+" Error exchanging oidc code: %q.", err.Error()), nil
+ }
+
+ // Extract the ID Token from OAuth2 token.
+ rawToken, ok := oauth2Token.Extra("id_token").(string)
+ if !ok {
+ return logical.ErrorResponse(errTokenVerification + " No id_token found in response."), nil
+ }
+
+ // Parse and verify ID Token payload.
+ allClaims, err := b.verifyToken(ctx, config, role, rawToken)
+ if err != nil {
+ return logical.ErrorResponse("%s %s", errTokenVerification, err.Error()), nil
+ }
+
+ // Attempt to fetch information from the /userinfo endpoint and merge it with
+ // the existing claims data. A failure to fetch additional information from this
+ // endpoint will not invalidate the authorization flow.
+ if userinfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token)); err == nil {
+ _ = userinfo.Claims(&allClaims)
+ } else {
+ logFunc := b.Logger().Warn
+ if strings.Contains(err.Error(), "user info endpoint is not supported") {
+ logFunc = b.Logger().Info
+ }
+ logFunc("error reading /userinfo endpoint", "error", err)
+ }
+
+ if allClaims["nonce"] != state.nonce {
+ return logical.ErrorResponse(errTokenVerification + " Invalid ID token nonce."), nil
+ }
+ delete(allClaims, "nonce")
+
+ alias, groupAliases, err := b.createIdentity(allClaims, role)
+ if err != nil {
+ return logical.ErrorResponse(err.Error()), nil
+ }
+
+ tokenMetadata := map[string]string{"role": roleName}
+ for k, v := range alias.Metadata {
+ tokenMetadata[k] = v
+ }
+
+ resp := &logical.Response{
+ Auth: &logical.Auth{
+ Policies: role.Policies,
+ DisplayName: alias.Name,
+ Period: role.Period,
+ NumUses: role.NumUses,
+ Alias: alias,
+ GroupAliases: groupAliases,
+ InternalData: map[string]interface{}{
+ "role": roleName,
+ },
+ Metadata: tokenMetadata,
+ LeaseOptions: logical.LeaseOptions{
+ Renewable: true,
+ TTL: role.TTL,
+ MaxTTL: role.MaxTTL,
+ },
+ BoundCIDRs: role.BoundCIDRs,
+ },
+ }
+
+ return resp, nil
+}
+
+// authURL returns a URL used for redirection to receive an authorization code.
+// This path requires a role name, or that a default_role has been configured.
+// Because this endpoint is unauthenticated, the response to invalid or non-OIDC
+// roles is intentionally non-descriptive and will simply be an empty string.
+func (b *jwtAuthBackend) authURL(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ logger := b.Logger()
+
+ // default response for most error/invalid conditions
+ resp := &logical.Response{
+ Data: map[string]interface{}{
+ "auth_url": "",
+ },
+ }
+
+ config, err := b.config(ctx, req.Storage)
+ if err != nil {
+ logger.Warn("error loading configuration", "error", err)
+ return resp, nil
+ }
+
+ if config == nil {
+ logger.Warn("nil configuration")
+ return resp, nil
+ }
+
+ roleName := d.Get("role").(string)
+ if roleName == "" {
+ roleName = config.DefaultRole
+ if roleName == "" {
+ return logical.ErrorResponse("missing role"), nil
+ }
+ }
+
+ redirectURI := d.Get("redirect_uri").(string)
+ if redirectURI == "" {
+ return logical.ErrorResponse("missing redirect_uri"), nil
+ }
+
+ role, err := b.role(ctx, req.Storage, roleName)
+ if err != nil {
+ logger.Warn("error loading role", "error", err)
+ return resp, nil
+ }
+
+ if role == nil || role.RoleType != "oidc" {
+ logger.Warn("invalid role type", "role type", role)
+ return resp, nil
+ }
+
+ if !strutil.StrListContains(role.AllowedRedirectURIs, redirectURI) {
+ logger.Warn("unauthorized redirect_uri", "redirect_uri", redirectURI)
+ return resp, nil
+ }
+
+ provider, err := b.getProvider(ctx, config)
+ if err != nil {
+ logger.Warn("error getting provider for login operation", "error", err)
+ return resp, nil
+ }
+
+ // "openid" is a required scope for OpenID Connect flows
+ scopes := append([]string{oidc.ScopeOpenID}, role.OIDCScopes...)
+
+ // Configure an OpenID Connect aware OAuth2 client
+ oauth2Config := oauth2.Config{
+ ClientID: config.OIDCClientID,
+ ClientSecret: config.OIDCClientSecret,
+ RedirectURL: redirectURI,
+ Endpoint: provider.Endpoint(),
+ Scopes: scopes,
+ }
+
+ stateID, nonce, err := b.createState(roleName, redirectURI)
+ if err != nil {
+ logger.Warn("error generating OAuth state", "error", err)
+ return resp, nil
+ }
+
+ resp.Data["auth_url"] = oauth2Config.AuthCodeURL(stateID, oidc.Nonce(nonce))
+
+ return resp, nil
+}
+
+// createState make an expiring state object, associated with a random state ID
+// that is passed throughout the OAuth process. A nonce is also included in the
+// auth process, and for simplicity will be identical in length/format as the state ID.
+func (b *jwtAuthBackend) createState(rolename, redirectURI string) (string, string, error) {
+ // Get enough bytes for 2 160-bit IDs (per rfc6749#section-10.10)
+ bytes, err := uuid.GenerateRandomBytes(2 * 20)
+ if err != nil {
+ return "", "", err
+ }
+
+ stateID := fmt.Sprintf("%x", bytes[:20])
+ nonce := fmt.Sprintf("%x", bytes[20:])
+
+ b.oidcStates.SetDefault(stateID, &oidcState{
+ rolename: rolename,
+ nonce: nonce,
+ redirectURI: redirectURI,
+ })
+
+ return stateID, nonce, nil
+}
+
+// verifyState tests whether the provided state ID is valid and returns the
+// associated state object if so. A nil state is returned if the ID is not found
+// or expired. The state should only ever be retrieved once and is deleted as
+// part of this request.
+func (b *jwtAuthBackend) verifyState(stateID string) *oidcState {
+ defer b.oidcStates.Delete(stateID)
+
+ if stateRaw, ok := b.oidcStates.Get(stateID); ok {
+ return stateRaw.(*oidcState)
+ }
+
+ return 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 9ca1fec415f8..6eb701601c4e 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,19 +7,25 @@ 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"
+ "github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
+var reservedMetadata = []string{"role"}
+
func pathRoleList(b *jwtAuthBackend) *framework.Path {
return &framework.Path{
Pattern: "role/?",
- Callbacks: map[logical.Operation]framework.OperationFunc{
- logical.ListOperation: b.pathRoleList,
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.ListOperation: &framework.PathOperation{
+ Callback: b.pathRoleList,
+ Summary: strings.TrimSpace(roleHelp["role-list"][0]),
+ Description: strings.TrimSpace(roleHelp["role-list"][1]),
+ },
},
HelpSynopsis: strings.TrimSpace(roleHelp["role-list"][0]),
HelpDescription: strings.TrimSpace(roleHelp["role-list"][1]),
@@ -35,6 +41,10 @@ func pathRole(b *jwtAuthBackend) *framework.Path {
Type: framework.TypeLowerCaseString,
Description: "Name of the role.",
},
+ "role_type": {
+ Type: framework.TypeString,
+ Description: "Type of the role, either 'jwt' or 'oidc'.",
+ },
"policies": {
Type: framework.TypeCommaStringSlice,
Description: "List of policies on the role.",
@@ -68,6 +78,14 @@ TTL will be set to the value of this parameter.`,
Type: framework.TypeCommaStringSlice,
Description: `Comma-separated list of 'aud' claims that are valid for login; any match is sufficient`,
},
+ "bound_claims": {
+ Type: framework.TypeMap,
+ Description: `Map of claims/values which must match for login`,
+ },
+ "claim_mappings": {
+ Type: framework.TypeKVPairs,
+ Description: `Mappings of claims (key) that will be copied to a metadata field (value)`,
+ },
"user_claim": {
Type: framework.TypeString,
Description: `The claim to use for the Identity entity alias name`,
@@ -76,22 +94,43 @@ TTL will be set to the value of this parameter.`,
Type: framework.TypeString,
Description: `The claim to use for the Identity group alias names`,
},
- "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`,
},
+ "oidc_scopes": {
+ Type: framework.TypeCommaStringSlice,
+ Description: `Comma-separated list of OIDC scopes`,
+ },
+ "allowed_redirect_uris": {
+ Type: framework.TypeCommaStringSlice,
+ Description: `Comma-separated list of allowed values for redirect_uri`,
+ },
},
ExistenceCheck: b.pathRoleExistenceCheck,
- Callbacks: map[logical.Operation]framework.OperationFunc{
- logical.CreateOperation: b.pathRoleCreateUpdate,
- logical.UpdateOperation: b.pathRoleCreateUpdate,
- logical.ReadOperation: b.pathRoleRead,
- logical.DeleteOperation: b.pathRoleDelete,
+ Operations: map[logical.Operation]framework.OperationHandler{
+ logical.ReadOperation: &framework.PathOperation{
+ Callback: b.pathRoleRead,
+ Summary: "Read an existing role.",
+ },
+
+ logical.UpdateOperation: &framework.PathOperation{
+ Callback: b.pathRoleCreateUpdate,
+ Summary: strings.TrimSpace(roleHelp["role"][0]),
+ Description: strings.TrimSpace(roleHelp["role"][1]),
+ },
+
+ logical.CreateOperation: &framework.PathOperation{
+ Callback: b.pathRoleCreateUpdate,
+ Summary: strings.TrimSpace(roleHelp["role"][0]),
+ Description: strings.TrimSpace(roleHelp["role"][1]),
+ },
+
+ logical.DeleteOperation: &framework.PathOperation{
+ Callback: b.pathRoleDelete,
+ Summary: "Delete an existing role.",
+ },
},
HelpSynopsis: strings.TrimSpace(roleHelp["role"][0]),
HelpDescription: strings.TrimSpace(roleHelp["role"][1]),
@@ -99,6 +138,8 @@ authenticate against this role`,
}
type jwtRole struct {
+ RoleType string `json:"role_type"`
+
// Policies that are to be required by the token to access this role
Policies []string `json:"policies"`
@@ -119,12 +160,15 @@ 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"`
- GroupsClaimDelimiterPattern string `json:"groups_claim_delimiter_pattern"`
+ BoundAudiences []string `json:"bound_audiences"`
+ BoundSubject string `json:"bound_subject"`
+ BoundClaims map[string]interface{} `json:"bound_claims"`
+ ClaimMappings map[string]string `json:"claim_mappings"`
+ BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"`
+ UserClaim string `json:"user_claim"`
+ GroupsClaim string `json:"groups_claim"`
+ OIDCScopes []string `json:"oidc_scopes"`
+ AllowedRedirectURIs []string `json:"allowed_redirect_uris"`
}
// role takes a storage backend and the name and returns the role's storage
@@ -182,17 +226,20 @@ 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,
- "groups_claim_delimiter_pattern": role.GroupsClaimDelimiterPattern,
+ "role_type": role.RoleType,
+ "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,
+ "bound_claims": role.BoundClaims,
+ "claim_mappings": role.ClaimMappings,
+ "user_claim": role.UserClaim,
+ "groups_claim": role.GroupsClaim,
+ "allowed_redirect_uris": role.AllowedRedirectURIs,
},
}
@@ -236,6 +283,15 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role = new(jwtRole)
}
+ roleType := data.Get("role_type").(string)
+ if roleType == "" {
+ roleType = "jwt"
+ }
+ if roleType != "jwt" && roleType != "oidc" {
+ return logical.ErrorResponse("invalid 'role_type': %s", roleType), nil
+ }
+ role.RoleType = roleType
+
if policiesRaw, ok := data.GetOk("policies"); ok {
role.Policies = policyutil.ParsePolicies(policiesRaw)
}
@@ -287,6 +343,29 @@ func (b *jwtAuthBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.
role.BoundCIDRs = parsedCIDRs
}
+ if boundClaimsRaw, ok := data.GetOk("bound_claims"); ok {
+ role.BoundClaims = boundClaimsRaw.(map[string]interface{})
+ }
+
+ if claimMappingsRaw, ok := data.GetOk("claim_mappings"); ok {
+ claimMappings := claimMappingsRaw.(map[string]string)
+
+ // sanity check mappings for duplicates and collision with reserved names
+ targets := make(map[string]bool)
+ for _, metadataKey := range claimMappings {
+ if strutil.StrListContains(reservedMetadata, metadataKey) {
+ return logical.ErrorResponse("metadata key '%s' is reserved and may not be a mapping destination", metadataKey), nil
+ }
+
+ if targets[metadataKey] {
+ return logical.ErrorResponse("multiple keys are mapped to metadata key '%s'", metadataKey), nil
+ }
+ targets[metadataKey] = true
+ }
+
+ role.ClaimMappings = claimMappings
+ }
+
if userClaim, ok := data.GetOk("user_claim"); ok {
role.UserClaim = userClaim.(string)
}
@@ -298,21 +377,24 @@ 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)
+ if oidcScopes, ok := data.GetOk("oidc_scopes"); ok {
+ role.OIDCScopes = oidcScopes.([]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
- }
+ allowedRedirectURIs := data.Get("allowed_redirect_uris").([]string)
+ if roleType == "oidc" && len(allowedRedirectURIs) == 0 {
+ return logical.ErrorResponse("'allowed_redirect_uris' must be set"), nil
}
+ role.AllowedRedirectURIs = allowedRedirectURIs
- if len(role.BoundAudiences) == 0 &&
- len(role.BoundCIDRs) == 0 &&
- role.BoundSubject == "" {
- return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil
+ // OIDC verifcation will enforce that the audience match the configured client_id.
+ // For other methods, require at least one bound constraint.
+ if roleType != "oidc" {
+ if len(role.BoundAudiences) == 0 &&
+ len(role.BoundCIDRs) == 0 &&
+ role.BoundSubject == "" {
+ return logical.ErrorResponse("must have at least one bound constraint when creating/updating a role"), nil
+ }
}
// Check that the TTL value provided is less than the MaxTTL.
@@ -340,32 +422,6 @@ 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/github.com/hashicorp/vault-plugin-auth-jwt/path_ui.go b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_ui.go
new file mode 100644
index 000000000000..7930f9ef8be5
--- /dev/null
+++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/path_ui.go
@@ -0,0 +1,37 @@
+// A throwaway file for super simple testing via a UI
+package jwtauth
+
+import (
+ "context"
+ "io/ioutil"
+
+ "github.com/hashicorp/vault/logical"
+ "github.com/hashicorp/vault/logical/framework"
+)
+
+func pathUI(b *jwtAuthBackend) *framework.Path {
+ return &framework.Path{
+ Pattern: `ui$`,
+
+ Callbacks: map[logical.Operation]framework.OperationFunc{
+ logical.ReadOperation: b.pathUI,
+ },
+ }
+}
+
+func (b *jwtAuthBackend) pathUI(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
+ data, err := ioutil.ReadFile("test_ui.html")
+ if err != nil {
+ panic(err)
+ }
+
+ resp := &logical.Response{
+ Data: map[string]interface{}{
+ logical.HTTPStatusCode: 200,
+ logical.HTTPRawBody: string(data),
+ logical.HTTPContentType: "text/html",
+ },
+ }
+
+ return resp, nil
+}
diff --git a/vendor/github.com/hashicorp/vault-plugin-auth-jwt/test_ui.html b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/test_ui.html
new file mode 100644
index 000000000000..dd6502ed73f1
--- /dev/null
+++ b/vendor/github.com/hashicorp/vault-plugin-auth-jwt/test_ui.html
@@ -0,0 +1,37 @@
+
+
+
+Role:
+
+
+
+
+
+
+
diff --git a/vendor/github.com/mitchellh/pointerstructure/README.md b/vendor/github.com/mitchellh/pointerstructure/README.md
new file mode 100644
index 000000000000..13e3358557f3
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/README.md
@@ -0,0 +1,74 @@
+# pointerstructure [![GoDoc](https://godoc.org/github.com/mitchellh/pointerstructure?status.svg)](https://godoc.org/github.com/mitchellh/pointerstructure)
+
+pointerstructure is a Go library for identifying a specific value within
+any Go structure using a string syntax.
+
+pointerstructure is based on
+[JSON Pointer (RFC 6901)](https://tools.ietf.org/html/rfc6901), but
+reimplemented for Go.
+
+The goal of pointerstructure is to provide a single, well-known format
+for addressing a specific value. This can be useful for user provided
+input on structures, diffs of structures, etc.
+
+## Features
+
+ * Get the value for an address
+
+ * Set the value for an address within an existing structure
+
+ * Delete the value at an address
+
+ * Sorting a list of addresses
+
+## Installation
+
+Standard `go get`:
+
+```
+$ go get github.com/mitchellh/pointerstructure
+```
+
+## Usage & Example
+
+For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/pointerstructure).
+
+A quick code example is shown below:
+
+```go
+complex := map[string]interface{}{
+ "alice": 42,
+ "bob": []interface{}{
+ map[string]interface{}{
+ "name": "Bob",
+ },
+ },
+}
+
+value, err := pointerstructure.Get(complex, "/bob/0/name")
+if err != nil {
+ panic(err)
+}
+
+fmt.Printf("%s", value)
+// Output:
+// Bob
+```
+
+Continuing the example above, you can also set values:
+
+```go
+value, err = pointerstructure.Set(complex, "/bob/0/name", "Alice")
+if err != nil {
+ panic(err)
+}
+
+value, err = pointerstructure.Get(complex, "/bob/0/name")
+if err != nil {
+ panic(err)
+}
+
+fmt.Printf("%s", value)
+// Output:
+// Alice
+```
diff --git a/vendor/github.com/mitchellh/pointerstructure/delete.go b/vendor/github.com/mitchellh/pointerstructure/delete.go
new file mode 100644
index 000000000000..5ed6b4bffc39
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/delete.go
@@ -0,0 +1,112 @@
+package pointerstructure
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Delete deletes the value specified by the pointer p in structure s.
+//
+// When deleting a slice index, all other elements will be shifted to
+// the left. This is specified in RFC6902 (JSON Patch) and not RFC6901 since
+// RFC6901 doesn't specify operations on pointers. If you don't want to
+// shift elements, you should use Set to set the slice index to the zero value.
+//
+// The structures s must have non-zero values set up to this pointer.
+// For example, if deleting "/bob/0/name", then "/bob/0" must be set already.
+//
+// The returned value is potentially a new value if this pointer represents
+// the root document. Otherwise, the returned value will always be s.
+func (p *Pointer) Delete(s interface{}) (interface{}, error) {
+ // if we represent the root doc, we've deleted everything
+ if len(p.Parts) == 0 {
+ return nil, nil
+ }
+
+ // Save the original since this is going to be our return value
+ originalS := s
+
+ // Get the parent value
+ var err error
+ s, err = p.Parent().Get(s)
+ if err != nil {
+ return nil, err
+ }
+
+ // Map for lookup of getter to call for type
+ funcMap := map[reflect.Kind]deleteFunc{
+ reflect.Array: p.deleteSlice,
+ reflect.Map: p.deleteMap,
+ reflect.Slice: p.deleteSlice,
+ }
+
+ val := reflect.ValueOf(s)
+ for val.Kind() == reflect.Interface {
+ val = val.Elem()
+ }
+
+ for val.Kind() == reflect.Ptr {
+ val = reflect.Indirect(val)
+ }
+
+ f, ok := funcMap[val.Kind()]
+ if !ok {
+ return nil, fmt.Errorf("delete %s: invalid value kind: %s", p, val.Kind())
+ }
+
+ result, err := f(originalS, val)
+ if err != nil {
+ return nil, fmt.Errorf("delete %s: %s", p, err)
+ }
+
+ return result, nil
+}
+
+type deleteFunc func(interface{}, reflect.Value) (interface{}, error)
+
+func (p *Pointer) deleteMap(root interface{}, m reflect.Value) (interface{}, error) {
+ part := p.Parts[len(p.Parts)-1]
+ key, err := coerce(reflect.ValueOf(part), m.Type().Key())
+ if err != nil {
+ return root, err
+ }
+
+ // Delete the key
+ var elem reflect.Value
+ m.SetMapIndex(key, elem)
+ return root, nil
+}
+
+func (p *Pointer) deleteSlice(root interface{}, s reflect.Value) (interface{}, error) {
+ // Coerce the key to an int
+ part := p.Parts[len(p.Parts)-1]
+ idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42))
+ if err != nil {
+ return root, err
+ }
+ idx := int(idxVal.Int())
+
+ // Verify we're within bounds
+ if idx < 0 || idx >= s.Len() {
+ return root, fmt.Errorf(
+ "index %d is out of range (length = %d)", idx, s.Len())
+ }
+
+ // Mimicing the following with reflection to do this:
+ //
+ // copy(a[i:], a[i+1:])
+ // a[len(a)-1] = nil // or the zero value of T
+ // a = a[:len(a)-1]
+
+ // copy(a[i:], a[i+1:])
+ reflect.Copy(s.Slice(idx, s.Len()), s.Slice(idx+1, s.Len()))
+
+ // a[len(a)-1] = nil // or the zero value of T
+ s.Index(s.Len() - 1).Set(reflect.Zero(s.Type().Elem()))
+
+ // a = a[:len(a)-1]
+ s = s.Slice(0, s.Len()-1)
+
+ // set the slice back on the parent
+ return p.Parent().Set(root, s.Interface())
+}
diff --git a/vendor/github.com/mitchellh/pointerstructure/get.go b/vendor/github.com/mitchellh/pointerstructure/get.go
new file mode 100644
index 000000000000..15137c1570ef
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/get.go
@@ -0,0 +1,91 @@
+package pointerstructure
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Get reads the value out of the total value v.
+func (p *Pointer) Get(v interface{}) (interface{}, error) {
+ // fast-path the empty address case to avoid reflect.ValueOf below
+ if len(p.Parts) == 0 {
+ return v, nil
+ }
+
+ // Map for lookup of getter to call for type
+ funcMap := map[reflect.Kind]func(string, reflect.Value) (reflect.Value, error){
+ reflect.Array: p.getSlice,
+ reflect.Map: p.getMap,
+ reflect.Slice: p.getSlice,
+ }
+
+ currentVal := reflect.ValueOf(v)
+ for i, part := range p.Parts {
+ for currentVal.Kind() == reflect.Interface {
+ currentVal = currentVal.Elem()
+ }
+
+ for currentVal.Kind() == reflect.Ptr {
+ currentVal = reflect.Indirect(currentVal)
+ }
+
+ f, ok := funcMap[currentVal.Kind()]
+ if !ok {
+ return nil, fmt.Errorf(
+ "%s: at part %d, invalid value kind: %s", p, i, currentVal.Kind())
+ }
+
+ var err error
+ currentVal, err = f(part, currentVal)
+ if err != nil {
+ return nil, fmt.Errorf("%s at part %d: %s", p, i, err)
+ }
+ }
+
+ return currentVal.Interface(), nil
+}
+
+func (p *Pointer) getMap(part string, m reflect.Value) (reflect.Value, error) {
+ var zeroValue reflect.Value
+
+ // Coerce the string part to the correct key type
+ key, err := coerce(reflect.ValueOf(part), m.Type().Key())
+ if err != nil {
+ return zeroValue, err
+ }
+
+ // Verify that the key exists
+ found := false
+ for _, k := range m.MapKeys() {
+ if k.Interface() == key.Interface() {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return zeroValue, fmt.Errorf("couldn't find key %#v", key.Interface())
+ }
+
+ // Get the key
+ return m.MapIndex(key), nil
+}
+
+func (p *Pointer) getSlice(part string, v reflect.Value) (reflect.Value, error) {
+ var zeroValue reflect.Value
+
+ // Coerce the key to an int
+ idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42))
+ if err != nil {
+ return zeroValue, err
+ }
+ idx := int(idxVal.Int())
+
+ // Verify we're within bounds
+ if idx < 0 || idx >= v.Len() {
+ return zeroValue, fmt.Errorf(
+ "index %d is out of range (length = %d)", idx, v.Len())
+ }
+
+ // Get the key
+ return v.Index(idx), nil
+}
diff --git a/vendor/github.com/mitchellh/pointerstructure/parse.go b/vendor/github.com/mitchellh/pointerstructure/parse.go
new file mode 100644
index 000000000000..c34e8d465fd7
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/parse.go
@@ -0,0 +1,57 @@
+package pointerstructure
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Parse parses a pointer from the input string. The input string
+// is expected to follow the format specified by RFC 6901: '/'-separated
+// parts. Each part can contain escape codes to contain '/' or '~'.
+func Parse(input string) (*Pointer, error) {
+ // Special case the empty case
+ if input == "" {
+ return &Pointer{}, nil
+ }
+
+ // We expect the first character to be "/"
+ if input[0] != '/' {
+ return nil, fmt.Errorf(
+ "parse Go pointer %q: first char must be '/'", input)
+ }
+
+ // Trim out the first slash so we don't have to +1 every index
+ input = input[1:]
+
+ // Parse out all the parts
+ var parts []string
+ lastSlash := -1
+ for i, r := range input {
+ if r == '/' {
+ parts = append(parts, input[lastSlash+1:i])
+ lastSlash = i
+ }
+ }
+
+ // Add last part
+ parts = append(parts, input[lastSlash+1:])
+
+ // Process each part for string replacement
+ for i, p := range parts {
+ // Replace ~1 followed by ~0 as specified by the RFC
+ parts[i] = strings.Replace(
+ strings.Replace(p, "~1", "/", -1), "~0", "~", -1)
+ }
+
+ return &Pointer{Parts: parts}, nil
+}
+
+// MustParse is like Parse but panics if the input cannot be parsed.
+func MustParse(input string) *Pointer {
+ p, err := Parse(input)
+ if err != nil {
+ panic(err)
+ }
+
+ return p
+}
diff --git a/vendor/github.com/mitchellh/pointerstructure/pointer.go b/vendor/github.com/mitchellh/pointerstructure/pointer.go
new file mode 100644
index 000000000000..3a8f88b918fb
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/pointer.go
@@ -0,0 +1,123 @@
+// Package pointerstructure provides functions for identifying a specific
+// value within any Go structure using a string syntax.
+//
+// The syntax used is based on JSON Pointer (RFC 6901).
+package pointerstructure
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+)
+
+// Pointer represents a pointer to a specific value. You can construct
+// a pointer manually or use Parse.
+type Pointer struct {
+ // Parts are the pointer parts. No escape codes are processed here.
+ // The values are expected to be exact. If you have escape codes, use
+ // the Parse functions.
+ Parts []string
+}
+
+// Get reads the value at the given pointer.
+//
+// This is a shorthand for calling Parse on the pointer and then calling Get
+// on that result. An error will be returned if the value cannot be found or
+// there is an error with the format of pointer.
+func Get(value interface{}, pointer string) (interface{}, error) {
+ p, err := Parse(pointer)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.Get(value)
+}
+
+// Set sets the value at the given pointer.
+//
+// This is a shorthand for calling Parse on the pointer and then calling Set
+// on that result. An error will be returned if the value cannot be found or
+// there is an error with the format of pointer.
+//
+// Set returns the complete document, which might change if the pointer value
+// points to the root ("").
+func Set(doc interface{}, pointer string, value interface{}) (interface{}, error) {
+ p, err := Parse(pointer)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.Set(doc, value)
+}
+
+// String returns the string value that can be sent back to Parse to get
+// the same Pointer result.
+func (p *Pointer) String() string {
+ if len(p.Parts) == 0 {
+ return ""
+ }
+
+ // Copy the parts so we can convert back the escapes
+ result := make([]string, len(p.Parts))
+ copy(result, p.Parts)
+ for i, p := range p.Parts {
+ result[i] = strings.Replace(
+ strings.Replace(p, "~", "~0", -1), "/", "~1", -1)
+
+ }
+
+ return "/" + strings.Join(result, "/")
+}
+
+// Parent returns a pointer to the parent element of this pointer.
+//
+// If Pointer represents the root (empty parts), a pointer representing
+// the root is returned. Therefore, to check for the root, IsRoot() should be
+// called.
+func (p *Pointer) Parent() *Pointer {
+ // If this is root, then we just return a new root pointer. We allocate
+ // a new one though so this can still be modified.
+ if p.IsRoot() {
+ return &Pointer{}
+ }
+
+ parts := make([]string, len(p.Parts)-1)
+ copy(parts, p.Parts[:len(p.Parts)-1])
+ return &Pointer{
+ Parts: parts,
+ }
+}
+
+// IsRoot returns true if this pointer represents the root document.
+func (p *Pointer) IsRoot() bool {
+ return len(p.Parts) == 0
+}
+
+// coerce is a helper to coerce a value to a specific type if it must
+// and if its possible. If it isn't possible, an error is returned.
+func coerce(value reflect.Value, to reflect.Type) (reflect.Value, error) {
+ // If the value is already assignable to the type, then let it go
+ if value.Type().AssignableTo(to) {
+ return value, nil
+ }
+
+ // If a direct conversion is possible, do that
+ if value.Type().ConvertibleTo(to) {
+ return value.Convert(to), nil
+ }
+
+ // Create a new value to hold our result
+ result := reflect.New(to)
+
+ // Decode
+ if err := mapstructure.WeakDecode(value.Interface(), result.Interface()); err != nil {
+ return result, fmt.Errorf(
+ "couldn't convert value %#v to type %s",
+ value.Interface(), to.String())
+ }
+
+ // We need to indirect the value since reflect.New always creates a pointer
+ return reflect.Indirect(result), nil
+}
diff --git a/vendor/github.com/mitchellh/pointerstructure/set.go b/vendor/github.com/mitchellh/pointerstructure/set.go
new file mode 100644
index 000000000000..a396ac62f295
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/set.go
@@ -0,0 +1,122 @@
+package pointerstructure
+
+import (
+ "fmt"
+ "reflect"
+)
+
+// Set writes a value v to the pointer p in structure s.
+//
+// The structures s must have non-zero values set up to this pointer.
+// For example, if setting "/bob/0/name", then "/bob/0" must be set already.
+//
+// The returned value is potentially a new value if this pointer represents
+// the root document. Otherwise, the returned value will always be s.
+func (p *Pointer) Set(s, v interface{}) (interface{}, error) {
+ // if we represent the root doc, return that
+ if len(p.Parts) == 0 {
+ return v, nil
+ }
+
+ // Save the original since this is going to be our return value
+ originalS := s
+
+ // Get the parent value
+ var err error
+ s, err = p.Parent().Get(s)
+ if err != nil {
+ return nil, err
+ }
+
+ // Map for lookup of getter to call for type
+ funcMap := map[reflect.Kind]setFunc{
+ reflect.Array: p.setSlice,
+ reflect.Map: p.setMap,
+ reflect.Slice: p.setSlice,
+ }
+
+ val := reflect.ValueOf(s)
+ for val.Kind() == reflect.Interface {
+ val = val.Elem()
+ }
+
+ for val.Kind() == reflect.Ptr {
+ val = reflect.Indirect(val)
+ }
+
+ f, ok := funcMap[val.Kind()]
+ if !ok {
+ return nil, fmt.Errorf("set %s: invalid value kind: %s", p, val.Kind())
+ }
+
+ result, err := f(originalS, val, reflect.ValueOf(v))
+ if err != nil {
+ return nil, fmt.Errorf("set %s: %s", p, err)
+ }
+
+ return result, nil
+}
+
+type setFunc func(interface{}, reflect.Value, reflect.Value) (interface{}, error)
+
+func (p *Pointer) setMap(root interface{}, m, value reflect.Value) (interface{}, error) {
+ part := p.Parts[len(p.Parts)-1]
+ key, err := coerce(reflect.ValueOf(part), m.Type().Key())
+ if err != nil {
+ return root, err
+ }
+
+ elem, err := coerce(value, m.Type().Elem())
+ if err != nil {
+ return root, err
+ }
+
+ // Set the key
+ m.SetMapIndex(key, elem)
+ return root, nil
+}
+
+func (p *Pointer) setSlice(root interface{}, s, value reflect.Value) (interface{}, error) {
+ // Coerce the value, we'll need that no matter what
+ value, err := coerce(value, s.Type().Elem())
+ if err != nil {
+ return root, err
+ }
+
+ // If the part is the special "-", that means to append it (RFC6901 4.)
+ part := p.Parts[len(p.Parts)-1]
+ if part == "-" {
+ return p.setSliceAppend(root, s, value)
+ }
+
+ // Coerce the key to an int
+ idxVal, err := coerce(reflect.ValueOf(part), reflect.TypeOf(42))
+ if err != nil {
+ return root, err
+ }
+ idx := int(idxVal.Int())
+
+ // Verify we're within bounds
+ if idx < 0 || idx >= s.Len() {
+ return root, fmt.Errorf(
+ "index %d is out of range (length = %d)", idx, s.Len())
+ }
+
+ // Set the key
+ s.Index(idx).Set(value)
+ return root, nil
+}
+
+func (p *Pointer) setSliceAppend(root interface{}, s, value reflect.Value) (interface{}, error) {
+ // Coerce the value, we'll need that no matter what. This should
+ // be a no-op since we expect it to be done already, but there is
+ // a fast-path check for that in coerce so do it anyways.
+ value, err := coerce(value, s.Type().Elem())
+ if err != nil {
+ return root, err
+ }
+
+ // We can assume "s" is the parent of pointer value. We need to actually
+ // write s back because Append can return a new slice.
+ return p.Parent().Set(root, reflect.Append(s, value).Interface())
+}
diff --git a/vendor/github.com/mitchellh/pointerstructure/sort.go b/vendor/github.com/mitchellh/pointerstructure/sort.go
new file mode 100644
index 000000000000..886d1183c69a
--- /dev/null
+++ b/vendor/github.com/mitchellh/pointerstructure/sort.go
@@ -0,0 +1,42 @@
+package pointerstructure
+
+import (
+ "sort"
+)
+
+// Sort does an in-place sort of the pointers so that they are in order
+// of least specific to most specific alphabetized. For example:
+// "/foo", "/foo/0", "/qux"
+//
+// This ordering is ideal for applying the changes in a way that ensures
+// that parents are set first.
+func Sort(p []*Pointer) { sort.Sort(PointerSlice(p)) }
+
+// PointerSlice is a slice of pointers that adheres to sort.Interface
+type PointerSlice []*Pointer
+
+func (p PointerSlice) Len() int { return len(p) }
+func (p PointerSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
+func (p PointerSlice) Less(i, j int) bool {
+ // Equal number of parts, do a string compare per part
+ for idx, ival := range p[i].Parts {
+ // If we're passed the length of p[j] parts, then we're done
+ if idx >= len(p[j].Parts) {
+ break
+ }
+
+ // Compare the values if they're not equal
+ jval := p[j].Parts[idx]
+ if ival != jval {
+ return ival < jval
+ }
+ }
+
+ // Equal prefix, take the shorter
+ if len(p[i].Parts) != len(p[j].Parts) {
+ return len(p[i].Parts) < len(p[j].Parts)
+ }
+
+ // Equal, it doesn't matter
+ return false
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index f6ddcf764d2d..316b1bd97069 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -1409,10 +1409,12 @@
"revisionTime": "2018-12-10T20:01:33Z"
},
{
- "checksumSHA1": "tt3FtyjXgdBI9Mb43UL4LtOZmAk=",
+ "checksumSHA1": "86jzaGc3dRpZ5BKQPFP7ecasQfg=",
"path": "github.com/hashicorp/vault-plugin-auth-jwt",
- "revision": "f428c77917331c1b87dae2dd37016bd1dd4c55da",
- "revisionTime": "2018-10-31T19:59:42Z"
+ "revision": "bf17a88bb5c43eb2cbdc08011cd76ecec028521c",
+ "revisionTime": "2019-02-07T06:35:46Z",
+ "version": "=oidc-cli",
+ "versionExact": "oidc-cli"
},
{
"checksumSHA1": "Ldg2jQeyPrpAupyQq4lRVN+jfFY=",
@@ -1788,6 +1790,12 @@
"revision": "3536a929edddb9a5b34bd6861dc4a9647cb459fe",
"revisionTime": "2018-10-05T04:51:35Z"
},
+ {
+ "checksumSHA1": "31atAEqGt+z8hZgyVZZokEeM6dM=",
+ "path": "github.com/mitchellh/pointerstructure",
+ "revision": "f2329fcfa9e280bdb5a3f2544aec815a508ad72f",
+ "revisionTime": "2017-02-05T20:42:03Z"
+ },
{
"checksumSHA1": "nxuST3bjBv5uDVPzrX9wdruOwv0=",
"path": "github.com/mitchellh/reflectwalk",