mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
* core: normalize JWT req client token to internal ID Fix enterprise JWT request handling to replace req.ClientToken with the internal jwt.<jti> token ID after JIT token entry creation, ensuring downstream auth/lease flows use internal IDs instead of raw JWT strings. Add regression assertions in request handling enterprise tests to verify req/auth/token entry IDs are normalized and raw JWT is not propagated. * vault: document perf-standby JIT forwarding * vault: fix enterprise JWT RAR enforcement Preserve internal JWT token-id normalization while enforcing RAR constraints from request-populated authorization details, with JWT parsing fallback for compatibility. * Fix perf-standby JWT forwarding token restoration Prefer inbound original token when restoring forwarding auth headers so perf-standby forwards raw JWT instead of normalized internal token ID. Also add regression tests for header restoration behavior and clarify godocs for InboundSSCToken semantics. * Add missing Go docs for forwarding tests Fix code-checker lint failure by adding go doc comments to new Test* functions in request_handling_test.go. * Address PR review feedback on type checks and CE wording Split map lookup and type assertion in getMapString for clarity, and adjust InboundSSCToken doc wording to avoid JWT-specific language in CE file. * Canonicalize enterprise token handling Normalize enterprise token inputs to canonical internal IDs in token store paths and remove dual-representation RAR fallback. * Address review nits on token normalization Rename enterprise token normalization helper for clarity and update tests to use require.NoError/require.Equal as requested in review feedback. * Guard sdk ent token tests with enterprise tag Add enterprise build constraint to sdk/logical/token_ent_test.go so CE-mode sdk/logical checks can run without enterprise-only EntToken fields. * Remove enterprise build tag from token_ent_test Revert the temporary build constraint addition in sdk/logical/token_ent_test.go. --------- Co-authored-by: Bianca <48203644+biazmoreira@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
8c58356d5e
commit
a3d0147dc4
@ -153,6 +153,11 @@ type Request struct {
|
||||
// EnterpriseTokenAuthorizationDetails stores enterprise token authorization details.
|
||||
EnterpriseTokenAuthorizationDetails []AuthorizationDetail `json:"enterprise_token_authorization_details,omitempty" structs:"enterprise_token_authorization_details" mapstructure:"enterprise_token_authorization_details"`
|
||||
|
||||
// EnterpriseTokenAuthorizationDetailsPresent indicates whether the inbound
|
||||
// enterprise token included an authorization_details claim at all. This lets
|
||||
// callers distinguish "claim missing" from "claim present but empty".
|
||||
EnterpriseTokenAuthorizationDetailsPresent bool `json:"enterprise_token_authorization_details_present,omitempty" structs:"enterprise_token_authorization_details_present" mapstructure:"enterprise_token_authorization_details_present"`
|
||||
|
||||
// ClientTokenAccessor is provided to the core so that the it can get
|
||||
// logged as part of request audit logging.
|
||||
ClientTokenAccessor string `json:"client_token_accessor" structs:"client_token_accessor" mapstructure:"client_token_accessor" sentinel:""`
|
||||
@ -279,8 +284,11 @@ type Request struct {
|
||||
// client token.
|
||||
ClientID string `json:"client_id" structs:"client_id" mapstructure:"client_id" sentinel:""`
|
||||
|
||||
// InboundSSCToken is the token that arrives on an inbound request, supplied
|
||||
// by the vault user.
|
||||
// InboundSSCToken stores the original token value as supplied by the caller
|
||||
// on the inbound request (header/body), before token decoding or
|
||||
// normalization (for example SSCT decoding or enterprise token normalization
|
||||
// to internal IDs). This allows response/forwarding paths to preserve the
|
||||
// caller-visible token representation when needed.
|
||||
InboundSSCToken string
|
||||
|
||||
// When a request has been forwarded, contains information of the host the request was forwarded 'from'
|
||||
|
||||
@ -252,6 +252,7 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req
|
||||
req.EnterpriseTokenMetadata = getEnterpriseTokenMetadata(tokenMetadataContainer)
|
||||
req.EnterpriseTokenIssuer = getEnterpriseTokenIssuer(tokenMetadataContainer)
|
||||
req.EnterpriseTokenAudience = getEnterpriseTokenAudience(tokenMetadataContainer)
|
||||
_, req.EnterpriseTokenAuthorizationDetailsPresent = tokenMetadataContainer["authorization_details"]
|
||||
req.EnterpriseTokenAuthorizationDetails = getEnterpriseTokenAuthorizationDetails(tokenMetadataContainer)
|
||||
secondEntity = actorEntity
|
||||
err = c.createAndStoreEnterpriseTokenEntry(ctx, req, tokenMetadataContainer, entity, actorEntity)
|
||||
@ -419,9 +420,19 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req
|
||||
}
|
||||
|
||||
// restoreForwardingTokenHeaders restores client token headers so forwarded
|
||||
// requests preserve the original auth source on the active node.
|
||||
// requests preserve the caller's original token representation on the active
|
||||
// node. It prefers Request.InboundSSCToken (captured before any token
|
||||
// normalization) and falls back to Request.ClientToken when no inbound value is
|
||||
// available.
|
||||
func restoreForwardingTokenHeaders(req *logical.Request) {
|
||||
if req == nil || req.ClientToken == "" {
|
||||
if req == nil {
|
||||
return
|
||||
}
|
||||
tokenToForward := req.InboundSSCToken
|
||||
if tokenToForward == "" {
|
||||
tokenToForward = req.ClientToken
|
||||
}
|
||||
if tokenToForward == "" {
|
||||
return
|
||||
}
|
||||
if req.Headers == nil {
|
||||
@ -429,9 +440,9 @@ func restoreForwardingTokenHeaders(req *logical.Request) {
|
||||
}
|
||||
switch req.ClientTokenSource {
|
||||
case logical.ClientTokenFromVaultHeader:
|
||||
req.Headers[consts.AuthHeaderName] = []string{req.ClientToken}
|
||||
req.Headers[consts.AuthHeaderName] = []string{tokenToForward}
|
||||
case logical.ClientTokenFromAuthzHeader:
|
||||
req.Headers["Authorization"] = append(req.Headers["Authorization"], fmt.Sprintf("Bearer %s", req.ClientToken))
|
||||
req.Headers["Authorization"] = append(req.Headers["Authorization"], fmt.Sprintf("Bearer %s", tokenToForward))
|
||||
}
|
||||
}
|
||||
|
||||
@ -675,12 +686,7 @@ func (c *Core) CheckToken(ctx context.Context, req *logical.Request, unauth bool
|
||||
// forward this request properly to the active node.
|
||||
if retErr.ErrorOrNil() != nil && checkErrControlGroupTokenNeedsCreated(retErr) &&
|
||||
c.perfStandby && len(req.ClientToken) != 0 {
|
||||
switch req.ClientTokenSource {
|
||||
case logical.ClientTokenFromVaultHeader:
|
||||
req.Headers[consts.AuthHeaderName] = []string{req.ClientToken}
|
||||
case logical.ClientTokenFromAuthzHeader:
|
||||
req.Headers["Authorization"] = append(req.Headers["Authorization"], fmt.Sprintf("Bearer %s", req.ClientToken))
|
||||
}
|
||||
restoreForwardingTokenHeaders(req)
|
||||
// We also return the appropriate error so that the caller can forward the
|
||||
// request to the active node
|
||||
return auth, te, logical.ErrPerfStandbyPleaseForward
|
||||
|
||||
@ -54,6 +54,56 @@ func TestRequiresMaterializedTokenState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreForwardingTokenHeaders_UsesInboundToken verifies Authorization
|
||||
// forwarding prefers the original inbound token when present.
|
||||
func TestRestoreForwardingTokenHeaders_UsesInboundToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &logical.Request{
|
||||
ClientToken: "jwt.internal-id",
|
||||
InboundSSCToken: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig",
|
||||
ClientTokenSource: logical.ClientTokenFromAuthzHeader,
|
||||
Headers: map[string][]string{
|
||||
"Authorization": {"Basic abc123"},
|
||||
},
|
||||
}
|
||||
|
||||
restoreForwardingTokenHeaders(req)
|
||||
|
||||
require.Equal(t, []string{"Basic abc123", "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig"}, req.Headers["Authorization"])
|
||||
}
|
||||
|
||||
// TestRestoreForwardingTokenHeaders_FallsBackToClientToken verifies fallback to
|
||||
// req.ClientToken when no inbound token is present.
|
||||
func TestRestoreForwardingTokenHeaders_FallsBackToClientToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &logical.Request{
|
||||
ClientToken: "jwt.jti-value",
|
||||
ClientTokenSource: logical.ClientTokenFromVaultHeader,
|
||||
}
|
||||
|
||||
restoreForwardingTokenHeaders(req)
|
||||
|
||||
require.Equal(t, []string{"jwt.jti-value"}, req.Headers["X-Vault-Token"])
|
||||
}
|
||||
|
||||
// TestRestoreForwardingTokenHeaders_UsesInboundTokenForVaultHeader verifies
|
||||
// X-Vault-Token forwarding prefers the original inbound token.
|
||||
func TestRestoreForwardingTokenHeaders_UsesInboundTokenForVaultHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := &logical.Request{
|
||||
ClientToken: "jwt.jti-value",
|
||||
InboundSSCToken: "jwt.raw.value",
|
||||
ClientTokenSource: logical.ClientTokenFromVaultHeader,
|
||||
}
|
||||
|
||||
restoreForwardingTokenHeaders(req)
|
||||
|
||||
require.Equal(t, []string{"jwt.raw.value"}, req.Headers["X-Vault-Token"])
|
||||
}
|
||||
|
||||
func TestRequestHandling_Wrapping(t *testing.T) {
|
||||
core, _, root := TestCoreUnsealed(t)
|
||||
|
||||
|
||||
@ -2684,7 +2684,8 @@ func (ts *TokenStore) handleCreate(ctx context.Context, req *logical.Request, d
|
||||
|
||||
// handleCreateCommon handles the auth/token/create path for creation of new tokens
|
||||
func (ts *TokenStore) handleCreateCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, orphan bool, role *tsRoleEntry) (*logical.Response, error) {
|
||||
if !orphan && IsEnterpriseToken(req.ClientToken) {
|
||||
normalizedClientToken := normalizeEnterpriseTokenToID(req.ClientToken)
|
||||
if !orphan && IsEnterpriseTokenId(normalizedClientToken) {
|
||||
return logical.ErrorResponse("enterprise tokens cannot create child tokens"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
@ -3355,7 +3356,8 @@ func (ts *TokenStore) handleRevokeTree(ctx context.Context, req *logical.Request
|
||||
}
|
||||
|
||||
func (ts *TokenStore) revokeCommon(ctx context.Context, req *logical.Request, data *framework.FieldData, id string) (*logical.Response, error) {
|
||||
if IsEnterpriseToken(id) {
|
||||
normalizedID := normalizeEnterpriseTokenToID(id)
|
||||
if IsEnterpriseTokenId(normalizedID) {
|
||||
return logical.ErrorResponse("cannot revoke ent token"), nil
|
||||
}
|
||||
te, err := ts.Lookup(ctx, id)
|
||||
@ -3402,7 +3404,8 @@ func (ts *TokenStore) handleRevokeOrphan(ctx context.Context, req *logical.Reque
|
||||
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
if IsEnterpriseToken(id) {
|
||||
normalizedID := normalizeEnterpriseTokenToID(id)
|
||||
if IsEnterpriseTokenId(normalizedID) {
|
||||
return logical.ErrorResponse("enterprise token cannot be revoked"), nil
|
||||
}
|
||||
|
||||
@ -3569,7 +3572,8 @@ func (ts *TokenStore) handleRenew(ctx context.Context, req *logical.Request, dat
|
||||
if id == "" {
|
||||
return logical.ErrorResponse("missing token ID"), logical.ErrInvalidRequest
|
||||
}
|
||||
if IsEnterpriseToken(id) {
|
||||
normalizedID := normalizeEnterpriseTokenToID(id)
|
||||
if IsEnterpriseTokenId(normalizedID) {
|
||||
return logical.ErrorResponse("enterprise tokens cannot be renewed"), nil
|
||||
}
|
||||
incrementRaw := data.Get("increment").(int)
|
||||
|
||||
@ -16,6 +16,10 @@ func getEnterpriseTokenId(_ string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeEnterpriseTokenToID(token string) string {
|
||||
return token
|
||||
}
|
||||
|
||||
func (ts *TokenStore) handleTidyEnterpriseTokens(_ context.Context, _ *namespace.Namespace, _ *multierror.Error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user