-
Notifications
You must be signed in to change notification settings - Fork 7
Authentication
Summary: StartER offers a complete and secure password-less authentication implementation (Magic Link), based on opaque tokens stored in the database and a session JWT stored in a secure cookie.
The process in StartER relies on a login link sent by email:
- The user enters their email address.
- The server generates an opaque token (random bytes), hashes it, stores it in the database, and sends the link via SMTP (or displays it in the terminal during development).
- The user clicks the link and reaches a verification page.
- The server checks the token's validity (not expired, not consumed).
- A session JWT token is generated and stored in an HTTP-only cookie.
- The React frontend is notified of the login via
AuthContext.
Authentication features are grouped in the auth module:
src/express/modules/auth/
├── authActions.ts (Business logic and middleware)
├── authRepository.ts (Database token access)
└── authRoutes.ts (Endpoint definitions)
| Method | Route | Action | Description |
|---|---|---|---|
| POST | /api/auth/magic-link |
sendMagicLink |
Sends the login link by email |
| POST | /api/auth/verify |
verifyMagicLink |
Verifies the opaque token and sets the cookie |
| POST | /api/auth/logout |
destroyAccessToken |
Logout (deletes the cookie) |
| GET | /api/users/me |
readMe |
Returns the connected user (req.me) |
When a user requests a link, the server finds or creates the user, generates a secure random token, and then stores its SHA-256 hash in the database.
To facilitate local development without requiring the immediate configuration of a tool like Docker or Mailpit, StartER adopts a hybrid email approach:
-
In development: if the
SMTP_URLvariable is not defined in your.env, the magic login link will not be sent by email, but will be displayed in the console of your terminal. You can click on it to log in. -
In production:
SMTP_URLbecomes mandatory. The server will refuse to start without it to ensure the application is actually capable of sending emails to real users.
const magicLink = `${trustedBaseUrl}/verify?token=${rawToken}`;
if (transporter) {
// The transporter has been configured with the SMTP_URL variable from the .env
await transporter.sendMail({
from: "starter@mail.com",
to: email,
subject: "Lien de connexion",
html: `<a href="${magicLink}">Cliquez ici pour vous connecter</a>`,
});
} else {
// Development fallback: display the link in the console
console.info(`Magic Link for ${email}:`);
console.info(magicLink);
}When the user clicks the link:
- The client sends the token to the server.
- The server compares the received hash with the one in the database.
- If valid, it generates a session JWT and places it in a secure cookie:
const cookieOptions: CookieOptions = {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
};
res.cookie("__Host-auth", sessionToken, cookieOptions);Some routes require identifying the user. StartER injects the complete User entity into req.me.
const verifyAccessToken: RequestHandler = (req, res, next) => {
const token = req.cookies["__Host-auth"];
const payload = auth.verify(token); // Verifies the JWT signature
// Retrieves the fresh user from the database
const me = userRepository.find(Number(payload.sub));
if (me == null) throw new Error("User not found");
// Refresh cookie (extends expiration)
const freshToken = auth.signSession({ sub: me.id.toString() });
res.cookie("__Host-auth", freshToken, cookieOptions);
req.me = me; // Injects the user into the request
next();
};This secures the application: if a user is deleted from the database, their JWT becomes instantly invalid even if it has not expired.
Logic is centralized in src/react/components/auth/AuthContext.tsx.
-
MagicLinkForm: Email entry and call to/api/auth/magic-link. -
VerifyPage:/verifyroute that retrieves the token from the URL and sends it to/api/auth/verify.
When the application loads, the React router (via the root route loader function) calls /api/users/me. If the cookie is present and valid, the user is retrieved and automatically injected into the AuthProvider component.
// src/react/routes.tsx
export const routes = [
{
loader: async ({ request }) => {
// On the server: explicit cookie forwarding
const response = await fetch("/api/users/me", {
headers: { cookie: request.headers.get("cookie") ?? "" },
});
const me = response.ok ? await response.json() : null;
return { me };
},
Component: () => {
const { me } = useLoaderData<{ me: User | null }>();
return <AuthProvider initialUser={me}>...</AuthProvider>;
}
}
];- Single-use tokens: once verified, the Magic Link token is marked as "consumed" in the database.
- Token hashing: opaque tokens are never stored in plain text (SHA-256).
- HTTP-only cookies: the session JWT is invisible to JavaScript, protecting against session theft via XSS.
-
Trusted identity (
req.me): using thereq.meobject in your controllers ensures you are handling an existing and up-to-date user.
AI co-creation
Getting started
Explanations
How-To Guides
Reference
Digging deeper