Repository: keycloak/keycloak (Java)

Issue:
Allow a JWT client assertion by considering a clock skew for smooth interoperability
### Description

By follwing [the latest FAPI 2.0 security profiles draft](https://openid.bitbucket.io/fapi/fapi-2_0-security-profile.html#name-general-requirements), 

An authorization server should allow the following JWT client assertion: 
- whose "iat" and "nbf" claim is within 0 to 10 seconds ahead in the future.

An authorization server should reject the follwoing JWT client assertion: 
- whose "iat" and "nbf" claim is over 60 seconds ahead in the future.

Whether a request object whose "iat" and "nbf" claim is within 11 to 60 seconds ahead in the future is accepted or not is up to the authorization server.

The aim of this enhancement is to achieve smooth interoperability. The specification describes this point.

I think the following two options:
1. Add a new configuration to determine a clock skew for JWT that a client crafts: a request object, client assertion, DPoP Proof
2. Add a hard-coded parameter to determine the clock skew - 15 seconds.

I would like to take the option 2 because adding a new configuration add some maintainance costs for Keycloak users and developer.


### Discussion

_No response_

### Motivation

To make Keycloak comply with FAPI 2.0 seccurity profiles, the enhancement is needed.
By using OAuth-SIG's automated testing platform, Keycloak lacks the enhancement.
https://github.com/keycloak/kc-sig-fapi/issues/629

### Details

_No response_



Fix (patch):
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java
index 94542ee0ecce..8d2352578eb0 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientValidator.java
@@ -57,6 +57,8 @@ public class JWTClientValidator {
     private JsonWebToken token;
     private ClientModel client;
 
+    private static final int ALLOWED_CLOCK_SKEW = 15; // sec
+
     public JWTClientValidator(ClientAuthenticationFlowContext context) {
         this.context = context;
         this.realm = context.getRealm();
@@ -163,13 +165,19 @@ public boolean validateSignatureAlgorithm() {
     public void validateToken() {
         if (token == null) throw new IllegalStateException("Incorrect usage. Variable 'token' is null. Need to read token first before validateToken");
 
-        if (!token.isActive()) {
+        if (!token.isActive(ALLOWED_CLOCK_SKEW)) {
             throw new RuntimeException("Token is not active");
         }
 
         // KEYCLOAK-2986, token-timeout or token-expiration in keycloak.json might not be used
-        if ((token.getExp() == null || token.getExp() <= 0) && token.getIat() + 10 < currentTime) {
-            throw new RuntimeException("Token is not active");
+        if (token.getExp() == null || token.getExp() <= 0) { // in case of "exp" not exist
+            if (token.getIat() + ALLOWED_CLOCK_SKEW + 10 < currentTime) { // consider "exp" = 10, client's clock delays from Keycloak's clock
+                throw new RuntimeException("Token is not active");
+            }
+        } else {
+            if ((token.getIat() != null && token.getIat() > 0) && token.getIat() - ALLOWED_CLOCK_SKEW > currentTime) { // consider client's clock is ahead from Keycloak's clock
+                throw new RuntimeException("Token was issued in the future");
+            }
         }
 
         if (token.getId() == null) {