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.

Overview

The authentication process in StartER is based on a simple exchange:

  1. The user sends their credentials (email, password)
  2. The Express server verifies them
  3. A JWT token is generated and stored in an HTTP-only cookie
  4. The React frontend is notified of the login
  5. Upon logout, the cookie is deleted

This approach ensures security, simplicity, and consistency between the front and back end.

Server-side: Express

Authentication features are grouped together in the auth module:

src/express/modules/auth/
├── authRoutes.ts
└── authActions.ts

Main Endpoints

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.

Creating the token (createAccessToken)

When a user logs in:

  1. The email address is retrieved from the database via userRepository.readByEmailWithPassword
  2. The password is verified with Argon2id
  3. If successful, a JWT is signed with jsonwebtoken
  4. 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.

Logout (destroyAccessToken)

The server simply deletes the cookie:

res.clearCookie("auth", cookieOptions);
res.sendStatus(204);

Token verification (verifyAccessToken)

Some routes require a logged-in user. The verifyAccessToken middleware:

  1. Reads the auth cookie
  2. Checks the validity of the JWT
  3. Adds the decoded payload to req.auth
  4. 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.

Client-side: React

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:

Login

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.

Logout

fetch("/api/access-tokens", {
  method: "delete",
}).then((response) => {
  if (response.status === 204) {
    setUser(null);
  }
});

User interface

The AuthForm component dynamically displays:

  • LoginRegisterForm if the user is not logged in
  • LogoutForm if 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.

Session persistence

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).

/api/me endpoint on the Express side

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.

React-side session restoration

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.

Summary

  1. When you log in or register, the server creates an auth cookie containing a signed JWT token.
  2. This cookie is automatically sent by the browser with each request.
  3. When the React application is mounted, the AuthContext calls /api/me to restore the current user from the token.
  4. If the token has expired or is invalid, the request returns a 401, and user remains null.

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.

Security and Best Practices

  1. Use HTTP-only cookies: Prevents all JavaScript access to the JWT
  2. 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.
  3. Configure secure: true and sameSite: "strict": protects against CSRF attacks
  4. Limit token lifetime (expiresIn: "1h")
  5. Always verify tokens on the server side before accessing protected resources
⚠️ **GitHub.com Fallback** ⚠️