Security model
What it is
Section titled “What it is”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.
When to use it
Section titled “When to use it”Read this page before exposing the studio anywhere other than your laptop, and when choosing between session and JWT auth.
When not to use it
Section titled “When not to use it”If you need roles or per-table permissions, the studio’s model is not enough on its own; see When to use the studio.
How it works
Section titled “How it works”Login is mandatory by default
Section titled “Login is mandatory by default”GisilaStudio picks its auth in this order:
- the
auth:argument, if given; - otherwise
username/passwordarguments, falling back to theSTUDIO_USERNAMEandSTUDIO_PASSWORDenvironment variables. With asecret(orSTUDIO_SECRET) this builds aJwtStudioAuth; without one, aSessionStudioAuth; - 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
allowUnauthenticatedisfalse; or StudioAuth.weaknessReason()returns a reason andallowInsecureisfalse(it defaults toGISILA_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.
Session vs JWT
Section titled “Session vs JWT”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
Section titled “Cookies”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.
Request gate
Section titled “Request gate”With auth configured, every request except login/ must pass isAuthenticated, or it is
redirected (302) to <prefix>/login/.
CSRF protection
Section titled “CSRF protection”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.
Sign-in throttling
Section titled “Sign-in throttling”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.
Secret columns
Section titled “Secret columns”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.
What is not protected
Section titled “What is not protected”- There is no per-user identity: all admins share one account (or whatever your custom
StudioAuthaccepts), and there is no audit log. JwtStudioAuth.revokeTokenis a no-op; a stolen JWT stays valid untilexp.- Database error messages are shown on the form (first line only).
Example
Section titled “Example”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), ),);