Skip to content

Auth

How these pieces fit together is explained in Security model.

Interface for studio authentication strategies.

abstract class StudioAuth {
bool isAuthenticated(Map<String, String> headers);
String? authenticate(String username, String password);
void revokeToken(String token);
String? currentToken(Map<String, String> headers);
String setCookieHeader(String token, {String prefix = ''});
String clearCookieHeader({String prefix = ''});
String? weaknessReason() => null;
}

When to use: implement it to check credentials against something other than one fixed username and password, such as your users table or LDAP. All signed-in users still get full access to every registered table. For the common case, use SessionStudioAuth or JwtStudioAuth.

Member Description
isAuthenticated(headers) true if the request headers (lower-case keys) carry a valid credential. Called on every request except login/.
authenticate(username, password) Returns a token to store in the cookie, or null for bad credentials. Synchronous.
revokeToken(token) Called on logout.
currentToken(headers) The raw token from the cookie or header, or null. The CSRF token is derived from it.
setCookieHeader(token, {prefix}) Set-Cookie value installing the token, scoped to prefix (/ when empty). The studio appends Secure on HTTPS.
clearCookieHeader({prefix}) Set-Cookie value clearing the cookie.
weaknessReason() Why the credentials are too weak to serve, or null. The studio refuses to serve while this is non-null, unless allowInsecure. Defaults to null.

Example

import 'dart:convert';
import 'dart:math';
import 'package:gisila_studio/gisila_studio.dart';
/// Accepts any staff member from an in-memory map of username -> password.
class StaffAuth extends StudioAuth {
StaffAuth(this.staff);
final Map<String, String> staff;
final _sessions = <String>{};
final _rng = Random.secure();
@override
String? authenticate(String username, String password) {
final expected = staff[username];
if (expected == null || !constantTimeEquals(password, expected)) return null;
final token = base64Url.encode(List.generate(32, (_) => _rng.nextInt(256)));
_sessions.add(token);
return token;
}
@override
bool isAuthenticated(Map<String, String> headers) {
final t = currentToken(headers);
return t != null && _sessions.contains(t);
}
@override
String? currentToken(Map<String, String> headers) {
for (final part in (headers['cookie'] ?? '').split(';')) {
final p = part.trim();
if (p.startsWith('staff_sid=')) return p.substring('staff_sid='.length);
}
return null;
}
@override
void revokeToken(String token) => _sessions.remove(token);
@override
String setCookieHeader(String token, {String prefix = ''}) =>
'staff_sid=$token; Path=${prefix.isEmpty ? '/' : prefix}; HttpOnly; SameSite=Lax';
@override
String clearCookieHeader({String prefix = ''}) =>
'staff_sid=; Path=${prefix.isEmpty ? '/' : prefix}; HttpOnly; SameSite=Lax; Max-Age=0';
@override
String? weaknessReason() {
for (final e in staff.entries) {
final reason = credentialWeakness(e.key, e.value);
if (reason != null) return '${e.key}: $reason';
}
return null;
}
}

In-memory sessions with a cookie.

class SessionStudioAuth extends StudioAuth {
SessionStudioAuth({required this.username, required this.password});
final String username;
final String password;
}

When to use: a single-isolate server where losing sessions on restart is acceptable. It is what GisilaStudio builds from username/password when no secret is given. For several isolates or instances use JwtStudioAuth.

  • Sign-in creates a random 32-byte token (base64url) stored in a server-side set and in the _studio_sid cookie (HttpOnly; SameSite=Lax, Path = prefix).
  • Logout removes the token from the set.
  • Sessions do not expire on their own; they last until logout or restart.
  • weaknessReason() is credentialWeakness(username, password).

Example

final studio = GisilaStudio(
db: db,
auth: SessionStudioAuth(username: 'admin', password: 'correct-horse-battery'),
);

Stateless HS256 JWTs.

class JwtStudioAuth extends StudioAuth {
JwtStudioAuth({
required this.username,
required this.password,
required this.secret,
this.expiry = const Duration(hours: 8),
this.httpOnly = true,
});
}

When to use: whenever the studio runs on more than one isolate or instance, or must keep admins signed in across restarts. GisilaStudio builds one when you pass secret (or set STUDIO_SECRET).

Parameter Type Default Description
username String required Admin username.
password String required Admin password.
secret String required HMAC-SHA256 signing key. At least 32 characters, or the studio refuses to serve.
expiry Duration 8 hours Token lifetime (exp claim).
httpOnly bool true When false, the _studio_jwt cookie is readable by page scripts.
  • The token carries sub (the username), iat and exp. A token is valid when its signature matches and exp is in the future.
  • Tokens are read from Authorization: Bearer <token> first, then from the _studio_jwt cookie. Bearer requests skip the CSRF check.
  • revokeToken does nothing: logout clears the cookie, but a copied token stays valid until it expires. Keep expiry short, or override isAuthenticated to consult a block list.
  • weaknessReason() checks the credentials, then secretWeakness(secret).

Example

import 'dart:io';
final studio = GisilaStudio(
db: db,
auth: JwtStudioAuth(
username: 'admin',
password: Platform.environment['STUDIO_PASSWORD']!,
secret: Platform.environment['STUDIO_SECRET']!,
expiry: const Duration(hours: 12),
),
);
String? credentialWeakness(String username, String password);

Returns why a username/password pair is too weak for the studio, or null.

When to use: in a custom StudioAuth.weaknessReason, or to validate credentials in a setup script. Rejects an empty username, a password shorter than minStudioPasswordLength, a password equal to the username (ignoring case), and passwords containing changeme, changethis, yoursecret or yourpassword (ignoring case and punctuation).

credentialWeakness('admin', 'admin'); // 'the password is shorter than 12 characters'
credentialWeakness('admin', 'correct-horse-battery'); // null
String? secretWeakness(String secret);

Returns why a signing secret is too weak, or null.

When to use: in a custom strategy that signs tokens. Rejects secrets shorter than minStudioSecretLength and placeholder values.

const minStudioPasswordLength = 12;

Shortest password credentialWeakness accepts.

const minStudioSecretLength = 32;

Shortest signing secret secretWeakness accepts.

String studioCsrfToken(String token);

The CSRF token for a session token: base64url SHA-256 of 'gisila-studio-csrf:' + token, without padding.

When to use: when you render your own POST form that is submitted to a studio route, or to your own route that checks the studio session. Put it in a hidden _csrf field. The studio adds the field to its own forms automatically.

final token = auth.currentToken(request.headers);
final field = token == null
? ''
: '<input type="hidden" name="_csrf" value="${studioCsrfToken(token)}">';
bool constantTimeEquals(String a, String b);

Compares two strings in time that does not depend on where they differ (both are SHA-256 hashed first, so lengths do not leak either).

When to use: comparing passwords, tokens or CSRF values in a custom StudioAuth or custom route.