Skip to content

Security model

The studio has one trust level: a signed-in admin can read, create, change and delete rows in every registered table. Its security features are about making sure only the right people sign in, and that a signed-in browser can’t be abused by other sites.

Read this page before exposing the studio anywhere other than your laptop, and when choosing between session and JWT auth.

If you need roles or per-table permissions, the studio’s model is not enough on its own; see When to use the studio.

GisilaStudio picks its auth in this order:

  1. the auth: argument, if given;
  2. otherwise username/password arguments, falling back to the STUDIO_USERNAME and STUDIO_PASSWORD environment variables. With a secret (or STUDIO_SECRET) this builds a JwtStudioAuth; without one, a SessionStudioAuth;
  3. otherwise no auth.

When handler() is called, the studio checks the result. It serves a “Studio is disabled” page with status 503 for every request, and logs the reason to stderr, when:

  • there is no auth and allowUnauthenticated is false; or
  • StudioAuth.weaknessReason() returns a reason and allowInsecure is false (it defaults to GISILA_ALLOW_INSECURE=1).

The built-in strategies call it weak when the username is empty, the password is shorter than 12 characters, equals the username (case-insensitively) or contains a placeholder such as changeme or yourpassword, or (JWT only) the signing secret is shorter than 32 characters or a placeholder. The rest of your app keeps running either way. Overriding either check logs a warning.

SessionStudioAuth JwtStudioAuth
Token 32 random bytes, kept in a server-side set HS256 JWT with sub, iat, exp
Cookie _studio_sid _studio_jwt (or Authorization: Bearer)
Survives restart No Yes, until exp (default 8 hours)
Multiple isolates / instances No: a session works only on the isolate that issued it Yes, if they share the secret
Logout Revokes the session Clears the cookie; the token stays valid until it expires

Use JWT when you run more than one isolate (shared: true) or more than one instance.

Cookies are HttpOnly; SameSite=Lax and scoped to the mount prefix (Path=/admin). The studio adds Secure when the request came over HTTPS, either directly or according to the first value of X-Forwarded-Proto. JwtStudioAuth(httpOnly: false) drops HttpOnly so page scripts can read the token; only do this if you need it.

With auth configured, every request except login/ must pass isAuthenticated, or it is redirected (302) to <prefix>/login/.

Every POST that authenticates with the cookie must carry a _csrf form field equal to studioCsrfToken(token), a SHA-256 derivation of the session token. Otherwise the studio answers 403. The studio inserts the hidden field into every <form method="post"> in the HTML it renders. Requests authenticated with Authorization: Bearer are exempt, since another site cannot make a browser send that header.

The token is derived, not stored, so all isolates agree on it.

Ten failed sign-ins from one client address within 15 minutes block further attempts from that address with 429 and Retry-After: 900 until the window passes. The counter is in memory and per isolate. The client address is the TCP peer, so behind a reverse proxy every client shares the proxy’s address and one attacker can lock everyone out for 15 minutes.

Credentials and JWT signatures are compared in constant time.

Columns in passwordFields (by default any column whose name contains password, passwd or secret) are rendered as password inputs that are never pre-filled, left out of the default list columns and the delete summary, and kept unchanged when left blank on edit.

The studio stores exactly what is typed. If the column holds a password hash, give it an onSave hook that hashes with the same function your app uses, or make it read-only. register() logs a warning for every editable password column without a hook.

  • There is no per-user identity: all admins share one account (or whatever your custom StudioAuth accepts), and there is no audit log.
  • JwtStudioAuth.revokeToken is a no-op; a stolen JWT stays valid until exp.
  • Database error messages are shown on the form (first line only).

A production-style setup: JWT auth from environment variables, served only over HTTPS behind a proxy.

import 'dart:io';
import 'package:gisila_studio/gisila_studio.dart';
final env = Platform.environment;
final studio = GisilaStudio(
db: db,
title: 'Shop Admin',
auth: JwtStudioAuth(
username: env['STUDIO_USERNAME']!,
password: env['STUDIO_PASSWORD']!,
secret: env['STUDIO_SECRET']!, // 32+ random characters
expiry: const Duration(hours: 4),
),
);