Spring Boot HandBook

    Introduction :#

    User session management with JWT (JSON Web Token) is like giving someone a guest pass to enter and use a service. When a user logs in, the server creates a token (like an entry pass) that contains user details. This token is handed to the user, who presents it with each request they make, instead of constantly logging in again. The server checks the token to validate the user.

    Example:

    Imagine visiting a library. Instead of re-entering your details every time, you're given a library card (JWT) when you first sign up. Each time you enter, you show your card, and they know who you are.

    Basic Flow :#

    1. Login: You get a JWT (your card).
    2. Requests: You send this JWT with each API call (like showing your card).
    3. Validation: The server checks if the JWT is valid, allowing access if it is.

    This eliminates the need for storing login sessions on the server and simplifies scalability.

    User Session Management#

    User Session Management refers to the process of controlling and maintaining a user's interaction with an application over a specific time period. It involves creating, tracking, and ending user sessions to ensure smooth navigation and secure access.

    • Tracking and Managing Sessions: When a user logs in, the application creates a session to keep track of their activities. This session persists for a specific period or until the user logs out.
    • Security: Proper session management ensures that unauthorized users cannot hijack a session, and tokens (like JWT) are used to prevent misuse.
    • Seamless Experience: Once logged in, users can navigate through the application without needing to re-authenticate for every interaction, maintaining ease of use.

    Example Analogy:

    Think of a theme park wristband. Once you enter the park (log in), you get a wristband (session) that gives you access to the rides (application features). You don’t need to keep showing your ticket (re-authenticating), but if your wristband expires or gets stolen, you need to get a new one (re-login).

    This ensures that only authorized guests can enjoy the rides (access services securely).

    JWT Session management#

    JWT (JSON Web Token) is often used in modern session management. Instead of storing session data on the server, the user's identity and claims are encoded into a token, which is sent with each request.

    Generate Access Token (AT) and Refresh Token (RT) and store the Session using this schema(session_id, refreshToken, userId, lastUsedAt)

    • A user logs in, and the system generates AT and RT.
    • The session information is stored in the database, allowing the server to track it and use the RT when needed.
      1. The Access Token (AT) is issued to allow the user to interact with the application without re-authenticating on every request.
      2. The Refresh Token (RT) is stored securely and used to request a new AT when the old one expires.
      3. After generating both tokens, a new session is stored in the database with the following schema: (session_id, refreshToken, userId, lastUsedAt)
      4. session_id: Unique identifier for the session.
      5. refreshToken: The token used to refresh the session.
      6. userId: The user associated with the session.
      7. lastUsedAt: The last time this session was used (for session expiration or least recently used checks).

    Renew Access Token using Refresh Token (RT)

    • When the client sends a request with an expired AT but a valid RT, the server:
      1. Looks up the RT in the session table.
      2. Ensures the session exists.
      3. Verifies the RT is still valid (not expired).
      4. Issues a new Access Token and updates the lastUsedAt timestamp for the session.

    Upon a New Login request, check if the session limit is full When the user logs in again (e.g., on a new device), the system checks if the user has exceeded the allowed session limit (for example, 5 concurrent sessions). If the limit is reached, the system removes the least recently used session before generating a new AT + RT pair.

    • Check session count:
      • The system queries the database for the number of active sessions (session_id) associated with the userId.
    • Session limit:
      • If the user has too many sessions (more than the allowed limit), the system:
        • Finds the session that was last used the longest time ago (based on lastUsedAt).
        • Deletes this least recently used session.
    • Create new session:
      • A new session is created with the generated AT + RT, and the session is stored in the database.

    Example Analogy: Imagine going to a movie theater. When you buy a ticket (log in), you can freely walk in and out of the theater (access services) until your ticket's validity expires (session expiration). If you leave or your ticket expires, you need a new ticket (log in again).

    Description of the Flow :#

    • Client:
      • The user sends credentials (e.g., username and password) to the server to authenticate.
    • Server:
      • The server validates the credentials. If valid, it forwards the request to the TokenService to generate the access and refresh tokens.
    • TokenService:
      • The TokenService checks whether the session limit for the user has been exceeded.
      • If the limit is exceeded, it triggers the SessionService to handle session management.
      • If the limit is not exceeded, the TokenService generates the Access Token (AT) and Refresh Token (RT) and stores the session in the database.
    • SessionService:
      • If the session limit is exceeded, the SessionService deletes the oldest session (likely based on lastUsedAt) to make space for the new session.
      • After that, the TokenService proceeds with generating and storing the tokens.
    • Return Tokens to the Client:
      • Once the tokens are generated and the session is successfully managed (either by deleting old sessions or simply creating a new one), the tokens (AT and RT) are returned to the client.
      • The client can now use the Access Token for further requests, and when the Access Token expires, the client can use the Refresh Token to renew it.

    Implementation :#

    Code

    Create a SessionEntity class.

    import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.CreationTimestamp; import java.time.LocalDateTime; @Entity @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class SessionEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String refreshToken; @CreationTimestamp private LocalDateTime lastUsedAt; @ManyToOne private UserEntity user; }

    Create a SessionRepository class.

    import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository public interface SessionRepo extends JpaRepository<SessionEntity,Long> { List<SessionEntity> findByUser(UserEntity user); Optional<SessionEntity> findByRefreshToken(String refreshToken); }

    Create a SessionService class.

    import lombok.RequiredArgsConstructor; import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; @Service @RequiredArgsConstructor public class SessionService { private final SessionRepo sessionRepo; private final int SESSION_LIMIT = 5; public void generateNewSession(UserEntity user, String refreshToken){ List<SessionEntity> userSession = sessionRepo.findByUser(user); if(userSession.size() == SESSION_LIMIT){ userSession.sort(Comparator.comparing(SessionEntity::getLastUsedAt)); SessionEntity leastRecentlyUsedSession = userSession.getFirst(); sessionRepo.delete(leastRecentlyUsedSession); //delete first session } SessionEntity newSession = SessionEntity //create new session .builder() .user(user) .refreshToken(refreshToken) .build(); sessionRepo.save(newSession); } public void validSession(String refreshToken){ SessionEntity session = sessionRepo.findByRefreshToken(refreshToken).orElseThrow(()-> new SessionAuthenticationException("Sessoin not found for refresh token: "+refreshToken)); session.setLastUsedAt(LocalDateTime.now()); sessionRepo.save(session); } }

    We have to generate the new session for every login. Here is our AuthService class.

    import lombok.RequiredArgsConstructor; import org.modelmapper.ModelMapper; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service @RequiredArgsConstructor public class AuthService{ private final UserRepo userRepo; private final ModelMapper modelMapper; private final PasswordEncoder passwordEncoder; private final AuthenticationManager authenticationManager; private final JWTService jwtService; private final UserServiceImpl userService; private final SessionService sessionService; @Transactional public UserDto signUp(SignupDto signupDto) { Optional<UserEntity> user = userRepo .findByEmail(signupDto.getEmail()); if(user.isPresent()) throw new BadCredentialsException("Cannot signup, User already exists with email "+signupDto.getEmail()); UserEntity mappedUser = modelMapper.map(signupDto,UserEntity.class); mappedUser.setPassword(passwordEncoder.encode(mappedUser.getPassword())); UserEntity savedUser = userRepo.save(mappedUser); return modelMapper.map(savedUser,UserDto.class); } public LoginResponseDto logIn(LoginDto loginDto) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginDto.getEmail(),loginDto.getPassword()) ); UserEntity user = (UserEntity) authentication.getPrincipal(); String accessToken = jwtService.createAccessToken(user); String refreshToken = jwtService.createRefreshToken(user); sessionService.generateNewSession(user,refreshToken); //generate new session return new LoginResponseDto(user.getId(),accessToken,refreshToken); } public LoginResponseDto refreshToken(String refreshToken) { Long uerId = jwtService.generateUserIdFromToken(refreshToken); //refresh token is valid sessionService.validSession(refreshToken); UserEntity user = userService.getUserById(uerId); String accessToken = jwtService.createAccessToken(user); return new LoginResponseDto(user.getId(),accessToken,refreshToken); } }

    Output#

    After running the application if you signup.

     login you can see these.

    Database

    If you send a login request with the same email and password five times, you can see the details in your database.


    After that (5 times) if you send a login request with the same email and password, the session will generate again but the session of session id 1 will be deleted.

    If we hits another request that is “/refresh” then we can see the lastUsedAt time will be updated.

    Before hitting ‘/refresh/ endpoint Database. You can see the ’lastUsedAt' of 'id = 6'

    When you hit  that endpoint.
    After hitting ‘/refresh/ endpoint Database. You can see the ’lastUsedAt' of 'id = 6'



     

    Last updated on Dec 09, 2024