Skip to content

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.

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.

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.

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);
},
)

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'},
);

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.

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.

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');

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'},
)

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.
typedef JwtPrincipalMapper = FutureOr<Principal?> Function(JwtClaims claims, Request request);

Signature of JwtAuthenticator.toPrincipal.

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.

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.