Skip to main content
Brautcloud implements a secure authentication system using JWT (JSON Web Tokens) for access control and refresh tokens for maintaining user sessions. This dual-token approach provides both security and a seamless user experience.

Authentication flow

The authentication system uses two types of tokens:
  • Access tokens: Short-lived JWT tokens used to authenticate API requests
  • Refresh tokens: Long-lived tokens stored as HTTP-only cookies to obtain new access tokens
Access tokens are sent in the Authorization header, while refresh tokens are stored in secure, HTTP-only cookies to prevent XSS attacks.

User registration

New users register by providing an email and password. The system validates that the email is unique and securely hashes the password before storage.

Registration endpoint

@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody AuthRequest request) {
    if (userRepository.findByEmail(request.email()).isPresent()) {
        return ResponseEntity.badRequest().body("Email already used!");
    }

    User user = new User();
    user.setEmail(request.email());
    user.setPassword(passwordEncoder.encode(request.password()));
    userRepository.save(user);

    return ResponseEntity.ok("User registered successfully");
}
The registration flow:
  1. User submits email and password
  2. System checks if email already exists
  3. Password is hashed using BCrypt
  4. User account is created in the database
Passwords are never stored in plain text. The system uses Spring Security’s BCryptPasswordEncoder to hash passwords before storage.

Login flow

When users log in, they receive both an access token and a refresh token. The access token is returned in the response body, while the refresh token is set as an HTTP-only cookie.

Login endpoint

@PostMapping("/login")
public ResponseEntity<AuthResponse> login(@RequestBody AuthRequest request, HttpServletResponse response) {
    authenticationManager
        .authenticate(new UsernamePasswordAuthenticationToken(request.email(), request.password()));

    User user = userRepository.findByEmail(request.email())
        .orElseThrow(() -> new UsernameNotFoundException("User not found"));

    String accessToken = jwtService.generateToken(request.email());
    RefreshToken refreshToken = refreshTokenService.createRefreshToken(user);

    ResponseCookie cookie = ResponseCookie.from("refresh_token", refreshToken.getToken())
        .httpOnly(true)
        .secure(false)
        .sameSite("Strict")
        .path("/api/auth")
        .maxAge(Duration.ofDays(30))
        .build();

    response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

    return ResponseEntity.ok(new AuthResponse(accessToken));
}
The login process:
  1. User submits credentials
  2. Spring Security validates the email and password
  3. Access token (JWT) is generated with user’s email as subject
  4. Refresh token is created and stored in database
  5. Refresh token is set as HTTP-only cookie with 30-day expiration
  6. Access token is returned in response body

JWT token generation

Access tokens are generated using the JJWT library:
public String generateToken(String email) {
    return Jwts.builder()
        .subject(email)
        .issuedAt(new Date())
        .expiration(new Date(System.currentTimeMillis() + expiration))
        .signWith(getSigningKey())
        .compact();
}
The access token contains the user’s email in the subject claim, which is used to identify the user for subsequent API requests.

Token refresh mechanism

Access tokens have a short expiration time for security. When they expire, the client can obtain a new access token using the refresh token without requiring the user to log in again.

Refresh endpoint

@PostMapping("/refresh")
public ResponseEntity<AuthResponse> refresh(@CookieValue(name = "refresh_token") String refreshToken,
        HttpServletResponse response) {

    RefreshToken existing = refreshTokenService.validateRefreshToken(refreshToken);

    String newAccessToken = jwtService.generateToken(existing.getUser().getEmail());

    RefreshToken newRefreshToken = refreshTokenService.createRefreshToken(existing.getUser());

    ResponseCookie cookie = ResponseCookie.from("refresh_token", newRefreshToken.getToken())
        .httpOnly(true)
        .secure(false)
        .sameSite("Strict")
        .path("/api/auth")
        .maxAge(Duration.ofDays(30))
        .build();

    response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
    return ResponseEntity.ok(new AuthResponse(newAccessToken));
}
The refresh process:
  1. Client sends request with refresh token cookie
  2. System validates the refresh token exists and hasn’t expired
  3. New access token is generated
  4. New refresh token is created (token rotation)
  5. Old refresh token is invalidated
  6. New tokens are returned to client

Refresh token validation

public RefreshToken validateRefreshToken(String token) {
    RefreshToken refreshToken = refreshTokenRepository.findByToken(token)
        .orElseThrow(() -> new InvalidRefreshTokenException("Refresh token not found"));

    if (refreshToken.getExpiresAt().isBefore(Instant.now())) {
        refreshTokenRepository.delete(refreshToken);
        throw new InvalidRefreshTokenException("Refresh token expired");
    }

    return refreshToken;
}
Brautcloud implements refresh token rotation: each time a refresh token is used, it’s replaced with a new one. This enhances security by limiting the lifetime of each token.
Refresh tokens are stored in HTTP-only cookies with the following security attributes:
  • httpOnly: true - Prevents JavaScript access to the cookie
  • sameSite: "Strict" - Prevents CSRF attacks
  • secure: false - Set to true in production for HTTPS-only transmission
  • path: "/api/auth" - Restricts cookie to authentication endpoints
  • maxAge: 30 days - Cookie expires after 30 days
HTTP-only cookies cannot be accessed by JavaScript, which protects against XSS (Cross-Site Scripting) attacks. Even if an attacker injects malicious JavaScript into the page, they cannot steal the refresh token. This makes cookies more secure than storing tokens in localStorage or sessionStorage.

Logout

The logout endpoint invalidates the refresh token and clears the cookie.

Logout endpoint

@PostMapping("/logout")
public ResponseEntity<String> logout(@CookieValue(name = "refresh_token", required = false) String refreshToken,
        HttpServletResponse response) {

    // Invalidate the refresh token in the DB if it exists
    if (refreshToken != null) {
        refreshTokenService.deleteByToken(refreshToken);
    }

    // Clear the cookie by setting maxAge to 0
    ResponseCookie cookie = ResponseCookie.from("refresh_token", "")
        .httpOnly(true)
        .secure(false)
        .sameSite("Strict")
        .path("/api/auth")
        .maxAge(0)
        .build();

    response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

    return ResponseEntity.ok("Logged out");
}
The logout process:
  1. Refresh token is removed from the database
  2. Cookie is cleared by setting maxAge to 0
  3. Client should discard the access token
After logout, any existing access tokens are still valid until they expire. Clients should immediately discard access tokens upon logout.

Security considerations

Password hashing

All passwords are hashed using BCrypt before storage, making them resistant to rainbow table attacks.

Token rotation

Refresh tokens are rotated on each use, limiting the window of opportunity for token theft.

HTTP-only cookies

Refresh tokens in HTTP-only cookies are protected from XSS attacks.

SameSite protection

SameSite=Strict cookies prevent CSRF attacks by blocking cross-site requests.

Build docs developers (and LLMs) love