Spring Authorization Server OAuth (JWT) that uses Spring Security and Spring Security OAuth2 Resource. Applying the new way to configure OAuth JWT without the need of writing a customized filter.
Spring Authorization Server OAuth that uses JWT Token
- A favorite text editor or IDE
 - JDK 1.8 or later
 - Gradle 4+ or Maven 3.2+
 
-  
Navigate to Spring initializr.
 -  
Define the project name example:
spring-authorization-server-oauth-jwt -  
Choose Project Maven and the language Java.
 -  
Choose Your Java version ex: 17
 -  
Click add dependencies and select:
- Spring Web
 - Spring Security
 - Spring Oauth2 Resource Server
 - Spring Data JPA
 - H2 Database
 - Lombok
 - Rest Assured
 
 -  
Click Generate.
 
Unzip the Downloaded Zip and open the Project using your favorite text editor or IDE
- Define The Permission Entity
 
@Entity(name = "Permission") @Table(name = "permissions") @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Permission { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; }- Define the User Entity and override the implemented methods from the UserDetails interface
 
@Entity(name = "User") @Table(name = "users") @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class User implements UserDetails { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; private String email; private String password; private boolean active; @ManyToMany(fetch = FetchType.EAGER) @JoinTable( name = "users_permissions", joinColumns = @JoinColumn(name = "users_id"), inverseJoinColumns = @JoinColumn(name = "permissions_id")) public List<Permission> permissions = new ArrayList<>(); @JsonIgnore @Override public Collection<? extends GrantedAuthority> getAuthorities() { return permissions.stream() .map(Permission::getName) .map(SimpleGrantedAuthority::new) .toList(); } @JsonIgnore @Override public String getPassword() { return password; } @JsonIgnore @Override public String getUsername() { return email; } @JsonIgnore @Override public boolean isAccountNonExpired() { return true; } @JsonIgnore @Override public boolean isAccountNonLocked() { return true; } @JsonIgnore @Override public boolean isCredentialsNonExpired() { return true; } @JsonIgnore @Override public boolean isEnabled() { return active; } }Define the User Repository and the Query findOneByEmailAndActive, we are going to use it for retrieving the user object when we try to authenticate
@Repository public interface UserRepository extends JpaRepository<User, Long> { @Query("select u from User u where u.email = :email and u.active = true") Optional<User> findOneByEmailAndActive(String email); }- Define the User Details Service 
@Serviceimplementation that will manage user authentication. - The annotation 
@RequiredArgsConstructoris a lombok annotation used to generate a constructor with required attributes 
@Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { return userRepository.findOneByEmailAndActive(email) .orElseThrow(() -> new UsernameNotFoundException("Email address does not exist")); } }- Define the Spring Security Configuration
 
@Configuration @RequiredArgsConstructor @EnableMethodSecurity(securedEnabled = true) public class SecurityConfig { /**  * Define the password encoder.  *  * @return {@link BCryptPasswordEncoder}  */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /**  * Define the filer chain.  * @param http HttpSecurity  * @return SecurityFilterChain  * @throws Exception Exception  */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // Enable cors & disable CSRF http.cors(withDefaults()).csrf(AbstractHttpConfigurer::disable); // Set session management to stateless http.sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); // Set unauthorized requests exception handler http.exceptionHandling( exceptionHandling -> exceptionHandling .authenticationEntryPoint((request, response, ex) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage())) .accessDeniedHandler((request, response, ex) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage())) ); // Set the secured endpoints and the permitted endpoints http.authorizeHttpRequests((authz) -> authz // Our public endpoints * .requestMatchers("/api/authenticate").permitAll() // Our private endpoints .anyRequest().authenticated()); // Add The JWT token filter http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } }- Define the Spring JWT Security Configuration
 
@Log4j2 @Configuration @RequiredArgsConstructor public class SecurityJwtConfiguration { private final ApplicationConfig config; /**  * The JWT Decoder.  *  * @return {@link JwtDecoder}  */ @Bean public JwtDecoder jwtDecoder() { NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(getSecretKey()).macAlgorithm(SecurityService.JWT_ALGORITHM).build(); return token -> { try { return jwtDecoder.decode(token); } catch (Exception e) { log.error(e.getMessage()); } return null; }; } /**  * The JWT Decoder.  *  * @return {@link JwtEncoder}  */ @Bean public JwtEncoder jwtEncoder() { return new NimbusJwtEncoder(new ImmutableSecret<>(getSecretKey())); } /**  * The Jwt Authentication converter  *  * @return JwtAuthenticationConverter  */ @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthorityPrefix(""); grantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityService.AUTHORITIES_KEY); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } /**  * Get the Secret key.  *  * @return {@link SecretKey}  */ private SecretKey getSecretKey() { byte[] keyBytes = Base64.from(config.getSecurity().getKey()).decode(); return new SecretKeySpec(keyBytes, 0, keyBytes.length, SecurityService.JWT_ALGORITHM.getName()); } }- Define the Security Service, used to generate a new token from an authentication object
 
@Service @RequiredArgsConstructor public class SecurityService { public static final MacAlgorithm JWT_ALGORITHM = MacAlgorithm.HS512; public static final String AUTHORITIES_KEY = "auth"; private final JwtEncoder jwtEncoder; private final ApplicationConfig config; public String createToken(Authentication authentication) { String authorities = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(" ")); Instant now = Instant.now(); Instant validity = now.plus(config.getSecurity().getValidity(), ChronoUnit.SECONDS); JwtClaimsSet claims = JwtClaimsSet.builder() .issuedAt(now) .expiresAt(validity) .subject(authentication.getName()) .claim(AUTHORITIES_KEY, authorities) .build(); JwsHeader jwsHeader = JwsHeader.with(JWT_ALGORITHM).build(); return this.jwtEncoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue(); } }- Define the Authenticate Controller
 
@RestController @RequestMapping("/api") @RequiredArgsConstructor public class AuthenticateController { private final AuthenticationManagerBuilder authenticationManagerBuilder; private final SecurityService securityService; @PostMapping("/authenticate") public ResponseEntity<JWTToken> authorize(@RequestBody LoginDTO loginDTO) { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( loginDTO.getEmail(), loginDTO.getPassword() ); Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken); SecurityContextHolder.getContext().setAuthentication(authentication); String jwt = securityService.createToken(authentication); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setBearerAuth(jwt); return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK); } }- Define the Secured Controller
 
@RestController @RequiredArgsConstructor @RequestMapping("/api") public class SecuredController { private final UserRepository userRepository; @GetMapping("/me") public User getAuth(HttpServletRequest request) { return userRepository.findOneByEmailAndActive(request.getRemoteUser()) .orElseThrow(() -> new UsernameNotFoundException("user not found")); } }- Create 
data.sqlunder the resources folder to populate our table 
INSERT INTO PERMISSIONS (ID, NAME) VALUES (1, 'read'), (2, 'write'); INSERT INTO USERS (ID, NAME, EMAIL, PASSWORD, ACTIVE) VALUES (1, 'john doe', 'john.doe@example.com', '$2a$12$x1pibFM7OeLeq..7/9rEkewNsSokhPIx7saguQsLg/jheUI2EBOEG', true); INSERT INTO USERS_PERMISSIONS (USERS_ID, PERMISSIONS_ID) VALUES (1, 1); When running the application, by default the `data.sql` will be executed before the entity creation into the database, to prevent that add the following property under the `application.properties`- Update the 
application.ymlwith the following properties 
spring: jpa: defer-datasource-initialization: true show-sql: true app: security: key: 'NWM3NDhkOTM5NDc4MTgyMTdiZDdmMzM5NjIyOTRkM2U4YWU2MzkyNDM3YjNlNzc3MzA3Yjg0MDA3MGU5MzEzZWY4ZjhiMGQ5MGYyOTU0YjUyNTJhOTliMzMzMWExOWRhNGUyNWVhMWE5ZWY3MzY0ZjYwMTRiNjNhZDQ0ZTkzNDA' validity: 3600Run the Java application as a SpringBootApplication with your IDE or use the following command line
 ./mvnw spring-boot:runNow, you can open the URL below on your browser, default port is 8080 you can set it under the application.yml
Write some test cases to test the authentication flow that work correctly
- Test 1 : check the authentication and retrieve JWT token
 - Test 2 : try to access a secured resource to get the authenticated user detail
 
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ImportAutoConfiguration({SecurityService.class}) public class AuthenticateTest { @LocalServerPort private int port; public static final String TOKEN_URL = "/api/authenticate"; public static final String AUTH_URL = "/api/me"; public static final String EMAIL = "john.doe@example.com"; public static final String PASSWORD = "password"; @Autowired private SecurityService securityService; @Test public void authenticateAndGetJWTToken() throws JSONException { JSONObject jsonObject = new JSONObject(); jsonObject.put("email", EMAIL); jsonObject.put("password", PASSWORD); Response response = RestAssured .given() .contentType(APPLICATION_JSON.getMimeType()) .body(jsonObject.toString()) .post("http://localhost:" + port + TOKEN_URL); assertThat(response.jsonPath().getString("jwt_token")).isNotBlank(); } @Test public void checkTheAuthenticatedUser() { String token = getToken(); Response response = RestAssured .given() .header("Authorization", "bearer " + token) .get("http://localhost:" + port + AUTH_URL); assertThat(response.jsonPath().getString("email")).isEqualTo(EMAIL); } public String getToken() { UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(EMAIL, null, Collections.emptyList()); return securityService.createToken(authenticationToken); } }Congratulations π ! You've created a Spring Authorization Server OAuth that uses JWT Token using Spring Security & Spring OAuth2 Resource
The tutorial can be found here on GitHub π
Check new tutorials on nonestack π