-
Notifications
You must be signed in to change notification settings - Fork 187
[ES-1944G] Added JWT expire time as TTL for jti cache #2018
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from 3 commits
fad8813
332ff33
a2d2ae5
802243c
446b98f
79b7ee3
c06269d
f725cfc
4b19bc9
9f4d2b2
5b25468
1f8da68
45b9b27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -85,6 +85,12 @@ public class TokenServiceImpl implements TokenService { | |
| @Value("${mosip.esignet.client-assertion-jwt.leeway-seconds:5}") | ||
| private int maxClockSkew; | ||
|
|
||
| @Value("${mosip.esignet.client-assertion.jti.cache.max-ttl-seconds:3600}") | ||
| private long maxJtiCacheTtlSeconds; | ||
|
|
||
| @Value("${mosip.esignet.client-assertion.jti.cache.skew-buffer-seconds:30}") | ||
| private long jtiCacheSkewBufferSeconds; | ||
|
|
||
| @Value("${mosip.esignet.dpop.nonce.expire.seconds:15}") | ||
| private long dpopNonceExpirySeconds; | ||
|
|
||
|
|
@@ -194,10 +200,8 @@ public void verifyClientAssertionToken(String clientId, String jwk, String clien | |
| List<String> validAudience = getValidAudienceForClientAssertion(audience); | ||
| NimbusJwtDecoder jwtDecoder = getNimbusJwtDecoderFromJwk(jwk, clientId, validAudience, maxClockSkew, alg); | ||
| jwtDecoder.decode(clientAssertion); | ||
| String jti = signedJWT.getJWTClaimsSet().getJWTID(); | ||
| if (uniqueJtiRequired && (jti == null || cacheUtilService.checkAndMarkJti(jti))) { | ||
| log.error("invalid jti {}", jti); | ||
| throw new EsignetException(ErrorConstants.INVALID_CLIENT); | ||
| if (uniqueJtiRequired) { | ||
| enforceJtiReplayProtection(signedJWT.getJWTClaimsSet(), clientId); | ||
|
sacrana0 marked this conversation as resolved.
|
||
| } | ||
| } catch (EsignetException e) { | ||
| throw e; | ||
|
|
@@ -207,6 +211,48 @@ public void verifyClientAssertionToken(String clientId, String jwk, String clien | |
| } | ||
| } | ||
|
|
||
| private void enforceJtiReplayProtection(JWTClaimsSet claims, String clientId) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how is this logic different from the logic written in dpopValidationFilter? can we not have a common method both can use?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @anushasunkada the two methods look similar but are for two different JWT. The client_assertion flow treats the exp claim as the source of truth for expiry and limits the token’s lifetime based on the declared expiration time. The DPoP proof flow relies on the iat timestamp for freshness rather than an exp claim, so its effective validity is determined by the accepted iat window and allowed clock skew. They also raise different exception types (INVALID_CLIENT vs InvalidDpopHeaderException) with different HTTP semantics. The genuine shared step - "set jti in cache with TTL, reject if it already exists" - is already extracted into CacheUtilService.checkAndMarkJti(jti, ttl) |
||
| String jti = claims.getJWTID(); | ||
| Date expDate = claims.getExpirationTime(); | ||
| Date iatDate = claims.getIssueTime(); | ||
|
|
||
| if (jti == null) { | ||
| log.error("Missing jti in client assertion for clientId {}", clientId); | ||
| throw new EsignetException(ErrorConstants.INVALID_CLIENT); | ||
| } | ||
| if (expDate == null || iatDate == null) { | ||
|
KashiwalHarsh marked this conversation as resolved.
Outdated
|
||
| log.error("Client assertion missing exp/iat for clientId {}", clientId); | ||
| throw new EsignetException(ErrorConstants.INVALID_CLIENT); | ||
| } | ||
|
|
||
| long now = Instant.now().getEpochSecond(); | ||
| long exp = expDate.toInstant().getEpochSecond(); | ||
| long iat = iatDate.toInstant().getEpochSecond(); | ||
|
|
||
| // Guarantee cache always outlives validity → close the (MAX_CAP, exp) replay window. | ||
| long declaredLifetime = exp - iat; | ||
| if (declaredLifetime <= 0 || declaredLifetime > maxJtiCacheTtlSeconds) { | ||
| log.error("Client assertion lifetime {}s outside (0,{}] for clientId {}", | ||
| declaredLifetime, maxJtiCacheTtlSeconds, clientId); | ||
| throw new EsignetException(ErrorConstants.INVALID_CLIENT); | ||
| } | ||
|
|
||
| long jtiTtlSeconds = Math.min( | ||
| (exp - now) + jtiCacheSkewBufferSeconds, | ||
| maxJtiCacheTtlSeconds | ||
| ); | ||
|
|
||
| if (jtiTtlSeconds <= 0) { | ||
| log.error("Client assertion already expired for clientId {}", clientId); | ||
| throw new EsignetException(ErrorConstants.INVALID_CLIENT); | ||
| } | ||
|
KashiwalHarsh marked this conversation as resolved.
Outdated
|
||
|
|
||
| if (cacheUtilService.checkAndMarkJti(jti, jtiTtlSeconds)) { | ||
| log.error("Replay detected for jti {} (clientId {})", jti, clientId); | ||
| throw new EsignetException(ErrorConstants.INVALID_CLIENT); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets the valid audience list for client assertion verification. | ||
| * If the server profile has 'client_auth_assertion_audience' configured for the 'strict_audience_check' feature, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.