When developing an API for a business, one of the key requirements often revolves around managing different user types and their respective permissions. In this scenario, we'll focus on three user types: admin, individual user, and corporate user.
User Roles Overview:
- Admin: Has all the privileges of the individual and corporate users, plus additional administrative capabilities.
- Individual User: Accesses standard user features.
- Corporate User: Similar to the individual user but may have access to additional resources or features tailored for corporate use.
In this post, we’ll focus on implementing the authentication and authorization mechanisms necessary to enforce these roles, ensuring the system adheres to the statelessness principle of REST.
Understanding Authentication and Authorization
Authentication vs Authorization image from javatpoint.com |
Authentication is the process of verifying the identity of a user or system. It’s like showing your ID card when a police officer asks for identification; it confirms who you are. Authentication methods can vary—passwords, PINs, security questions, fingerprints, etc.
Authorization determines what actions an authenticated user can perform. It defines which resources and operations a user has access to, based on their role within the system.
South Park Eric Cartman |
We’ll implement both authentication and authorization while maintaining the stateless nature of REST.
"Stateless" refers to a fundamental design principle where each request from a client to a server must contain all the information needed to understand and process that request and this happens in complete isolation. This principle is crucial for RESTful architecture. The server never relies on information from previous requests from the client. If any such information is important then the client will send that as part of the current request.
Implementing Token-Based Authentication
To enforce statelessness and ensure secure communication, we’ll use a token-based authentication system. Specifically, we'll implement JWT (JSON Web Token). This approach has several advantages:
- Scalability: Since the server does not need to retain session information, it’s easier to scale horizontally.
- Flexibility: Tokens can be easily validated, and requests can be processed by any server in a cluster.
For brevity, this post won’t cover the user registration process. If you’re implementing this in your application, make sure to include a registration feature unless users are manually added to your database.
Security Configuration
Let’s start by configuring our security settings with the SecurityConfiguration
class using Spring Security’s SecurityFilterChain
.
Here’s a breakdown:
- Public Endpoints:
/api/auth/login
is an open endpoint that anyone can access. - Protected Endpoints:
/api/admin
is restricted to admin users./api/user
is accessible to both users and admins.
- JWT Authentication Filter: A custom
JwtAuthenticationFilter
is added before other filters. This is crucial for validating JWTs in each request.
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("api/auth/**" , "/api/auth/login","/api/auth/login/**" ).permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("INDIVIDUAL_USER", "CORPORATE_USER")
.anyRequest().authenticated()
);
// Add JWT token filter
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
// @Bean
// public CorsConfigurationSource corsConfigurationSource() {
// CorsConfiguration configuration = new CorsConfiguration();
// configuration.setAllowedOrigins(List.of("http://can.kurttekin.com"));
// configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
// configuration.setAllowedHeaders(List.of("*"));
// configuration.setAllowCredentials(true);
// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// source.registerCorsConfiguration("/**", configuration);
// return source;
// }
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
// Use NoOp PasswordEncoder to handle plain text passwords for now
return NoOpPasswordEncoder.getInstance();
}
/*
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
*/
}
I defined some security configurations here. To authenticate users we are gonna need an unprotected endpoint: /api/auth/login can be accessed by anyone as you may expected. But /api/admin only accessible by admins; /api/user/ is accessible by user and admins. I added filter before called jwtauthenticationfilter, this is crucial for our application as we will implement JWT.
JWT Utility Class
The JwtUtil
class handles token creation, extraction of usernames, and validation. This utility will be the backbone of our token-based authentication.
@Component
public class JwtUtil {
private SecretKey SECRET_KEY = Jwts.SIG.HS256.key().build(); //or HS384.key() or HS512.key()
@Value("${app.jwtExpirationInMs}")
private int EXPIRATION_TIME;
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expireDate = new Date(now.getTime() + EXPIRATION_TIME);
return Jwts.builder()
.subject(userDetails.getUsername())
.claim("role", userDetails.getAuthorities())
.issuedAt(new Date())
.expiration(expireDate)
.signWith(SECRET_KEY)
.compact();
}
public String getUsernameFromJwt(String token) {
Claims claims = Jwts.parser()
.verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(token)
.getPayload();
//return Long.parseLong(claims.getSubject()); refactored returns username instead of user id
return claims.getSubject();
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().
verifyWith(SECRET_KEY)
.build()
.parseSignedClaims(authToken);
return true;
} catch (MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException ex) {
// Handle exceptions
}
return false;
}
Custom JWT Filter
Next, we create a custom filter class that extends OncePerRequestFilter
. This filter processes each incoming request, ensuring that JWTs are properly validated before any protected resource is accessed. We exclude the /api/auth/login
endpoint from this filter so that users can access the login API without requiring a token.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired private JwtUtil jwtUtil;
@Autowired private CustomUserDetailsService userDetails;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
if (request.getServletPath().contains("/api/auth/login")) {
filterChain.doFilter(request, response);
return;
}
String jwt = getJwtFromRequest(request);
if (jwt != null && jwtUtil.validateToken(jwt)) {
String username = jwtUtil.getUsernameFromJwt(jwt);
// Load user associated with JWT
UserDetails theUserDetails = userDetails.loadUserByUsername(username);
if (theUserDetails != null) {
// Set authentication context
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
theUserDetails, null,
theUserDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
Auth Service
The authentication service interacts with UserDetailsService
to load user details by username and authenticate them. This ensures that only known users can access protected endpoints.
@Service
public class AuthService {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AuthenticationManager authenticationManager;
public String login(String username, String password) throws AuthenticationException {
// UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// // REMOVED PASS ENCRYPT FOR EASY TESTING PURPOSES DONT DO THAT IN PRODUCTION
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
username,
password
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtil.generateToken((UserDetails) authentication.getPrincipal());
return jwt;
/*
if (new BCryptPasswordEncoder().matches(password, userDetails.getPassword())) {
return jwtUtil.generateToken(userDetails);
} else {
throw new AuthenticationException("Invalid credentials") {};
}
*/
}
I added some users to database
INSERT INTO "app_user" (email, first_name, last_name, password, role, username)
VALUES
('admin@x.com', 'Can', 'Kurttekin', 'adminpass', 'ADMIN', 'cankurttekin'),
('individual@example.com', 'Alice', 'last', 'userpass', 'INDIVIDUAL_USER', 'alice26'),
('corporate@example.com', 'Bob', 'last', 'corpuserpass', 'CORPORATE_USER', 'bob06');
Conclusion
With JWT tokens in place, our API is now secured. Users must authenticate themselves to gain access to the API’s features. This method ensures a robust and scalable approach to managing user roles and permissions.
You can find source code on my github: https://github.com/cankurttekin/SpringSecurityJWT