Implementing JWT Authorization with Spring Boot using a Secret Key.

Implementing JWT Authorization with Spring Boot using a Secret Key.

ยท

6 min read

Introduction:

In this article, I will guide you through setting up JWT Authorization based on a secret key in a Spring Boot 3 application. We will develop a small and straightforward service featuring two endpoints. The first endpoint will handle token generation, covering the process of constructing JWTs. The second endpoint will greet the guest and be secured with JWT Authorization.

Spring OAuth2 Resource Server vs Custom Security Filters:

It's crucial to note that the Spring Framework already provides the necessary filters and configurations to establish JWT Authorization based on a secret key. All these components are conveniently packaged within the Spring Boot OAuth2 Resource Server starter.

While many articles demonstrate the creation of custom Spring Security filters for JWT Authorization setup, it's essential to understand that Spring's existing components are often sufficient for standard use cases. In this article, I'll illustrate that Spring's built-in features can streamline the JWT Authorization process, eliminating the need for extensive customizations.

For further details on the prepared Spring components facilitating JWT Authorization and Authentication, you can refer to the dedicated Spring documentation.

Tech Stack

Java 21

Spring Boot 3

Gradle 8.5

Development

Dependencies

Here is the gradle file with dependencies.

plugins {
    id "java"
    id "org.springframework.boot" version "3.2.0"
    id "io.spring.dependency-management" version "1.1.4"
}

...

dependencies {
    // Spring
    implementation "org.springframework.boot:spring-boot-starter-web"
    implementation "org.springframework.boot:spring-boot-starter-validation"
    implementation "org.springframework.boot:spring-boot-starter-oauth2-resource-server"

    // Lombok
    compileOnly "org.projectlombok:lombok"
    annotationProcessor "org.projectlombok:lombok"
}

Application Properties

Below is the content of the application properties file, encompassing the fundamental properties required for creating, signing, and verifying JWTs. Take note of the secret key, ensuring its consistency with the selected algorithm.

app:
  jwt:
    key: ${APP_JWT_SECRET}
    algorithm: ${APP_JWT_ALGORITHM}
    issuer: ${APP_JWT_ISSUER}
    expiresIn: ${APP_JWT_EXPIRES_IN}

Here are the sample values that can be used:

app:
  jwt:
    key: 7qbq6zTyZ8bC3dA2wS7gN1mK9jF0hL4tUoP6iBvE3nG8xZaQrY7cW2fA # according to SHA-256 requirements
    algorithm: HS256
    issuer: http://localhost:8080
    expiresIn: 1m

Configuration class

This is our class for application properties storing.

@Getter
@Setter
@Validated
@ConfigurationProperties(prefix = "app.jwt")
public class AppJwtProperties {

    @NotNull
    private SecretKey key;

    @NotEmpty
    private String issuer;

    @NotNull
    private JWSAlgorithm algorithm;

    @NotNull
    @DurationMin(seconds = 1)
    private Duration expiresIn;

    public void setAlgorithm(String algorithm) {
        this.algorithm = JWSAlgorithm.parse(algorithm);
    }

    public void setKey(String key) {
        var jwk = new OctetSequenceKey.Builder(key.getBytes())
                .algorithm(algorithm)
                .build();

        this.key = jwk.toSecretKey();
    }

}

I think the code is self-explanatory, except for two points:

  1. I included validation in the class to ensure safety and consistency. This helps prevent oversight in declaring application properties or setting inappropriate values.

  2. We employ setters and the values from application properties (algorithm name and secret key) to construct essential Java objects (SecretKey and JWSAlgorithm, respectively). This approach is more suitable than parsing the raw values each time.

Main Class:

Now that we've established the application properties configuration class, let's briefly examine the main class. There's nothing particular to highlight; all the content, except for the @EnableConfigurationProperties, is default.

@SpringBootApplication
@EnableConfigurationProperties(AppJwtProperties.class)
public class SimpleJwtAuthService {

    public static void main(String[] args) {
        SpringApplication.run(SimpleJwtAuthService.class, args);
    }

}

Security Configuration:

I would consider this class the most intriguing in our application. Here are some notable points:

  1. @EnableMethodSecurity is applied since we will utilize @PreAuthorize for enhanced readability in controllers.

  2. By using oauth2ResourceServer with JWT configurer, we establish the Spring JWT Authorization and Authentication flow.

  3. Unfortunately, as of now, Spring lacks application properties support for JWT secret, even though there is a specialized decoder. Hence, we introduced our own application property for a JWT secret.

  4. Due to the previous point, we explicitly declare a JwtDecoder bean. Despite this, it remains quite convenient because, as mentioned, there is already a prepared class for that.

  5. CSRF is disabled for the sake of simplicity.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final AppJwtProperties appJwtProperties;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .oauth2ResourceServer(configurer -> configurer.jwt(Customizer.withDefaults()))
                .build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withSecretKey(appJwtProperties.getKey()).build();
    }

}

JWT Service:

This class is specifically designed for JWT generation. We utilize application properties and the Nimbus-JOSE-JWT library, included with the Spring Boot OAuth2 Resource Server starter, to construct and sign JWTs.

@Component
@RequiredArgsConstructor
public class JwtService {

    private final AppJwtProperties appJwtProperties;


    public String generateJWT(Map<String, Object> claims) {
        var key = appJwtProperties.getKey();
        var algorithm = appJwtProperties.getAlgorithm();

        var header = new JWSHeader(algorithm);
        var claimsSet = buildClaimsSet(claims);

        var jwt = new SignedJWT(header, claimsSet);

        try {
            var signer = new MACSigner(key);
            jwt.sign(signer);
        } catch (JOSEException e) {
            throw new RuntimeException("Unable to generate JWT", e);
        }

        return jwt.serialize();
    }

    private JWTClaimsSet buildClaimsSet(Map<String, Object> claims) {
        var issuer = appJwtProperties.getIssuer();
        var issuedAt = Instant.now();
        var expirationTime = issuedAt.plus(appJwtProperties.getExpiresIn());

        var builder = new JWTClaimsSet.Builder()
                .issuer(issuer)
                .issueTime(Date.from(issuedAt))
                .expirationTime(Date.from(expirationTime));

        claims.forEach(builder::claim);

        return builder.build();
    }

}

Authorization Controller:

Now that we've implemented and configured all the necessary components, we can proceed to create the initial controller featuring an endpoint for token generation:

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {

    private final JwtService jwtService;

    @PostMapping(path = "/token", consumes = APPLICATION_JSON_VALUE)
    public String getToken(@RequestBody Map<String, Object> claims) {
        return jwtService.generateJWT(claims);
    }

}

In this instance, we employ the previously implemented JWT service for token generation. To keep it concise, we simply send claims, which will be used for token generation, in the request body. Since this endpoint is not secured, we only require the request body.

Greeting Controller

The last part of our application is a controller where we will check the security setup:

@RestController
@RequestMapping("/greeting")
public class GreetingController {

    @GetMapping
    @PreAuthorize("hasAuthority('SCOPE_GUEST')")
    public String greet(Authentication authentication) {
        return "Hello, %s. You have next permissions: %s"
                .formatted(authentication.getName(), authentication.getAuthorities());
    }

}

@PreAuthorize is used to check that an authorized user has GUEST one among his authorities.

We return some information that was retrieved from JWT.

Now, let's inspect our application. We'll utilize the CURL utility for this purpose.

To begin, let's focus on token generation. As a reminder, this endpoint is not secured. Here is the command:

HereAndBeyond@MSI MINGW64 ~
$ curl -s --location "http://localhost:8080/auth/token" \
> --header "Content-Type: application/json" \
> --data "{
>     \"sub\": \"Ram\",
>     \"scope\": [
>         \"role1\",
>         \"role2\",
>         \"GUEST\"
>     ]
> }"

As observed, we incorporated multiple scopes in the request body, and one of them aligns with the security rules outlined in the greeting controller.

Having obtained a token, we can now employ it for the greeting endpoint call:

curl -s --location "http://localhost:8080/greeting" \
--header "Authorization: Bearer erJhbGciOiOPUzI1NiJ9.eyJpc3NiOiJodTHwOi8vbG9jYWxob3N0OjgwODAiLCJzdWIiOiJkbWl0cnkiLCJleHAiOjE3MDM5NTg4MjcsImlhdCI6MTcwMzk1ODIyNywic2NvcGUiOlsicm9sZTEiLCJyb2xlMiIsIkdVRVNUIl19.sgwvUVJazeEdhM1Vy8eXGjvGIXkAYWFfRg_VaNpISdU"

Hello, Ram. You have next permissions: [SCOPE_role1, SCOPE_role2, SCOPE_GUEST]

We were successfully greeted and the appropriate data was returned in the response.

Negative scenario (No authorization header)

Another case is calling the greeting endpoint without the authorization header:

$ curl -s --location -i "http://localhost:8080/greeting"

HTTP/1.1 401

As expected, we received a 401 response. and if we enable a debug logging level for our filter by placing logging.level.org.springframework.security: TRACE in the application properties, we will be able to see the reason:

Conclusion:

In conclusion, it's worth noting that in most cases, the Spring Boot OAuth2 Resource Server starter provides sufficient capabilities to establish the JWT Authorization and Authentication flow. Even when customization is necessary, there's often no need to reinvent the wheel. For instance:

  • If you prefer using other JWT libraries (e.g., JJWT), you can simply declare a JwtDecoder bean and use a parser from the desired library.

  • If you wish to customize the parsing process within the workflow, you can extend BearerTokenAuthenticationFilter and modify the aspects you want to adjust.

More such articles:

https://medium.com/techwasti

https://www.youtube.com/@maheshwarligade

https://techwasti.com/series/spring-boot-tutorials

https://techwasti.com/series/go-language

Did you find this article valuable?

Support techwasti by becoming a sponsor. Any amount is appreciated!

ย