Authentication en US - rocambille/start-express-react GitHub Wiki
Authentication is a critical component of modern applications. StartER offers a complete and secure implementation, based on Express (backend) and React (frontend), with a JWT stored in a secure cookie. This system illustrates best practices for stateless authentication.
The authentication process in StartER is based on a simple exchange:
- The user sends their credentials (email, password)
- The Express server verifies them
- A JWT token is generated and stored in an HTTP-only cookie
- The React frontend is notified of the login
- Upon logout, the cookie is deleted
This approach ensures security, simplicity, and consistency between the front and back end.
Authentication features are grouped together in the auth module:
src/express/modules/auth/
├── authRoutes.ts
└── authActions.ts
| Method | Route | Main action | Description |
|---|---|---|---|
| POST | /api/access-tokens |
createAccessToken |
Login (generates JWT and cookie) |
| DELETE | /api/access-tokens |
destroyAccessToken |
Logout (delete cookie) |
| GET | /api/me |
verifyAccessToken |
Check the JWT |
These routes are mounted in src/express/modules/auth/authRoutes.ts.
When a user logs in:
- The email address is retrieved from the database via
userRepository.readByEmailWithPassword - The password is verified with Argon2id
- If successful, a JWT is signed with
jsonwebtoken - This token is stored in a secure cookie:
const cookieOptions: CookieOptions = {
httpOnly: true,
secure: true,
sameSite: "strict",
};
res.cookie("auth", token, cookieOptions);This makes the cookie inaccessible to client JavaScript, preventing XSS attacks.
The server simply deletes the cookie:
res.clearCookie("auth", cookieOptions);
res.sendStatus(204);Some routes require a logged-in user.
The verifyAccessToken middleware:
- Reads the
authcookie - Checks the validity of the JWT
- Adds the decoded payload to
req.auth - Rejects the request (
401) if the token is invalid
const token = req.cookies.auth;
req.auth = jwt.verify(token, appSecret);
next();This allows subsequent middlewares to access req.auth.sub to identify the logged in user.
On the React side, the authentication logic is centralized in a context:
src/react/components/auth/AuthContext.tsx.
This context provides:
-
user: the current user -
check(): indicates whether the user is logged in -
login(credentials): login -
logout(): logout -
register(credentials): registration
See the full code for details:
fetch("/api/access-tokens", {
method: "post",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credentials),
})
.then((response) => {
if (response.status === 201) {
return response.json();
}
})
.then((user: User) => {
setUser(user);
});The authentication cookie is automatically managed by the browser.
fetch("/api/access-tokens", {
method: "delete",
}).then((response) => {
if (response.status === 204) {
setUser(null);
}
});The AuthForm component dynamically displays:
-
LoginRegisterFormif the user is not logged in -
LogoutFormif the user is logged in
function AuthForm() {
const auth = useAuth();
return auth.check() ? <LogoutForm /> : <LoginRegisterForm />;
}With context, this component is integrated into the main layout src/react/components/Layout.tsx:
<AuthProvider>
<header>
<NavBar />
<BurgerMenu>
<AuthForm />
</BurgerMenu>
</header>
<main>{children}</main>
</AuthProvider>This makes the authentication context available throughout the application.
Authentication with HTTP-only cookies already allows server-side session persistence.
However, without a restore mechanism, the React state (user) is lost after a page reload.
To solve this problem, StartER integrates a /api/me endpoint and automatic loading of user information into the authentication context (AuthContext).
The /api/me endpoint is used to retrieve the information of the currently authenticated user.
It relies on the presence of the auth cookie, verified and decoded by the verifyAccessToken middleware.
In src/express/modules/auth/authRoutes.ts:
router.get("/api/me", authActions.verifyAccessToken, authActions.readMe);And in src/express/modules/auth/authActions.ts:
const readMe: RequestHandler = async (req, res) => {
const me = await userRepository.read(Number(req.auth.sub));
res.json(me);
};This route returns the user information (id, email) corresponding to the identifier (sub) contained in the JWT token.
So, even after a browser restart or page refresh, the client can retrieve session information from the server.
The authentication context (AuthContext) automatically queries /api/me when the component is mounted to restore the user state if the authentication cookie is still valid.
useEffect(() => {
fetch("/api/me")
.then((response) => {
if (response.status === 200) {
return response.json();
}
})
.then((user: User) => {
setUser(user);
});
}, []);This effect allows the login to persist across page reloads without requiring reauthentication.
- When you log in or register, the server creates an
authcookie containing a signed JWT token. - This cookie is automatically sent by the browser with each request.
- When the React application is mounted, the
AuthContextcalls/api/meto restore the current user from the token. - If the token has expired or is invalid, the request returns a
401, anduserremainsnull.
This session persistence provides a smooth user experience while maintaining a high level of security. The browser stores the cookie, the server checks its validity, and React automatically restores the authentication state when the application starts.
- Use HTTP-only cookies: Prevents all JavaScript access to the JWT
- Follow the OWASP recommendations for password hashing: At the time of writing, Argon2id with a minimum configuration of 19 MB of memory, an iteration count of 2, and 1 degree of parallelism.
-
Configure
secure: trueandsameSite: "strict": protects against CSRF attacks - Limit token lifetime (
expiresIn: "1h") - Always verify tokens on the server side before accessing protected resources