---
id: web--jwt-validator
title: "JWT validator"
summary: "Validates a JSON Web Token: from **Http authentication** (type `2` JWT, or Bearer), or a token sent as a `TextRecord`. It checks the signature with a shared secret, a public key or the keys of an OpenID Connect provider (Entra ID, Auth0, Keycloak, ...), then the lifetime, issuer, audience and required claims, and puts the claims into the flow metadata. An invalid token raises an error that the Http listener answers with `401` (`403` for a missing required claim)."
audience: user
keywords: "input, output, keysource, metadataaddress, algorithm, signingkey, keyencoding, validatelifetime, clockskew, validateissuer, issuer, validateaudience, audience, tokenuse, requiredclaims"
---
# JWT validator

Validates a JSON Web Token: from **Http authentication** (type `2` JWT, or Bearer), or a token sent as a `TextRecord`. It checks the signature with a shared secret, a public key or the keys of an OpenID Connect provider (Entra ID, Auth0, Keycloak, ...), then the lifetime, issuer, audience and required claims, and puts the claims into the flow metadata. An invalid token raises an error that the Http listener answers with `401` (`403` for a missing required claim).

## Pins

| Pin | Kind | Types | Description |
|---|---|---|---|
| Input | input | `Viaducta.Plugins.Http.Types.HttpAuthentificationData`, `Viaducta.Plugins.TextPlugin.Types.TextRecord` | Uses `Jwt`, else `Token`; or the `TextRecord` `Value` (`Bearer ` in front is allowed). |
| Output | output | `Viaducta.Plugins.JWTValidator.JwtClaimsData` | Optional. The claims of a valid token: `UserName`, `UserId`, `Email`, `Issuer`, `Audience`, `IssuedAt`, `ValidTo`, `Claims`, `Token`. |

## Configuration

| Field | Type | Default | Description |
|---|---|---|---|
| `KeySource` | int | `0` | `0` the `SigningKey` below; `1` OpenID Connect provider (`MetadataAddress` is the authority or its `.well-known/openid-configuration` URL); `2` JWKS URL (`MetadataAddress`). |
| `MetadataAddress` | string | | For `KeySource` `1` and `2`. HTTPS only, plain http is allowed for localhost. |
| `Algorithm` | int | `0` | `KeySource` `0`: the algorithm the issuer signs with, the same numbers as in the JWT issuer (`0` HS256 ... `11` PS512). Other algorithms are rejected. With `1` and `2` the RS*, ES* and PS* algorithms are accepted. |
| `SigningKey` | string | | `KeySource` `0`: HS* the shared secret of the JWT issuer; RS*, ES*, PS* the public key (or certificate) in PEM format. |
| `KeyEncoding` | int | `0` | Shared secret only: `0` text (UTF-8), `1` Base64. |
| `ValidateLifetime` | boolean | `true` | Rejects expired tokens, tokens not valid yet and tokens without `exp`. |
| `ClockSkew` | string | `00:01:00` | Tolerance for clock differences in the lifetime check, up to one hour. Empty means one minute. |
| `ValidateIssuer` | boolean | `true` | Checks `iss`. Always on when `Issuer` is set. With an OpenID Connect provider and an empty `Issuer` the provider's issuer is expected; otherwise `Issuer` must be set. |
| `Issuer` | string | | Expected `iss`, comma separated for several. |
| `ValidateAudience` | boolean | `false` | Checks `aud`. Always on when `Audience` is set; on without an `Audience` stops the flow at start. |
| `Audience` | string | | Expected `aud`, comma separated for several. |
| `TokenUse` | int | `0` | `0` Access: refresh tokens of the JWT issuer (`token_use` = `refresh`) are rejected. `1` Refresh: only refresh tokens, for a refresh endpoint. `2` Any. |
| `RequiredClaims` | string | | Claims the token must have, else `403`. Entries separated by `;` are all required; an entry is a claim name (must be present) or `name=value` with `\|` between accepted values, e.g. `role=admin\|editor; scope=orders.read`. `scope` and `scp` are matched per space separated scope. |

## Behaviour

- Status codes: `401` for a missing, malformed, expired or untrusted token (with `WWW-Authenticate: Bearer error="invalid_token"`), `403` for a missing required claim (`error="insufficient_scope"`), `503` when the provider's keys cannot be loaded.
- The error message names the reason (`The token has expired`, `The token signature is not valid`, `The token issuer is not accepted`, ...) without the library's diagnostics.
- On success the `UserInfo` metadata (`Viaducta.Plugins.Http.Types.UserInfoMetaData`) is filled: `UserName` = `sub`, `Email` = `email` claim, `UserId` = `userId` claim, `Token` = the token, `Claims` = all claims of the token (including `iss`, `aud`, `exp`, ...). A claim with several values (roles, several audiences) is written as a JSON array, e.g. `["admin","editor"]`. `ApiKey` set by Http authentication is kept.
- Provider keys are cached for 12 hours. A token signed with an unknown key (`kid`) reloads them at most every 5 minutes, so key rotation works without a restart. When a reload fails the old keys keep working.
- A wrong configuration (missing key, unreadable PEM, missing `Issuer`/`Audience` for an enabled check, http metadata address) stops the flow at start.

## Notes

- Flows saved with older versions keep their stored check settings; turn on `ValidateLifetime` where it is off.
- In the Http listener's authentication branch the `UserInfo` metadata reaches the `Output` branch when the validator is the only connection of Http authentication. With several connections each gets its own copy of the metadata.
- Use an expression setting (`=Secrets.JwtKey`) for `SigningKey`.

## Example

```json
{ "ValidateLifetime": true, "ValidateIssuer": true, "Issuer": "my-api", "Audience": "my-clients", "SigningKey": "=Secrets.JwtKey", "RequiredClaims": "role=admin|editor" }
```

```json
{ "KeySource": 1, "MetadataAddress": "https://login.microsoftonline.com/{tenant}/v2.0", "Audience": "api://my-api", "RequiredClaims": "scp=orders.read" }
```

Http listener (`Authentificate`) → Http authentification (`AuthentificationType` `2`) → **JWT validator**
