Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.msohailse.app.incident.adapters.in.rest;

import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import javax.crypto.SecretKey;
import org.eclipse.microprofile.config.inject.ConfigProperty;

// Tier B: the actual enforcement gate for the tokens JwtIssuer signs. Only checks that a
// request carries a validly-signed, non-expired token — it does not derive identity from
// the token to replace the actingUserId fields the *Service classes already check; that
// remains a separate, larger change.
@Provider
@Priority(Priorities.AUTHENTICATION)
@ApplicationScoped
public class JwtAuthFilter implements ContainerRequestFilter {

@ConfigProperty(name = "jwt.secret")
String secret;

@Override
public void filter(ContainerRequestContext ctx) {
String method = ctx.getMethod();
String path = ctx.getUriInfo().getPath();

if ("GET".equals(method)) {
return;
}
if ("POST".equals(method) && ("/users/login".equals(path) || "/users/register".equals(path))) {
return;
}

String authHeader = ctx.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
return;
}

String token = authHeader.substring("Bearer ".length());
SecretKey key = Keys.hmacShaKeyFor(secret.getBytes());
try {
Jwts.parser().verifyWith(key).build().parseSignedClaims(token);
} catch (JwtException | IllegalArgumentException e) {
ctx.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ public class IncidentResourceTest {
@Inject
UserTransaction userTransaction;

@Inject
JwtIssuer jwtIssuer;

private int reporterId;
private int adminId;
// JwtAuthFilter only checks that a token is validly signed, not whose it is — any
// authenticated user's token satisfies it for these tests.
private String authHeader;

// Setup writes directly through the repository port (skipping the REST layer) but must
// really commit — the actual test calls go over HTTP, which runs in its own separate
Expand All @@ -48,6 +54,8 @@ void setup() throws Exception {
userRepository.save(admin);
adminId = admin.getId();
userTransaction.commit();

authHeader = "Bearer " + jwtIssuer.issue(admin);
}

private String createIncidentBody() {
Expand All @@ -58,13 +66,15 @@ private String createIncidentBody() {
@Test
void createFindUpdateCloseDeleteIncident() {
int deptId = given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + adminId + ",\"name\":\"Support-" + System.nanoTime() + "\"}")
.when().post("/departments")
.then().statusCode(200)
.extract().path("id");

int id = given()
.header("Authorization", authHeader)
.contentType("application/json")
.body(createIncidentBody())
.when().post("/incidents")
Expand All @@ -78,6 +88,7 @@ void createFindUpdateCloseDeleteIncident() {
.body("severity", equalTo("HIGH"));

given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + reporterId + ",\"title\":\"Smoke cleared\",\"description\":\"Resolved\",\"severity\":\"LOW\"}")
.when().put("/incidents/" + id)
Expand All @@ -86,6 +97,7 @@ void createFindUpdateCloseDeleteIncident() {
.body("severity", equalTo("LOW"));

given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + adminId + ",\"commentText\":\"Resolved, fixed the wiring\",\"assignedDepartmentId\":" + deptId + "}")
.when().patch("/incidents/" + id + "/close")
Expand All @@ -99,6 +111,7 @@ void createFindUpdateCloseDeleteIncident() {
.body("[0].text", equalTo("Resolved, fixed the wiring"));

given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"authorUserId\":" + reporterId + ",\"text\":\"Thanks, confirming it's fixed\"}")
.when().post("/incidents/" + id + "/comments")
Expand All @@ -112,20 +125,23 @@ void createFindUpdateCloseDeleteIncident() {
.body("[1].text", equalTo("Thanks, confirming it's fixed"));

given()
.header("Authorization", authHeader)
.when().delete("/incidents/" + id + "?actingUserId=" + reporterId)
.then().statusCode(204);
}

@Test
void closeByNonAdminReturns400() {
int id = given()
.header("Authorization", authHeader)
.contentType("application/json")
.body(createIncidentBody())
.when().post("/incidents")
.then().statusCode(200)
.extract().path("id");

given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + reporterId + ",\"commentText\":\"trying to close my own\"}")
.when().patch("/incidents/" + id + "/close")
Expand All @@ -135,6 +151,7 @@ void closeByNonAdminReturns400() {
@Test
void updateByNonReportingNonAdminUserReturns400() throws Exception {
int id = given()
.header("Authorization", authHeader)
.contentType("application/json")
.body(createIncidentBody())
.when().post("/incidents")
Expand All @@ -151,6 +168,7 @@ void updateByNonReportingNonAdminUserReturns400() throws Exception {
userTransaction.commit();

given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + otherReporter.getId()
+ ",\"title\":\"Hijacked\",\"description\":\"Not mine\",\"severity\":\"LOW\"}")
Expand All @@ -160,7 +178,7 @@ void updateByNonReportingNonAdminUserReturns400() throws Exception {

@Test
void findByUserReturnsIncidentsForThatUser() {
given().contentType("application/json").body(createIncidentBody())
given().header("Authorization", authHeader).contentType("application/json").body(createIncidentBody())
.when().post("/incidents")
.then().statusCode(200);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ public class TagResourceTest {
@Inject
UserTransaction userTransaction;

@Inject
JwtIssuer jwtIssuer;

private int adminId;
private int reporterId;
// JwtAuthFilter only checks that a token is validly signed, not whose it is — any
// authenticated user's token satisfies it for these tests.
private String authHeader;

@BeforeEach
void setup() throws Exception {
Expand All @@ -45,13 +51,16 @@ void setup() throws Exception {
userRepository.save(reporter);
reporterId = reporter.getId();
userTransaction.commit();

authHeader = "Bearer " + jwtIssuer.issue(admin);
}

@Test
void createFindUpdateDeleteTag() {
String title = "flood-" + System.nanoTime();

int id = given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + adminId + ",\"tagTitle\":\"" + title + "\",\"tagDescription\":\"flood-related incidents\"}")
.when().post("/tags")
Expand All @@ -66,6 +75,7 @@ void createFindUpdateDeleteTag() {

String updatedTitle = title + "-updated";
given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + adminId + ",\"tagTitle\":\"" + updatedTitle + "\",\"tagDescription\":\"still flood\"}")
.when().put("/tags/" + id)
Expand All @@ -78,6 +88,7 @@ void createFindUpdateDeleteTag() {
.body("size()", greaterThan(0));

given()
.header("Authorization", authHeader)
.when().delete("/tags/" + id + "?actingUserId=" + adminId)
.then().statusCode(204);

Expand All @@ -89,6 +100,7 @@ void createFindUpdateDeleteTag() {
@Test
void createTagByNonAdminReturns400() {
given()
.header("Authorization", authHeader)
.contentType("application/json")
.body("{\"actingUserId\":" + reporterId + ",\"tagTitle\":\"blocked-" + System.nanoTime() + "\",\"tagDescription\":\"nope\"}")
.when().post("/tags")
Expand Down
5 changes: 2 additions & 3 deletions frontend/src/app/interceptors/auth.interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

// Tier A: attaches the JWT issued at login to every outgoing request, ready for the
// backend to enforce later. Not enforced yet — actingUserId-based checks still do the
// real authorization.
// Attaches the JWT issued at login to every outgoing request. The backend's
// JwtAuthFilter now enforces it on mutating requests.
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = inject(AuthService).getToken();
if (!token) {
Expand Down