Skip to content

Commit f27dc4e

Browse files
author
samirgarg
committed
Add configurable JWT token validation and then populating auth details in a request scope object
1 parent 0bc093e commit f27dc4e

22 files changed

Lines changed: 533 additions & 24 deletions

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,8 @@ RUN chmod 755 /opt/app/app.jar
2929
# ---- Runtime ----
3030
EXPOSE 4550
3131

32+
# Documented runtime configuration
33+
# JWT secret for token verification (Base64-encoded HS256 key)
34+
ENV JWT_SECRET_KEY="it-must-be-a-string-secret-at-least-256-bits-long"
35+
3236
CMD ["java", "-jar", "/opt/app/app.jar"]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ gradle pactVerificationTest
109109

110110
Contributions are welcome! Please see the [CONTRIBUTING.md](.github/CONTRIBUTING.md) file for guidelines.
111111

112+
See also: [JWTFilter documentation](docs/JWTFilter.md)
113+
112114
## License
113115

114116
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ dependencies {
270270

271271
implementation 'com.fasterxml.jackson.core:jackson-databind:2.20.0'
272272

273+
implementation 'io.jsonwebtoken:jjwt:0.13.0'
274+
273275
compileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion
274276
annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion
275277

docs/JWTFilter.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# JWTFilter
2+
3+
`JWTFilter` enforces the presence of a JWT on incoming requests, validates it, and exposes user details for the lifetime of the request.
4+
5+
## Purpose
6+
- Requires the `jwt` header on requests (except excluded paths)
7+
- Validates and parses the token via `JWTService`
8+
- Stores `userName` and `scope` in a request-scoped `AuthDetails` bean
9+
10+
## Configuration
11+
Defined in `src/main/resources/application.yaml`:
12+
13+
```yaml
14+
jwt:
15+
secretKey: "it-must-be-a-string-secret-at-least-256-bits-long"
16+
filter:
17+
enabled: false
18+
```
19+
20+
- `jwt.secretKey`: Base64 key suitable for HS256 (≥ 256 bits)
21+
- `jwt.filter.enabled`: When false, the filter is skipped entirely. When true, it runs for all paths except those excluded.
22+
23+
### Enabling per environment
24+
- Env var: `JWT_FILTER_ENABLED=true`
25+
- Tests: `@SpringBootTest(properties = "jwt.filter.enabled=true")`
26+
- Profile override: `application-<profile>.yaml`
27+
28+
## Excluded paths
29+
Currently excluded: `/health`. Extend in `JWTFilter.shouldNotFilter(...)` if needed.
30+
31+
## Error behaviour
32+
- Missing header: 401 UNAUTHORIZED ("No jwt token passed")
33+
- Invalid token: 400 BAD_REQUEST with details
34+
35+
## Related classes
36+
- `uk.gov.hmcts.cp.filters.jwt.JWTFilter`
37+
- `uk.gov.hmcts.cp.filters.jwt.JWTService`
38+
- `uk.gov.hmcts.cp.filters.jwt.AuthDetails`
39+
- `uk.gov.hmcts.cp.config.JWTConfig`

src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerIT.java renamed to src/integrationTest/java/uk/gov/hmcts/cp/controllers/CourtScheduleControllerIntegrationTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import com.fasterxml.jackson.databind.JsonNode;
44
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import jakarta.annotation.Resource;
56
import org.junit.jupiter.api.Test;
67
import org.junit.jupiter.api.extension.ExtendWith;
78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
9-
import org.springframework.beans.factory.annotation.Autowired;
1010
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
1111
import org.springframework.boot.test.context.SpringBootTest;
1212
import org.springframework.http.MediaType;
@@ -22,10 +22,10 @@
2222
@ExtendWith(SpringExtension.class)
2323
@SpringBootTest
2424
@AutoConfigureMockMvc
25-
class CourtScheduleControllerIT {
26-
private static final Logger log = LoggerFactory.getLogger(CourtScheduleControllerIT.class);
25+
class CourtScheduleControllerIntegrationTest {
26+
private static final Logger log = LoggerFactory.getLogger(CourtScheduleControllerIntegrationTest.class);
2727

28-
@Autowired
28+
@Resource
2929
private MockMvc mockMvc;
3030

3131
@Test

src/integrationTest/java/uk/gov/hmcts/cp/controllers/RootControllerIntegrationIT.java renamed to src/integrationTest/java/uk/gov/hmcts/cp/controllers/HealthCheckIntegrationTest.java

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,27 @@
11
package uk.gov.hmcts.cp.controllers;
22

3+
import jakarta.annotation.Resource;
34
import org.junit.jupiter.api.DisplayName;
45
import org.junit.jupiter.api.Test;
56
import org.junit.jupiter.api.extension.ExtendWith;
6-
import org.springframework.beans.factory.annotation.Autowired;
77
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
88
import org.springframework.boot.test.context.SpringBootTest;
99
import org.springframework.test.context.junit.jupiter.SpringExtension;
1010
import org.springframework.test.web.servlet.MockMvc;
1111

12-
import static org.hamcrest.Matchers.containsString;
1312
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
1413
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
15-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
1614
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
1715
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
1816

1917
@ExtendWith(SpringExtension.class)
20-
@SpringBootTest
2118
@AutoConfigureMockMvc
22-
class RootControllerIntegrationIT {
19+
@SpringBootTest
20+
class HealthCheckIntegrationTest {
2321

24-
@Autowired
22+
@Resource
2523
private MockMvc mockMvc;
2624

27-
@DisplayName("Should welcome upon root request with 200 response code")
28-
@Test
29-
void shouldCallRootAndGet200() throws Exception {
30-
mockMvc.perform(get("/"))
31-
.andDo(print())
32-
.andExpect(status().isOk())
33-
.andExpect(content()
34-
.string(containsString("Welcome to service-hmcts-marketplace-springboot-template")));
35-
}
36-
3725
@DisplayName("Actuator health status should be UP")
3826
@Test
3927
void shouldCallActuatorAndGet200() throws Exception {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package uk.gov.hmcts.cp.filters.jwt;
2+
3+
import static org.hamcrest.Matchers.containsString;
4+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
5+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6+
7+
import jakarta.annotation.Resource;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.test.web.servlet.MockMvc;
13+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
14+
15+
@SpringBootTest
16+
@AutoConfigureMockMvc
17+
class JWTFilterDisabledIntegrationTest {
18+
19+
@Resource
20+
MockMvc mockMvc;
21+
22+
@DisplayName("JWT filter should not complain of missing JWT when the filter is disabled")
23+
@Test
24+
void shouldNotFailWhenTokenIsMissingAndFilterIsDisabled() throws Exception {
25+
mockMvc
26+
.perform(
27+
MockMvcRequestBuilders.get("/")
28+
).andExpectAll(
29+
status().isOk(),
30+
content().string(containsString("Welcome to service-hmcts-springboot-template"))
31+
);
32+
}
33+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package uk.gov.hmcts.cp.filters.jwt;
2+
3+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
4+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
5+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6+
import static uk.gov.hmcts.cp.filters.jwt.JWTFilter.JWT_TOKEN_HEADER;
7+
8+
import jakarta.annotation.Resource;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
11+
import org.springframework.boot.test.context.SpringBootTest;
12+
import org.springframework.test.web.servlet.MockMvc;
13+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
14+
import org.springframework.web.client.HttpClientErrorException;
15+
16+
@SpringBootTest(properties = {"jwt.filter.enabled=true"})
17+
@AutoConfigureMockMvc
18+
class JWTFilterIntegrationTest {
19+
20+
@Resource
21+
MockMvc mockMvc;
22+
23+
@Resource
24+
private JWTService jwtService;
25+
26+
@Test
27+
void shouldPassWhenTokenIsValid() throws Exception {
28+
String jwtToken = jwtService.createToken();
29+
mockMvc
30+
.perform(
31+
MockMvcRequestBuilders.get("/")
32+
.header(JWT_TOKEN_HEADER, jwtToken)
33+
).andExpectAll(
34+
status().isOk(),
35+
content().string("Welcome to service-hmcts-springboot-template, " + JWTService.USER)
36+
);
37+
}
38+
39+
@Test
40+
void shouldFailWhenTokenIsMissing() {
41+
assertThatExceptionOfType(HttpClientErrorException.class)
42+
.isThrownBy(() -> mockMvc
43+
.perform(
44+
MockMvcRequestBuilders.get("/")
45+
))
46+
.withMessageContaining("No jwt token passed");
47+
}
48+
}

src/main/java/uk/gov/hmcts/cp/Application.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
@SpringBootApplication
77
@SuppressWarnings("HideUtilityClassConstructor")
8+
89
public class Application {
910

1011
public static void main(final String[] args) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package uk.gov.hmcts.cp.config;
2+
3+
import uk.gov.hmcts.cp.filters.jwt.AuthDetails;
4+
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.web.context.annotation.RequestScope;
8+
9+
@Configuration
10+
public class JWTConfig {
11+
12+
@Bean
13+
@RequestScope
14+
// attributes are set in the filter
15+
AuthDetails jwt(){
16+
return AuthDetails.builder().build();
17+
}
18+
}

0 commit comments

Comments
 (0)