JWT
import 'package:gisila/gisila.dart';Signatures are checked by package:dart_jsonwebtoken. Everything around the signature (allowed algorithms, which key may verify which algorithm, exp/nbf/iat with leeway, issuer, audience, token type) is enforced by gisila, so a token can’t choose its own algorithm or skip expiry.
JwtAuthenticator
Section titled “JwtAuthenticator”Verifies Authorization: Bearer <jwt> and maps the claims to a Principal.
class JwtAuthenticator extends Authenticator { JwtAuthenticator({ JwtKey? key, JwtKeySet? keys, required Set<String> algorithms, String? issuer, Set<String> audience = const {}, Duration leeway = const Duration(seconds: 30), bool requireExpiry = true, Duration? maxAge, String? requiredType, String scheme = 'Bearer', String? Function(Request request)? tokenFromRequest, JwtPrincipalMapper? toPrincipal, DateTime Function()? now, });
Future<JwtClaims> verify(String token); static Principal? defaultPrincipal(JwtClaims claims, Request request);}When to use: stateless auth for SPAs and mobile apps, tokens you issue yourself with JwtSigner, or tokens from an identity provider (Auth0, Keycloak, Cognito) through RemoteJwks. If you need instant revocation, sessions are simpler; with JWTs keep lifetimes short or check a deny-list in toPrincipal.
| Parameter | Type | Default | Description |
|---|---|---|---|
key |
JwtKey? |
The verification key. Give exactly one of key and keys. A private RSA/EC key is refused (verify with the public key). |
|
keys |
JwtKeySet? |
A key set for rotation or a remote JWKS. | |
algorithms |
Set<String> |
required | Accepted alg values, from supportedJwtAlgorithms. Each must fit key’s type, so an RSA key with {'HS256'} is refused at construction. Must not be empty. |
issuer |
String? |
null |
Required iss. |
audience |
Set<String> |
{} |
When non-empty, the token’s aud must contain one of these. |
leeway |
Duration |
30 seconds | Clock skew allowed on exp, nbf and iat. |
requireExpiry |
bool |
true |
Reject tokens without exp. |
maxAge |
Duration? |
null |
Tokens need an iat no older than this. |
requiredType |
String? |
null |
Header typ must equal this (case-insensitive), for example 'at+jwt' so ID tokens can’t pass as access tokens. |
scheme |
String |
'Bearer' |
Authorization scheme, compared case-insensitively. |
tokenFromRequest |
String? Function(Request)? |
null |
Read the token from somewhere else: a cookie, or a query parameter for EventSource. |
toPrincipal |
JwtPrincipalMapper? |
defaultPrincipal |
Map verified claims to a principal; return null to reject (user deleted or disabled). |
now |
DateTime Function()? |
DateTime.now |
Clock, for tests. |
No token: anonymous (null). A token that fails any check: 401 with code invalid_token, or token_expired for exp/maxAge, so clients know when to refresh. toPrincipal returning null is invalid_token.
verify()
Section titled “verify()”Future<JwtClaims> verify(String token)Runs every check and returns the claims, or throws the 401 HttpException. Useful outside requests, for example to validate a refresh token.
defaultPrincipal()
Section titled “defaultPrincipal()”static Principal? defaultPrincipal(JwtClaims claims, Request request)The mapping used without toPrincipal: sub becomes id (no sub means null, which rejects the token), the roles claim becomes roles, and scope plus permissions become permissions (each may be a list or a space-separated string). All claims go into Principal.claims.
Example
final env = Platform.environment;
final auth = JwtAuthenticator( key: JwtKey.hmac(env['JWT_SECRET']!), algorithms: {'HS256'}, issuer: 'my-api', toPrincipal: (claims, request) async { final userId = claims.subject; if (userId == null) return null; return Principal( id: userId, roles: claims.strings('roles'), permissions: claims.strings('scope'), claims: {'tenant': claims['tenant']}, ); },);
final app = GisilaApp(config: AppConfig(authenticator: auth));Reading the token from a cookie:
JwtAuthenticator( key: JwtKey.hmac(secret), algorithms: {'HS256'}, tokenFromRequest: (request) { final cookies = request.headers['cookie'] ?? ''; final match = RegExp(r'(?:^|;\s*)access_token=([^;]+)').firstMatch(cookies); return match?.group(1); },)JwtSigner
Section titled “JwtSigner”Issues tokens that a JwtAuthenticator with the same key, issuer and audience accepts.
class JwtSigner { JwtSigner({ required JwtKey key, required String algorithm, String? issuer, String? audience, Duration expiresIn = const Duration(hours: 1), DateTime Function()? now, });
String sign({ String? subject, Map<String, Object?> claims = const {}, Duration? expiresIn, String? jwtId, });}When to use: when your API issues its own tokens (login endpoints). The key must be able to sign: an HMAC key or an RSA/EC private key; a public key, or an algorithm that doesn’t fit the key, throws ArgumentError.
| Parameter | Type | Default | Description |
|---|---|---|---|
key |
JwtKey |
required | Signing key. Its id, when set, is written as the header kid. |
algorithm |
String |
required | One of supportedJwtAlgorithms that fits the key. |
issuer |
String? |
null |
Written as iss. |
audience |
String? |
null |
Written as aud. |
expiresIn |
Duration |
1 hour | Default lifetime. |
sign() always writes iat and exp, plus sub, iss, aud and jti when set. Entries in claims can’t overwrite those.
final signer = JwtSigner( key: JwtKey.hmac(env['JWT_SECRET']!), algorithm: 'HS256', issuer: 'my-api', expiresIn: const Duration(minutes: 15),);
final token = signer.sign( subject: user.id, claims: {'roles': ['staff'], 'scope': 'orders:read orders:write'},);JwtKey
Section titled “JwtKey”A key that signs or verifies tokens. HMAC keys do both; for RSA, EC and EdDSA the public key verifies and the private key signs.
class JwtKey { factory JwtKey.hmac(String secret, {String? id}); factory JwtKey.rsaPublicPem(String pem, {String? id}); factory JwtKey.rsaPrivatePem(String pem, {String? id}); factory JwtKey.ecPublicPem(String pem, {String? id}); factory JwtKey.ecPrivatePem(String pem, {String? id}); factory JwtKey.fromJwk(Map<String, Object?> jwk);
final bool isPrivate; final String? id; final String? algorithm;}| Constructor / field | Description |
|---|---|
JwtKey.hmac(secret) |
HS256/384/512. Throws ArgumentError when the secret is shorter than minJwtHmacSecretBytes (32 bytes). |
JwtKey.rsaPublicPem(pem) |
RS*/PS* verification. Accepts -----BEGIN PUBLIC KEY----- or an X.509 certificate. |
JwtKey.rsaPrivatePem(pem) |
RS*/PS* signing. |
JwtKey.ecPublicPem(pem) |
ES* verification. Public key or certificate. |
JwtKey.ecPrivatePem(pem) |
ES* signing. |
JwtKey.fromJwk(jwk) |
A JWK (kty oct, RSA, EC or OKP). Its kid becomes id and alg becomes algorithm. This is the only way to load an EdDSA key. An oct key under 32 bytes, an unknown kty or an alg that doesn’t fit kty throws ArgumentError. |
isPrivate |
true for RSA/EC/EdDSA private keys (which sign but don’t verify). |
id |
Key id, matched against the token header’s kid. |
algorithm |
When set, the only algorithm this key is used with. |
When to use: HMAC when one service both issues and verifies tokens; RSA/EC when other services verify tokens they can’t issue.
JwtKeySet
Section titled “JwtKeySet”abstract class JwtKeySet { const JwtKeySet(); Future<JwtKey?> find(String? kid);}Where JwtAuthenticator finds the key for a token. Implement it to load keys from your own store.
StaticJwtKeySet
Section titled “StaticJwtKeySet”class StaticJwtKeySet extends JwtKeySet { StaticJwtKeySet(Iterable<JwtKey> keys);}A fixed list of keys, for rotation: keep the old key in the list until the tokens it signed have expired. Tokens are matched by kid; a token without kid is accepted only when the set holds exactly one key. Throws ArgumentError when empty.
JwtAuthenticator( keys: StaticJwtKeySet([ JwtKey.hmac(newSecret, id: '2026-10'), JwtKey.hmac(oldSecret, id: '2026-07'), ]), algorithms: {'HS256'},)// Sign new tokens with the new key so they carry kid '2026-10':final signer = JwtSigner(key: JwtKey.hmac(newSecret, id: '2026-10'), algorithm: 'HS256');RemoteJwks
Section titled “RemoteJwks”Keys published as a JWKS document by an identity provider.
class RemoteJwks extends JwtKeySet { RemoteJwks( Uri url, { Duration cacheFor = const Duration(minutes: 10), Duration minRefreshInterval = const Duration(seconds: 30), Duration timeout = const Duration(seconds: 5), Future<String> Function(Uri url)? fetch, DateTime Function()? now, }); static const int maxDocumentBytes = 262144; final Uri url; final Duration cacheFor; final Duration minRefreshInterval; final Duration timeout;}When to use: verifying tokens from Auth0, Keycloak, Cognito, Google and similar.
| Parameter | Default | Description |
|---|---|---|
url |
required | Must be https (plain http only on a loopback host), otherwise ArgumentError. |
cacheFor |
10 minutes | How long the document is cached. |
minRefreshInterval |
30 seconds | A token with an unknown kid triggers an early refresh at most this often. |
timeout |
5 seconds | Fetch timeout. Redirects are not followed; documents over 256 KiB are refused. |
fetch |
null |
Load the document some other way (tests, a proxy). |
Only signature keys (use absent or sig) that can verify are kept; oct (HMAC) keys in a JWKS are ignored. When a refresh fails the previously loaded keys keep working; with none loaded, authentication fails with 401.
JwtAuthenticator( keys: RemoteJwks(Uri.parse('https://example.eu.auth0.com/.well-known/jwks.json')), algorithms: {'RS256'}, issuer: 'https://example.eu.auth0.com/', audience: {'https://api.example.com'},)JwtClaims
Section titled “JwtClaims”The verified contents of a token, passed to toPrincipal.
class JwtClaims { JwtClaims(Map<String, Object?> claims, {Map<String, Object?> header = const {}}); final Map<String, Object?> header; Object? operator [](String name); bool containsKey(String name); Map<String, Object?> toMap(); String? get subject; String? get issuer; String? get id; List<String> get audience; DateTime? get expiresAt; DateTime? get issuedAt; DateTime? get notBefore; Set<String> strings(String name);}| Member | Description |
|---|---|
header |
Token header (alg, kid, typ). |
[name], containsKey, toMap() |
Raw claim access. |
subject, issuer, id |
sub, iss, jti; numeric values are returned as strings. |
audience |
aud as a list (a single string becomes a one-element list). |
expiresAt, issuedAt, notBefore |
exp, iat, nbf as UTC DateTimes. |
strings(name) |
A claim holding a list of strings or a space-separated string (OAuth scope). Empty when absent. |
JwtPrincipalMapper
Section titled “JwtPrincipalMapper”typedef JwtPrincipalMapper = FutureOr<Principal?> Function(JwtClaims claims, Request request);Signature of JwtAuthenticator.toPrincipal.
supportedJwtAlgorithms
Section titled “supportedJwtAlgorithms”const Set<String> supportedJwtAlgorithms = { 'HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512', 'ES256', 'ES384', 'ES512', 'ES256K', 'EdDSA',};Algorithm names JwtAuthenticator and JwtSigner accept. none is never accepted.
minJwtHmacSecretBytes
Section titled “minJwtHmacSecretBytes”const int minJwtHmacSecretBytes = 32;Shortest HMAC secret JwtKey.hmac and JwtKey.fromJwk accept, in bytes. Generate one with openssl rand -base64 48.
See also: JWT authentication guide, Authentication and permissions.