# `jwt`

The `jwt` library provides compact JWT parsing, JWS signing and verification, JWK/JWKS key selection, and reusable registered-claim validation predicates.

The current implementation supports native `HS256` signing/verification and OpenSSL-backed `RS256`/`ES256` verification. `ES256` signatures are converted from JOSE raw `R || S` encoding to DER before calling OpenSSL.

This library requires a Prolog backend supporting unbound integer arithmetic.

## Requirements

The `openssl` command must be available on the current `PATH`.

On macOS, it can be installed using e.g. Homebrew:

    $ brew install openssl

Or using MacPorts:

    $ sudo port install openssl

On Ubuntu, it can be installed using:

    $ sudo apt install openssl

On RedHat distributions (8.x and later):

    $ sudo dnf install openssl

For older RedHat distributions:

    $ sudo yum install openssl

On Windows, it can be installed using e.g. Chocolatey:

    > choco install openssl

## API documentation

Open the ../../apis/library_index.html#jwt link in a web browser.

## Loading

To load the library, load the `loader.lgt` file:

    | ?- logtalk_load(jwt(loader)).

## Testing

To test this library, load the `tester.lgt` file:

    | ?- logtalk_load(jwt(tester)).

## Options

The facade predicates that take an options list validate the following option terms. Each option only affects predicates that perform the corresponding operation.

Signature and algorithm options:

- `allow_algorithms(Algorithms)` - List of allowed JOSE `alg` atom values for verification and signing. The default is `['HS256', 'RS256', 'ES256']`.

- `algorithm(Algorithm)` - Requires one exact JOSE `alg` atom value. When used with `allow_algorithms/1`, both checks must pass.

- `allow_none(Boolean)` - Allows or rejects the unsecured `none` algorithm. The default is `false`.

Claim validation options:

- `allow_missing_exp(Boolean)` - Allows tokens without an `exp` claim when set to `true`. The default is `false`.

- `clock_skew(Seconds)` - Non-negative numeric leeway, in seconds, for time claim validation. The default is `60`.

- `now(Timestamp)` - Numeric timestamp to use instead of the current operating system time when validating time claims.

- `max_age(Seconds)` - Non-negative maximum age, in seconds, for policies that validate an `iat` claim as `time(issued_at)`.

- `required_claims(Names)` - List of claim-name atoms that must be present when validating claims. The default is `[]`.

- `claim_policy(Policy)` - Claim policy list applied by `verify/4` and `verify/5` after signature verification. When calling `validate_claims/3` directly, pass the policy as the second argument instead.

OpenSSL options:

- `openssl_executable(Executable)` - Atom naming the OpenSSL executable or its path. The default command name is `openssl`.

- `openssl_arguments(Arguments)` - List accepted by the option checker for OpenSSL command customization. The current facade implementation does not add these arguments to the generated OpenSSL command.

## Validation policy

The verification predicates apply a conservative JWT validation policy in addition to signature verification:

- JWT headers and claims must decode to JSON objects. Objects with duplicate member names are rejected instead of using first-wins or last-wins semantics.

- JWS `crit` header parameters are rejected unless explicitly supported by the library. The current implementation does not support extension header parameters that can be marked critical.

- The `exp` claim is required by default. This is stricter than the base JWT specification, where registered claims are optional. Use the option `allow_missing_exp(true)` when verifying tokens whose issuer legitimately omits this claim.

- Use `allow_algorithms/1` to configure the allowed algorithm set. Use `algorithm/1` when the caller expects one exact JOSE `alg` value. When both options are present, both constraints must be satisfied.

- When verifying with a JWK Set, all keys matching the token header `alg` and optional `kid` are tried in set order until a signature verifies. Verification fails only after all matching candidate keys fail.

- `HS256` symmetric keys must contain at least 32 bytes of key material, matching the algorithm’s 256-bit security requirement.

- RSA JWK `n` and `e` members must be minimally encoded positive Base64urlUInt values. `RS256` public moduli must be odd and at least 2048 bits, and public exponents must be odd integers greater than one and smaller than the modulus.

- P-256 EC JWK coordinates must be 32-byte unsigned values in range and the point `(x,y)` must lie on the P-256 curve.

- Octet, RSA, and P-256 EC JWKs are validated before key material is used. Malformed key objects raise JWT key-domain errors.

OpenSSL-backed `RS256` and `ES256` verification requires an `openssl` command in the current command search path, unless overridden using `openssl_executable/1`.

## Basic usage

Decode a compact JWT without verifying its signature:

    | ?- jwt::decode(Token, Header, Claims).

Inspect only selected header or claims data:

    | ?- jwt::peek_algorithm(Token, Algorithm).

    | ?- jwt::claim(Claims, sub, Subject).

Sign and verify a token using native `HS256` support:

    | ?- Header = {alg-'HS256', typ-'JWT'},
         Claims = {sub-'123', exp-4102444800},
         jwt::sign(Header, Claims, '0123456789abcdef0123456789abcdef', Token, []),
         jwt::verify(Token, '0123456789abcdef0123456789abcdef', VerifiedClaims, []).

Validate claims independently from signature verification:

    | ?- Policy = [
             claim(iss, expected('https://issuer.example')),
             claim(aud, contains('client-id')),
             claim(exp, time(expiration))
         ],
         jwt::validate_claims(Claims, Policy, [now(1700000001)]).

Verify an asymmetric token using a JWK Set. The library selects matching keys using the JWT header `alg` and optional `kid` values:

    | ?- JWKSet = {keys-[PublicJWK]},
         jwt::verify(Token, JWKSet, Claims, [
             allow_algorithms(['RS256', 'ES256']),
             claim_policy([
                 claim(iss, expected('https://issuer.example')),
                 claim(aud, contains('client-id'))
             ])
         ]).

Use the `openssl_executable/1` option when the OpenSSL command is not named `openssl` or is not found in the default command search path:

    | ?- jwt::verify(Token, JWKSet, Claims, [
             openssl_executable('/opt/local/bin/openssl')
         ]).