diff --git a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java index 92cbcf02..2a824a33 100644 --- a/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java +++ b/src/main/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuer.java @@ -101,7 +101,12 @@ private Mono createClaims(ServerWebExchange exchange, Authenticati .filter(exp -> exp.compareTo(maxExpiration) <= 0) .orElse(maxExpiration)); })) - .issueTime(Objects.requireNonNullElseGet(claims.getIssueTime(), Date::new)) + .claim("re-iat", new Date()) + .issueTime(Objects.requireNonNullElseGet(claims.getIssueTime(), () -> { + return findIssuedAtTime(authentication) + .map(Date::from) + .orElseGet(Date::new); + })) .build(); }); } @@ -116,6 +121,16 @@ private Optional findExpirationTime(Authentication authentication) { return Optional.empty(); } + private Optional findIssuedAtTime(Authentication authentication) { + var principal = authentication.getPrincipal(); + if(principal instanceof Jwt jwt) { + return Optional.ofNullable(jwt.getIssuedAt()); + } else if(principal instanceof OidcUser oidcUser) { + return Optional.ofNullable(oidcUser.getIdToken().getIssuedAt()); + } + return Optional.empty(); + } + private Map reconstructActorChain(Actor actor) { var claims = new HashMap<>(actor.getClaims().getClaims()); if(actor.getParent() != null) { diff --git a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuerTest.java b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuerTest.java index 104b69dc..7bf7fca5 100644 --- a/src/test/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuerTest.java +++ b/src/test/java/com/contentgrid/gateway/security/jwt/issuer/SignedJwtIssuerTest.java @@ -92,10 +92,15 @@ void creates_derived_jwt_for_delegation_token() { @Test void creates_derived_jwt_for_oidc_user() { var issuer = new SignedJwtIssuer(CLAIMS_SIGNER, JwtClaimsResolver.empty()); + + var iat = Instant.now().minus(3, ChronoUnit.MINUTES); + var exp = Instant.now().plus(1, ChronoUnit.MINUTES); var oidcUser = new DefaultOidcUser( List.of(), OidcIdToken.withTokenValue("XXX") .subject("my-user") + .issuedAt(iat) + .expiresAt(exp) .build() ); @@ -110,8 +115,8 @@ void creates_derived_jwt_for_oidc_user() { assertThat(issuer.issueSubstitutionToken(exchange).block()).isInstanceOfSatisfying(Jwt.class, token -> { assertThat(token.getIssuer()).hasToString("https://upstream-issuer.example"); assertThat(token.getSubject()).isEqualTo("my-user"); - assertThat(token.getIssuedAt()).isBeforeOrEqualTo(Instant.now()); - assertThat(token.getExpiresAt()).isBetween(Instant.now().plus(4, ChronoUnit.MINUTES), Instant.now().plus(5, ChronoUnit.MINUTES)); + assertThat(token.getIssuedAt()).isCloseTo(iat, within(1, ChronoUnit.SECONDS)); + assertThat(token.getExpiresAt()).isCloseTo(exp, within(1, ChronoUnit.SECONDS)); assertThat(token.getTokenValue()).satisfies(verifyJwtSignedBy(issuer)); }); } @@ -179,6 +184,33 @@ void new_jwt_with_expiry_shorter_than_max() { }); } + @Test + void derived_jwt_for_just_expired_jwt() { + var issuer = new SignedJwtIssuer(CLAIMS_SIGNER, JwtClaimsResolver.empty()); + + var iat = Instant.now().minus(5, ChronoUnit.MINUTES); + var expiry = Instant.now().minus(10, ChronoUnit.SECONDS); + + var exchange = createExchange( + new JwtAuthenticationToken(Jwt.withTokenValue("XXXX") + .header("alg", "RS256") + .issuedAt(iat) + .expiresAt(expiry) + .build(), + List.of(new PrincipalAuthenticationDetailsGrantedAuthority(new Actor( + ActorType.USER, + () -> Map.of("iss", "https://upstream-issuer.example", "sub", "my-user"), + null + ))) + ) + ); + + assertThat(issuer.issueSubstitutionToken(exchange).block()).isInstanceOfSatisfying(Jwt.class, token -> { + assertThat(token.getIssuedAt()).isCloseTo(iat, within(1, ChronoUnit.SECONDS)); + assertThat(token.getExpiresAt()).isCloseTo(expiry, within(1, ChronoUnit.SECONDS)); + }); + } + static ServerWebExchange createExchange(Authentication authentication) { var request = MockServerHttpRequest.get("/").build(); var securityContext = new SecurityContextImpl(authentication);