mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
Limit access Token expiration for jwt authorization grant (#44775)
Closes #43972 Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
This commit is contained in:
parent
f5a3086027
commit
790fb557db
Binary file not shown.
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 140 KiB |
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -58,6 +58,12 @@ export const JWTAuthorizationGrantAssertionSettings = () => {
|
||||
defaultValue: "",
|
||||
}}
|
||||
/>
|
||||
<DefaultSwitchControl
|
||||
name="config.jwtAuthorizationGrantLimitAccessTokenExp"
|
||||
label={t("jwtAuthorizationGrantLimitAccessTokenExp")}
|
||||
labelIcon={t("jwtAuthorizationGrantLimitAccessTokenExpHelp")}
|
||||
stringify
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -36,4 +36,6 @@ public interface JWTAuthorizationGrantProvider <C extends IdentityProviderModel>
|
||||
int getMaxAllowedExpiration();
|
||||
|
||||
String getAssertionSignatureAlg();
|
||||
|
||||
boolean isLimitAccessTokenExpiration();
|
||||
}
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1116,4 +1116,9 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
||||
public String getAssertionSignatureAlg() {
|
||||
return getConfig().getJWTAuthorizationGrantAssertionSignatureAlg();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLimitAccessTokenExpiration() {
|
||||
return getConfig().isJwtAuthorizationGrantLimitAccessTokenExp();
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,7 +129,13 @@ public class JWTAuthorizationGrantType extends OAuth2GrantTypeBase {
|
||||
event.session(userSession);
|
||||
ClientSessionContext clientSessionCtx = TokenManager.attachAuthenticationSession(this.session, userSession,
|
||||
authSession, authorizationGrantContext.getRestrictedScopes(), false);
|
||||
return createTokenResponse(user, userSession, clientSessionCtx, scopeParam, true, null);
|
||||
TokenManager.AccessTokenResponseBuilder responseBuilder = createTokenResponseBuilder(user, userSession, clientSessionCtx, scopeParam, null);
|
||||
if (jwtAuthorizationGrantProvider.isLimitAccessTokenExpiration()) {
|
||||
if (authorizationGrantContext.getJWT().getExp() < responseBuilder.getAccessToken().getExp()) {
|
||||
responseBuilder.getAccessToken().exp(authorizationGrantContext.getJWT().getExp());
|
||||
}
|
||||
}
|
||||
return createTokenResponse(responseBuilder, clientSessionCtx, true);
|
||||
} catch (CorsErrorResponseException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -111,13 +111,12 @@ public abstract class OAuth2GrantTypeBase implements OAuth2GrantType {
|
||||
this.tokenManager = (TokenManager) context.tokenManager;
|
||||
}
|
||||
|
||||
protected Response createTokenResponse(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx,
|
||||
String scopeParam, boolean code, Function<TokenManager.AccessTokenResponseBuilder, ClientPolicyContext> clientPolicyContextGenerator) {
|
||||
protected TokenManager.AccessTokenResponseBuilder createTokenResponseBuilder(UserModel user, UserSessionModel userSession, ClientSessionContext clientSessionCtx, String scopeParam, Function<TokenManager.AccessTokenResponseBuilder, ClientPolicyContext> 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<TokenManager.AccessTokenResponseBuilder, ClientPolicyContext> 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
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user