server_auth 0.1.0
server_auth: ^0.1.0 copied to clipboard
Framework-agnostic authentication runtime capabilities for server ecosystems.
server_auth #
Framework-agnostic authentication runtime primitives and provider implementations.
Includes built-in providers for Google, Discord, Microsoft Entra, Apple, Twitter/X, Facebook, GitLab, Slack, Spotify, LinkedIn, Twitch, Dropbox, and Telegram.
server_auth is designed to be consumed by framework adapters. It provides auth
building blocks (providers, JWT, CSRF, gates/authorization, callbacks, token
utilities) without requiring Routed-specific runtime types.
Installation #
dependencies:
server_auth: ^0.1.0
Entry points #
package:server_auth/server_auth.dart(umbrella export)
Use the package umbrella library for the public API.
Package Selection #
- Use
server_authfor auth runtime primitives and provider implementations. - Use
server_contractsfor contract-only abstractions. - Use adapter packages (
routed,shelf_auth, etc.) for framework-specific HTTP/session wiring.
Quick start #
import 'package:server_auth/server_auth.dart';
final google = googleProvider(
GoogleProviderOptions(
clientId: 'google-client-id',
clientSecret: 'google-client-secret',
redirectUri: 'https://example.com/auth/callback/google',
),
);
final providers = <AuthProvider>[google];
Use providers with your framework adapter to wire callback routes, session
handling, and auth lifecycle.
For Routed applications, use routed_auth: initialize AuthServiceProvider
alongside Engine.defaultProviders, then add the adapter's auth middleware and
routes. server_auth itself never registers framework providers.
Using with Shelf #
Use shelf_auth for Shelf-specific middleware while keeping providers and
auth contracts in server_auth.
dependencies:
server_auth: ^0.1.0
shelf_auth: ^0.1.0
import 'package:server_auth/server_auth.dart';
import 'package:shelf_auth/shelf_auth.dart';
final providers = <AuthProvider>[
const AuthProvider(
id: 'credentials',
name: 'Credentials',
type: AuthProviderType.credentials,
),
];
final middleware = authProvidersEndpoint(providers: providers);
JWT issue + verify example #
import 'package:server_auth/server_auth.dart';
final options = const JwtSessionOptions(
secret: 'replace-with-a-strong-secret',
issuer: 'example-app',
audience: <String>['example-api'],
maxAge: Duration(minutes: 30),
);
final issued = issueAuthJwtToken(
options: options,
claims: <String, dynamic>{'sub': 'user_42', 'roles': <String>['admin']},
);
final verifier = JwtVerifier(options: options.toVerifierOptions());
final payload = await verifier.verifyToken(issued.token);
print(payload.subject);
final refreshed = await refreshAuthJwtTokenIfNeeded(
options: options,
claims: payload.claims,
updateAge: const Duration(minutes: 15),
resolveClaims: (claims) => claims,
);
final verifiedSession = await verifyAuthJwtSessionToken(
token: issued.token,
options: options,
);
print(verifiedSession?.user.id);
final resolvedSession = await resolveAuthJwtSessionWithRefresh(
token: issued.token,
options: options,
updateAge: const Duration(minutes: 15),
resolveClaims: (claims, user) => claims,
);
print(resolvedSession?.refreshCookie != null); // refreshed or not
Bearer validation helpers for adapters #
Use verifyJwtBearerAuthorizationAndWriteAttributes in framework middleware to
share JWT bearer auth orchestration (token extraction, verification, request
attribute writes, and optional callback):
final result = await verifyJwtBearerAuthorizationAndWriteAttributes<MyContext>(
authorizationHeader: request.header('authorization'),
verifier: jwtVerifier,
setAttribute: request.setAttribute,
context: context,
onVerified: (payload, ctx) async {
if (payload.claims['scope'] == null) {
throw JwtAuthException('missing_scope');
}
},
);
print(result.payload.subject);
For OAuth introspection middleware, use
validateOAuthBearerAuthorizationAndWriteAttributes:
final validation =
await validateOAuthBearerAuthorizationAndWriteAttributes<MyContext>(
authorizationHeader: request.header('authorization'),
introspector: oauthIntrospector,
setAttribute: request.setAttribute,
context: context,
onValidated: (result, ctx) async {
if (result.scope?.contains('api:read') != true) {
throw OAuth2Exception('insufficient_scope');
}
},
);
print(validation.result.raw);
Email verification orchestration helpers #
Use startAuthEmailSignIn to share the email verification start flow:
final payload = await startAuthEmailSignIn<MyContext>(
adapter: adapter,
tokenStore: tokenStore,
provider: emailProvider,
context: context,
email: 'user@example.com',
callbackUrl: '/dashboard',
sessionStrategy: AuthSessionStrategy.session,
callbackKey: '_auth.callback',
writeSession: (key, value) => sessionStore[key] = value,
);
print(payload.pendingResult.session.strategy);
Use resolveAuthEmailVerificationSignIn in callback handlers:
final resolved = await resolveAuthEmailVerificationSignIn(
adapter: adapter,
tokenStore: tokenStore,
email: email,
token: token,
callbackKey: '_auth.callback',
readSession: (key) => sessionStore[key],
);
Credentials orchestration helpers #
Use requireAuthorizedCredentialsSignIn and
requireAuthorizedCredentialsRegistration to perform provider/adapter
credential resolution with standard auth errors:
final user = await requireAuthorizedCredentialsSignIn(
adapter: adapter,
provider: credentialsProvider,
context: context,
credentials: credentials,
);
OAuth callback orchestration helper #
Use resolveOAuthSignInForProvider in framework adapters to share OAuth
callback exchange/profile/user resolution logic without depending on Routed.
final resolved = await resolveOAuthSignInForProvider<MyContext, Map<String, dynamic>>(
adapter: adapter,
context: context,
provider: oauthProvider,
code: authorizationCode,
codeVerifier: pkceVerifier,
httpClient: httpClient,
);
await adapter.linkAccount(resolved.account);
print(resolved.user.id);
print(resolved.profile);
Use resolveOAuthAuthorizationStart to share OAuth begin-flow state/PKCE/callback
session persistence:
final start = await resolveOAuthAuthorizationStart<MyContext, Map<String, dynamic>>(
context: context,
provider: oauthProvider,
stateKey: '_auth.state',
pkceKey: '_auth.pkce',
callbackKey: '_auth.callback',
callbackUrl: '/dashboard',
writeSession: (key, value) => sessionStore[key] = value,
);
return start.authorizationUri;
For callback handlers, resolveOAuthCallbackSessionValues reads state/verifier/
callback URL using the same key contract:
final values = resolveOAuthCallbackSessionValues(
providerId: oauthProvider.id,
stateKey: '_auth.state',
pkceKey: '_auth.pkce',
callbackKey: '_auth.callback',
readSession: (key) => sessionStore[key],
);
Or use the higher-level callback helper to validate state, resolve sign-in, and persist linked accounts in one call:
final callback = await resolveOAuthCallbackSignInForProvider<MyContext, Map<String, dynamic>>(
adapter: adapter,
context: context,
provider: oauthProvider,
code: authorizationCode,
receivedState: stateFromRequest,
stateKey: '_auth.state',
pkceKey: '_auth.pkce',
callbackKey: '_auth.callback',
readSession: (key) => sessionStore[key],
httpClient: httpClient,
);
print(callback.signIn.user.id);
print(callback.callbackUrl);
Adapter attribute mapping helpers #
When writing framework adapters, use these helpers to store verified auth payloads with consistent attribute keys:
import 'package:server_auth/server_auth.dart';
final attributes = <String, Object?>{};
writeJwtPayloadAttributes(
payload,
setAttribute: (key, value) => attributes[key] = value,
);
writeOAuthValidationAttributes(
oauthValidation,
setAttribute: (key, value) => attributes[key] = value,
);
final callbackUrl = resolveAndSanitizeRedirectCandidate(
payload,
queryParameters,
requestUri: requestUri,
fallbackHost: 'app.test',
fallbackScheme: 'https',
);
final callbackFromResolver = await resolveAndSanitizeRedirectWithResolver(
payload,
queryParameters,
requestUri: requestUri,
fallbackHost: 'app.test',
fallbackScheme: 'https',
resolveRedirect: (candidate) async => candidate,
);
Callback helper for redirect fallbacks #
Use resolveAuthRedirectTargetWithFallback when your adapter supports
redirect callbacks but should preserve a framework-provided fallback URL.
final resolvedUrl = await resolveAuthRedirectTargetWithFallback<MyContext>(
callback: callbacks.redirect,
context: AuthRedirectCallbackContext<MyContext>(
context: context,
url: '/requested',
baseUrl: 'https://app.test',
),
fallbackUrl: '/requested',
);
When you already have an AuthCallbacks<TContext> instance, use the compact
wrapper:
final resolvedFromCallbacks = await resolveAuthRedirectWithCallbacks<MyContext>(
callbacks: callbacks,
context: context,
url: '/requested',
baseUrl: 'https://app.test',
);
The same pattern exists for JWT/session callback orchestration:
final signInRedirect = await resolveAuthSignInRedirectWithCallbacks<MyContext>(
callbacks: callbacks,
context: context,
user: user,
strategy: AuthSessionStrategy.session,
callbackUrl: '/requested',
);
// Throws AuthFlowException('sign_in_blocked') if callbacks.signIn denies.
final finalRedirect = await resolveAuthSignInRedirectTarget<MyContext>(
callbacks: callbacks,
context: context,
user: user,
strategy: AuthSessionStrategy.session,
callbackUrl: '/requested',
resolveRedirect: (candidate) => adapterResolveRedirect(candidate),
);
final claims = await resolveAuthJwtClaimsWithCallbacks<MyContext>(
callbacks: callbacks,
context: context,
user: user,
strategy: AuthSessionStrategy.jwt,
);
final sessionPayload = await resolveAuthSessionPayloadWithCallbacks<MyContext>(
callbacks: callbacks,
context: context,
session: session,
strategy: AuthSessionStrategy.session,
);
final jwtIssue = await issueAuthJwtSessionWithCallbacks<MyContext>(
callbacks: callbacks,
context: context,
options: jwtOptions,
user: user,
strategy: AuthSessionStrategy.jwt,
);
final signInResult = await resolveAuthSignInResultForStrategyWithCallbacks<MyContext>(
callbacks: callbacks,
context: context,
strategy: AuthSessionStrategy.jwt,
user: user,
redirectUrl: finalRedirect,
jwtOptions: jwtOptions,
);
final authResult = signInResult.result;
final issuedJwtCookie = signInResult.issuedJwt?.cookie;
Authorization and gates example #
import 'package:server_auth/server_auth.dart';
final gates = AuthGateService<Map<String, dynamic>>();
gates.register('posts.update', rolesGate(<String>['editor', 'admin'], any: true));
final principal = AuthPrincipal(id: 'user_42', roles: <String>['admin']);
final allowed = await gates.can(
'posts.update',
context: <String, dynamic>{'resourceId': 'post_1'},
principal: principal,
);
print(allowed); // true
// Managed gate registration that preserves unmanaged entries:
final managed = <String>{};
final registered = registerGateCallbacksSafely<Map<String, dynamic>>(
gates.registry,
<String, AuthGateCallback<Map<String, dynamic>>>{
'posts.publish': rolesGate(<String>['editor']),
},
managed: managed,
);
managed
..clear()
..addAll(registered);
Framework Adapter Session Runtime #
RememberSessionAuthRuntime<TContext> provides framework-agnostic
remember-me/session principal logic. Frameworks supply an
AuthSessionRuntimeAdapter<TContext> to map request/session/cookie behavior.
import 'package:server_auth/server_auth.dart';
final runtime = RememberSessionAuthRuntime<MyContext>(
adapter: myAdapter,
rememberCookieName: 'remember_token',
defaultRememberDuration: const Duration(days: 30),
sessionPrincipalKey: '__auth.principal',
);
await runtime.login(
context,
AuthPrincipal(id: 'user-1', roles: const <String>['user']),
rememberMe: true,
);
await runtime.hydrate(context); // restore/rotate remember token when needed
await runtime.logout(context);
For adapters that keep issued-at metadata, use
syncAuthSessionRefresh to apply initialize/refresh/keep behavior:
syncAuthSessionRefresh(
issuedAtValue: session['__auth.session.issued_at'] as String?,
updateAge: const Duration(minutes: 5),
writeIssuedAt: (value) {
session['__auth.session.issued_at'] = serializeAuthSessionIssuedAt(value);
},
touchSession: () => sessionTouch(),
);
Minimal adapter skeleton #
Use a small framework-specific adapter that maps your persistence layer into
server_auth contracts:
class MyAuthAdapter extends AuthAdapter {
final Map<String, AuthUser> usersById = <String, AuthUser>{};
@override
FutureOr<AuthUser?> getUserById(String id) {
return usersById[id];
}
@override
FutureOr<AuthUser> createUser(AuthUser user) {
usersById[user.id] = user;
return user;
}
@override
FutureOr<AuthSession?> getSession(String sessionToken) {
// Query your DB or cache here.
return null;
}
}
Keep adapters focused on boundary mapping and keep provider/JWT/gate logic in
server_auth.
Typed Profiles #
Every OAuth provider includes a typed profile model and serializer/parsers, so user info mapping can stay type-safe.
Telegram (Non-OAuth) #
Telegram uses widget-based auth with HMAC verification via telegramProvider.
Runnable example #
dart run example/main.dart
See example/main.dart for provider registration, JWT flows, and gate checks.
See example/README.md for run instructions and expected output.
Migration Notes #
If older code imported provider factories or auth primitives from Routed
entrypoints, switch to direct server_auth imports to keep auth logic reusable
across frameworks.
Validation #
dart analyze
dart test
dart run example/main.dart
License #
MIT