End-to-end Guide to building a Reliable Authentication System for a Startup MVP - Part 5

Taiwo OgunolaTaiwo Ogunola
5 min read

Some applications need a sign-out-all-devices feature to log you out from all devices.

For us, it's tricky with JWT-based authentication, but we'll try to make it work without major changes.

With session-based authentication, we create a session table in our database to store user sessions. Each user gets a sessionId, making it easy to sign out from all sessions with DELETE FROM session WHERE userId = <userId>;.

Before we proceed with JWT-based sign-out, let's discuss the pros and cons of JWT-based and session-based authentication.

Session-Based Authentication

Pros:

  1. Security:

    • Server-Side Storage: Sessions are stored on the server-side, which means sensitive information (such as session identifiers) is not exposed to the client. This reduces the risk of token theft through XSS (Cross-Site Scripting).

    • Revocable: Sessions can be easily revoked by the server, providing a way to log out users from all devices instantly.

  2. Control: The server can have complete control over the session lifecycle, such as extending, expiring, or revoking sessions based on various criteria like IP address changes or inactivity.

  3. Widely Supported: Session-based authentication is a mature and widely supported method across various frameworks and libraries.

  4. Simple to Implement: No need for custom token management**,** since the server manages sessions, developers don’t have to handle token generation, validation, and expiration.

Cons:

  1. Scalability: Sessions are stateful, so the server must maintain state, which can cause scalability issues. Managing session data in a distributed environment needs extra infrastructure like sticky sessions or centralized session stores like Redis.

  2. Performance (Database Load): Every request requires a lookup in the session store, which adds additional load on the database or session store.

  3. Complexity in Distributed Systems: When using multiple servers or microservice architecture, session data needs to be replicated across them or stored in a shared database, which can complicate the architecture.

JWT-Based Authentication

Pros:

  1. Scalability: JWTs are stateless; the server does not need to maintain session data, which allows for easier scaling. Each server can independently verify JWTs without needing to consult a centralized session store.

  2. Performance: No server-side lookup, JWTs are self-contained, meaning they carry all the information needed for authentication. This removes the need for additional database lookups, reducing latency.

  3. Cross-Domain Authentication: JWTs can be easily used across different domains and services, which is particularly useful in microservices architectures or Single Sign-On (SSO) implementations.

Cons:

  1. Security (Token Theft and Revocation): They are more vulnerable to XSS attacks and token theft if they are not stored properly. If a JWT is compromised, it remains valid until it expires. JWTs cannot be easily revoked without additional mechanisms like a blacklist or shortening the token lifespan, adding complexity.

  2. Size: JWTs can become large if they include many claims, leading to increased bandwidth usage and affecting performance, especially on mobile networks.

  3. Risk of Misuse: If the private key used to sign JWTs is compromised, all tokens can be forged, and the server cannot easily distinguish legitimate tokens from fraudulent ones.

We took care of the majority of the issues with JWT-based authentication by using http-only cookies for JWT storage and adding minimal data in the JWT payload to reduce bandwidth size. We also implemented a Secret Manager for secrets rotation.

Implementing sign out all devices

Let's proceed in implementing our signing out feature.

Edit your schema.prisma file,

model User {
  // ...other columns
  authTokenVersion Int     @default(1) // new colmn
}

Generate new migration file:

$ npm run prisma:migration:save

Apply the migration to your DB:

$ npm run prisma:migration:up

Now, we have to include this new property in our JWT user payload, inside token.service.ts, this is only important in generateRefreshToken method:

    this.jwt.signAsync(
      {
        userId: user.id,
        authTokenVersion: user.authTokenVersion,
      },
      signOptions,
    );

We also need to create a new method inside auth.service.ts for incrementing authTokenVersion when the user logs out of all device:

@Injectable()
export class AuthService {
  // ...other methods
  async incrementUserAuthTokenVersion(userId: string) {
    try {
      const user = await this.prismaService.user.update({
        where: {
          id: userId,
        },
        data: {
          authTokenVersion: {
            increment: 1,
          },
        },
      });

      delete user.password;

      return user;
    } catch (e) {
      throw BadRequestError('user', 'User not found');
    }
  }
}

We need to prevent our JwtAuthGuard from issuing new tokens if the token version doesn't match, inside jwt-auth.guard.ts:

    // ...rest of the code
    const user = await this.authService.findUserById(data.userId);

    /**
     * Make sure the `authTokenVersion` matches the one in the payload
     */
    if (!user || data.authTokenVersion !== user.authTokenVersion) {
      throw new UnauthorizedException();
    }

Finally, we can create our API route for this feature inside our local-auth.controller.ts file:

  @UseGuards(JwtAuthGuard)
  @Delete('logout/all')
  public async logoutAll(
    @Req() req: RequestWithAuthUser,
    @Res({ passthrough: true }) res: Response,
  ) {
    this.tokenService.deleteAuthCookies(res);

    await this.authService.incrementUserAuthTokenVersion(req.user.userId);

    return {
      status: 'success',
      data: null,
      meta: null,
    };
  }

This feature will be available at DELETE /api/v1/local-auth/logout/all

Frontend Integration

Our API integration follows the same way we have been doing it, we are going to create a custom hook in our user.service-hooks.ts file:

export const useLogoutAll = () => {
    const queryClient = useQueryClient();
    const { toast } = useToast();

    const { mutate } = useMutation({
        mutationFn: UserServices.logoutAll,
        onSuccess: () => {
            queryClient.removeQueries();
        },
        onError: (e: AxiosError<{ message: string }>) => {
            toast({
                variant: 'destructive',
                description: e.response?.data.message,
            });
        },
    });

    return {
        logoutAll: mutate,
    };
};

Here's our UserServices.logoutAll:

    // ...other services
    logoutAll: async function () {
        return await axios.delete(`/local-auth/logout/all`);
    },

There you go, we have completed the log out all devices feature. To test this:

  • Log into at least two different browser window profiles.

  • (Optional) set ACCESS_TOKEN_EXPIRES_IN to a shorter period for testing purposes.

  • Click on the Log out all devices button on one of the browser windows.

  • Once the access token has expired, all the sessions will be logged out automatically because the BE will not be able to provide new tokens since the authTokenVersion has changed.

Conclusion

In this guide, we explored how to implement a reliable authentication system for a startup MVP, focusing on both session-based and JWT-based methods. We discussed the pros and cons of each approach, highlighting their security, scalability, and performance. By adding a sign-out-all-devices feature, we showed how to enhance user security and control in a JWT-based system.

In the next part, we will scale our backend system, making it more performant and secure. See you in the next part 🙂

0
Subscribe to my newsletter

Read articles from Taiwo Ogunola directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Taiwo Ogunola
Taiwo Ogunola

With over 5 years of experience in delivering technical solutions, I specialize in web page performance, scalability, and developer experience. My passion lies in crafting elegant solutions to complex problems, leveraging a diverse expertise in web technologies.