Details
diff --git a/core/src/main/java/org/keycloak/representations/JsonWebToken.java b/core/src/main/java/org/keycloak/representations/JsonWebToken.java
index 04071b6..9b2f1d5 100755
--- a/core/src/main/java/org/keycloak/representations/JsonWebToken.java
+++ b/core/src/main/java/org/keycloak/representations/JsonWebToken.java
@@ -91,10 +91,9 @@ public class JsonWebToken implements Serializable {
return this;
}
-
@JsonIgnore
- public boolean isNotBefore() {
- return Time.currentTime() >= notBefore;
+ public boolean isNotBefore(int allowedTimeSkew) {
+ return Time.currentTime() + allowedTimeSkew >= notBefore;
}
/**
@@ -104,7 +103,12 @@ public class JsonWebToken implements Serializable {
*/
@JsonIgnore
public boolean isActive() {
- return (!isExpired() || expiration == 0) && (isNotBefore() || notBefore == 0);
+ return isActive(0);
+ }
+
+ @JsonIgnore
+ public boolean isActive(int allowedTimeSkew) {
+ return (!isExpired() || expiration == 0) && (isNotBefore(allowedTimeSkew) || notBefore == 0);
}
public int getIssuedAt() {
diff --git a/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java b/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
index addd6f3..0830cde 100644
--- a/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
+++ b/core/src/test/java/org/keycloak/jose/JsonWebTokenTest.java
@@ -18,11 +18,13 @@
package org.keycloak.jose;
import org.junit.Test;
+import org.keycloak.common.util.Time;
import org.keycloak.representations.JsonWebToken;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
+import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertTrue;
@@ -35,7 +37,7 @@ public class JsonWebTokenTest {
public void testAudSingle() throws IOException {
String single = "{ \"aud\": \"test\" }";
JsonWebToken s = JsonSerialization.readValue(single, JsonWebToken.class);
- assertArrayEquals(new String[] { "test" }, s.getAudience());
+ assertArrayEquals(new String[]{"test"}, s.getAudience());
}
@Test
@@ -59,4 +61,40 @@ public class JsonWebTokenTest {
assertTrue(JsonSerialization.writeValueAsPrettyString(jsonWebToken).contains("\"aud\" : [ \"test\", \"test2\" ]"));
}
+ @Test
+ public void isActiveReturnFalseWhenBeforeTimeInFuture() {
+ int currentTime = Time.currentTime();
+ int futureTime = currentTime + 10;
+ JsonWebToken jsonWebToken = new JsonWebToken();
+ jsonWebToken.notBefore(futureTime);
+ assertFalse(jsonWebToken.isActive());
+ }
+
+ @Test
+ public void isActiveReturnTrueWhenBeforeTimeInPast() {
+ int currentTime = Time.currentTime();
+ int pastTime = currentTime - 10;
+ JsonWebToken jsonWebToken = new JsonWebToken();
+ jsonWebToken.notBefore(pastTime);
+ assertTrue(jsonWebToken.isActive());
+ }
+
+ @Test
+ public void isActiveShouldReturnTrueWhenBeforeTimeInFutureWithinTimeSkew() {
+ int notBeforeTime = Time.currentTime() + 5;
+ int allowedClockSkew = 10;
+ JsonWebToken jsonWebToken = new JsonWebToken();
+ jsonWebToken.notBefore(notBeforeTime);
+ assertTrue(jsonWebToken.isActive(allowedClockSkew));
+ }
+
+ @Test
+ public void isActiveShouldReturnFalseWhenWhenBeforeTimeInFutureOutsideTimeSkew() {
+ int notBeforeTime = Time.currentTime() + 10;
+ int allowedClockSkew = 5;
+ JsonWebToken jsonWebToken = new JsonWebToken();
+ jsonWebToken.notBefore(notBeforeTime);
+ assertFalse(jsonWebToken.isActive(allowedClockSkew));
+ }
+
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index 30c05b3..5251522 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -21,7 +21,6 @@ import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.OAuthErrorException;
import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
-import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ExchangeExternalToken;
import org.keycloak.broker.provider.IdentityBrokerException;
@@ -478,7 +477,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
String iss = token.getIssuer();
- if (!token.isActive()) {
+ if (!token.isActive(getConfig().getAllowedClockSkew())) {
throw new IdentityBrokerException("Token is no longer valid");
}
diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
index ce0ccff..ff541ac 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
@@ -110,7 +110,16 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
getConfig().put("disableUserInfo", String.valueOf(disable));
}
-
-
-
+ public int getAllowedClockSkew() {
+ String allowedClockSkew = getConfig().get("allowedClockSkew");
+ if (allowedClockSkew == null || allowedClockSkew.isEmpty()) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(getConfig().get("allowedClockSkew"));
+ } catch (NumberFormatException e) {
+ // ignore it and use default
+ return 0;
+ }
+ }
}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 239a7ce..3fa744f 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -561,6 +561,8 @@ validating-public-key=Validating Public Key
identity-provider.validating-public-key.tooltip=The public key in PEM format that must be used to verify external IDP signatures.
validating-public-key-id=Validating Public Key Id
identity-provider.validating-public-key-id.tooltip=Explicit ID of the validating public key given above if the key ID. Leave blank if the key above should be used always, regardless of key ID specified by external IDP; set it if the key should only be used for verifying if key ID from external IDP matches.
+identity-provider.allowed-clock-skew=Allowed clock skew
+identity-provider.allowed-clock-skew.tooltip=Clock skew in seconds that is tolerated when validating identity provider tokens. Default value is zero.
import-external-idp-config=Import External IDP Config
import-external-idp-config.tooltip=Allows you to load external IDP metadata from a config file or to download it from a URL.
import-from-url=Import from URL
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
index 54bbcd6..ac32db7 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
@@ -246,6 +246,14 @@
</div>
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="allowedClockSkew">{{:: 'allowed-clock-skew' | translate}}</label>
+ <div class="col-md-6">
+ <input ng-model="identityProvider.config.allowedClockSkew" id="allowedClockSkew" type="text" class="form-control"/>
+ </div>
+ <kc-tooltip>{{:: 'identity-provider.allowed-clock-skew.tooltip' | translate}}</kc-tooltip>
+ </div>
+
</fieldset>
<fieldset data-ng-show="newIdentityProvider">
<legend uncollapsed><span class="text">{{:: 'import-external-idp-config' | translate}}</span> <kc-tooltip>{{:: 'import-external-idp-config.tooltip' | translate}}</kc-tooltip></legend>