sso session idle and max lifespan

This commit is contained in:
Bill Burke 2014-05-15 17:25:57 -04:00
parent 27efd3c0a4
commit bc2360e985
15 changed files with 86 additions and 64 deletions

View file

@ -121,7 +121,7 @@ public class FreeMarkerAccount implements Account {
attributes.put("log", new LogBean(events)); attributes.put("log", new LogBean(events));
break; break;
case SESSIONS: case SESSIONS:
attributes.put("sessions", new SessionsBean(sessions)); attributes.put("sessions", new SessionsBean(realm, sessions));
break; break;
} }

View file

@ -1,5 +1,6 @@
package org.keycloak.account.freemarker.model; package org.keycloak.account.freemarker.model;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.util.Time; import org.keycloak.util.Time;
@ -13,11 +14,12 @@ import java.util.List;
public class SessionsBean { public class SessionsBean {
private List<UserSessionBean> events; private List<UserSessionBean> events;
private RealmModel realm;
public SessionsBean(List<UserSessionModel> sessions) { public SessionsBean(RealmModel realm, List<UserSessionModel> sessions) {
this.events = new LinkedList<UserSessionBean>(); this.events = new LinkedList<UserSessionBean>();
for (UserSessionModel session : sessions) { for (UserSessionModel session : sessions) {
this.events.add(new UserSessionBean(session)); this.events.add(new UserSessionBean(realm, session));
} }
} }
@ -28,8 +30,10 @@ public class SessionsBean {
public static class UserSessionBean { public static class UserSessionBean {
private UserSessionModel session; private UserSessionModel session;
private RealmModel realm;
public UserSessionBean(UserSessionModel session) { public UserSessionBean(RealmModel realm, UserSessionModel session) {
this.realm = realm;
this.session = session; this.session = session;
} }
@ -42,7 +46,8 @@ public class SessionsBean {
} }
public Date getExpires() { public Date getExpires() {
return Time.toDate(session.getExpires()); int max = session.getStarted() + realm.getSsoSessionMaxLifespan();
return Time.toDate(max);
} }
} }

View file

@ -21,8 +21,8 @@ public interface UserSessionModel {
void setStarted(int started); void setStarted(int started);
int getExpires(); int getLastSessionRefresh();
void setExpires(int expires); void setLastSessionRefresh(int seconds);
} }

View file

@ -1389,10 +1389,9 @@ public class RealmAdapter implements RealmModel {
entity.setIpAddress(ipAddress); entity.setIpAddress(ipAddress);
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();
int expires = currentTime + realm.getSsoSessionIdleTimeout();
entity.setStarted(currentTime); entity.setStarted(currentTime);
entity.setExpires(expires); entity.setLastSessionRefresh(currentTime);
em.persist(entity); em.persist(entity);
return new UserSessionAdapter(entity); return new UserSessionAdapter(entity);
@ -1429,7 +1428,10 @@ public class RealmAdapter implements RealmModel {
@Override @Override
public void removeExpiredUserSessions() { public void removeExpiredUserSessions() {
em.createNamedQuery("removeUserSessionExpired").setParameter("currentTime", Time.currentTime()).executeUpdate(); em.createNamedQuery("removeUserSessionExpired")
.setParameter("maxTime", Time.currentTime() - getSsoSessionMaxLifespan())
.setParameter("idleTime", Time.currentTime() - getSsoSessionIdleTimeout())
.executeUpdate();
} }
} }

View file

@ -60,13 +60,12 @@ public class UserSessionAdapter implements UserSessionModel {
} }
@Override @Override
public int getExpires() { public int getLastSessionRefresh() {
return entity.getExpires(); return entity.getLastSessionRefresh();
} }
@Override @Override
public void setExpires(int expires) { public void setLastSessionRefresh(int seconds) {
entity.setExpires(expires); entity.setLastSessionRefresh(seconds);
} }
} }

View file

@ -17,7 +17,7 @@ import javax.persistence.NamedQuery;
@NamedQueries({ @NamedQueries({
@NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"), @NamedQuery(name = "getUserSessionByUser", query = "select s from UserSessionEntity s where s.user = :user"),
@NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"), @NamedQuery(name = "removeUserSessionByUser", query = "delete from UserSessionEntity s where s.user = :user"),
@NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.expires < :currentTime") @NamedQuery(name = "removeUserSessionExpired", query = "delete from UserSessionEntity s where s.started < :maxTime or s.lastSessionRefresh < :idleTime")
}) })
public class UserSessionEntity { public class UserSessionEntity {
@ -33,7 +33,7 @@ public class UserSessionEntity {
int started; int started;
int expires; int lastSessionRefresh;
public String getId() { public String getId() {
return id; return id;
@ -67,12 +67,11 @@ public class UserSessionEntity {
this.started = started; this.started = started;
} }
public int getExpires() { public int getLastSessionRefresh() {
return expires; return lastSessionRefresh;
} }
public void setExpires(int expires) { public void setLastSessionRefresh(int lastSessionRefresh) {
this.expires = expires; this.lastSessionRefresh = lastSessionRefresh;
} }
} }

View file

@ -1356,10 +1356,9 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
entity.setIpAddress(ipAddress); entity.setIpAddress(ipAddress);
int currentTime = Time.currentTime(); int currentTime = Time.currentTime();
int expires = currentTime + realm.getSsoSessionIdleTimeout();
entity.setStarted(currentTime); entity.setStarted(currentTime);
entity.setExpires(expires); entity.setLastSessionRefresh(currentTime);
getMongoStore().insertEntity(entity, invocationContext); getMongoStore().insertEntity(entity, invocationContext);
return new UserSessionAdapter(entity, this, invocationContext); return new UserSessionAdapter(entity, this, invocationContext);
@ -1398,8 +1397,14 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override @Override
public void removeExpiredUserSessions() { public void removeExpiredUserSessions() {
int currentTime = Time.currentTime();
DBObject query = new QueryBuilder() DBObject query = new QueryBuilder()
.and("expires").lessThan(Time.currentTime()) .and("started").lessThan(currentTime - realm.getSsoSessionMaxLifespan())
.get();
getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);
query = new QueryBuilder()
.and("lastSessionRefresh").lessThan(currentTime - realm.getSsoSessionIdleTimeout())
.get(); .get();
getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext); getMongoStore().removeEntities(MongoUserSessionEntity.class, query, invocationContext);

View file

@ -65,13 +65,13 @@ public class UserSessionAdapter implements UserSessionModel {
} }
@Override @Override
public int getExpires() { public int getLastSessionRefresh() {
return entity.getExpires(); return entity.getLastSessionRefresh();
} }
@Override @Override
public void setExpires(int expires) { public void setLastSessionRefresh(int seconds) {
entity.setExpires(expires); entity.setLastSessionRefresh(seconds);
} }
} }

View file

@ -17,7 +17,7 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
private int started; private int started;
private int expires; private int lastSessionRefresh;
public String getUser() { public String getUser() {
return user; return user;
@ -43,12 +43,12 @@ public class MongoUserSessionEntity extends AbstractIdentifiableEntity implement
this.started = started; this.started = started;
} }
public int getExpires() { public int getLastSessionRefresh() {
return expires; return lastSessionRefresh;
} }
public void setExpires(int expires) { public void setLastSessionRefresh(int lastSessionRefresh) {
this.expires = expires; this.lastSessionRefresh = lastSessionRefresh;
} }
@Override @Override

View file

@ -2,22 +2,13 @@ package org.keycloak.services.managers;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.UnauthorizedException; import org.jboss.resteasy.spi.UnauthorizedException;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderSession; import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.AccessToken;
import org.jboss.resteasy.spi.BadRequestException;
import javax.ws.rs.core.Cookie; import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.net.URI;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -33,6 +24,11 @@ public class AppAuthManager extends AuthenticationManager {
public UserModel authenticateRequest(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { public UserModel authenticateRequest(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) {
AuthResult authResult = authenticateIdentityCookie(realm, uriInfo, headers); AuthResult authResult = authenticateIdentityCookie(realm, uriInfo, headers);
if (authResult != null) { if (authResult != null) {
Cookie remember = headers.getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
boolean rememberMe = remember != null;
// refresh the cookies!
createLoginCookie(realm, authResult.getUser(), authResult.getSession(), uriInfo, rememberMe);
if (rememberMe) createRememberMeCookie(realm, uriInfo);
return authResult.getUser(); return authResult.getUser();
} else { } else {
return authenticateBearerToken(realm, uriInfo, headers); return authenticateBearerToken(realm, uriInfo, headers);

View file

@ -61,6 +61,11 @@ public class AuthenticationManager {
this.protector = protector; this.protector = protector;
} }
public static boolean isSessionValid(RealmModel realm, UserSessionModel session) {
int currentTime = Time.currentTime();
return session == null || session.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() < currentTime || session.getStarted() + realm.getSsoSessionMaxLifespan() < currentTime;
}
public AccessToken createIdentityToken(RealmModel realm, UserModel user, UserSessionModel session) { public AccessToken createIdentityToken(RealmModel realm, UserModel user, UserSessionModel session) {
logger.info("createIdentityToken"); logger.info("createIdentityToken");
AccessToken token = new AccessToken(); AccessToken token = new AccessToken();
@ -77,7 +82,7 @@ public class AuthenticationManager {
return token; return token;
} }
public void createLoginCookie(Response.ResponseBuilder builder, RealmModel realm, UserModel user, UserSessionModel session, UriInfo uriInfo, boolean rememberMe) { public void createLoginCookie(RealmModel realm, UserModel user, UserSessionModel session, UriInfo uriInfo, boolean rememberMe) {
logger.info("createLoginCookie"); logger.info("createLoginCookie");
String cookiePath = getIdentityCookiePath(realm, uriInfo); String cookiePath = getIdentityCookiePath(realm, uriInfo);
AccessToken identityToken = createIdentityToken(realm, user, session); AccessToken identityToken = createIdentityToken(realm, user, session);
@ -97,11 +102,11 @@ public class AuthenticationManager {
sessionCookieValue += "-" + session.getId(); sessionCookieValue += "-" + session.getId();
} }
// THIS SHOULD NOT BE A HTTPONLY COOKIE! It is used for OpenID Connect Iframe Session support! // THIS SHOULD NOT BE A HTTPONLY COOKIE! It is used for OpenID Connect Iframe Session support!
builder.cookie(new NewCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, maxAge, secureOnly)); CookieHelper.addCookie(KEYCLOAK_SESSION_COOKIE, sessionCookieValue, cookiePath, null, null, maxAge, secureOnly, false);
} }
public void createRememberMeCookie(HttpResponse response, RealmModel realm, UriInfo uriInfo) { public void createRememberMeCookie(RealmModel realm, UriInfo uriInfo) {
String path = getIdentityCookiePath(realm, uriInfo); String path = getIdentityCookiePath(realm, uriInfo);
boolean secureOnly = !realm.isSslNotRequired(); boolean secureOnly = !realm.isSslNotRequired();
// remember me cookie should be persistent // remember me cookie should be persistent
@ -121,6 +126,7 @@ public class AuthenticationManager {
String path = getIdentityCookiePath(realm, uriInfo); String path = getIdentityCookiePath(realm, uriInfo);
expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true); expireCookie(realm, KEYCLOAK_IDENTITY_COOKIE, path, true);
expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false); expireCookie(realm, KEYCLOAK_SESSION_COOKIE, path, false);
expireRememberMeCookie(realm, uriInfo);
} }
public void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo) { public void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo) {
logger.debug("Expiring remember me cookie"); logger.debug("Expiring remember me cookie");
@ -146,15 +152,9 @@ public class AuthenticationManager {
public AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) { public AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) {
logger.info("authenticateIdentityCookie"); logger.info("authenticateIdentityCookie");
String cookieName = KEYCLOAK_IDENTITY_COOKIE; Cookie cookie = headers.getCookies().get(KEYCLOAK_IDENTITY_COOKIE);
return authenticateIdentityCookie(realm, uriInfo, headers, cookieName, checkActive);
}
private AuthResult authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) {
logger.info("authenticateIdentityCookie");
Cookie cookie = headers.getCookies().get(cookieName);
if (cookie == null) { if (cookie == null) {
logger.infov("authenticateCookie could not find cookie: {0}", cookieName); logger.infov("authenticateCookie could not find cookie: {0}", KEYCLOAK_IDENTITY_COOKIE);
return null; return null;
} }
@ -162,7 +162,9 @@ public class AuthenticationManager {
AuthResult authResult = verifyIdentityToken(realm, uriInfo, checkActive, tokenString); AuthResult authResult = verifyIdentityToken(realm, uriInfo, checkActive, tokenString);
if (authResult == null) { if (authResult == null) {
expireIdentityCookie(realm, uriInfo); expireIdentityCookie(realm, uriInfo);
return null;
} }
authResult.getSession().setLastSessionRefresh(Time.currentTime());
return authResult; return authResult;
} }
@ -174,7 +176,6 @@ public class AuthenticationManager {
logger.info("Checking if identity token is active"); logger.info("Checking if identity token is active");
if (!token.isActive() || token.getIssuedAt() < realm.getNotBefore()) { if (!token.isActive() || token.getIssuedAt() < realm.getNotBefore()) {
logger.info("identity cookie expired"); logger.info("identity cookie expired");
expireIdentityCookie(realm, uriInfo);
return null; return null;
} else { } else {
logger.info("token.isActive() : " + token.isActive()); logger.info("token.isActive() : " + token.isActive());
@ -195,9 +196,12 @@ public class AuthenticationManager {
} }
UserSessionModel session = realm.getUserSession(token.getSessionState()); UserSessionModel session = realm.getUserSession(token.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { int currentTime = Time.currentTime();
if (isSessionValid(realm, session)) {
if (session != null) {
realm.removeUserSession(session);
}
logger.info("User session not active"); logger.info("User session not active");
expireIdentityCookie(realm, uriInfo);
return null; return null;
} }

View file

@ -137,7 +137,11 @@ public class TokenManager {
} }
UserSessionModel session = realm.getUserSession(refreshToken.getSessionState()); UserSessionModel session = realm.getUserSession(refreshToken.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { int currentTime = Time.currentTime();
if (AuthenticationManager.isSessionValid(realm, session)) {
if (session != null) {
realm.removeUserSession(session);
}
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active"); throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Session not active", "Session not active");
} }
@ -191,6 +195,12 @@ public class TokenManager {
AccessToken accessToken = initToken(realm, client, user, session); AccessToken accessToken = initToken(realm, client, user, session);
accessToken.setRealmAccess(refreshToken.getRealmAccess()); accessToken.setRealmAccess(refreshToken.getRealmAccess());
accessToken.setResourceAccess(refreshToken.getResourceAccess()); accessToken.setResourceAccess(refreshToken.getResourceAccess());
// only refresh session if next token refresh will be after idle timeout
if (currentTime + realm.getAccessTokenLifespan() > session.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout()) {
session.setLastSessionRefresh(currentTime);
}
return accessToken; return accessToken;
} }

View file

@ -408,7 +408,7 @@ public class RequiredActionsService {
AuthenticationManager authManager = new AuthenticationManager(providerSession); AuthenticationManager authManager = new AuthenticationManager(providerSession);
UserSessionModel session = realm.getUserSession(accessCode.getSessionState()); UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { if (AuthenticationManager.isSessionValid(realm, session)) {
return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri()); return Flows.oauth(realm, request, uriInfo, authManager, tokenManager).redirectError(accessCode.getClient(), "access_denied", accessCode.getState(), accessCode.getRedirectUri());
} }
audit.session(session); audit.session(session);

View file

@ -58,7 +58,6 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext; import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -374,7 +373,7 @@ public class TokenService {
AuthenticationStatus status = authManager.authenticateForm(clientConnection, realm, formData); AuthenticationStatus status = authManager.authenticateForm(clientConnection, realm, formData);
if (remember) { if (remember) {
authManager.createRememberMeCookie(response, realm, uriInfo); authManager.createRememberMeCookie(realm, uriInfo);
} else { } else {
authManager.expireRememberMeCookie(realm, uriInfo); authManager.expireRememberMeCookie(realm, uriInfo);
} }
@ -636,7 +635,7 @@ public class TokenService {
} }
UserSessionModel session = realm.getUserSession(accessCode.getSessionState()); UserSessionModel session = realm.getUserSession(accessCode.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { if (AuthenticationManager.isSessionValid(realm, session)) {
Map<String, String> res = new HashMap<String, String>(); Map<String, String> res = new HashMap<String, String>();
res.put(OAuth2Constants.ERROR, "invalid_grant"); res.put(OAuth2Constants.ERROR, "invalid_grant");
res.put(OAuth2Constants.ERROR_DESCRIPTION, "Session not active"); res.put(OAuth2Constants.ERROR_DESCRIPTION, "Session not active");
@ -915,7 +914,8 @@ public class TokenService {
} }
UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState()); UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { int currentTime = Time.currentTime();
if (AuthenticationManager.isSessionValid(realm, session)) {
audit.error(Errors.INVALID_CODE); audit.error(Errors.INVALID_CODE);
return oauth.forwardToSecurityFailure("Session not active"); return oauth.forwardToSecurityFailure("Session not active");
} }

View file

@ -93,7 +93,9 @@ public class OAuthFlows {
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build()); Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME); Cookie remember = request.getHttpHeaders().getCookies().get(AuthenticationManager.KEYCLOAK_REMEMBER_ME);
rememberMe = rememberMe || remember != null; rememberMe = rememberMe || remember != null;
authManager.createLoginCookie(location, realm, accessCode.getUser(), session, uriInfo, rememberMe); // refresh the cookies!
authManager.createLoginCookie(realm, accessCode.getUser(), session, uriInfo, rememberMe);
if (rememberMe) authManager.createRememberMeCookie(realm, uriInfo);
return location.build(); return location.build();
} }
} }