hashicorp / hashicorp/vault-plugin-auth-jwt

Float Truncation in JWT Auth Bound Claims Matching

Open
#376 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
107
Forks
73
Avg merge
13h 49m
Merged PRs (30d)
7

Description

## Describe the bug

The JWT auth method's `getClaim()` function truncates fractional `float64` claim values to integers, causing incorrect `bound_claims` matching. A JWT with `accountId: 42.9` passes a role's `bound_claims: {"accountId": 42}` check — the fractional part is silently dropped.

The root cause is in `vault-plugin-auth-jwt`, `claims.go:60-64`:

```go
switch v := val.(type) {
case float32:
return json.Number(strconv.Itoa(int(v)))
case float64:
return json.Number(strconv.Itoa(int(v))) // int(42.9) → 42
}
```

The comment above this code (lines 55-59) acknowledges the `float64` mismatch from go-oidc's JSON unmarshalling, but the conversion is lossy — `int(42.9)` produces `42`, not `43` or an error.

This bug exists in vault-plugin-auth-jwt v0.25.0 (bundled in Vault 1.21.4) and persists through the latest v0.26.1.

## To Reproduce

1. Start Vault in dev mode and enable JWT auth:

```bash
vault auth enable jwt
vault write auth/jwt/config \
jwt_validation_pubkeys=""
```

2. Create a role with a numeric `bound_claims` value:

```bash
vault write auth/jwt/role/test-role \
role_type="jwt" \
bound_audiences="test" \
user_claim="sub" \
bound_claims='{"accountId": 42}' \
bound_claims_type="string" \
token_policies="default"
```

3. Create and sign a JWT with `accountId: 42.9` (a **different** account):

```json
{
"iss": "test-issuer",
"sub": "testuser",
"aud": "test",
"accountId": 42.9
}
```

4. Login with this JWT:

```bash
vault write auth/jwt/login jwt="$TOKEN" role="test-role"
# → Success. 42.9 is truncated to 42 and matches bound_claims.
```

5. For comparison, `accountId: 43` is correctly rejected.

## Expected behavior

Login with `accountId: 42.9` should **fail** because `42.9 ≠ 42`. Fractional numeric claim values should be preserved during comparison, not truncated.

## Environment

- **Vault Server Version**: 1.21.4
- **vault-plugin-auth-jwt Version**: v0.25.0 (bundled), also verified in v0.26.1
- **Server Operating System/Architecture**: linux/amd64

## Additional context

The fix is straightforward — use `strconv.FormatFloat` to preserve the full numeric value:

```go
case float64:
if v == float64(int64(v)) {
return json.Number(strconv.FormatInt(int64(v), 10))
}
return json.Number(strconv.FormatFloat(v, 'f', -1, 64))
```

This preserves whole numbers as integers (`42.0` → `"42"`) while keeping fractional values intact (`42.9` → `"42.9"`).

[report.md](https://github.com/user-attachments/files/26163281/report.md)
[poc.py](https://github.com/user-attachments/files/26163283/poc.py)
[docker-compose.yml](https://github.com/user-attachments/files/26163282/docker-compose.yml)

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in claims.go:60-64 and read the surrounding getClaim() conversion logic, then use the provided JWT reproduction to check bound_claims matching. Done means fractional values such as 42.9 are preserved and do not match 42, while whole-number behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
authentication
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.