JWT Auth — vyuh_server_plugin_auth_jwt
AuthPlugin that contributes a single JwtBearerStrategy. The strategy extracts a Bearer <token> from the Authorization header, calls your verifier callback, and maps the resulting claims to an Actor.
Install
dependencies:
vyuh_server_plugin_auth_jwt:
hosted: https://pub.vyuh.tech
version: ^0.1.0Wiring
The plugin asks one thing of you: a verifier callback that, given a token, returns claims (or throws on invalid token). Use any JWT library — dart_jsonwebtoken, jose, an OAuth provider SDK.
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:vyuh_server/vyuh_server.dart';
import 'package:vyuh_server_plugin_auth_jwt/vyuh_server_plugin_auth_jwt.dart';
Future<Map<String, Object?>> verifyToken(String token) async {
final jwt = JWT.verify(token, RSAPublicKey(publicKeyPem));
return jwt.payload as Map<String, Object?>;
}
final runtime = await VyuhServer.bootstrap(
name: 'saas-api',
plugins: [
JwtAuthPlugin(verifier: verifyToken),
],
);Claim Mapping
By default the plugin maps these JWT claims onto Actor:
| Claim | Actor field |
|---|---|
sub | actor.id |
tenant_id | actor.tenantId |
roles | actor.roles |
email | actor.email |
Customize per-deployment:
JwtAuthPlugin(
verifier: verifyToken,
subjectClaim: 'user_id', // override sub
tenantClaim: 'org_id', // override tenant_id
rolesClaim: 'https://app/roles', // namespaced Auth0 claim
emailClaim: 'preferred_email',
);Any unmapped claims are still available as actor.claims['...'].
JWKS / OAuth Providers
For Auth0, Cognito, Firebase, Okta, and similar identity providers, write a verifier that fetches and caches the provider's JWKS:
import 'package:http/http.dart' as http;
import 'package:jose/jose.dart';
class JwksVerifier {
JwksVerifier({required this.jwksUri, required this.audience, required this.issuer});
final Uri jwksUri;
final String audience;
final String issuer;
JsonWebKeyStore? _store;
DateTime? _fetchedAt;
static const _cacheTtl = Duration(hours: 12);
Future<Map<String, Object?>> verify(String token) async {
final store = await _getStore();
final jwt = await JsonWebToken.decodeAndVerify(token, store);
final claims = jwt.claims;
if (claims.issuer.toString() != issuer) {
throw const AuthFailed(reason: 'bad_issuer');
}
if (!claims.audience!.any((a) => a == audience)) {
throw const AuthFailed(reason: 'bad_audience');
}
if (claims.expiry == null || claims.expiry!.isBefore(DateTime.now().toUtc())) {
throw const AuthFailed(reason: 'expired');
}
return claims.toJson();
}
Future<JsonWebKeyStore> _getStore() async {
final now = DateTime.now();
if (_store == null ||
_fetchedAt == null ||
now.difference(_fetchedAt!) > _cacheTtl) {
final res = await http.get(jwksUri);
_store = JsonWebKeyStore()
..addKeySet(JsonWebKeySet.fromJson(jsonDecode(res.body)));
_fetchedAt = now;
}
return _store!;
}
}
// Wire it:
final verifier = JwksVerifier(
jwksUri: Uri.parse('https://auth.example.com/.well-known/jwks.json'),
audience: 'https://api.example.com',
issuer: 'https://auth.example.com/',
);
final runtime = await VyuhServer.bootstrap(
plugins: [
JwtAuthPlugin(verifier: verifier.verify),
],
);Fallback Actor
By default an unauthenticated request gets AnonymousActor. Override for services that should reject every anonymous request:
JwtAuthPlugin(
verifier: verifyToken,
fallback: const ServiceActor(id: 'reject', serviceName: 'reject'),
);GlobalMiddleware.auth() resolves an actor for every request when you include it in runtime.start. Only protected paths reject the fallback actor; public endpoints can still see it without 401-ing.
Combining with API Keys
Multiple auth plugins coexist. Stack them:
plugins: [
JwtAuthPlugin(verifier: verifyToken), // user-facing
ApiKeyAuthPlugin(lookup: lookupApiKey), // partner integrations
],A request walks both strategies in order. The first one to return an Actor wins. The first to throw AuthFailed stops the chain (the framework surfaces the structured error).
Protecting Routes
The plugin itself doesn't enforce anything — auth runs on paths the framework knows are protected. Declare them through RouteModule:
RouteModule(
name: 'app.profiles',
basePath: '/profiles',
protectedPaths: ['/'],
setup: (scope) {
scope.get('/me', _meHandler);
},
);Or imperatively in extend:
extend: (scope) {
scope.protect(['/admin', '/internal']);
scope.get('/admin/users', _listUsersHandler);
}The framework installs GlobalMiddleware.requireAuthenticated() on protected paths. Inside the handler:
Future<Response> meHandler(Request req) async {
final actor = req.actor;
return jsonResponse(body: {
'success': true,
'data': {
'user_id': actor.id,
'email': actor.claims['email'],
'tenant_id': actor.tenantId,
},
});
}Where to Go Next
- API Key Auth — partner / webhook auth
- Middleware & Context — where auth sits in the onion
- The
vyuhAccessor —vyuh.authin depth