KEYCLOAK-12406 Add "Client Session Max" and "Client Session Idle" for OIDC
This commit is contained in:
parent
5b017e930d
commit
874642fe9e
19 changed files with 379 additions and 7 deletions
|
@ -50,6 +50,8 @@ public class RealmRepresentation {
|
|||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
protected Boolean offlineSessionMaxLifespanEnabled;
|
||||
protected Integer offlineSessionMaxLifespan;
|
||||
protected Integer clientSessionIdleTimeout;
|
||||
protected Integer clientSessionMaxLifespan;
|
||||
protected Integer accessCodeLifespan;
|
||||
protected Integer accessCodeLifespanUserAction;
|
||||
protected Integer accessCodeLifespanLogin;
|
||||
|
@ -369,6 +371,22 @@ public class RealmRepresentation {
|
|||
this.offlineSessionMaxLifespan = offlineSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public Integer getClientSessionIdleTimeout() {
|
||||
return clientSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public void setClientSessionIdleTimeout(Integer clientSessionIdleTimeout) {
|
||||
this.clientSessionIdleTimeout = clientSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public Integer getClientSessionMaxLifespan() {
|
||||
return clientSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public void setClientSessionMaxLifespan(Integer clientSessionMaxLifespan) {
|
||||
this.clientSessionMaxLifespan = clientSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public List<ScopeMappingRepresentation> getScopeMappings() {
|
||||
return scopeMappings;
|
||||
}
|
||||
|
|
|
@ -480,6 +480,32 @@ public class RealmAdapter implements CachedRealmModel {
|
|||
updated.setOfflineSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClientSessionIdleTimeout() {
|
||||
if (isUpdated())
|
||||
return updated.getClientSessionIdleTimeout();
|
||||
return cached.getClientSessionIdleTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSessionIdleTimeout(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setClientSessionIdleTimeout(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClientSessionMaxLifespan() {
|
||||
if (isUpdated())
|
||||
return updated.getClientSessionMaxLifespan();
|
||||
return cached.getClientSessionMaxLifespan();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSessionMaxLifespan(int seconds) {
|
||||
getDelegateForUpdate();
|
||||
updated.setClientSessionMaxLifespan(seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessTokenLifespan() {
|
||||
if (isUpdated()) return updated.getAccessTokenLifespan();
|
||||
|
|
|
@ -86,6 +86,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
protected boolean offlineSessionMaxLifespanEnabled;
|
||||
protected int offlineSessionMaxLifespan;
|
||||
protected int clientSessionIdleTimeout;
|
||||
protected int clientSessionMaxLifespan;
|
||||
protected int accessTokenLifespan;
|
||||
protected int accessTokenLifespanForImplicitFlow;
|
||||
protected int accessCodeLifespan;
|
||||
|
@ -197,6 +199,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
offlineSessionMaxLifespanEnabled = model.isOfflineSessionMaxLifespanEnabled();
|
||||
offlineSessionMaxLifespan = model.getOfflineSessionMaxLifespan();
|
||||
clientSessionIdleTimeout = model.getClientSessionIdleTimeout();
|
||||
clientSessionMaxLifespan = model.getClientSessionMaxLifespan();
|
||||
accessTokenLifespan = model.getAccessTokenLifespan();
|
||||
accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
|
||||
accessCodeLifespan = model.getAccessCodeLifespan();
|
||||
|
@ -447,6 +451,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
|
|||
return offlineSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getClientSessionIdleTimeout() {
|
||||
return clientSessionIdleTimeout;
|
||||
}
|
||||
|
||||
public int getClientSessionMaxLifespan() {
|
||||
return clientSessionMaxLifespan;
|
||||
}
|
||||
|
||||
public int getAccessTokenLifespan() {
|
||||
return accessTokenLifespan;
|
||||
}
|
||||
|
|
|
@ -523,6 +523,26 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
|
|||
setAttribute(RealmAttributes.OFFLINE_SESSION_MAX_LIFESPAN, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClientSessionIdleTimeout() {
|
||||
return getAttribute(RealmAttributes.CLIENT_SESSION_IDLE_TIMEOUT, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSessionIdleTimeout(int seconds) {
|
||||
setAttribute(RealmAttributes.CLIENT_SESSION_IDLE_TIMEOUT, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getClientSessionMaxLifespan() {
|
||||
return getAttribute(RealmAttributes.CLIENT_SESSION_MAX_LIFESPAN, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientSessionMaxLifespan(int seconds) {
|
||||
setAttribute(RealmAttributes.CLIENT_SESSION_MAX_LIFESPAN, seconds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAccessCodeLifespan() {
|
||||
return realm.getAccessCodeLifespan();
|
||||
|
|
|
@ -34,7 +34,8 @@ public interface RealmAttributes {
|
|||
String OFFLINE_SESSION_MAX_LIFESPAN_ENABLED = "offlineSessionMaxLifespanEnabled";
|
||||
|
||||
String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan";
|
||||
|
||||
String CLIENT_SESSION_IDLE_TIMEOUT = "clientSessionIdleTimeout";
|
||||
String CLIENT_SESSION_MAX_LIFESPAN = "clientSessionMaxLifespan";
|
||||
String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName";
|
||||
String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms";
|
||||
|
||||
|
|
|
@ -364,6 +364,8 @@ public class ModelToRepresentation {
|
|||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
rep.setOfflineSessionMaxLifespanEnabled(realm.isOfflineSessionMaxLifespanEnabled());
|
||||
rep.setOfflineSessionMaxLifespan(realm.getOfflineSessionMaxLifespan());
|
||||
rep.setClientSessionIdleTimeout(realm.getClientSessionIdleTimeout());
|
||||
rep.setClientSessionMaxLifespan(realm.getClientSessionMaxLifespan());
|
||||
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
|
||||
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
|
||||
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());
|
||||
|
|
|
@ -220,6 +220,11 @@ public class RepresentationToModel {
|
|||
newRealm.setOfflineSessionMaxLifespan(rep.getOfflineSessionMaxLifespan());
|
||||
else newRealm.setOfflineSessionMaxLifespan(Constants.DEFAULT_OFFLINE_SESSION_MAX_LIFESPAN);
|
||||
|
||||
if (rep.getClientSessionIdleTimeout() != null)
|
||||
newRealm.setClientSessionIdleTimeout(rep.getClientSessionIdleTimeout());
|
||||
if (rep.getClientSessionMaxLifespan() != null)
|
||||
newRealm.setClientSessionMaxLifespan(rep.getClientSessionMaxLifespan());
|
||||
|
||||
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
|
||||
else newRealm.setAccessCodeLifespan(60);
|
||||
|
||||
|
@ -1088,6 +1093,10 @@ public class RepresentationToModel {
|
|||
if (rep.getOfflineSessionMaxLifespanEnabled() != null) realm.setOfflineSessionMaxLifespanEnabled(rep.getOfflineSessionMaxLifespanEnabled());
|
||||
if (rep.getOfflineSessionMaxLifespan() != null)
|
||||
realm.setOfflineSessionMaxLifespan(rep.getOfflineSessionMaxLifespan());
|
||||
if (rep.getClientSessionIdleTimeout() != null)
|
||||
realm.setClientSessionIdleTimeout(rep.getClientSessionIdleTimeout());
|
||||
if (rep.getClientSessionMaxLifespan() != null)
|
||||
realm.setClientSessionMaxLifespan(rep.getClientSessionMaxLifespan());
|
||||
if (rep.getRequiredCredentials() != null) {
|
||||
realm.updateRequiredCredentials(rep.getRequiredCredentials());
|
||||
}
|
||||
|
|
|
@ -196,6 +196,12 @@ public interface RealmModel extends RoleContainerModel {
|
|||
int getOfflineSessionMaxLifespan();
|
||||
void setOfflineSessionMaxLifespan(int seconds);
|
||||
|
||||
int getClientSessionIdleTimeout();
|
||||
void setClientSessionIdleTimeout(int seconds);
|
||||
|
||||
int getClientSessionMaxLifespan();
|
||||
void setClientSessionMaxLifespan(int seconds);
|
||||
|
||||
void setAccessTokenLifespan(int seconds);
|
||||
|
||||
int getAccessTokenLifespanForImplicitFlow();
|
||||
|
|
|
@ -44,7 +44,8 @@ public final class OIDCConfigAttributes {
|
|||
public static final String ACCESS_TOKEN_SIGNED_RESPONSE_ALG = "access.token.signed.response.alg";
|
||||
|
||||
public static final String ACCESS_TOKEN_LIFESPAN = "access.token.lifespan";
|
||||
|
||||
public static final String CLIENT_SESSION_IDLE_TIMEOUT = "client.session.idle.timeout";
|
||||
public static final String CLIENT_SESSION_MAX_LIFESPAN = "client.session.max.lifespan";
|
||||
public static final String PKCE_CODE_CHALLENGE_METHOD = "pkce.code.challenge.method";
|
||||
|
||||
private OIDCConfigAttributes() {
|
||||
|
|
|
@ -687,6 +687,19 @@ public class TokenManager {
|
|||
expiration = expiration <= sessionExpires ? expiration : sessionExpires;
|
||||
}
|
||||
|
||||
int clientSessionMaxLifespan;
|
||||
String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
|
||||
if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) {
|
||||
clientSessionMaxLifespan = Integer.parseInt(clientSessionMaxLifespanPerClient);
|
||||
} else {
|
||||
clientSessionMaxLifespan = realm.getClientSessionMaxLifespan();
|
||||
}
|
||||
|
||||
if (clientSessionMaxLifespan > 0) {
|
||||
int clientSessionExpiration = userSession.getStarted() + clientSessionMaxLifespan;
|
||||
return expiration < clientSessionExpiration ? expiration : clientSessionExpiration;
|
||||
}
|
||||
|
||||
return expiration;
|
||||
}
|
||||
|
||||
|
@ -777,10 +790,41 @@ public class TokenManager {
|
|||
}
|
||||
|
||||
private int getRefreshExpiration() {
|
||||
int sessionExpires = userSession.getStarted() + (userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0 ?
|
||||
realm.getSsoSessionMaxLifespanRememberMe() : realm.getSsoSessionMaxLifespan());
|
||||
int expiration = Time.currentTime() + (userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0 ?
|
||||
realm.getSsoSessionIdleTimeoutRememberMe() : realm.getSsoSessionIdleTimeout());
|
||||
int sessionExpires = userSession.getStarted()
|
||||
+ (userSession.isRememberMe() && realm.getSsoSessionMaxLifespanRememberMe() > 0
|
||||
? realm.getSsoSessionMaxLifespanRememberMe()
|
||||
: realm.getSsoSessionMaxLifespan());
|
||||
|
||||
int clientSessionMaxLifespan;
|
||||
String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
|
||||
if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) {
|
||||
clientSessionMaxLifespan = Integer.parseInt(clientSessionMaxLifespanPerClient);
|
||||
} else {
|
||||
clientSessionMaxLifespan = realm.getClientSessionMaxLifespan();
|
||||
}
|
||||
|
||||
if (clientSessionMaxLifespan > 0) {
|
||||
int clientSessionMaxExpiration = userSession.getStarted() + clientSessionMaxLifespan;
|
||||
sessionExpires = sessionExpires < clientSessionMaxExpiration ? sessionExpires : clientSessionMaxExpiration;
|
||||
}
|
||||
|
||||
int expiration = Time.currentTime() + (userSession.isRememberMe() && realm.getSsoSessionIdleTimeoutRememberMe() > 0
|
||||
? realm.getSsoSessionIdleTimeoutRememberMe()
|
||||
: realm.getSsoSessionIdleTimeout());
|
||||
|
||||
int clientSessionIdleTimeout;
|
||||
String clientSessionIdleTimeoutPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT);
|
||||
if (clientSessionIdleTimeoutPerClient != null && !clientSessionIdleTimeoutPerClient.trim().isEmpty()) {
|
||||
clientSessionIdleTimeout = Integer.parseInt(clientSessionIdleTimeoutPerClient);
|
||||
} else {
|
||||
clientSessionIdleTimeout = realm.getClientSessionIdleTimeout();
|
||||
}
|
||||
|
||||
if (clientSessionIdleTimeout > 0) {
|
||||
int clientSessionIdleExpiration = Time.currentTime() + clientSessionIdleTimeout;
|
||||
expiration = expiration < clientSessionIdleExpiration ? expiration : clientSessionIdleExpiration;
|
||||
}
|
||||
|
||||
return expiration <= sessionExpires ? expiration : sessionExpires;
|
||||
}
|
||||
|
||||
|
|
|
@ -620,6 +620,10 @@ public class RealmTest extends AbstractAdminTest {
|
|||
if (realm.getSsoSessionMaxLifespan() != null) assertEquals(realm.getSsoSessionMaxLifespan(), storedRealm.getSsoSessionMaxLifespan());
|
||||
if (realm.getSsoSessionIdleTimeoutRememberMe() != null) Assert.assertEquals(realm.getSsoSessionIdleTimeoutRememberMe(), storedRealm.getSsoSessionIdleTimeoutRememberMe());
|
||||
if (realm.getSsoSessionMaxLifespanRememberMe() != null) Assert.assertEquals(realm.getSsoSessionMaxLifespanRememberMe(), storedRealm.getSsoSessionMaxLifespanRememberMe());
|
||||
if (realm.getClientSessionIdleTimeout() != null)
|
||||
Assert.assertEquals(realm.getClientSessionIdleTimeout(), storedRealm.getClientSessionIdleTimeout());
|
||||
if (realm.getClientSessionMaxLifespan() != null)
|
||||
Assert.assertEquals(realm.getClientSessionMaxLifespan(), storedRealm.getClientSessionMaxLifespan());
|
||||
if (realm.getRequiredCredentials() != null) {
|
||||
assertNotNull(storedRealm.getRequiredCredentials());
|
||||
for (String cred : realm.getRequiredCredentials()) {
|
||||
|
|
|
@ -1121,6 +1121,47 @@ public class AccessTokenTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientSessionMaxLifespan() throws Exception {
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
|
||||
RealmResource realm = adminClient.realm("test");
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
int accessTokenLifespan = rep.getAccessTokenLifespan();
|
||||
Integer originalClientSessionMaxLifespan = rep.getClientSessionMaxLifespan();
|
||||
|
||||
try {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getExpiresIn(), accessTokenLifespan);
|
||||
|
||||
rep.setClientSessionMaxLifespan(accessTokenLifespan - 100);
|
||||
realm.update(rep);
|
||||
|
||||
String refreshToken = response.getRefreshToken();
|
||||
response = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getExpiresIn(), accessTokenLifespan - 100);
|
||||
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN,
|
||||
Integer.toString(accessTokenLifespan - 200));
|
||||
client.update(clientRepresentation);
|
||||
|
||||
refreshToken = response.getRefreshToken();
|
||||
response = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getExpiresIn(), accessTokenLifespan - 200);
|
||||
} finally {
|
||||
rep.setClientSessionMaxLifespan(originalClientSessionMaxLifespan);
|
||||
realm.update(rep);
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, null);
|
||||
client.update(clientRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void accessTokenRequest_ClientPS384_RealmRS256() throws Exception {
|
||||
conductAccessTokenRequest(Algorithm.HS256, Algorithm.PS384, Algorithm.RS256);
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.junit.BeforeClass;
|
|||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
|
@ -35,10 +36,12 @@ import org.keycloak.jose.jws.JWSInput;
|
|||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.SessionTimeoutHelper;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
|
@ -77,6 +80,7 @@ import static org.junit.Assert.assertFalse;
|
|||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.testsuite.Assert.assertExpiration;
|
||||
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
||||
import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.AUTH_SERVER_SSL_REQUIRED;
|
||||
|
@ -1046,6 +1050,93 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
assertNotNull(response.getRefreshToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientSessionMaxLifespan() throws Exception {
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
|
||||
RealmResource realm = adminClient.realm("test");
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
Integer originalSsoSessionMaxLifespan = rep.getSsoSessionMaxLifespan();
|
||||
int ssoSessionMaxLifespan = rep.getSsoSessionIdleTimeout() - 100;
|
||||
Integer originalClientSessionMaxLifespan = rep.getClientSessionMaxLifespan();
|
||||
|
||||
try {
|
||||
rep.setSsoSessionMaxLifespan(ssoSessionMaxLifespan);
|
||||
realm.update(rep);
|
||||
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan);
|
||||
|
||||
rep.setClientSessionMaxLifespan(ssoSessionMaxLifespan - 100);
|
||||
realm.update(rep);
|
||||
|
||||
String refreshToken = response.getRefreshToken();
|
||||
response = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan - 100);
|
||||
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN,
|
||||
Integer.toString(ssoSessionMaxLifespan - 200));
|
||||
client.update(clientRepresentation);
|
||||
|
||||
refreshToken = response.getRefreshToken();
|
||||
response = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getRefreshExpiresIn(), ssoSessionMaxLifespan - 200);
|
||||
} finally {
|
||||
rep.setSsoSessionMaxLifespan(originalSsoSessionMaxLifespan);
|
||||
rep.setClientSessionMaxLifespan(originalClientSessionMaxLifespan);
|
||||
realm.update(rep);
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN, null);
|
||||
client.update(clientRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClientSessionIdleTimeout() throws Exception {
|
||||
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
|
||||
ClientRepresentation clientRepresentation = client.toRepresentation();
|
||||
|
||||
RealmResource realm = adminClient.realm("test");
|
||||
RealmRepresentation rep = realm.toRepresentation();
|
||||
int ssoSessionIdleTimeout = rep.getSsoSessionIdleTimeout();
|
||||
Integer originalClientSessionIdleTimeout = rep.getClientSessionIdleTimeout();
|
||||
|
||||
try {
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout);
|
||||
|
||||
rep.setClientSessionIdleTimeout(ssoSessionIdleTimeout - 100);
|
||||
realm.update(rep);
|
||||
|
||||
String refreshToken = response.getRefreshToken();
|
||||
response = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout - 100);
|
||||
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT,
|
||||
Integer.toString(ssoSessionIdleTimeout - 200));
|
||||
client.update(clientRepresentation);
|
||||
|
||||
refreshToken = response.getRefreshToken();
|
||||
response = oauth.doRefreshTokenRequest(refreshToken, "password");
|
||||
assertEquals(200, response.getStatusCode());
|
||||
assertExpiration(response.getRefreshExpiresIn(), ssoSessionIdleTimeout - 200);
|
||||
} finally {
|
||||
rep.setClientSessionIdleTimeout(originalClientSessionIdleTimeout);
|
||||
realm.update(rep);
|
||||
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_SESSION_IDLE_TIMEOUT, null);
|
||||
client.update(clientRepresentation);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tokenRefreshRequest_ClientRS384_RealmRS384() throws Exception {
|
||||
conductTokenRefreshRequest(Algorithm.HS256, Algorithm.RS384, Algorithm.RS384);
|
||||
|
|
|
@ -272,4 +272,14 @@ public class RealmBuilder {
|
|||
rep.setOfflineSessionMaxLifespanEnabled(offlineSessionMaxLifespanEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmBuilder clientSessionIdleTimeout(int clientSessionIdleTimeout) {
|
||||
rep.setClientSessionIdleTimeout(clientSessionIdleTimeout);
|
||||
return this;
|
||||
}
|
||||
|
||||
public RealmBuilder clientSessionMaxLifespan(int clientSessionMaxLifespan) {
|
||||
rep.setClientSessionMaxLifespan(clientSessionMaxLifespan);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,10 @@ offline-session-max-limited=Offline Session Max Limited
|
|||
offline-session-max-limited.tooltip=Enable Offline Session Max.
|
||||
offline-session-max=Offline Session Max
|
||||
offline-session-max.tooltip=Max time before an offline session is expired regardless of activity.
|
||||
|
||||
client-session-idle=Client Session Idle
|
||||
client-session-idle.tooltip=Time a client session is allowed to be idle before it expires. Tokens are invalidated when a client session is expired. If not set it uses the standard SSO Session Idle value.
|
||||
client-session-max=Client Session Max
|
||||
client-session-max.tooltip=Max time before a client session is expired. Tokens are invalidated when a client session is expired. If not set, it uses the standard SSO Session Max value.
|
||||
access-token-lifespan=Access Token Lifespan
|
||||
access-token-lifespan.tooltip=Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.
|
||||
access-token-lifespan-for-implicit-flow=Access Token Lifespan For Implicit Flow
|
||||
|
|
|
@ -1090,6 +1090,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
|
||||
$scope.accessTokenLifespan = TimeUnit2.asUnit(client.attributes['access.token.lifespan']);
|
||||
$scope.samlAssertionLifespan = TimeUnit2.asUnit(client.attributes['saml.assertion.lifespan']);
|
||||
$scope.clientSessionIdleTimeout = TimeUnit2.asUnit(client.attributes['client.session.idle.timeout']);
|
||||
$scope.clientSessionMaxLifespan = TimeUnit2.asUnit(client.attributes['client.session.max.lifespan']);
|
||||
|
||||
if(client.origin) {
|
||||
if ($scope.access.viewRealm) {
|
||||
|
@ -1424,6 +1426,22 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
|
|||
}
|
||||
}
|
||||
|
||||
$scope.updateClientSessionIdleTimeout = function() {
|
||||
if ($scope.clientSessionIdleTimeout.time) {
|
||||
$scope.clientEdit.attributes['client.session.idle.timeout'] = $scope.clientSessionIdleTimeout.toSeconds();
|
||||
} else {
|
||||
$scope.clientEdit.attributes['client.session.idle.timeout'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.updateClientSessionMaxLifespan = function() {
|
||||
if ($scope.clientSessionMaxLifespan.time) {
|
||||
$scope.clientEdit.attributes['client.session.max.lifespan'] = $scope.clientSessionMaxLifespan.toSeconds();
|
||||
} else {
|
||||
$scope.clientEdit.attributes['client.session.max.lifespan'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
function configureAuthorizationServices() {
|
||||
if ($scope.clientEdit.authorizationServicesEnabled) {
|
||||
if ($scope.accessType == 'public') {
|
||||
|
|
|
@ -1160,6 +1160,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
$scope.realm.offlineSessionIdleTimeout = TimeUnit2.asUnit(realm.offlineSessionIdleTimeout);
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
$scope.realm.offlineSessionMaxLifespan = TimeUnit2.asUnit(realm.offlineSessionMaxLifespan);
|
||||
$scope.realm.clientSessionIdleTimeout = TimeUnit2.asUnit(realm.clientSessionIdleTimeout);
|
||||
$scope.realm.clientSessionMaxLifespan = TimeUnit2.asUnit(realm.clientSessionMaxLifespan);
|
||||
$scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan);
|
||||
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
|
||||
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
|
||||
|
@ -1215,6 +1217,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
|||
$scope.realm.offlineSessionIdleTimeout = $scope.realm.offlineSessionIdleTimeout.toSeconds();
|
||||
// KEYCLOAK-7688 Offline Session Max for Offline Token
|
||||
$scope.realm.offlineSessionMaxLifespan = $scope.realm.offlineSessionMaxLifespan.toSeconds();
|
||||
$scope.realm.clientSessionIdleTimeout = $scope.realm.clientSessionIdleTimeout.toSeconds();
|
||||
$scope.realm.clientSessionMaxLifespan = $scope.realm.clientSessionMaxLifespan.toSeconds();
|
||||
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
|
||||
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
|
||||
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds();
|
||||
|
|
|
@ -547,6 +547,38 @@
|
|||
<kc-tooltip>{{:: 'saml-assertion-lifespan.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-ng-show="protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="clientSessionIdleTimeout">{{:: 'client-session-idle' | translate}}</label>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" min="0"
|
||||
max="31536000" data-ng-model="clientSessionIdleTimeout.time"
|
||||
id="clientSessionIdleTimeout" name="clientSessionIdleTimeout"
|
||||
data-ng-change="updateClientSessionIdleTimeout()"/>
|
||||
<select class="form-control" name="clientSessionIdleTimeoutUnit" data-ng-model="clientSessionIdleTimeout.unit" data-ng-change="updateClientSessionIdleTimeout()">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-session-idle.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group" data-ng-show="protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="clientSessionMaxLifespan">{{:: 'client-session-max' | translate}}</label>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" min="0"
|
||||
max="31536000" data-ng-model="clientSessionMaxLifespan.time"
|
||||
id="clientSessionMaxLifespan" name="clientSessionMaxLifespan"
|
||||
data-ng-change="updateClientSessionMaxLifespan()"/>
|
||||
<select class="form-control" name="clientSessionMaxLifespanUnit" data-ng-model="clientSessionMaxLifespan.unit" data-ng-change="updateClientSessionMaxLifespan()">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-session-max.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="tlsClientCertificateBoundAccessTokens">{{:: 'tls-client-certificate-bound-access-tokens' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
|
|
|
@ -145,6 +145,36 @@
|
|||
<kc-tooltip>{{:: 'offline-session-max.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="clientSessionIdleTimeout">{{:: 'client-session-idle' | translate}}</label>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" required min="0"
|
||||
max="31536000" data-ng-model="realm.clientSessionIdleTimeout.time"
|
||||
id="clientSessionIdleTimeout" name="clientSessionIdleTimeout"/>
|
||||
<select class="form-control" name="clientSessionIdleTimeoutUnit" data-ng-model="realm.clientSessionIdleTimeout.unit">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-session-idle.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="clientSessionMaxLifespan">{{:: 'client-session-max' | translate}}</label>
|
||||
<div class="col-md-6 time-selector">
|
||||
<input class="form-control" type="number" required min="0"
|
||||
max="31536000" data-ng-model="realm.clientSessionMaxLifespan.time"
|
||||
id="clientSessionMaxLifespan" name="clientSessionMaxLifespan"/>
|
||||
<select class="form-control" name="clientSessionMaxLifespanUnit" data-ng-model="realm.clientSessionMaxLifespan.unit">
|
||||
<option value="Minutes">{{:: 'minutes' | translate}}</option>
|
||||
<option value="Hours">{{:: 'hours' | translate}}</option>
|
||||
<option value="Days">{{:: 'days' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-session-max.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="accessTokenLifespan">{{:: 'access-token-lifespan' | translate}}</label>
|
||||
|
||||
|
|
Loading…
Reference in a new issue