Implementing authorization and authentication no @RestController.

mindlunny

MINDLUNNY

Posted on October 9, 2023

Implementing authorization and authentication no @RestController.

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>
Enter fullscreen mode Exit fullscreen mode

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
*/
    }
Enter fullscreen mode Exit fullscreen mode

3. Create the UserRepository for sourcing the User by username:

public interface UserRepository extends JpaRepository<User, Long>{ //User, Password
    Optional<User> findByUsername(String username);
}
Enter fullscreen mode Exit fullscreen mode

4. Create two 'Enum': Permission and Role.


public enum Permission {READ_OVERVIEW;}
Enter fullscreen mode Exit fullscreen mode

@AllArgsConstructor
public enum Role {
    ADMIN(Arrays.asList(Permission.READ_OVERVIEW));

    @Getter
    @Setter
    private List<Permission> permissions;
}
Enter fullscreen mode Exit fullscreen mode

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;}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Image Storage-Inspect


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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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);

    }
}
Enter fullscreen mode Exit fullscreen mode

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."));
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
        };
    }

}
Enter fullscreen mode Exit fullscreen mode

Note: AuthenticationProvider is particularly a Spring Security class.

To make the long story short You can find guidance in this Github repository

💖 💪 🙅 🚩
mindlunny
MINDLUNNY

Posted on October 9, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related