vault/sdk/logical/token.go
Vault Automation f5dbe55f55
VAULT-42657: Merge feature branch for OAuth and Agent Registry into main (#12587) (#12754)
* feat(identity): accept oauth tokens

type: draft

* feat(identity): make basic token lookup op work

type: draft

* fix(identity): failing tests

type: draft

* feat(identity): add new tests + handle renew & revoke

type: draft

* feat(identity): clean up and tests

type: draft

* feat(identity)(Accept OAuth JWT tokens): Add more tests

* feat(identity)(Accept OAuth JWT tokens): Test updates

* feat(identity)(Accept OAuth JWT tokens): Revert go version changes

* feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests

* feat(identity)(Accept OAuth JWT tokens): fix tests

* feat(identity)(Accept OAuth JWT tokens): fix more CI issues

* [POC] Accept and Validate External OAuth Token for Agent Identity (#10991)

* feat(identity): accept oauth tokens

type: draft

* feat(identity): make basic token lookup op work

type: draft

* fix(identity): failing tests

type: draft

* feat(identity): add new tests + handle renew & revoke

type: draft

* feat(identity): clean up and tests

type: draft

* feat(identity)(Accept OAuth JWT tokens): Add more tests

* feat(identity)(Accept OAuth JWT tokens): Test updates

* feat(identity)(Accept OAuth JWT tokens): Revert go version changes

* feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests

* feat(identity)(Accept OAuth JWT tokens): fix tests

* feat(identity)(Accept OAuth JWT tokens): fix more CI issues

* Add JWT token tidy cleanup functionality

* Refactor JWT tidy tests to use retryUntil instead of time.Sleep

* feat(identity)(Accept OAuth JWT tokens): fix leaky lease



* feat(identity)(Accept OAuth JWT tokens): fix leaky lease II and add claims to TE



* Add TestLogicalWithJwtAndSCIM e2e test

* feat(identity)(Accept OAuth JWT tokens): add more tests



* feat(identity)(Accept OAuth JWT tokens): change email



* feat(identity)(Accept OAuth JWT tokens): e2e test for scim



* Accept and validate RAR (#11005)



* fix(identity)(Accept OAuth JWT tokens): add missing headers



---------




* wip

* wip

* wip

* feat(identity): auth with delegation jwts



* feat(identity): optimize jwks fetching



* wip

* add lookup by entity-id

* feat(identity): optimize jwks fetching



* feat(identity): optimize jwks fetching and refactoring



* feat(identity): fix breaking tests



* feat(identity): accept oauth tokens

type: draft

* feat(identity): make basic token lookup op work

type: draft

* fix(identity): failing tests

type: draft

* feat(identity): add new tests + handle renew & revoke

type: draft

* feat(identity): clean up and tests

type: draft

* feat(identity)(Accept OAuth JWT tokens): Add more tests

* feat(identity)(Accept OAuth JWT tokens): Test updates

* feat(identity)(Accept OAuth JWT tokens): Revert go version changes

* feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests

* feat(identity)(Accept OAuth JWT tokens): fix tests

* feat(identity)(Accept OAuth JWT tokens): fix more CI issues

* [POC] Accept and Validate External OAuth Token for Agent Identity (#10991)

* feat(identity): accept oauth tokens

type: draft

* feat(identity): make basic token lookup op work

type: draft

* fix(identity): failing tests

type: draft

* feat(identity): add new tests + handle renew & revoke

type: draft

* feat(identity): clean up and tests

type: draft

* feat(identity)(Accept OAuth JWT tokens): Add more tests

* feat(identity)(Accept OAuth JWT tokens): Test updates

* feat(identity)(Accept OAuth JWT tokens): Revert go version changes

* feat(identity)(Accept OAuth JWT tokens): add missing godoc for tests

* feat(identity)(Accept OAuth JWT tokens): fix tests

* feat(identity)(Accept OAuth JWT tokens): fix more CI issues

* Add JWT token tidy cleanup functionality

* Refactor JWT tidy tests to use retryUntil instead of time.Sleep

* feat(identity)(Accept OAuth JWT tokens): fix leaky lease



* feat(identity)(Accept OAuth JWT tokens): fix leaky lease II and add claims to TE



* Add TestLogicalWithJwtAndSCIM e2e test

* feat(identity)(Accept OAuth JWT tokens): add more tests



* feat(identity)(Accept OAuth JWT tokens): change email



* feat(identity)(Accept OAuth JWT tokens): e2e test for scim



* Accept and validate RAR (#11005)



* fix(identity)(Accept OAuth JWT tokens): add missing headers



---------




* feat(identity): auth with delegation jwts



* feat(identity): optimize jwks fetching



* wip

* wip

* wip

* wip

* add lookup by entity-id

* feat(identity): optimize jwks fetching



* feat(identity): optimize jwks fetching and refactoring



* feat(identity): fix breaking tests



* VAULT-42642 Fix feature branch tests, some CE -> Ent moving (#12417)

* VAULT-42642 Fix feathre branch tests, some CE -> Ent moving

* VAULT-42642 fix last test?

* skip test

* proto

* more test fixes

* buf format

* Fix createVaultEntityForUser signature (#12439)

* VAULT-42533 Agent Registry: enforce entity invariants, miscellaneous improvements (#12399)

* VAULT-42533 enforce entity invariants, miscellaneous improvements

* typos

* Improve path handling for rar + add tests (#11934)

* Moving stuff around for CE

* fmt

* backend

* more CE changes

* more fixes

* further fixes

* rework clone

* VAULT-42621 Move JWT tests (and some standalone functionality) to enterprise files (#12460)

* VAULT-42621 Move JWT tests (and some standalone functionality) to enterprise files

* logical

* skip failing tests

* more skips

* missed tests

* one more

* VAULT-42619 Refactor token_ent and surrounding files to move logic into enterprise (#12473)

* VAULT-42619 Refactor token_ent and surrounding files to move logic into enterprise

* test fixes

* feedback

* VAULT-42631 Rename ISJWT to IsEnterpriseToken, some refactoring (#12509)

* VAULT-42631 Rename ISJWT to IsEnterpriseToken, some refactoring

* godocs

* VAULT-42779 refactor JWT parts of acl.go to enterprise files (#12512)

* VAULT-42630 move token tidy for JWT to ent files (#12534)

* VAULT-42818 rename jwtjti in request struct (#12539)

* VAULT-42633 CE-ify request handling and flow around validateJwtAndFetchEntity (#12541)

* VAULT-42633 CE-ify request handling

* Copyright headers

* VAULT-42633 Move IsJWT back to CE code, add explanations (#12576)

* VAULT-42633 CE-ify request handling

* Copyright headers

* VAULT-42633 Move IsJWT back to CE code, add explanations

* VAULT-42870 Move jwtAuthManager to enterprise (#12586)

* VAULT-42796 Move config to enterprise files (#12604)

* VAULT-42796 Move config to enterprise files

* more build failures

* fix test util

* two more compilatin things

* VAULT42796 - fix missing return (#12617)

* VAULT-42796 Move config to enterprise files

* more build failures

* fix test util

* two more compilatin things

* VAUlt-42796 fix test

* Fix linting for rar_ent (#12618)

* VAULT-42796 Move config to enterprise files

* more build failures

* fix test util

* two more compilatin things

* VAUlt-42796 fix test

* Fix linter for rar code

* fix authresults

* typo

* return error

* fix method signature

---------

Signed-off-by: Arnab Chatterjee <arnab.chatterjee@hashicorp.com>
Co-authored-by: Violet Hynes <violet.hynes@hashicorp.com>
Co-authored-by: Arnab Chatterjee <arnab.chatterjee@hashicorp.com>
Co-authored-by: Arnab Chatterjee <arnabkaycee@gmail.com>
Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com>
Co-authored-by: davidadeleon <ddeleon@hashicorp.com>
Co-authored-by: Bianca Moreira <bianca.moreira@hashicorp.com>
2026-03-05 10:25:59 -05:00

298 lines
9.4 KiB
Go

// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0
package logical
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"sort"
"strings"
"time"
"github.com/hashicorp/go-sockaddr"
)
//go:generate enumer -type=TokenType -trimprefix=TokenType -transform=kebab
type TokenType uint8
const (
// TokenTypeDefault means "use the default, if any, that is currently set
// on the mount". If not set, results in a Service token.
TokenTypeDefault TokenType = iota
// TokenTypeService is a "normal" Vault token for long-lived services
TokenTypeService
// TokenTypeBatch is a batch token
TokenTypeBatch
// TokenTypeDefaultService configured on a mount, means that if
// TokenTypeDefault is sent back by the mount, create Service tokens
TokenTypeDefaultService
// TokenTypeDefaultBatch configured on a mount, means that if
// TokenTypeDefault is sent back by the mount, create Batch tokens
TokenTypeDefaultBatch
TokenTypeEnt
// ClientIDTWEDelimiter Delimiter between the string fields used to generate a client
// ID for tokens without entities. This is the 0 character, which
// is a non-printable string. Please see unicode.IsPrint for details.
ClientIDTWEDelimiter = rune('\x00')
// SortedPoliciesTWEDelimiter Delimiter between each policy in the sorted policies used to
// generate a client ID for tokens without entities. This is the 127
// character, which is a non-printable string. Please see unicode.IsPrint
// for details.
SortedPoliciesTWEDelimiter = rune('\x7F')
)
func (t *TokenType) UnmarshalJSON(b []byte) error {
if len(b) == 1 {
*t = TokenType(b[0] - '0')
return nil
}
// Handle upgrade from pre-1.2 where we were serialized as string:
s := string(b)
switch s {
case `"default"`, `""`:
*t = TokenTypeDefault
case `"service"`:
*t = TokenTypeService
case `"batch"`:
*t = TokenTypeBatch
case `"default-service"`:
*t = TokenTypeDefaultService
case `"default-batch"`:
*t = TokenTypeDefaultBatch
case `"ent"`:
*t = TokenTypeEnt
default:
return fmt.Errorf("unknown token type %q", s)
}
return nil
}
// TokenEntry is used to represent a given token
type TokenEntry struct {
EntToken
Type TokenType `json:"type" mapstructure:"type" structs:"type" sentinel:""`
// ID of this entry, generally a random UUID
ID string `json:"id" mapstructure:"id" structs:"id" sentinel:""`
// ExternalID is the ID of a newly created service
// token that will be returned to a user
ExternalID string `json:"-"`
// Accessor for this token, a random UUID
Accessor string `json:"accessor" mapstructure:"accessor" structs:"accessor" sentinel:""`
// Parent token, used for revocation trees
Parent string `json:"parent" mapstructure:"parent" structs:"parent" sentinel:""`
// Which named policies should be used
Policies []string `json:"policies" mapstructure:"policies" structs:"policies"`
// InlinePolicy specifies ACL rules to be applied to this token entry.
InlinePolicy string `json:"inline_policy" mapstructure:"inline_policy" structs:"inline_policy"`
// Used for audit trails, this is something like "auth/user/login"
Path string `json:"path" mapstructure:"path" structs:"path"`
// Used for auditing. This could include things like "source", "user", "ip"
Meta map[string]string `json:"meta" mapstructure:"meta" structs:"meta" sentinel:"meta"`
// InternalMeta is used to store internal metadata. This metadata will not be audit logged or returned from lookup APIs.
InternalMeta map[string]string `json:"internal_meta" mapstructure:"internal_meta" structs:"internal_meta"`
// Used for operators to be able to associate with the source
DisplayName string `json:"display_name" mapstructure:"display_name" structs:"display_name"`
// Used to restrict the number of uses (zero is unlimited). This is to
// support one-time-tokens (generalized). There are a few special values:
// if it's -1 it has run through its use counts and is executing its final
// use; if it's -2 it is tainted, which means revocation is currently
// running on it; and if it's -3 it's also tainted but revocation
// previously ran and failed, so this hints the tidy function to try it
// again.
NumUses int `json:"num_uses" mapstructure:"num_uses" structs:"num_uses"`
// Time of token creation
CreationTime int64 `json:"creation_time" mapstructure:"creation_time" structs:"creation_time" sentinel:""`
// Duration set when token was created
TTL time.Duration `json:"ttl" mapstructure:"ttl" structs:"ttl" sentinel:""`
// Explicit maximum TTL on the token
ExplicitMaxTTL time.Duration `json:"explicit_max_ttl" mapstructure:"explicit_max_ttl" structs:"explicit_max_ttl" sentinel:""`
// If set, the role that was used for parameters at creation time
Role string `json:"role" mapstructure:"role" structs:"role"`
// If set, the period of the token. This is only used when created directly
// through the create endpoint; periods managed by roles or other auth
// backends are subject to those renewal rules.
Period time.Duration `json:"period" mapstructure:"period" structs:"period" sentinel:""`
// These are the deprecated fields
DisplayNameDeprecated string `json:"DisplayName" mapstructure:"DisplayName" structs:"DisplayName" sentinel:""`
NumUsesDeprecated int `json:"NumUses" mapstructure:"NumUses" structs:"NumUses" sentinel:""`
CreationTimeDeprecated int64 `json:"CreationTime" mapstructure:"CreationTime" structs:"CreationTime" sentinel:""`
ExplicitMaxTTLDeprecated time.Duration `json:"ExplicitMaxTTL" mapstructure:"ExplicitMaxTTL" structs:"ExplicitMaxTTL" sentinel:""`
// EntityID is the ID of the entity associated with this token.
EntityID string `json:"entity_id" mapstructure:"entity_id" structs:"entity_id"`
// If NoIdentityPolicies is true, the token will not inherit
// identity policies from the associated EntityID.
NoIdentityPolicies bool `json:"no_identity_policies" mapstructure:"no_identity_policies" structs:"no_identity_policies"`
// The set of CIDRs that this token can be used with
BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs" sentinel:""`
// NamespaceID is the identifier of the namespace to which this token is
// confined to. Do not return this value over the API when the token is
// being looked up.
NamespaceID string `json:"namespace_id" mapstructure:"namespace_id" structs:"namespace_id" sentinel:""`
// CubbyholeID is the identifier of the cubbyhole storage belonging to this
// token
CubbyholeID string `json:"cubbyhole_id" mapstructure:"cubbyhole_id" structs:"cubbyhole_id" sentinel:""`
}
// CreateClientID returns the client ID, and a boolean which is false if the clientID
// has an entity, and true otherwise
func (te *TokenEntry) CreateClientID() (string, bool) {
var clientIDInputBuilder strings.Builder
// if entry has an associated entity ID, return it
if te.EntityID != "" {
return te.EntityID, false
}
// The entry is associated with a TWE (token without entity). In this case
// we must create a client ID by calculating the following formula:
// clientID = SHA256(sorted policies + namespace)
// Step 1: Copy entry policies to a new struct
sortedPolicies := make([]string, len(te.Policies))
copy(sortedPolicies, te.Policies)
// Step 2: Sort and join copied policies
sort.Strings(sortedPolicies)
for _, pol := range sortedPolicies {
clientIDInputBuilder.WriteRune(SortedPoliciesTWEDelimiter)
clientIDInputBuilder.WriteString(pol)
}
// Step 3: Add namespace ID
clientIDInputBuilder.WriteRune(ClientIDTWEDelimiter)
clientIDInputBuilder.WriteString(te.NamespaceID)
if clientIDInputBuilder.Len() == 0 {
return "", true
}
// Step 4: Remove the first character in the string, as it's an unnecessary delimiter
clientIDInput := clientIDInputBuilder.String()[1:]
// Step 5: Hash the sum
hashed := sha256.Sum256([]byte(clientIDInput))
return base64.StdEncoding.EncodeToString(hashed[:]), true
}
func (te *TokenEntry) SentinelGet(key string) (interface{}, error) {
if te == nil {
return nil, nil
}
switch key {
case "policies":
return te.Policies, nil
case "path":
return te.Path, nil
case "display_name":
return te.DisplayName, nil
case "num_uses":
return te.NumUses, nil
case "role":
return te.Role, nil
case "entity_id":
return te.EntityID, nil
case "period":
return te.Period, nil
case "period_seconds":
return int64(te.Period.Seconds()), nil
case "explicit_max_ttl":
return te.ExplicitMaxTTL, nil
case "explicit_max_ttl_seconds":
return int64(te.ExplicitMaxTTL.Seconds()), nil
case "creation_ttl":
return te.TTL, nil
case "creation_ttl_seconds":
return int64(te.TTL.Seconds()), nil
case "creation_time":
return time.Unix(te.CreationTime, 0).Format(time.RFC3339Nano), nil
case "creation_time_unix":
return time.Unix(te.CreationTime, 0), nil
case "meta", "metadata":
return te.Meta, nil
case "type":
teType := te.Type
switch teType {
case TokenTypeBatch, TokenTypeService, TokenTypeEnt:
case TokenTypeDefault:
teType = TokenTypeService
default:
return "unknown", nil
}
return teType.String(), nil
}
return nil, nil
}
func (te *TokenEntry) SentinelKeys() []string {
return []string{
"period",
"period_seconds",
"explicit_max_ttl",
"explicit_max_ttl_seconds",
"creation_ttl",
"creation_ttl_seconds",
"creation_time",
"creation_time_unix",
"meta",
"metadata",
"type",
}
}
// IsRoot returns false if the token is not root (or doesn't exist)
func (te *TokenEntry) IsRoot() bool {
if te == nil {
return false
}
return len(te.Policies) == 1 && te.Policies[0] == "root"
}