Skip to main content
Planned implementation: This guide describes the intended JWT authentication flow for OrgStack. The current codebase includes Spring Security as a dependency but does not yet implement authentication endpoints, JWT token generation, or user authentication logic.
OrgStack will use JSON Web Tokens (JWT) for stateless authentication. This guide explains how JWT authentication will be implemented with Spring Security and how tokens will carry both user identity and tenant context.

Authentication architecture

OrgStack’s authentication flow follows these steps:
1

User logs in with credentials

The client sends username and password to the /auth/login endpoint:
POST /api/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "securePassword123"
}
2

Server validates credentials

Spring Security authenticates the credentials against the user database:
@Service
public class AuthService {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider;
    
    public AuthResponse login(LoginRequest request) {
        User user = userRepository.findByEmail(request.getEmail())
            .orElseThrow(() -> new BadCredentialsException("Invalid credentials"));
        
        if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
            throw new BadCredentialsException("Invalid credentials");
        }
        
        String token = jwtTokenProvider.createToken(user);
        return new AuthResponse(token, user);
    }
}
3

Server generates JWT token

A JWT token is generated containing the user’s ID, email, and tenant ID:
public String createToken(User user) {
    Claims claims = Jwts.claims().setSubject(user.getId().toString());
    claims.put("email", user.getEmail());
    claims.put("tenantId", user.getTenantId().toString());
    claims.put("roles", user.getRoles());
    
    Date now = new Date();
    Date validity = new Date(now.getTime() + validityInMilliseconds);
    
    return Jwts.builder()
        .setClaims(claims)
        .setIssuedAt(now)
        .setExpiration(validity)
        .signWith(secretKey, SignatureAlgorithm.HS256)
        .compact();
}
The tenant ID is embedded in the JWT token, ensuring every request carries the user’s organizational context.
4

Client includes token in requests

The client stores the JWT token and includes it in the Authorization header for all subsequent requests:
GET /api/projects
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
5

Server validates token

A JWT filter intercepts requests, validates the token, and sets the security context:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtTokenProvider jwtTokenProvider;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                   HttpServletResponse response,
                                   FilterChain filterChain) {
        String token = resolveToken(request);
        
        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        
        filterChain.doFilter(request, response);
    }
}

Spring Security configuration

OrgStack includes spring-boot-starter-security in its dependencies:
pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
This dependency provides the core Spring Security framework for authentication and authorization.

Security configuration class

The security configuration defines which endpoints require authentication:
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/actuator/health").permitAll()
                .anyRequest().authenticated())
            .addFilterBefore(jwtAuthenticationFilter, 
                           UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
The configuration disables CSRF protection since JWT tokens are not vulnerable to CSRF attacks. Session management is set to STATELESS because JWTs provide authentication state.

JWT token structure

A JWT token consists of three parts separated by dots:
header.payload.signature
Specifies the token type and signing algorithm:
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload (Claims)

Contains user information and metadata:
{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "email": "[email protected]",
  "tenantId": "org-uuid-here",
  "roles": ["USER", "ADMIN"],
  "iat": 1709251200,
  "exp": 1709337600
}

sub

Subject - The user’s UUID identifier

email

User’s email address

tenantId

Organization UUID for multi-tenant isolation

roles

User’s authorization roles

iat

Issued At - Token creation timestamp

exp

Expiration - Token expiry timestamp

Signature

Ensures the token hasn’t been tampered with:
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)
The signing secret must be kept secure and never committed to version control. Use environment variables or a secure secret management system.

Implementing JWT token provider

Token generation

Create tokens with user claims and expiration:
@Component
public class JwtTokenProvider {
    @Value("${jwt.secret}")
    private String secretKey;
    
    @Value("${jwt.expiration:3600000}") // 1 hour default
    private long validityInMilliseconds;
    
    public String createToken(User user) {
        Claims claims = Jwts.claims().setSubject(user.getId().toString());
        claims.put("email", user.getEmail());
        claims.put("tenantId", user.getTenantId().toString());
        claims.put("roles", user.getRoles().stream()
            .map(Role::getName)
            .collect(Collectors.toList()));
        
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);
        
        return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(validity)
            .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), 
                     SignatureAlgorithm.HS256)
            .compact();
    }
}

Token validation

Validate tokens and extract claims:
public boolean validateToken(String token) {
    try {
        Jws<Claims> claims = Jwts.parserBuilder()
            .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
            .build()
            .parseClaimsJws(token);
        
        return !claims.getBody().getExpiration().before(new Date());
    } catch (JwtException | IllegalArgumentException e) {
        return false;
    }
}

public Claims getClaims(String token) {
    return Jwts.parserBuilder()
        .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
        .build()
        .parseClaimsJws(token)
        .getBody();
}

Authentication object

Convert JWT claims to Spring Security authentication:
public Authentication getAuthentication(String token) {
    Claims claims = getClaims(token);
    
    UUID userId = UUID.fromString(claims.getSubject());
    String email = claims.get("email", String.class);
    UUID tenantId = UUID.fromString(claims.get("tenantId", String.class));
    List<String> roles = claims.get("roles", List.class);
    
    UserPrincipal principal = new UserPrincipal(
        userId, 
        email, 
        tenantId, 
        roles.stream()
            .map(SimpleGrantedAuthority::new)
            .collect(Collectors.toList())
    );
    
    return new UsernamePasswordAuthenticationToken(
        principal, 
        token, 
        principal.getAuthorities()
    );
}

User principal

The UserPrincipal class represents the authenticated user:
@Getter
public class UserPrincipal implements UserDetails {
    private final UUID id;
    private final String email;
    private final UUID tenantId;
    private final Collection<? extends GrantedAuthority> authorities;
    
    @Override
    public String getUsername() {
        return email;
    }
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    
    @Override
    public boolean isEnabled() {
        return true;
    }
    
    @Override
    public String getPassword() {
        return null; // JWT authentication doesn't use password
    }
}
The UserPrincipal includes the tenantId, making it available throughout the request lifecycle for tenant isolation.

Accessing the authenticated user

In controllers

Use @AuthenticationPrincipal to inject the current user:
@RestController
@RequestMapping("/api/profile")
public class ProfileController {
    @GetMapping
    public UserProfile getProfile(@AuthenticationPrincipal UserPrincipal principal) {
        return userService.getProfile(principal.getId());
    }
}

In services

Access via SecurityContextHolder:
@Service
public class ProjectService {
    public List<Project> getProjects() {
        UserPrincipal principal = (UserPrincipal) SecurityContextHolder
            .getContext()
            .getAuthentication()
            .getPrincipal();
        
        UUID tenantId = principal.getTenantId();
        return projectRepository.findByTenantId(tenantId);
    }
}

Tenant context utility

Create a helper to extract tenant ID:
public class TenantContext {
    public static UUID getCurrentTenantId() {
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        
        if (auth == null || !auth.isAuthenticated()) {
            throw new SecurityException("No authenticated user");
        }
        
        UserPrincipal principal = (UserPrincipal) auth.getPrincipal();
        return principal.getTenantId();
    }
    
    public static UUID getCurrentUserId() {
        Authentication auth = SecurityContextHolder.getContext()
            .getAuthentication();
        
        if (auth == null || !auth.isAuthenticated()) {
            throw new SecurityException("No authenticated user");
        }
        
        UserPrincipal principal = (UserPrincipal) auth.getPrincipal();
        return principal.getId();
    }
}

Password security

OrgStack uses BCrypt for password hashing:
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Hashing passwords on registration

@Service
public class UserService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;
    
    public User registerUser(RegisterRequest request) {
        if (userRepository.existsByEmail(request.getEmail())) {
            throw new DuplicateEmailException("Email already registered");
        }
        
        User user = new User();
        user.setEmail(request.getEmail());
        user.setPassword(passwordEncoder.encode(request.getPassword()));
        user.setTenantId(request.getTenantId());
        
        return userRepository.save(user);
    }
}
BCrypt automatically includes salt and uses multiple hashing rounds, making it resistant to rainbow table and brute-force attacks.

Token refresh strategy

Short-lived access tokens

Access tokens should expire quickly (e.g., 1 hour):
jwt.expiration=3600000  # 1 hour in milliseconds

Refresh tokens

Implement refresh tokens for seamless re-authentication:
public class AuthResponse {
    private String accessToken;
    private String refreshToken;
    private long expiresIn;
    
    // Constructor, getters, setters
}

public AuthResponse login(LoginRequest request) {
    User user = authenticateUser(request);
    
    String accessToken = jwtTokenProvider.createToken(user);
    String refreshToken = jwtTokenProvider.createRefreshToken(user);
    
    return new AuthResponse(accessToken, refreshToken, 3600);
}

public AuthResponse refreshToken(String refreshToken) {
    if (!jwtTokenProvider.validateToken(refreshToken)) {
        throw new InvalidTokenException("Invalid refresh token");
    }
    
    Claims claims = jwtTokenProvider.getClaims(refreshToken);
    UUID userId = UUID.fromString(claims.getSubject());
    
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException("User not found"));
    
    String newAccessToken = jwtTokenProvider.createToken(user);
    return new AuthResponse(newAccessToken, refreshToken, 3600);
}

Security best practices

Generate a secure random secret for JWT signing:
openssl rand -base64 64
Store it securely in environment variables:
jwt.secret=${JWT_SECRET}
Never hardcode secrets or commit them to version control.
Balance security and user experience:
  • Access tokens: 15 minutes to 1 hour
  • Refresh tokens: 7-30 days
Shorter expiration reduces the window of vulnerability if a token is compromised.
Check expiration, issuer, and audience:
public boolean validateToken(String token) {
    try {
        Jws<Claims> claims = Jwts.parserBuilder()
            .setSigningKey(secretKey)
            .requireIssuer("orgstack")
            .build()
            .parseClaimsJws(token);
        
        return !claims.getBody().getExpiration().before(new Date());
    } catch (JwtException e) {
        return false;
    }
}
Always transmit JWT tokens over HTTPS to prevent interception:
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_PASSWORD}

Testing authentication

Mock authenticated users in tests

@SpringBootTest
class ProjectServiceTest {
    @Autowired
    private ProjectService projectService;
    
    @Test
    @WithMockUser(username = "[email protected]")
    void testGetProjects() {
        mockSecurityContext();
        
        List<Project> projects = projectService.getProjects();
        
        assertNotNull(projects);
    }
    
    private void mockSecurityContext() {
        UUID tenantId = UUID.randomUUID();
        UserPrincipal principal = new UserPrincipal(
            UUID.randomUUID(),
            "[email protected]",
            tenantId,
            List.of(new SimpleGrantedAuthority("ROLE_USER"))
        );
        
        Authentication auth = new UsernamePasswordAuthenticationToken(
            principal, null, principal.getAuthorities()
        );
        
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
}

Next steps

Tenant isolation

Learn how JWT tokens enable multi-tenant data isolation

Audit logging

Track entity changes with JPA auditing

Build docs developers (and LLMs) love