Implementing authorization and authentication no @RestController.
MINDLUNNY
Posted on October 9, 2023
A reference below all this.
STEPS
1. Add dependencies to your pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.0.12</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--Template-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mustache</artifactId>
</dependency>
<!--********-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.2</version>
</dependency>
<!--Devtools automatically reload the server when saving a change or press 'Ctrl+s'-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<version>3.1.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
2. Create the User model:
Note: 'USER_TABLE' is just an example, you can rename it.
@Entity
@ToString
@Table(name="USER_TABLE", schema = "public")
@Getter
@Setter
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long user_id;
String email;
String username;
String password;
LocalDateTime createdAt;
LocalDateTime updatedAt;
@Enumerated(EnumType.STRING)
private Role user_role;
public User(){}
public User (String username, String password) {
this.username = username;
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = user_role.getPermissions().stream()
.map(permissionEnum -> new SimpleGrantedAuthority(permissionEnum.name()))
.collect(Collectors.toList());
return authorities;
/*
Down, others
methods implemented
of UserDetails
*/
}
3. Create the UserRepository for sourcing the User by username:
public interface UserRepository extends JpaRepository<User, Long>{ //User, Password
Optional<User> findByUsername(String username);
}
4. Create two 'Enum': Permission and Role.
public enum Permission {READ_OVERVIEW;}
@AllArgsConstructor
public enum Role {
ADMIN(Arrays.asList(Permission.READ_OVERVIEW));
@Getter
@Setter
private List<Permission> permissions;
}
5. Before We generate the JWT We have to create an AuthenticationResponse class to store the token.
@AllArgsConstructor
@Getter
@Setter
public class AuthenticationResponse {private String JWT;}
6. At this point, We define the JWTService class, which is responsible for generating JSON Web Token (JWT). It are essential for the authentication and authorization process in our application. The class encapsulates the logic to create a JWT model, including the construction of the token's header, payload, and signature.
@Service
public class JWTService {
@Value("${security.jwt.expiration-minutes}")
private long EXPIRATION_MINUTES;
@Value("${security.jwt.secret-key}")
private String SECRET_KEY;
public String generateToken (User user, Map<String, Object> extraClaims) {
LocalDateTime emitedTime = LocalDateTime.now();
LocalDateTime expirationTime = emitedTime.plusMinutes(EXPIRATION_MINUTES);
Date issuedAt = Date.from(emitedTime.atZone(ZoneId.systemDefault()).toInstant());
Date expiration = Date.from(expirationTime.atZone(ZoneId.systemDefault()).toInstant());
return Jwts.builder()
.setClaims(extraClaims)
.setSubject(user.getUsername())
.setIssuedAt(issuedAt)
.setExpiration(expiration)
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) //Building the JWT's header.
.signWith(generateKey(), SignatureAlgorithm.HS256) //Building the JWT's signature.
.compact();
}
private Key generateKey () {
byte[] secretAsBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(secretAsBytes);
}
public String extractUsername (String jwt) {
return extractAllClaims(jwt).getSubject();
}
private Claims extractAllClaims(String jwt) {
return Jwts.parserBuilder().setSigningKey(generateKey()).build()
.parseClaimsJws(jwt).getBody();
}
}
Note: To customize the expiration time and secret key you can modify the 'application.properties' file.
//Add these lines into the 'application.properties' file
security.jwt.expiration-minutes=30
security.jwt.secret-key=bWluaW5ldC1zZXJ2aWNlX21pbmRsdW5ueV9waW5lYmVycnljb2Rl
The security.jwt.expiration-minutes property sets the token expiration time in minutes, while security.jwt.secret-key holds the encrypted secret key. You can use this link to encrypt your secret key remember make it before setting it in the configuration file.
7. We introduce the AuthenticationService class, responsible for authenticating user credentials and enhancing the JWT payload with specific attributes. The login method verifies the provided username and password, authenticates the user, and generates a JWT token. Moreover, it includes custom claims such as the user's role and permissions in the JWT payload.
@Service
public class AuthenticationService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserRepository userRepository;
@Autowired
private JWTService jwtService;
public AuthenticationResponse login (User user) {
User sourceUser = userRepository.findByUsername(user.getUsername()).get();
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword()
); //Authenticate just username and password.
authenticationManager.authenticate(authToken);
SecurityContextHolder.getContext().setAuthentication(authToken);
String jwt = jwtService.generateToken(sourceUser, generateExtraClaims(sourceUser));
return new AuthenticationResponse(jwt);
}
private Map<String, Object> generateExtraClaims (User user) {
Map<String, Object> extraClaims = new HashMap<>();
extraClaims.put("user_role", user.getUser_role().name());
extraClaims.put("permissions", user.getAuthorities());
return extraClaims;
}
}
8. Create two controllers for the "testing". Each of the 'returns' to the endpoint is just an example.
The purpose of this file is to authenticate the User. Additionally, If you can appreciate the login method there is a cookie named token. This cookie is used to store the JWT on the client side, allowing communication between the client and server. So How can we send the token to the client's storage? For that, We support ourselves with HttpServletResponse. Once the JWT is generated through the AuthenticationService class, the token is stored in a cookie and sent to the client. Subsequently, redirects the 'overview' template.
@Controller
@RequestMapping("/restricted")
public class LoginController {
@Value("${security.jwt.expiration-minutes}")
private int EXPIRATION_MINUTES;
@Autowired
private AuthenticationService authenticationService;
private AuthenticationResponse jwt;
@GetMapping("/admin")
public String login () {
return "login";
}
@PostMapping("/admin/login")
public String login (
@RequestParam("username") String username,
@RequestParam("password") String password,
HttpServletResponse response) throws IOException, InterruptedException {
jwt = authenticationService.login(new AuthenticationRequest(username, password));
Cookie jwtCookie = new Cookie("token", jwt.getJWT());
jwtCookie.setMaxAge(EXPIRATION_MINUTES*60);
response.addCookie(jwtCookie);
return "redirect:/restricted/admin/overview";
}
}
For adding, the value of the variable EXPIRATION_MINUTES is estimated in seconds you can remove and add it directly in the setMaxAge function.
Reference about the storage section.
The OverviewController contains a crucial method called logout. Its finality is to capture the token for then remove it. Setting the setMaxAge to 0, the cookie is effectively deleted. Within this method, the server identifies the cookie's name, resets its age to 0, and specifies the path. This ensures the removal of the token once the user prefers to log out.
@Controller
@RequestMapping("/restricted/admin")
public class OverviewController {
@Autowired
HttpServletRequest request;
@Autowired
HttpServletResponse response;
@GetMapping("/overview")
@PreAuthorize("hasAuthority('READ_OVERVIEW')")
public String overview () {
return Render.OVERVIEW;
}
@PostMapping("/overview/logout")
public String logout () {
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
cookie.setMaxAge(0);
cookie.setPath("/restricted/admin");
response.addCookie(cookie);
break;
}
}
}
return "redirect:/restricted/admin";
}
}
9. **We have three classes called **HttpSecurityConfig, SecurityBeansInjector, and JWTAuthenticationFilter.
The class mentioned is responsible for verifying authorization after receiving the token sent by the client. It checks the received token from the client's cookie and authenticates the user based on the extracted username. If the token is valid and contains a username, the user is authenticated and allowed to proceed.
@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JWTService jwtService;
@Autowired
private UserRepository userRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//Save me
Cookie[] cookies = request.getCookies();
String token = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
token = cookie.getValue();
System.out.println("Cookie: "+token.toString());
break;
}
}
}
if (token == null || token.isEmpty()) {
filterChain.doFilter(request, response);
return;
}
String username = jwtService.extractUsername(token);
User user = userRepository.findByUsername(username).get();
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
username, null, user.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authToken);
filterChain.doFilter(request, response);
}
}
SecurityBeansInjector checks whether a user exists or not. then It is utilized by the AuthenticationManager component, which, in turn, leverages the "authenticate" function in the AuthenticationService class. This class is responsible for configuring crucial security components, namely AuthenticationProvider and AuthenticationManager. Besides, it interacts with the UserRepository to access the database and retrieve user information.
@Component
public class SecurityBeansInjector {
@Autowired
private UserRepository userRepository;
@Bean
public AuthenticationManager authenticationManager (AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider () {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService());
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder () {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService () {
return username -> {
return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found."));
};
}
}
In this class, We configured the endpoints that require specific permissions. Only two endpoints necessitate permissions. Additionally, We have disabled the Spring Security CSRF protection.
@Component
@EnableWebSecurity
@EnableMethodSecurity
public class HttpSecurityConfig {
@Autowired
private AuthenticationProvider authenticationProvider;
@Autowired
private JWTAuthenticationFilter authenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain (HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf(csrfConfig -> csrfConfig.disable())
.sessionManagement(sessionManagementConfig -> sessionManagementConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(builderRequestMatchers());
return httpSecurity.build();
}
private static Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> builderRequestMatchers () {
return authConfig -> {
authConfig.requestMatchers(HttpMethod.POST, "/restricted/admin/login").permitAll();
authConfig.requestMatchers(HttpMethod.GET, "/restricted/admin/overview").hasAuthority(Permission.READ_OVERVIEW.name());
authConfig.requestMatchers(HttpMethod.POST, "/restricted/admin/overview/logout").hasAuthority(Permission.READ_OVERVIEW.name());
authConfig.requestMatchers("/error").permitAll();
authConfig.anyRequest().denyAll();
};
}
}
Note: AuthenticationProvider is particularly a Spring Security class.
To make the long story short You can find guidance in this Github repository
Posted on October 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.