diff --git a/docs/guides/images/jwt-authorization-grant-oidc-provider-config.png b/docs/guides/images/jwt-authorization-grant-oidc-provider-config.png
index 908d1094a7c..6ec7f63a524 100644
Binary files a/docs/guides/images/jwt-authorization-grant-oidc-provider-config.png and b/docs/guides/images/jwt-authorization-grant-oidc-provider-config.png differ
diff --git a/docs/guides/securing-apps/jwt-authorization-grant.adoc b/docs/guides/securing-apps/jwt-authorization-grant.adoc
index 6242e20c261..8856fe67336 100644
--- a/docs/guides/securing-apps/jwt-authorization-grant.adoc
+++ b/docs/guides/securing-apps/jwt-authorization-grant.adoc
@@ -58,6 +58,7 @@ The Identity Provider (both types commented in the introduction) needs to also b
* **Max allowed assertion expiration**: The maximum expiration the server will allow in the assertion. Default 5 minutes.
* **Assertion signature algorithm**: The signature algorithm that is valid for assertions. If not specified any signature is valid.
* **Allowed clock skew**: Clock skew in seconds that is tolerated when validating identity provider tokens. Default value is zero.
+ * **Limit access token expiration**: If enabled the access token lifespan will be limited to the expiration of the JWT assertion but only if the JWT assertion expiration is less than the calculated access token expiration.
.OIDC Identity Provider configuration for JWT Authorization Grant
image::jwt-authorization-grant-oidc-provider-config.png[Identity Provider configuration for JWT Authorization Grant]
diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
index 9505cd06cac..496c9e5d914 100644
--- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
+++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties
@@ -906,6 +906,8 @@ jwtAuthorizationGrantMaxAllowedAssertionExpiration=Max allowed assertion expirat
jwtAuthorizationGrantMaxAllowedAssertionExpirationHelp=Insert the max allowed expiration that the assertion can have.
jwtAuthorizationGrantAssertionSignatureAlg=Assertion signature algorithm
jwtAuthorizationGrantAssertionSignatureAlgHelp=Signature algorithm that should be used to sign the assertion, if not specified any signature algorithm will be valid.
+jwtAuthorizationGrantLimitAccessTokenExp=Limit access token expiration
+jwtAuthorizationGrantLimitAccessTokenExpHelp=If enabled the access token lifespan will be limited to the expiration of the JWT assertion but only if the JWT assertion expiration is less than the calculated access token expiration.
addJWTAuthorizationGrantProvider=Add JWT Authorization Grant Provider
jwtAuthorizationGrantJWKSUrl=JWKS URL
jwtAuthorizationGrantJWKSUrlHelp=URL where identity provider keys in JWK format are stored. See the JWK specification for more details
diff --git a/js/apps/admin-ui/src/identity-providers/add/JWTAuthorizationGrantAssertionSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/JWTAuthorizationGrantAssertionSettings.tsx
index abb213d4edb..6de607aca8d 100644
--- a/js/apps/admin-ui/src/identity-providers/add/JWTAuthorizationGrantAssertionSettings.tsx
+++ b/js/apps/admin-ui/src/identity-providers/add/JWTAuthorizationGrantAssertionSettings.tsx
@@ -58,6 +58,12 @@ export const JWTAuthorizationGrantAssertionSettings = () => {
defaultValue: "",
}}
/>
+
>
);
};
diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/JWTAuthorizationGrantProvider.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/JWTAuthorizationGrantProvider.java
index e4fc644a999..b1d6178a3fd 100644
--- a/server-spi-private/src/main/java/org/keycloak/broker/provider/JWTAuthorizationGrantProvider.java
+++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/JWTAuthorizationGrantProvider.java
@@ -36,4 +36,6 @@ public interface JWTAuthorizationGrantProvider
int getMaxAllowedExpiration();
String getAssertionSignatureAlg();
+
+ boolean isLimitAccessTokenExpiration();
}
diff --git a/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantConfig.java b/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantConfig.java
index a57d689f26b..3a5dbc63c99 100644
--- a/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantConfig.java
+++ b/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantConfig.java
@@ -17,6 +17,8 @@ public interface JWTAuthorizationGrantConfig {
String JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG = "jwtAuthorizationGrantAssertionSignatureAlg";
+ String JWT_AUTHORIZATION_GRANT_LIMIT_ACCESS_TOKEN_EXP = "jwtAuthorizationGrantLimitAccessTokenExp";
+
String JWT_AUTHORIZATION_GRANT_ALLOWED_CLOCK_SKEW = "jwtAuthorizationGrantAllowedClockSkew";
String PUBLIC_KEY_SIGNATURE_VERIFIER = "publicKeySignatureVerifier";
@@ -45,6 +47,10 @@ public interface JWTAuthorizationGrantConfig {
return getConfig().get(JWT_AUTHORIZATION_GRANT_ASSERTION_SIGNATURE_ALG);
}
+ default boolean isJwtAuthorizationGrantLimitAccessTokenExp() {
+ return Boolean.parseBoolean(getConfig().getOrDefault(JWT_AUTHORIZATION_GRANT_LIMIT_ACCESS_TOKEN_EXP, "false"));
+ }
+
default int getJWTAuthorizationGrantAllowedClockSkew() {
String allowedClockSkew = getConfig().get(JWT_AUTHORIZATION_GRANT_ALLOWED_CLOCK_SKEW);
if (allowedClockSkew == null || allowedClockSkew.isEmpty()) {
diff --git a/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantIdentityProvider.java b/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantIdentityProvider.java
index 900788fc921..08205796e5c 100644
--- a/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/jwtauthorizationgrant/JWTAuthorizationGrantIdentityProvider.java
@@ -74,6 +74,11 @@ public class JWTAuthorizationGrantIdentityProvider implements JWTAuthorizationGr
return StringUtil.isBlank(alg) ? null : alg;
}
+ @Override
+ public boolean isLimitAccessTokenExpiration() {
+ return getConfig().isJwtAuthorizationGrantLimitAccessTokenExp();
+ }
+
@Override
public JWTAuthorizationGrantIdentityProviderConfig getConfig() {
return this.config instanceof JWTAuthorizationGrantIdentityProviderConfig ? (JWTAuthorizationGrantIdentityProviderConfig)this.config : null;
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 3ccf0f5d312..cd5e355aed6 100755
--- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -1116,4 +1116,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider clientPolicyContextGenerator) {
+ protected TokenManager.AccessTokenResponseBuilder createTokenResponseBuilder(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx, String scopeParam, Function clientPolicyContextGenerator) {
clientSessionCtx.setAttribute(Constants.GRANT_TYPE, context.getGrantType());
AccessToken token = tokenManager.createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
TokenManager.AccessTokenResponseBuilder responseBuilder = tokenManager
- .responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token);
+ .responseBuilder(realm, client, event, session, userSession, clientSessionCtx).accessToken(token);
boolean useRefreshToken = useRefreshToken();
if (useRefreshToken) {
responseBuilder.generateRefreshToken();
@@ -153,6 +152,10 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
}
}
+ return responseBuilder;
+ }
+
+ protected Response createTokenResponse(TokenManager.AccessTokenResponseBuilder responseBuilder, ClientSessionContext clientSessionCtx, boolean code) {
AccessTokenResponse res = null;
if (code) {
try {
@@ -160,7 +163,7 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
} catch (RuntimeException re) {
if ("can not get encryption KEK".equals(re.getMessage())) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST,
- "can not get encryption KEK", Response.Status.BAD_REQUEST);
+ "can not get encryption KEK", Response.Status.BAD_REQUEST);
} else {
throw re;
}
@@ -177,6 +180,13 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
return cors.add(Response.ok(res).type(MediaType.APPLICATION_JSON_TYPE));
}
+ protected Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
+ String scopeParam, boolean code, Function clientPolicyContextGenerator) {
+ TokenManager.AccessTokenResponseBuilder responseBuilder = createTokenResponseBuilder(user, userSession,
+ clientSessionCtx, scopeParam, clientPolicyContextGenerator);
+ return createTokenResponse(responseBuilder, clientSessionCtx, code);
+ }
+
protected void checkAndBindMtlsHoKToken(TokenManager.AccessTokenResponseBuilder responseBuilder, boolean useRefreshToken) {
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/AbstractJWTAuthorizationGrantTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/AbstractJWTAuthorizationGrantTest.java
index 71408feaa98..8878b288c81 100644
--- a/tests/base/src/test/java/org/keycloak/tests/oauth/AbstractJWTAuthorizationGrantTest.java
+++ b/tests/base/src/test/java/org/keycloak/tests/oauth/AbstractJWTAuthorizationGrantTest.java
@@ -78,6 +78,7 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
//reduce max expiration to 10 seconds
realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_MAX_ALLOWED_ASSERTION_EXPIRATION, "10");
+ rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_LIMIT_ACCESS_TOKEN_EXP, "false");
});
jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 11L));
@@ -275,6 +276,22 @@ public abstract class AbstractJWTAuthorizationGrantTest extends BaseAbstractJWTA
}
}
+ @Test
+ public void textLimitAccessTokenExpiration() {
+ realm.updateIdentityProviderWithCleanup(IDP_ALIAS, rep -> {
+ rep.getConfig().put(OIDCIdentityProviderConfig.JWT_AUTHORIZATION_GRANT_LIMIT_ACCESS_TOKEN_EXP, "true");
+ });
+
+ int accessCodeLifeSpan = realm.admin().toRepresentation().getAccessTokenLifespan();
+ String jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken(accessCodeLifeSpan));
+ AccessTokenResponse response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
+ MatcherAssert.assertThat(response.getExpiresIn(), Matchers.allOf(Matchers.lessThanOrEqualTo(accessCodeLifeSpan), Matchers.greaterThan(accessCodeLifeSpan - 5)));
+
+ jwt = getIdentityProvider().encodeToken(createAuthorizationGrantToken(120L));
+ response = oAuthClient.jwtAuthorizationGrantRequest(jwt).send();
+ MatcherAssert.assertThat(response.getExpiresIn(), Matchers.allOf(Matchers.lessThanOrEqualTo(120), Matchers.greaterThan(115)));
+ }
+
@Test
public void testSuccessGrant() {
String jwt = getIdentityProvider().encodeToken(createDefaultAuthorizationGrantToken());
diff --git a/tests/base/src/test/java/org/keycloak/tests/oauth/BaseAbstractJWTAuthorizationGrantTest.java b/tests/base/src/test/java/org/keycloak/tests/oauth/BaseAbstractJWTAuthorizationGrantTest.java
index 5bcf5fcd626..41287e957ea 100644
--- a/tests/base/src/test/java/org/keycloak/tests/oauth/BaseAbstractJWTAuthorizationGrantTest.java
+++ b/tests/base/src/test/java/org/keycloak/tests/oauth/BaseAbstractJWTAuthorizationGrantTest.java
@@ -87,7 +87,11 @@ public class BaseAbstractJWTAuthorizationGrantTest {
TimeOffSet timeOffSet;
protected JsonWebToken createDefaultAuthorizationGrantToken() {
- return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L, null, null);
+ return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + 300L);
+ }
+
+ protected JsonWebToken createAuthorizationGrantToken(long expiration) {
+ return createAuthorizationGrantToken("basic-user-id", oAuthClient.getEndpoints().getIssuer(), IDP_ISSUER, Time.currentTime() + expiration);
}
protected JsonWebToken createAuthorizationGrantToken(String subject, String audience, String issuer) {