Spring Authorization Server - OAuth JWT

. . 9 min read

display

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.

What You Will build

Spring Authorization Server OAuth that uses JWT Token

What You Need

  • A favorite text editor or IDE
  • JDK 1.8 or later
  • Gradle 4+ or Maven 3.2+

Setup Project With Spring Initializr

  • 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

Start the implementation

  • 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 @Service implementation that will manage user authentication.
  • The annotation @RequiredArgsConstructor is 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.sql under 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', '[email protected]', '$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.yml with the following properties
spring:
    jpa:
        defer-datasource-initialization: true
        show-sql: true

app:
    security:
        key: 'NWM3NDhkOTM5NDc4MTgyMTdiZDdmMzM5NjIyOTRkM2U4YWU2MzkyNDM3YjNlNzc3MzA3Yjg0MDA3MGU5MzEzZWY4ZjhiMGQ5MGYyOTU0YjUyNTJhOTliMzMzMWExOWRhNGUyNWVhMWE5ZWY3MzY0ZjYwMTRiNjNhZDQ0ZTkzNDA'
        validity: 3600

Run

Run the Java application as a SpringBootApplication with your IDE or use the following command line

 ./mvnw spring-boot:run

Now, you can open the URL below on your browser, default port is 8080 you can set it under the application.yml

Testing

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 = "[email protected]";
    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);
    }

}

Summary

Congratulations 🎉 ! You've created a Spring Authorization Server OAuth that uses JWT Token using Spring Security & Spring OAuth2 Resource

Github

The tutorial can be found here on GitHub 👋

Blog

Check new tutorials on nonestack 👋

31
1116

Recent articles

Spring Data - JPA DataTable

spring data

This guide walks you through the process of building a Spring boot application that uses JPA DataTable.

Continue reading
10 min read
.
854 views

Spring Web - Rest Client

spring boot

Spring Framework introduces RestClient a new Fluent API to make synchronous HTTP requests

Continue reading
4 min read
.
1072 views

Spring Security - Basic Authentication

spring security

This guide walks you through the process of building a Spring boot application that uses Spring Security and Spring Data JPA. Applying the new way to configure Spring Security without the WebSecurityConfigurerAdapter

Continue reading
6 min read
.
923 views

Spring Boot - Get Started

spring boot

Spring Boot is a popular framework that simplifies and accelerate the process of developing a java based application

Continue reading
5 min read
.
874 views