KEYCLOAK-14145 OIDC support for Client "offline" session lifespan

This commit is contained in:
Yoshiyuki Tabata 2020-05-13 14:09:58 +09:00 committed by Marek Posolda
parent 82cfb8e821
commit f03ee2ec98
19 changed files with 413 additions and 12 deletions

View file

@ -52,6 +52,8 @@ public class RealmRepresentation {
protected Integer offlineSessionMaxLifespan; protected Integer offlineSessionMaxLifespan;
protected Integer clientSessionIdleTimeout; protected Integer clientSessionIdleTimeout;
protected Integer clientSessionMaxLifespan; protected Integer clientSessionMaxLifespan;
protected Integer clientOfflineSessionIdleTimeout;
protected Integer clientOfflineSessionMaxLifespan;
protected Integer accessCodeLifespan; protected Integer accessCodeLifespan;
protected Integer accessCodeLifespanUserAction; protected Integer accessCodeLifespanUserAction;
protected Integer accessCodeLifespanLogin; protected Integer accessCodeLifespanLogin;
@ -387,6 +389,22 @@ public class RealmRepresentation {
this.clientSessionMaxLifespan = clientSessionMaxLifespan; this.clientSessionMaxLifespan = clientSessionMaxLifespan;
} }
public Integer getClientOfflineSessionIdleTimeout() {
return clientOfflineSessionIdleTimeout;
}
public void setClientOfflineSessionIdleTimeout(Integer clientOfflineSessionIdleTimeout) {
this.clientOfflineSessionIdleTimeout = clientOfflineSessionIdleTimeout;
}
public Integer getClientOfflineSessionMaxLifespan() {
return clientOfflineSessionMaxLifespan;
}
public void setClientOfflineSessionMaxLifespan(Integer clientOfflineSessionMaxLifespan) {
this.clientOfflineSessionMaxLifespan = clientOfflineSessionMaxLifespan;
}
public List<ScopeMappingRepresentation> getScopeMappings() { public List<ScopeMappingRepresentation> getScopeMappings() {
return scopeMappings; return scopeMappings;
} }

View file

@ -506,6 +506,32 @@ public class RealmAdapter implements CachedRealmModel {
updated.setClientSessionMaxLifespan(seconds); updated.setClientSessionMaxLifespan(seconds);
} }
@Override
public int getClientOfflineSessionIdleTimeout() {
if (isUpdated())
return updated.getClientOfflineSessionIdleTimeout();
return cached.getClientOfflineSessionIdleTimeout();
}
@Override
public void setClientOfflineSessionIdleTimeout(int seconds) {
getDelegateForUpdate();
updated.setClientOfflineSessionIdleTimeout(seconds);
}
@Override
public int getClientOfflineSessionMaxLifespan() {
if (isUpdated())
return updated.getClientOfflineSessionMaxLifespan();
return cached.getClientOfflineSessionMaxLifespan();
}
@Override
public void setClientOfflineSessionMaxLifespan(int seconds) {
getDelegateForUpdate();
updated.setClientOfflineSessionMaxLifespan(seconds);
}
@Override @Override
public int getAccessTokenLifespan() { public int getAccessTokenLifespan() {
if (isUpdated()) return updated.getAccessTokenLifespan(); if (isUpdated()) return updated.getAccessTokenLifespan();

View file

@ -88,6 +88,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected int offlineSessionMaxLifespan; protected int offlineSessionMaxLifespan;
protected int clientSessionIdleTimeout; protected int clientSessionIdleTimeout;
protected int clientSessionMaxLifespan; protected int clientSessionMaxLifespan;
protected int clientOfflineSessionIdleTimeout;
protected int clientOfflineSessionMaxLifespan;
protected int accessTokenLifespan; protected int accessTokenLifespan;
protected int accessTokenLifespanForImplicitFlow; protected int accessTokenLifespanForImplicitFlow;
protected int accessCodeLifespan; protected int accessCodeLifespan;
@ -201,6 +203,8 @@ public class CachedRealm extends AbstractExtendableRevisioned {
offlineSessionMaxLifespan = model.getOfflineSessionMaxLifespan(); offlineSessionMaxLifespan = model.getOfflineSessionMaxLifespan();
clientSessionIdleTimeout = model.getClientSessionIdleTimeout(); clientSessionIdleTimeout = model.getClientSessionIdleTimeout();
clientSessionMaxLifespan = model.getClientSessionMaxLifespan(); clientSessionMaxLifespan = model.getClientSessionMaxLifespan();
clientOfflineSessionIdleTimeout = model.getClientOfflineSessionIdleTimeout();
clientOfflineSessionMaxLifespan = model.getClientOfflineSessionMaxLifespan();
accessTokenLifespan = model.getAccessTokenLifespan(); accessTokenLifespan = model.getAccessTokenLifespan();
accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow(); accessTokenLifespanForImplicitFlow = model.getAccessTokenLifespanForImplicitFlow();
accessCodeLifespan = model.getAccessCodeLifespan(); accessCodeLifespan = model.getAccessCodeLifespan();
@ -459,6 +463,14 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientSessionMaxLifespan; return clientSessionMaxLifespan;
} }
public int getClientOfflineSessionIdleTimeout() {
return clientOfflineSessionIdleTimeout;
}
public int getClientOfflineSessionMaxLifespan() {
return clientOfflineSessionMaxLifespan;
}
public int getAccessTokenLifespan() { public int getAccessTokenLifespan() {
return accessTokenLifespan; return accessTokenLifespan;
} }

View file

@ -543,6 +543,26 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
setAttribute(RealmAttributes.CLIENT_SESSION_MAX_LIFESPAN, seconds); setAttribute(RealmAttributes.CLIENT_SESSION_MAX_LIFESPAN, seconds);
} }
@Override
public int getClientOfflineSessionIdleTimeout() {
return getAttribute(RealmAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT, 0);
}
@Override
public void setClientOfflineSessionIdleTimeout(int seconds) {
setAttribute(RealmAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT, seconds);
}
@Override
public int getClientOfflineSessionMaxLifespan() {
return getAttribute(RealmAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN, 0);
}
@Override
public void setClientOfflineSessionMaxLifespan(int seconds) {
setAttribute(RealmAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN, seconds);
}
@Override @Override
public int getAccessCodeLifespan() { public int getAccessCodeLifespan() {
return realm.getAccessCodeLifespan(); return realm.getAccessCodeLifespan();

View file

@ -36,6 +36,8 @@ public interface RealmAttributes {
String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan"; String OFFLINE_SESSION_MAX_LIFESPAN = "offlineSessionMaxLifespan";
String CLIENT_SESSION_IDLE_TIMEOUT = "clientSessionIdleTimeout"; String CLIENT_SESSION_IDLE_TIMEOUT = "clientSessionIdleTimeout";
String CLIENT_SESSION_MAX_LIFESPAN = "clientSessionMaxLifespan"; String CLIENT_SESSION_MAX_LIFESPAN = "clientSessionMaxLifespan";
String CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT = "clientOfflineSessionIdleTimeout";
String CLIENT_OFFLINE_SESSION_MAX_LIFESPAN = "clientOfflineSessionMaxLifespan";
String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName"; String WEBAUTHN_POLICY_RP_ENTITY_NAME = "webAuthnPolicyRpEntityName";
String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms"; String WEBAUTHN_POLICY_SIGNATURE_ALGORITHMS = "webAuthnPolicySignatureAlgorithms";

View file

@ -366,6 +366,8 @@ public class ModelToRepresentation {
rep.setOfflineSessionMaxLifespan(realm.getOfflineSessionMaxLifespan()); rep.setOfflineSessionMaxLifespan(realm.getOfflineSessionMaxLifespan());
rep.setClientSessionIdleTimeout(realm.getClientSessionIdleTimeout()); rep.setClientSessionIdleTimeout(realm.getClientSessionIdleTimeout());
rep.setClientSessionMaxLifespan(realm.getClientSessionMaxLifespan()); rep.setClientSessionMaxLifespan(realm.getClientSessionMaxLifespan());
rep.setClientOfflineSessionIdleTimeout(realm.getClientOfflineSessionIdleTimeout());
rep.setClientOfflineSessionMaxLifespan(realm.getClientOfflineSessionMaxLifespan());
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin()); rep.setAccessCodeLifespanLogin(realm.getAccessCodeLifespanLogin());

View file

@ -225,6 +225,11 @@ public class RepresentationToModel {
if (rep.getClientSessionMaxLifespan() != null) if (rep.getClientSessionMaxLifespan() != null)
newRealm.setClientSessionMaxLifespan(rep.getClientSessionMaxLifespan()); newRealm.setClientSessionMaxLifespan(rep.getClientSessionMaxLifespan());
if (rep.getClientOfflineSessionIdleTimeout() != null)
newRealm.setClientOfflineSessionIdleTimeout(rep.getClientOfflineSessionIdleTimeout());
if (rep.getClientOfflineSessionMaxLifespan() != null)
newRealm.setClientOfflineSessionMaxLifespan(rep.getClientOfflineSessionMaxLifespan());
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
else newRealm.setAccessCodeLifespan(60); else newRealm.setAccessCodeLifespan(60);
@ -1097,6 +1102,10 @@ public class RepresentationToModel {
realm.setClientSessionIdleTimeout(rep.getClientSessionIdleTimeout()); realm.setClientSessionIdleTimeout(rep.getClientSessionIdleTimeout());
if (rep.getClientSessionMaxLifespan() != null) if (rep.getClientSessionMaxLifespan() != null)
realm.setClientSessionMaxLifespan(rep.getClientSessionMaxLifespan()); realm.setClientSessionMaxLifespan(rep.getClientSessionMaxLifespan());
if (rep.getClientOfflineSessionIdleTimeout() != null)
realm.setClientOfflineSessionIdleTimeout(rep.getClientOfflineSessionIdleTimeout());
if (rep.getClientOfflineSessionMaxLifespan() != null)
realm.setClientOfflineSessionMaxLifespan(rep.getClientOfflineSessionMaxLifespan());
if (rep.getRequiredCredentials() != null) { if (rep.getRequiredCredentials() != null) {
realm.updateRequiredCredentials(rep.getRequiredCredentials()); realm.updateRequiredCredentials(rep.getRequiredCredentials());
} }

View file

@ -202,6 +202,12 @@ public interface RealmModel extends RoleContainerModel {
int getClientSessionMaxLifespan(); int getClientSessionMaxLifespan();
void setClientSessionMaxLifespan(int seconds); void setClientSessionMaxLifespan(int seconds);
int getClientOfflineSessionIdleTimeout();
void setClientOfflineSessionIdleTimeout(int seconds);
int getClientOfflineSessionMaxLifespan();
void setClientOfflineSessionMaxLifespan(int seconds);
void setAccessTokenLifespan(int seconds); void setAccessTokenLifespan(int seconds);
int getAccessTokenLifespanForImplicitFlow(); int getAccessTokenLifespanForImplicitFlow();

View file

@ -46,6 +46,8 @@ public final class OIDCConfigAttributes {
public static final String ACCESS_TOKEN_LIFESPAN = "access.token.lifespan"; 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_IDLE_TIMEOUT = "client.session.idle.timeout";
public static final String CLIENT_SESSION_MAX_LIFESPAN = "client.session.max.lifespan"; public static final String CLIENT_SESSION_MAX_LIFESPAN = "client.session.max.lifespan";
public static final String CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT = "client.offline.session.idle.timeout";
public static final String CLIENT_OFFLINE_SESSION_MAX_LIFESPAN = "client.offline.session.max.lifespan";
public static final String PKCE_CODE_CHALLENGE_METHOD = "pkce.code.challenge.method"; public static final String PKCE_CODE_CHALLENGE_METHOD = "pkce.code.challenge.method";
public static final String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token.endpoint.auth.signing.alg"; public static final String TOKEN_ENDPOINT_AUTH_SIGNING_ALG = "token.endpoint.auth.signing.alg";

View file

@ -689,6 +689,21 @@ public class TokenManager {
if (realm.isOfflineSessionMaxLifespanEnabled()) { if (realm.isOfflineSessionMaxLifespanEnabled()) {
int sessionExpires = userSession.getStarted() + realm.getOfflineSessionMaxLifespan(); int sessionExpires = userSession.getStarted() + realm.getOfflineSessionMaxLifespan();
expiration = expiration <= sessionExpires ? expiration : sessionExpires; expiration = expiration <= sessionExpires ? expiration : sessionExpires;
int clientOfflineSessionMaxLifespan;
String clientOfflineSessionMaxLifespanPerClient = client
.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN);
if (clientOfflineSessionMaxLifespanPerClient != null
&& !clientOfflineSessionMaxLifespanPerClient.trim().isEmpty()) {
clientOfflineSessionMaxLifespan = Integer.parseInt(clientOfflineSessionMaxLifespanPerClient);
} else {
clientOfflineSessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
}
if (clientOfflineSessionMaxLifespan > 0) {
int clientOfflineSessionExpiration = userSession.getStarted() + clientOfflineSessionMaxLifespan;
return expiration < clientOfflineSessionExpiration ? expiration : clientOfflineSessionExpiration;
}
} }
} else { } else {
int sessionExpires = userSession.getStarted() int sessionExpires = userSession.getStarted()
@ -696,19 +711,19 @@ public class TokenManager {
? realm.getSsoSessionMaxLifespanRememberMe() ? realm.getSsoSessionMaxLifespanRememberMe()
: realm.getSsoSessionMaxLifespan()); : realm.getSsoSessionMaxLifespan());
expiration = expiration <= sessionExpires ? expiration : sessionExpires; expiration = expiration <= sessionExpires ? expiration : sessionExpires;
}
int clientSessionMaxLifespan; int clientSessionMaxLifespan;
String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN); String clientSessionMaxLifespanPerClient = client.getAttribute(OIDCConfigAttributes.CLIENT_SESSION_MAX_LIFESPAN);
if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) { if (clientSessionMaxLifespanPerClient != null && !clientSessionMaxLifespanPerClient.trim().isEmpty()) {
clientSessionMaxLifespan = Integer.parseInt(clientSessionMaxLifespanPerClient); clientSessionMaxLifespan = Integer.parseInt(clientSessionMaxLifespanPerClient);
} else { } else {
clientSessionMaxLifespan = realm.getClientSessionMaxLifespan(); clientSessionMaxLifespan = realm.getClientSessionMaxLifespan();
} }
if (clientSessionMaxLifespan > 0) { if (clientSessionMaxLifespan > 0) {
int clientSessionExpiration = userSession.getStarted() + clientSessionMaxLifespan; int clientSessionExpiration = userSession.getStarted() + clientSessionMaxLifespan;
return expiration < clientSessionExpiration ? expiration : clientSessionExpiration; return expiration < clientSessionExpiration ? expiration : clientSessionExpiration;
}
} }
return expiration; return expiration;
@ -842,9 +857,41 @@ public class TokenManager {
} }
private int getOfflineExpiration() { private int getOfflineExpiration() {
int expiration = Time.currentTime() + realm.getOfflineSessionIdleTimeout();
int sessionExpires = userSession.getStarted() + realm.getOfflineSessionMaxLifespan(); int sessionExpires = userSession.getStarted() + realm.getOfflineSessionMaxLifespan();
int clientOfflineSessionMaxLifespan;
String clientOfflineSessionMaxLifespanPerClient = client
.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN);
if (clientOfflineSessionMaxLifespanPerClient != null
&& !clientOfflineSessionMaxLifespanPerClient.trim().isEmpty()) {
clientOfflineSessionMaxLifespan = Integer.parseInt(clientOfflineSessionMaxLifespanPerClient);
} else {
clientOfflineSessionMaxLifespan = realm.getClientOfflineSessionMaxLifespan();
}
if (clientOfflineSessionMaxLifespan > 0) {
int clientOfflineSessionMaxExpiration = userSession.getStarted() + clientOfflineSessionMaxLifespan;
sessionExpires = sessionExpires < clientOfflineSessionMaxExpiration ? sessionExpires
: clientOfflineSessionMaxExpiration;
}
int expiration = Time.currentTime() + realm.getOfflineSessionIdleTimeout();
int clientOfflineSessionIdleTimeout;
String clientOfflineSessionIdleTimeoutPerClient = client
.getAttribute(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT);
if (clientOfflineSessionIdleTimeoutPerClient != null
&& !clientOfflineSessionIdleTimeoutPerClient.trim().isEmpty()) {
clientOfflineSessionIdleTimeout = Integer.parseInt(clientOfflineSessionIdleTimeoutPerClient);
} else {
clientOfflineSessionIdleTimeout = realm.getClientOfflineSessionIdleTimeout();
}
if (clientOfflineSessionIdleTimeout > 0) {
int clientOfflineSessionIdleExpiration = Time.currentTime() + clientOfflineSessionIdleTimeout;
expiration = expiration < clientOfflineSessionIdleExpiration ? expiration : clientOfflineSessionIdleExpiration;
}
return expiration <= sessionExpires ? expiration : sessionExpires; return expiration <= sessionExpires ? expiration : sessionExpires;
} }

View file

@ -624,6 +624,10 @@ public class RealmTest extends AbstractAdminTest {
Assert.assertEquals(realm.getClientSessionIdleTimeout(), storedRealm.getClientSessionIdleTimeout()); Assert.assertEquals(realm.getClientSessionIdleTimeout(), storedRealm.getClientSessionIdleTimeout());
if (realm.getClientSessionMaxLifespan() != null) if (realm.getClientSessionMaxLifespan() != null)
Assert.assertEquals(realm.getClientSessionMaxLifespan(), storedRealm.getClientSessionMaxLifespan()); Assert.assertEquals(realm.getClientSessionMaxLifespan(), storedRealm.getClientSessionMaxLifespan());
if (realm.getClientOfflineSessionIdleTimeout() != null)
Assert.assertEquals(realm.getClientOfflineSessionIdleTimeout(), storedRealm.getClientOfflineSessionIdleTimeout());
if (realm.getClientOfflineSessionMaxLifespan() != null)
Assert.assertEquals(realm.getClientOfflineSessionMaxLifespan(), storedRealm.getClientOfflineSessionMaxLifespan());
if (realm.getRequiredCredentials() != null) { if (realm.getRequiredCredentials() != null) {
assertNotNull(storedRealm.getRequiredCredentials()); assertNotNull(storedRealm.getRequiredCredentials());
for (String cred : realm.getRequiredCredentials()) { for (String cred : realm.getRequiredCredentials()) {

View file

@ -1162,6 +1162,53 @@ public class AccessTokenTest extends AbstractKeycloakTest {
} }
} }
@Test
public void testClientOfflineSessionMaxLifespan() 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();
Boolean originalOfflineSessionMaxLifespanEnabled = rep.getOfflineSessionMaxLifespanEnabled();
Integer originalClientOfflineSessionMaxLifespan = rep.getClientOfflineSessionMaxLifespan();
try {
rep.setOfflineSessionMaxLifespanEnabled(true);
realm.update(rep);
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
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.setClientOfflineSessionMaxLifespan(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_OFFLINE_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.setOfflineSessionMaxLifespanEnabled(originalOfflineSessionMaxLifespanEnabled);
rep.setClientOfflineSessionMaxLifespan(originalClientOfflineSessionMaxLifespan);
realm.update(rep);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN, null);
client.update(clientRepresentation);
}
}
@Test @Test
public void accessTokenRequest_ClientPS384_RealmRS256() throws Exception { public void accessTokenRequest_ClientPS384_RealmRS256() throws Exception {
conductAccessTokenRequest(Algorithm.HS256, Algorithm.PS384, Algorithm.RS256); conductAccessTokenRequest(Algorithm.HS256, Algorithm.PS384, Algorithm.RS256);

View file

@ -39,6 +39,7 @@ import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.SessionTimeoutHelper; import org.keycloak.models.utils.SessionTimeoutHelper;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
@ -78,6 +79,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.Assert.assertExpiration;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.admin.ApiUtil.findRealmRoleByName; import static org.keycloak.testsuite.admin.ApiUtil.findRealmRoleByName;
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername; import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
@ -881,4 +883,104 @@ public class OfflineTokenTest extends AbstractKeycloakTest {
} }
} }
@Test
public void testClientOfflineSessionMaxLifespan() throws Exception {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client");
ClientRepresentation clientRepresentation = client.toRepresentation();
RealmResource realm = adminClient.realm("test");
RealmRepresentation rep = realm.toRepresentation();
Boolean originalOfflineSessionMaxLifespanEnabled = rep.getOfflineSessionMaxLifespanEnabled();
Integer originalOfflineSessionMaxLifespan = rep.getOfflineSessionMaxLifespan();
int offlineSessionMaxLifespan = rep.getOfflineSessionIdleTimeout() - 100;
Integer originalClientOfflineSessionMaxLifespan = rep.getClientOfflineSessionMaxLifespan();
try {
rep.setOfflineSessionMaxLifespanEnabled(true);
rep.setOfflineSessionMaxLifespan(offlineSessionMaxLifespan);
realm.update(rep);
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "secret1");
assertEquals(200, response.getStatusCode());
assertExpiration(response.getRefreshExpiresIn(), offlineSessionMaxLifespan);
rep.setClientOfflineSessionMaxLifespan(offlineSessionMaxLifespan - 100);
realm.update(rep);
String refreshToken = response.getRefreshToken();
response = oauth.doRefreshTokenRequest(refreshToken, "secret1");
assertEquals(200, response.getStatusCode());
assertExpiration(response.getRefreshExpiresIn(), offlineSessionMaxLifespan - 100);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN,
Integer.toString(offlineSessionMaxLifespan - 200));
client.update(clientRepresentation);
refreshToken = response.getRefreshToken();
response = oauth.doRefreshTokenRequest(refreshToken, "secret1");
assertEquals(200, response.getStatusCode());
assertExpiration(response.getRefreshExpiresIn(), offlineSessionMaxLifespan - 200);
} finally {
rep.setOfflineSessionMaxLifespanEnabled(originalOfflineSessionMaxLifespanEnabled);
rep.setOfflineSessionMaxLifespan(originalOfflineSessionMaxLifespan);
rep.setClientOfflineSessionMaxLifespan(originalClientOfflineSessionMaxLifespan);
realm.update(rep);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_MAX_LIFESPAN, null);
client.update(clientRepresentation);
}
}
@Test
public void testClientOfflineSessionIdleTimeout() throws Exception {
ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "offline-client");
ClientRepresentation clientRepresentation = client.toRepresentation();
RealmResource realm = adminClient.realm("test");
RealmRepresentation rep = realm.toRepresentation();
Boolean originalOfflineSessionMaxLifespanEnabled = rep.getOfflineSessionMaxLifespanEnabled();
int offlineSessionIdleTimeout = rep.getOfflineSessionIdleTimeout();
Integer originalClientOfflineSessionIdleTimeout = rep.getClientOfflineSessionIdleTimeout();
try {
rep.setOfflineSessionMaxLifespanEnabled(true);
realm.update(rep);
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("offline-client");
oauth.redirectUri(offlineClientAppUri);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "secret1");
assertEquals(200, response.getStatusCode());
assertExpiration(response.getRefreshExpiresIn(), offlineSessionIdleTimeout);
rep.setClientOfflineSessionIdleTimeout(offlineSessionIdleTimeout - 100);
realm.update(rep);
String refreshToken = response.getRefreshToken();
response = oauth.doRefreshTokenRequest(refreshToken, "secret1");
assertEquals(200, response.getStatusCode());
assertExpiration(response.getRefreshExpiresIn(), offlineSessionIdleTimeout - 100);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT,
Integer.toString(offlineSessionIdleTimeout - 200));
client.update(clientRepresentation);
refreshToken = response.getRefreshToken();
response = oauth.doRefreshTokenRequest(refreshToken, "secret1");
assertEquals(200, response.getStatusCode());
assertExpiration(response.getRefreshExpiresIn(), offlineSessionIdleTimeout - 200);
} finally {
rep.setOfflineSessionMaxLifespanEnabled(originalOfflineSessionMaxLifespanEnabled);
rep.setClientOfflineSessionIdleTimeout(originalClientOfflineSessionIdleTimeout);
realm.update(rep);
clientRepresentation.getAttributes().put(OIDCConfigAttributes.CLIENT_OFFLINE_SESSION_IDLE_TIMEOUT, null);
client.update(clientRepresentation);
}
}
} }

View file

@ -282,4 +282,14 @@ public class RealmBuilder {
rep.setClientSessionMaxLifespan(clientSessionMaxLifespan); rep.setClientSessionMaxLifespan(clientSessionMaxLifespan);
return this; return this;
} }
public RealmBuilder clientOfflineSessionIdleTimeout(int clientOfflineSessionIdleTimeout) {
rep.setClientOfflineSessionIdleTimeout(clientOfflineSessionIdleTimeout);
return this;
}
public RealmBuilder clientOfflineSessionMaxLifespan(int clientOfflineSessionMaxLifespan) {
rep.setClientOfflineSessionMaxLifespan(clientOfflineSessionMaxLifespan);
return this;
}
} }

View file

@ -132,6 +132,10 @@ 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-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=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. 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.
client-offline-session-idle=Client Offline Session Idle
client-offline-session-idle.tooltip=Time a client offline session is allowed to be idle before it expires. Offline tokens are invalidated when a client offline session is expired. If not set it uses the Offline Session Idle value.
client-offline-session-max=Client Offline Session Max
client-offline-session-max.tooltip=Max time before a client offline session is expired. Offline tokens are invalidated when a client offline session is expired. If not set, it uses the Offline Session Max value.
access-token-lifespan=Access Token Lifespan 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.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 access-token-lifespan-for-implicit-flow=Access Token Lifespan For Implicit Flow

View file

@ -1114,6 +1114,8 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.samlAssertionLifespan = TimeUnit2.asUnit(client.attributes['saml.assertion.lifespan']); $scope.samlAssertionLifespan = TimeUnit2.asUnit(client.attributes['saml.assertion.lifespan']);
$scope.clientSessionIdleTimeout = TimeUnit2.asUnit(client.attributes['client.session.idle.timeout']); $scope.clientSessionIdleTimeout = TimeUnit2.asUnit(client.attributes['client.session.idle.timeout']);
$scope.clientSessionMaxLifespan = TimeUnit2.asUnit(client.attributes['client.session.max.lifespan']); $scope.clientSessionMaxLifespan = TimeUnit2.asUnit(client.attributes['client.session.max.lifespan']);
$scope.clientOfflineSessionIdleTimeout = TimeUnit2.asUnit(client.attributes['client.offline.session.idle.timeout']);
$scope.clientOfflineSessionMaxLifespan = TimeUnit2.asUnit(client.attributes['client.offline.session.max.lifespan']);
if(client.origin) { if(client.origin) {
if ($scope.access.viewRealm) { if ($scope.access.viewRealm) {
@ -1464,6 +1466,22 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
} }
} }
$scope.updateClientOfflineSessionIdleTimeout = function() {
if ($scope.clientOfflineSessionIdleTimeout.time) {
$scope.clientEdit.attributes['client.offline.session.idle.timeout'] = $scope.clientOfflineSessionIdleTimeout.toSeconds();
} else {
$scope.clientEdit.attributes['client.offline.session.idle.timeout'] = null;
}
}
$scope.updateClientOfflineSessionMaxLifespan = function() {
if ($scope.clientOfflineSessionMaxLifespan.time) {
$scope.clientEdit.attributes['client.offline.session.max.lifespan'] = $scope.clientOfflineSessionMaxLifespan.toSeconds();
} else {
$scope.clientEdit.attributes['client.offline.session.max.lifespan'] = null;
}
}
function configureAuthorizationServices() { function configureAuthorizationServices() {
if ($scope.clientEdit.authorizationServicesEnabled) { if ($scope.clientEdit.authorizationServicesEnabled) {
if ($scope.accessType == 'public') { if ($scope.accessType == 'public') {

View file

@ -1162,6 +1162,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.offlineSessionMaxLifespan = TimeUnit2.asUnit(realm.offlineSessionMaxLifespan); $scope.realm.offlineSessionMaxLifespan = TimeUnit2.asUnit(realm.offlineSessionMaxLifespan);
$scope.realm.clientSessionIdleTimeout = TimeUnit2.asUnit(realm.clientSessionIdleTimeout); $scope.realm.clientSessionIdleTimeout = TimeUnit2.asUnit(realm.clientSessionIdleTimeout);
$scope.realm.clientSessionMaxLifespan = TimeUnit2.asUnit(realm.clientSessionMaxLifespan); $scope.realm.clientSessionMaxLifespan = TimeUnit2.asUnit(realm.clientSessionMaxLifespan);
$scope.realm.clientOfflineSessionIdleTimeout = TimeUnit2.asUnit(realm.clientOfflineSessionIdleTimeout);
$scope.realm.clientOfflineSessionMaxLifespan = TimeUnit2.asUnit(realm.clientOfflineSessionMaxLifespan);
$scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan); $scope.realm.accessCodeLifespan = TimeUnit2.asUnit(realm.accessCodeLifespan);
$scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin); $scope.realm.accessCodeLifespanLogin = TimeUnit2.asUnit(realm.accessCodeLifespanLogin);
$scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction); $scope.realm.accessCodeLifespanUserAction = TimeUnit2.asUnit(realm.accessCodeLifespanUserAction);
@ -1219,6 +1221,8 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.offlineSessionMaxLifespan = $scope.realm.offlineSessionMaxLifespan.toSeconds(); $scope.realm.offlineSessionMaxLifespan = $scope.realm.offlineSessionMaxLifespan.toSeconds();
$scope.realm.clientSessionIdleTimeout = $scope.realm.clientSessionIdleTimeout.toSeconds(); $scope.realm.clientSessionIdleTimeout = $scope.realm.clientSessionIdleTimeout.toSeconds();
$scope.realm.clientSessionMaxLifespan = $scope.realm.clientSessionMaxLifespan.toSeconds(); $scope.realm.clientSessionMaxLifespan = $scope.realm.clientSessionMaxLifespan.toSeconds();
$scope.realm.clientOfflineSessionIdleTimeout = $scope.realm.clientOfflineSessionIdleTimeout.toSeconds();
$scope.realm.clientOfflineSessionMaxLifespan = $scope.realm.clientOfflineSessionMaxLifespan.toSeconds();
$scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds(); $scope.realm.accessCodeLifespan = $scope.realm.accessCodeLifespan.toSeconds();
$scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds(); $scope.realm.accessCodeLifespanUserAction = $scope.realm.accessCodeLifespanUserAction.toSeconds();
$scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds(); $scope.realm.accessCodeLifespanLogin = $scope.realm.accessCodeLifespanLogin.toSeconds();

View file

@ -579,6 +579,42 @@
<kc-tooltip>{{:: 'client-session-max.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'client-session-max.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-show="protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="clientOfflineSessionIdleTimeout">{{::
'client-offline-session-idle' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" min="0" max="31536000"
data-ng-model="clientOfflineSessionIdleTimeout.time" id="clientOfflineSessionIdleTimeout"
name="clientOfflineSessionIdleTimeout" data-ng-change="updateClientOfflineSessionIdleTimeout()" /> <select
class="form-control" name="clientOfflineSessionIdleTimeoutUnit"
data-ng-model="clientOfflineSessionIdleTimeout.unit"
data-ng-change="updateClientOfflineSessionIdleTimeout()">
<option value="Minutes">{{:: 'minutes' | translate}}</option>
<option value="Hours">{{:: 'hours' | translate}}</option>
<option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'client-offline-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="clientOfflineSessionMaxLifespan">{{::
'client-offline-session-max' | translate}}</label>
<div class="col-md-6 time-selector">
<input class="form-control" type="number" min="0" max="31536000"
data-ng-model="clientOfflineSessionMaxLifespan.time" id="clientOfflineSessionMaxLifespan"
name="clientOfflineSessionMaxLifespan" data-ng-change="updateClientOfflineSessionMaxLifespan()" /> <select
class="form-control" name="clientOfflineSessionMaxLifespanUnit"
data-ng-model="clientOfflineSessionMaxLifespan.unit"
data-ng-change="updateClientOfflineSessionMaxLifespan()">
<option value="Minutes">{{:: 'minutes' | translate}}</option>
<option value="Hours">{{:: 'hours' | translate}}</option>
<option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'client-offline-session-max.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'"> <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> <label class="col-md-2 control-label" for="tlsClientCertificateBoundAccessTokens">{{:: 'tls-client-certificate-bound-access-tokens' | translate}}</label>
<div class="col-sm-6"> <div class="col-sm-6">

View file

@ -145,6 +145,38 @@
<kc-tooltip>{{:: 'offline-session-max.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'offline-session-max.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group" data-ng-show="realm.offlineSessionMaxLifespanEnabled">
<label class="col-md-2 control-label" for="clientOfflineSessionIdleTimeout">{{::
'client-offline-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.clientOfflineSessionIdleTimeout.time" id="clientOfflineSessionIdleTimeout"
name="clientOfflineSessionIdleTimeout" /> <select class="form-control"
name="clientOfflineSessionIdleTimeoutUnit" data-ng-model="realm.clientOfflineSessionIdleTimeout.unit">
<option value="Minutes">{{:: 'minutes' | translate}}</option>
<option value="Hours">{{:: 'hours' | translate}}</option>
<option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'client-offline-session-idle.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="realm.offlineSessionMaxLifespanEnabled">
<label class="col-md-2 control-label" for="clientOfflineSessionMaxLifespan">{{::
'client-offline-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.clientOfflineSessionMaxLifespan.time" id="clientOfflineSessionMaxLifespan"
name="clientOfflineSessionMaxLifespan" /> <select class="form-control"
name="clientOfflineSessionMaxLifespanUnit" data-ng-model="realm.clientOfflineSessionMaxLifespan.unit">
<option value="Minutes">{{:: 'minutes' | translate}}</option>
<option value="Hours">{{:: 'hours' | translate}}</option>
<option value="Days">{{:: 'days' | translate}}</option>
</select>
</div>
<kc-tooltip>{{:: 'client-offline-session-max.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-md-2 control-label" for="clientSessionIdleTimeout">{{:: 'client-session-idle' | translate}}</label> <label class="col-md-2 control-label" for="clientSessionIdleTimeout">{{:: 'client-session-idle' | translate}}</label>
<div class="col-md-6 time-selector"> <div class="col-md-6 time-selector">