hashicorp / hashicorp/vault-plugin-auth-jwt
Float Truncation in JWT Auth Bound Claims Matching
- 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