Merge pull request #388 from patriot1burke/master

session idle and max lifespan
This commit is contained in:
Bill Burke 2014-05-15 23:18:43 -04:00
commit 1318ea85be
30 changed files with 454 additions and 241 deletions

View file

@ -10,13 +10,12 @@ import java.util.Set;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class RealmRepresentation { public class RealmRepresentation {
protected String self; // link
protected String id; protected String id;
protected String realm; protected String realm;
protected Integer notBefore; protected Integer notBefore;
protected Integer accessTokenLifespan; protected Integer accessTokenLifespan;
protected Integer refreshTokenLifespan; protected Integer ssoSessionIdleTimeout;
protected Integer centralLoginLifespan; protected Integer ssoSessionMaxLifespan;
protected Integer accessCodeLifespan; protected Integer accessCodeLifespan;
protected Integer accessCodeLifespanUserAction; protected Integer accessCodeLifespanUserAction;
protected Boolean enabled; protected Boolean enabled;
@ -62,14 +61,6 @@ public class RealmRepresentation {
protected long auditExpiration; protected long auditExpiration;
protected List<String> auditListeners; protected List<String> auditListeners;
public String getSelf() {
return self;
}
public void setSelf(String self) {
this.self = self;
}
public String getId() { public String getId() {
return id; return id;
} }
@ -142,20 +133,20 @@ public class RealmRepresentation {
this.accessTokenLifespan = accessTokenLifespan; this.accessTokenLifespan = accessTokenLifespan;
} }
public Integer getRefreshTokenLifespan() { public Integer getSsoSessionIdleTimeout() {
return refreshTokenLifespan; return ssoSessionIdleTimeout;
} }
public Integer getCentralLoginLifespan() { public void setSsoSessionIdleTimeout(Integer ssoSessionIdleTimeout) {
return centralLoginLifespan; this.ssoSessionIdleTimeout = ssoSessionIdleTimeout;
} }
public void setCentralLoginLifespan(Integer centralLoginLifespan) { public Integer getSsoSessionMaxLifespan() {
this.centralLoginLifespan = centralLoginLifespan; return ssoSessionMaxLifespan;
} }
public void setRefreshTokenLifespan(Integer refreshTokenLifespan) { public void setSsoSessionMaxLifespan(Integer ssoSessionMaxLifespan) {
this.refreshTokenLifespan = refreshTokenLifespan; this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
} }
public List<UserRoleMappingRepresentation> getRoleMappings() { public List<UserRoleMappingRepresentation> getRoleMappings() {

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

@ -623,16 +623,16 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.realm.accessTokenLifespan = TimeUnit.convert($scope.realm.accessTokenLifespan, from, to); $scope.realm.accessTokenLifespan = TimeUnit.convert($scope.realm.accessTokenLifespan, from, to);
}); });
$scope.realm.centralLoginLifespanUnit = TimeUnit.autoUnit(realm.centralLoginLifespan); $scope.realm.ssoSessionIdleTimeoutUnit = TimeUnit.autoUnit(realm.ssoSessionIdleTimeout);
$scope.realm.centralLoginLifespan = TimeUnit.toUnit(realm.centralLoginLifespan, $scope.realm.centralLoginLifespanUnit); $scope.realm.ssoSessionIdleTimeout = TimeUnit.toUnit(realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit);
$scope.$watch('realm.centralLoginLifespanUnit', function(to, from) { $scope.$watch('realm.ssoSessionIdleTimeoutUnit', function(to, from) {
$scope.realm.centralLoginLifespan = TimeUnit.convert($scope.realm.centralLoginLifespan, from, to); $scope.realm.ssoSessionIdleTimeout = TimeUnit.convert($scope.realm.ssoSessionIdleTimeout, from, to);
}); });
$scope.realm.refreshTokenLifespanUnit = TimeUnit.autoUnit(realm.refreshTokenLifespan); $scope.realm.ssoSessionMaxLifespanUnit = TimeUnit.autoUnit(realm.ssoSessionMaxLifespan);
$scope.realm.refreshTokenLifespan = TimeUnit.toUnit(realm.refreshTokenLifespan, $scope.realm.refreshTokenLifespanUnit); $scope.realm.ssoSessionMaxLifespan = TimeUnit.toUnit(realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit);
$scope.$watch('realm.refreshTokenLifespanUnit', function(to, from) { $scope.$watch('realm.ssoSessionMaxLifespanUnit', function(to, from) {
$scope.realm.refreshTokenLifespan = TimeUnit.convert($scope.realm.refreshTokenLifespan, from, to); $scope.realm.ssoSessionMaxLifespan = TimeUnit.convert($scope.realm.ssoSessionMaxLifespan, from, to);
}); });
$scope.realm.accessCodeLifespanUnit = TimeUnit.autoUnit(realm.accessCodeLifespan); $scope.realm.accessCodeLifespanUnit = TimeUnit.autoUnit(realm.accessCodeLifespan);
@ -660,14 +660,14 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
$scope.save = function() { $scope.save = function() {
var realmCopy = angular.copy($scope.realm); var realmCopy = angular.copy($scope.realm);
delete realmCopy["accessTokenLifespanUnit"]; delete realmCopy["accessTokenLifespanUnit"];
delete realmCopy["refreshTokenLifespanUnit"]; delete realmCopy["ssoSessionMaxLifespanUnit"];
delete realmCopy["accessCodeLifespanUnit"]; delete realmCopy["accessCodeLifespanUnit"];
delete realmCopy["centralLoginLifespanUnit"]; delete realmCopy["ssoSessionIdleTimeoutUnit"];
delete realmCopy["accessCodeLifespanUserActionUnit"]; delete realmCopy["accessCodeLifespanUserActionUnit"];
realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit) realmCopy.accessTokenLifespan = TimeUnit.toSeconds($scope.realm.accessTokenLifespan, $scope.realm.accessTokenLifespanUnit)
realmCopy.centralLoginLifespan = TimeUnit.toSeconds($scope.realm.centralLoginLifespan, $scope.realm.centralLoginLifespanUnit) realmCopy.ssoSessionIdleTimeout = TimeUnit.toSeconds($scope.realm.ssoSessionIdleTimeout, $scope.realm.ssoSessionIdleTimeoutUnit)
realmCopy.refreshTokenLifespan = TimeUnit.toSeconds($scope.realm.refreshTokenLifespan, $scope.realm.refreshTokenLifespanUnit) realmCopy.ssoSessionMaxLifespan = TimeUnit.toSeconds($scope.realm.ssoSessionMaxLifespan, $scope.realm.ssoSessionMaxLifespanUnit)
realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit) realmCopy.accessCodeLifespan = TimeUnit.toSeconds($scope.realm.accessCodeLifespan, $scope.realm.accessCodeLifespanUnit)
realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit) realmCopy.accessCodeLifespanUserAction = TimeUnit.toSeconds($scope.realm.accessCodeLifespanUserAction, $scope.realm.accessCodeLifespanUserActionUnit)

View file

@ -2,8 +2,8 @@
<div id="content-area" class="col-sm-9" role="main"> <div id="content-area" class="col-sm-9" role="main">
<ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create"> <ul class="nav nav-tabs nav-tabs-pf" data-ng-show="!create">
<li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li> <li><a href="#/realms/{{realm.realm}}/sessions/realm">Realm Sessions</a></li>
<li><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</a></li> <li class="active"><a href="#/realms/{{realm.realm}}/token-settings">Token Settings</a></li>
<li class="active"><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li> <li><a href="#/realms/{{realm.realm}}/sessions/revocation">Revocation</a></li>
<li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force</a></li> <li><a href="#/realms/{{realm.realm}}/sessions/brute-force">Brute Force</a></li>
</ul> </ul>
<div id="content"> <div id="content">
@ -15,17 +15,37 @@
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"> <form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm">
<fieldset class="border-top"> <fieldset class="border-top">
<div class="form-group input-select"> <div class="form-group input-select">
<label class="col-sm-2 control-label" for="centralLoginLifespan">Central Login Lifespan</label> <label class="col-sm-2 control-label" for="ssoSessionIdleTimeout">SSO Session Idle Timeout</label>
<div class="col-sm-10"> <div class="col-sm-10">
<div class="row"> <div class="row">
<div class="col-sm-2"> <div class="col-sm-2">
<input class="form-control" type="number" required min="1" <input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.centralLoginLifespan" max="31536000" data-ng-model="realm.ssoSessionIdleTimeout"
id="centralLoginLifespan" name="centralLoginLifespan"/> id="ssoSessionIdleTimeout" name="ssoSessionIdleTimeout"/>
</div> </div>
<div class="col-sm-2 select-kc"> <div class="col-sm-2 select-kc">
<select name="centralLoginLifespanUnit" data-ng-model="realm.centralLoginLifespanUnit" > <select name="ssoSessionIdleTimeoutUnit" data-ng-model="realm.ssoSessionIdleTimeoutUnit" >
<option data-ng-selected="!realm.centralLoginLifespanUnit">Seconds</option> <option data-ng-selected="!realm.ssoSessionIdleTimeoutUnit">Seconds</option>
<option>Minutes</option>
<option>Hours</option>
<option>Days</option>
</select>
</div>
</div>
</div>
</div>
<div class="form-group input-select">
<label class="col-sm-2 control-label" for="ssoSessionMaxLifespan">SSO Session Max Lifespan</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-2">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.ssoSessionMaxLifespan"
id="ssoSessionMaxLifespan" name="ssoSessionMaxLifespan"/>
</div>
<div class="col-sm-2 select-kc">
<select name="ssoSessionMaxLifespanUnit" data-ng-model="realm.ssoSessionMaxLifespanUnit" >
<option data-ng-selected="!realm.ssoSessionMaxLifespanUnit">Seconds</option>
<option>Minutes</option> <option>Minutes</option>
<option>Hours</option> <option>Hours</option>
<option>Days</option> <option>Days</option>
@ -90,26 +110,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group input-select">
<label class="col-sm-2 control-label" for="refreshTokenLifespan">Refresh token lifespan</label>
<div class="col-sm-10">
<div class="row">
<div class="col-sm-2">
<input class="form-control" type="number" required min="1"
max="31536000" data-ng-model="realm.refreshTokenLifespan"
id="refreshTokenLifespan" name="refreshTokenLifespan"/>
</div>
<div class="col-sm-2 select-kc">
<select name="refreshTokenLifespanUnit" data-ng-model="realm.refreshTokenLifespanUnit" >
<option data-ng-selected="!realm.refreshTokenLifespanUnit">Seconds</option>
<option>Minutes</option>
<option>Hours</option>
<option>Days</option>
</select>
</div>
</div>
</div>
</div>
</fieldset> </fieldset>
<div class="pull-right form-actions" data-ng-show="access.manageRealm"> <div class="pull-right form-actions" data-ng-show="access.manageRealm">
<button kc-reset data-ng-show="changed">Clear changes</button> <button kc-reset data-ng-show="changed">Clear changes</button>

View file

@ -59,25 +59,23 @@ public interface RealmModel extends RoleContainerModel, RoleMapperModel, ScopeMa
void setResetPasswordAllowed(boolean resetPasswordAllowed); void setResetPasswordAllowed(boolean resetPasswordAllowed);
int getCentralLoginLifespan(); int getSsoSessionIdleTimeout();
void setSsoSessionIdleTimeout(int seconds);
void setCentralLoginLifespan(int lifespan); int getSsoSessionMaxLifespan();
void setSsoSessionMaxLifespan(int seconds);
int getAccessTokenLifespan(); int getAccessTokenLifespan();
void setAccessTokenLifespan(int tokenLifespan); void setAccessTokenLifespan(int seconds);
int getRefreshTokenLifespan();
void setRefreshTokenLifespan(int tokenLifespan);
int getAccessCodeLifespan(); int getAccessCodeLifespan();
void setAccessCodeLifespan(int accessCodeLifespan); void setAccessCodeLifespan(int seconds);
int getAccessCodeLifespanUserAction(); int getAccessCodeLifespanUserAction();
void setAccessCodeLifespanUserAction(int accessCodeLifespanUserAction); void setAccessCodeLifespanUserAction(int seconds);
String getPublicKeyPem(); String getPublicKeyPem();

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

@ -30,11 +30,11 @@ public class RealmEntity extends AbstractIdentifiableEntity {
private int failureFactor; private int failureFactor;
//--- end brute force settings //--- end brute force settings
private int centralLoginLifespan; private int ssoSessionIdleTimeout;
private int ssoSessionMaxLifespan;
private int accessTokenLifespan; private int accessTokenLifespan;
private int accessCodeLifespan; private int accessCodeLifespan;
private int accessCodeLifespanUserAction; private int accessCodeLifespanUserAction;
private int refreshTokenLifespan;
private int notBefore; private int notBefore;
private String publicKeyPem; private String publicKeyPem;
@ -196,12 +196,20 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.failureFactor = failureFactor; this.failureFactor = failureFactor;
} }
public int getCentralLoginLifespan() { public int getSsoSessionIdleTimeout() {
return centralLoginLifespan; return ssoSessionIdleTimeout;
} }
public void setCentralLoginLifespan(int centralLoginLifespan) { public void setSsoSessionIdleTimeout(int ssoSessionIdleTimeout) {
this.centralLoginLifespan = centralLoginLifespan; this.ssoSessionIdleTimeout = ssoSessionIdleTimeout;
}
public int getSsoSessionMaxLifespan() {
return ssoSessionMaxLifespan;
}
public void setSsoSessionMaxLifespan(int ssoSessionMaxLifespan) {
this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
} }
public int getAccessTokenLifespan() { public int getAccessTokenLifespan() {
@ -228,14 +236,6 @@ public class RealmEntity extends AbstractIdentifiableEntity {
this.accessCodeLifespanUserAction = accessCodeLifespanUserAction; this.accessCodeLifespanUserAction = accessCodeLifespanUserAction;
} }
public int getRefreshTokenLifespan() {
return refreshTokenLifespan;
}
public void setRefreshTokenLifespan(int refreshTokenLifespan) {
this.refreshTokenLifespan = refreshTokenLifespan;
}
public int getNotBefore() { public int getNotBefore() {
return notBefore; return notBefore;
} }

View file

@ -246,25 +246,23 @@ public class RealmAdapter implements RealmModel {
} }
@Override @Override
public int getCentralLoginLifespan() { public int getSsoSessionIdleTimeout() {
return realm.getCentralLoginLifespan(); return realm.getSsoSessionIdleTimeout();
} }
@Override @Override
public void setCentralLoginLifespan(int lifespan) { public void setSsoSessionIdleTimeout(int seconds) {
realm.setCentralLoginLifespan(lifespan); realm.setSsoSessionIdleTimeout(seconds);
em.flush();
} }
@Override @Override
public int getRefreshTokenLifespan() { public int getSsoSessionMaxLifespan() {
return realm.getRefreshTokenLifespan(); return realm.getSsoSessionMaxLifespan();
} }
@Override @Override
public void setRefreshTokenLifespan(int tokenLifespan) { public void setSsoSessionMaxLifespan(int seconds) {
realm.setRefreshTokenLifespan(tokenLifespan); realm.setSsoSessionMaxLifespan(seconds);
em.flush();
} }
@Override @Override
@ -1391,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.getCentralLoginLifespan();
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);
@ -1431,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

@ -60,11 +60,11 @@ public class RealmEntity {
protected boolean updateProfileOnInitialSocialLogin; protected boolean updateProfileOnInitialSocialLogin;
protected String passwordPolicy; protected String passwordPolicy;
protected int centralLoginLifespan; private int ssoSessionIdleTimeout;
private int ssoSessionMaxLifespan;
protected int accessTokenLifespan; protected int accessTokenLifespan;
protected int accessCodeLifespan; protected int accessCodeLifespan;
protected int accessCodeLifespanUserAction; protected int accessCodeLifespanUserAction;
protected int refreshTokenLifespan;
protected int notBefore; protected int notBefore;
@Column(length = 2048) @Column(length = 2048)
@ -201,20 +201,20 @@ public class RealmEntity {
this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin; this.updateProfileOnInitialSocialLogin = updateProfileOnInitialSocialLogin;
} }
public int getCentralLoginLifespan() { public int getSsoSessionIdleTimeout() {
return centralLoginLifespan; return ssoSessionIdleTimeout;
} }
public void setCentralLoginLifespan(int centralLoginLifespan) { public void setSsoSessionIdleTimeout(int ssoSessionIdleTimeout) {
this.centralLoginLifespan = centralLoginLifespan; this.ssoSessionIdleTimeout = ssoSessionIdleTimeout;
} }
public int getRefreshTokenLifespan() { public int getSsoSessionMaxLifespan() {
return refreshTokenLifespan; return ssoSessionMaxLifespan;
} }
public void setRefreshTokenLifespan(int refreshTokenLifespan) { public void setSsoSessionMaxLifespan(int ssoSessionMaxLifespan) {
this.refreshTokenLifespan = refreshTokenLifespan; this.ssoSessionMaxLifespan = ssoSessionMaxLifespan;
} }
public int getAccessTokenLifespan() { public int getAccessTokenLifespan() {

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

@ -280,6 +280,26 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
} }
@Override
public int getSsoSessionIdleTimeout() {
return realm.getSsoSessionIdleTimeout();
}
@Override
public void setSsoSessionIdleTimeout(int seconds) {
realm.setSsoSessionIdleTimeout(seconds);
}
@Override
public int getSsoSessionMaxLifespan() {
return realm.getSsoSessionMaxLifespan();
}
@Override
public void setSsoSessionMaxLifespan(int seconds) {
realm.setSsoSessionMaxLifespan(seconds);
}
@Override @Override
public int getAccessTokenLifespan() { public int getAccessTokenLifespan() {
return realm.getAccessTokenLifespan(); return realm.getAccessTokenLifespan();
@ -291,28 +311,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
updateRealm(); updateRealm();
} }
@Override
public int getCentralLoginLifespan() {
return realm.getCentralLoginLifespan();
}
@Override
public void setCentralLoginLifespan(int lifespan) {
realm.setCentralLoginLifespan(lifespan);
updateRealm();
}
@Override
public int getRefreshTokenLifespan() {
return realm.getRefreshTokenLifespan();
}
@Override
public void setRefreshTokenLifespan(int tokenLifespan) {
realm.setRefreshTokenLifespan(tokenLifespan);
updateRealm();
}
@Override @Override
public int getAccessCodeLifespan() { public int getAccessCodeLifespan() {
@ -1357,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.getCentralLoginLifespan();
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);
@ -1399,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

@ -726,7 +726,7 @@ public class AdapterTest extends AbstractModelTest {
@Test @Test
public void userSessions() throws InterruptedException { public void userSessions() throws InterruptedException {
realmManager.createRealm("userSessions"); realmManager.createRealm("userSessions");
realmManager.getRealmByName("userSessions").setCentralLoginLifespan(5); realmManager.getRealmByName("userSessions").setSsoSessionIdleTimeout(5);
UserModel user = realmManager.getRealmByName("userSessions").addUser("userSessions1"); UserModel user = realmManager.getRealmByName("userSessions").addUser("userSessions1");
@ -749,7 +749,7 @@ public class AdapterTest extends AbstractModelTest {
assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId())); assertNull(realmManager.getRealmByName("userSessions").getUserSession(userSession.getId()));
realmManager.getRealmByName("userSessions").setCentralLoginLifespan(1); realmManager.getRealmByName("userSessions").setSsoSessionIdleTimeout(1);
userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1"); userSession = realmManager.getRealmByName("userSessions").createUserSession(user, "127.0.0.1");
commit(); commit();

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

@ -54,9 +54,9 @@ public class ApplianceBootstrap {
realm.setName(adminRealmName); realm.setName(adminRealmName);
realm.setEnabled(true); realm.setEnabled(true);
realm.addRequiredCredential(CredentialRepresentation.PASSWORD); realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm.setCentralLoginLifespan(3000); realm.setSsoSessionIdleTimeout(300);
realm.setAccessTokenLifespan(60); realm.setAccessTokenLifespan(60);
realm.setRefreshTokenLifespan(3600); realm.setSsoSessionMaxLifespan(36000);
realm.setAccessCodeLifespan(60); realm.setAccessCodeLifespan(60);
realm.setAccessCodeLifespanUserAction(300); realm.setAccessCodeLifespanUserAction(300);
realm.setSslNotRequired(true); realm.setSslNotRequired(true);

View file

@ -5,6 +5,7 @@ import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.RSATokenVerifier; import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException; import org.keycloak.VerificationException;
import org.keycloak.audit.Audit;
import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticationLinkModel; import org.keycloak.models.AuthenticationLinkModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
@ -61,6 +62,29 @@ public class AuthenticationManager {
this.protector = protector; this.protector = protector;
} }
public static boolean isSessionValid(RealmModel realm, UserSessionModel session) {
if (session == null) return false;
int currentTime = Time.currentTime();
int max = session.getStarted() + realm.getSsoSessionMaxLifespan();
boolean valid = session != null && session.getLastSessionRefresh() + realm.getSsoSessionIdleTimeout() > currentTime && max > currentTime;
return valid;
}
public static void logout(RealmModel realm, UserSessionModel session, UriInfo uriInfo) {
if (session == null) return;
UserModel user = session.getUser();
logger.infov("Logging out: {0} ({1})", user.getLoginName(), session.getId());
realm.removeUserSession(session);
expireIdentityCookie(realm, uriInfo);
expireRememberMeCookie(realm, uriInfo);
new ResourceAdminManager().logoutUser(uriInfo.getRequestUri(), realm, user.getId(), session.getId());
}
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();
@ -71,26 +95,25 @@ public class AuthenticationManager {
if (session != null) { if (session != null) {
token.setSessionState(session.getId()); token.setSessionState(session.getId());
} }
if (realm.getCentralLoginLifespan() > 0) { if (realm.getSsoSessionIdleTimeout() > 0) {
token.expiration(Time.currentTime() + realm.getCentralLoginLifespan()); token.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
} }
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 cookieName = KEYCLOAK_IDENTITY_COOKIE;
String cookiePath = getIdentityCookiePath(realm, uriInfo); String cookiePath = getIdentityCookiePath(realm, uriInfo);
AccessToken identityToken = createIdentityToken(realm, user, session); AccessToken identityToken = createIdentityToken(realm, user, session);
String encoded = encodeToken(realm, identityToken); String encoded = encodeToken(realm, identityToken);
boolean secureOnly = !realm.isSslNotRequired(); boolean secureOnly = !realm.isSslNotRequired();
logger.debugv("creatingLoginCookie - name: {0} path: {1}", cookieName, cookiePath); logger.debugv("creatingLoginCookie - name: {0} path: {1}", KEYCLOAK_IDENTITY_COOKIE, cookiePath);
int maxAge = NewCookie.DEFAULT_MAX_AGE; int maxAge = NewCookie.DEFAULT_MAX_AGE;
if (rememberMe) { if (rememberMe) {
maxAge = realm.getCentralLoginLifespan(); maxAge = realm.getSsoSessionIdleTimeout();
logger.info("createLoginCookie maxAge: " + maxAge); logger.info("createLoginCookie maxAge: " + maxAge);
} }
CookieHelper.addCookie(cookieName, encoded, cookiePath, null, null, maxAge, secureOnly, true); CookieHelper.addCookie(KEYCLOAK_IDENTITY_COOKIE, encoded, cookiePath, null, null, maxAge, secureOnly, true);
//builder.cookie(new NewCookie(cookieName, encoded, cookiePath, null, null, maxAge, secureOnly));// todo httponly , true); //builder.cookie(new NewCookie(cookieName, encoded, cookiePath, null, null, maxAge, secureOnly));// todo httponly , true);
String sessionCookieValue = realm.getName() + "-" + user.getId(); String sessionCookieValue = realm.getName() + "-" + user.getId();
@ -98,16 +121,16 @@ 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
//NewCookie cookie = new NewCookie(KEYCLOAK_REMEMBER_ME, "true", path, null, null, realm.getCentralLoginLifespan(), secureOnly);// todo httponly , true); //NewCookie cookie = new NewCookie(KEYCLOAK_REMEMBER_ME, "true", path, null, null, realm.getCentralLoginLifespan(), secureOnly);// todo httponly , true);
CookieHelper.addCookie(KEYCLOAK_REMEMBER_ME, "true", path, null, null, realm.getCentralLoginLifespan(), secureOnly, true); CookieHelper.addCookie(KEYCLOAK_REMEMBER_ME, "true", path, null, null, realm.getSsoSessionIdleTimeout(), secureOnly, true);
} }
protected String encodeToken(RealmModel realm, Object token) { protected String encodeToken(RealmModel realm, Object token) {
@ -117,25 +140,26 @@ public class AuthenticationManager {
return encodedToken; return encodedToken;
} }
public void expireIdentityCookie(RealmModel realm, UriInfo uriInfo) { public static void expireIdentityCookie(RealmModel realm, UriInfo uriInfo) {
logger.debug("Expiring identity cookie"); logger.debug("Expiring identity cookie");
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 static void expireRememberMeCookie(RealmModel realm, UriInfo uriInfo) {
logger.debug("Expiring remember me cookie"); logger.debug("Expiring remember me cookie");
String path = getIdentityCookiePath(realm, uriInfo); String path = getIdentityCookiePath(realm, uriInfo);
String cookieName = KEYCLOAK_REMEMBER_ME; String cookieName = KEYCLOAK_REMEMBER_ME;
expireCookie(realm, cookieName, path, true); expireCookie(realm, cookieName, path, true);
} }
protected String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) { protected static String getIdentityCookiePath(RealmModel realm, UriInfo uriInfo) {
URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()); URI uri = RealmsResource.realmBaseUrl(uriInfo).build(realm.getName());
return uri.getRawPath(); return uri.getRawPath();
} }
public void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly) { public static void expireCookie(RealmModel realm, String cookieName, String path, boolean httpOnly) {
logger.debugv("Expiring cookie: {0} path: {1}", cookieName, path); logger.debugv("Expiring cookie: {0} path: {1}", cookieName, path);
boolean secureOnly = !realm.isSslNotRequired(); boolean secureOnly = !realm.isSslNotRequired();
CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly); CookieHelper.addCookie(cookieName, "", path, null, "Expiring cookie", 0, secureOnly, httpOnly);
@ -147,15 +171,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;
} }
@ -163,7 +181,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;
} }
@ -175,7 +195,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());
@ -196,9 +215,9 @@ public class AuthenticationManager {
} }
UserSessionModel session = realm.getUserSession(token.getSessionState()); UserSessionModel session = realm.getUserSession(token.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { if (!isSessionValid(realm, session)) {
if (session != null) logout(realm, session, uriInfo);
logger.info("User session not active"); logger.info("User session not active");
expireIdentityCookie(realm, uriInfo);
return null; return null;
} }

View file

@ -90,8 +90,8 @@ public class ModelToRepresentation {
rep.setVerifyEmail(realm.isVerifyEmail()); rep.setVerifyEmail(realm.isVerifyEmail());
rep.setResetPasswordAllowed(realm.isResetPasswordAllowed()); rep.setResetPasswordAllowed(realm.isResetPasswordAllowed());
rep.setAccessTokenLifespan(realm.getAccessTokenLifespan()); rep.setAccessTokenLifespan(realm.getAccessTokenLifespan());
rep.setCentralLoginLifespan(realm.getCentralLoginLifespan()); rep.setSsoSessionIdleTimeout(realm.getSsoSessionIdleTimeout());
rep.setRefreshTokenLifespan(realm.getRefreshTokenLifespan()); rep.setSsoSessionMaxLifespan(realm.getSsoSessionMaxLifespan());
rep.setAccessCodeLifespan(realm.getAccessCodeLifespan()); rep.setAccessCodeLifespan(realm.getAccessCodeLifespan());
rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction()); rep.setAccessCodeLifespanUserAction(realm.getAccessCodeLifespanUserAction());
rep.setSmtpServer(realm.getSmtpConfig()); rep.setSmtpServer(realm.getSmtpConfig());

View file

@ -186,8 +186,8 @@ public class RealmManager {
realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction()); realm.setAccessCodeLifespanUserAction(rep.getAccessCodeLifespanUserAction());
if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore()); if (rep.getNotBefore() != null) realm.setNotBefore(rep.getNotBefore());
if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); if (rep.getAccessTokenLifespan() != null) realm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
if (rep.getRefreshTokenLifespan() != null) realm.setRefreshTokenLifespan(rep.getRefreshTokenLifespan()); if (rep.getSsoSessionIdleTimeout() != null) realm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
if (rep.getCentralLoginLifespan() != null) realm.setCentralLoginLifespan(rep.getCentralLoginLifespan()); if (rep.getSsoSessionMaxLifespan() != null) realm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
if (rep.getRequiredCredentials() != null) { if (rep.getRequiredCredentials() != null) {
realm.updateRequiredCredentials(rep.getRequiredCredentials()); realm.updateRequiredCredentials(rep.getRequiredCredentials());
} }
@ -313,10 +313,10 @@ public class RealmManager {
if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan()); if (rep.getAccessTokenLifespan() != null) newRealm.setAccessTokenLifespan(rep.getAccessTokenLifespan());
else newRealm.setAccessTokenLifespan(300); else newRealm.setAccessTokenLifespan(300);
if (rep.getRefreshTokenLifespan() != null) newRealm.setRefreshTokenLifespan(rep.getRefreshTokenLifespan()); if (rep.getSsoSessionIdleTimeout() != null) newRealm.setSsoSessionIdleTimeout(rep.getSsoSessionIdleTimeout());
else newRealm.setRefreshTokenLifespan(36000); else newRealm.setSsoSessionIdleTimeout(600);
if (rep.getCentralLoginLifespan() != null) newRealm.setCentralLoginLifespan(rep.getCentralLoginLifespan()); if (rep.getSsoSessionMaxLifespan() != null) newRealm.setSsoSessionMaxLifespan(rep.getSsoSessionMaxLifespan());
else newRealm.setCentralLoginLifespan(300); else newRealm.setSsoSessionMaxLifespan(36000);
if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan()); if (rep.getAccessCodeLifespan() != null) newRealm.setAccessCodeLifespan(rep.getAccessCodeLifespan());
else newRealm.setAccessCodeLifespan(60); else newRealm.setAccessCodeLifespan(60);

View file

@ -23,6 +23,7 @@ import org.keycloak.representations.RefreshToken;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.UriInfo;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.HashSet; import java.util.HashSet;
@ -106,7 +107,7 @@ public class TokenManager {
return code; return code;
} }
public AccessToken refreshAccessToken(RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException { public AccessToken refreshAccessToken(UriInfo uriInfo, RealmModel realm, ClientModel client, String encodedRefreshToken, Audit audit) throws OAuthErrorException {
JWSInput jws = new JWSInput(encodedRefreshToken); JWSInput jws = new JWSInput(encodedRefreshToken);
RefreshToken refreshToken = null; RefreshToken refreshToken = null;
try { try {
@ -137,7 +138,9 @@ 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)) {
AuthenticationManager.logout(realm, session, uriInfo);
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 +194,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;
} }
@ -375,7 +384,7 @@ public class TokenManager {
refreshToken = new RefreshToken(accessToken); refreshToken = new RefreshToken(accessToken);
refreshToken.id(KeycloakModelUtils.generateId()); refreshToken.id(KeycloakModelUtils.generateId());
refreshToken.issuedNow(); refreshToken.issuedNow();
refreshToken.expiration(Time.currentTime() + realm.getRefreshTokenLifespan()); refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
return this; return this;
} }

View file

@ -408,7 +408,8 @@ 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)) {
AuthenticationManager.logout(realm, session, uriInfo);
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;
@ -297,13 +296,14 @@ public class TokenService {
String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN); String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
AccessToken accessToken = null; AccessToken accessToken = null;
try { try {
accessToken = tokenManager.refreshAccessToken(realm, client, refreshToken, audit); accessToken = tokenManager.refreshAccessToken(uriInfo, realm, client, refreshToken, audit);
} catch (OAuthErrorException e) { } catch (OAuthErrorException e) {
Map<String, String> error = new HashMap<String, String>(); Map<String, String> error = new HashMap<String, String>();
error.put(OAuth2Constants.ERROR, e.getError()); error.put(OAuth2Constants.ERROR, e.getError());
if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription()); if (e.getDescription() != null) error.put(OAuth2Constants.ERROR_DESCRIPTION, e.getDescription());
audit.error(Errors.INVALID_TOKEN); audit.error(Errors.INVALID_TOKEN);
throw new BadRequestException("OAuth Error", e, Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build()); logger.error("OAuth Error", e);
return Response.status(Response.Status.BAD_REQUEST).entity(error).type("application/json").build();
} }
AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit) AccessTokenResponse res = tokenManager.responseBuilder(realm, client, audit)
@ -374,7 +374,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 +636,8 @@ 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)) {
AuthenticationManager.logout(realm, session, uriInfo);
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");
@ -855,15 +856,7 @@ public class TokenService {
private void logout(UserSessionModel session) { private void logout(UserSessionModel session) {
UserModel user = session.getUser(); UserModel user = session.getUser();
authManager.logout(realm, session, uriInfo);
logger.infov("Logging out: {0} ({1})", user.getLoginName(), session.getId());
realm.removeUserSession(session);
authManager.expireIdentityCookie(realm, uriInfo);
authManager.expireRememberMeCookie(realm, uriInfo);
resourceAdminManager.logoutUser(uriInfo.getRequestUri(), realm, user.getId(), session.getId());
audit.user(user).session(session).success(); audit.user(user).session(session).success();
} }
@ -915,7 +908,8 @@ public class TokenService {
} }
UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState()); UserSessionModel session = realm.getUserSession(accessCodeEntry.getSessionState());
if (session == null || session.getExpires() < Time.currentTime()) { if (!AuthenticationManager.isSessionValid(realm, session)) {
AuthenticationManager.logout(realm, session, uriInfo);
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();
} }
} }

View file

@ -28,6 +28,7 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.models.ApplicationModel; import org.keycloak.models.ApplicationModel;
import org.keycloak.models.Constants; import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
@ -156,4 +157,67 @@ public class AdapterTest {
} }
@Test
public void testLoginSSOIdle() throws Exception {
// test login to customer-portal which does a bearer request to customer-db
driver.navigate().to("http://localhost:8081/customer-portal");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
loginPage.login("bburke@redhat.com", "password");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
String pageSource = driver.getPageSource();
System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.getRealmByName("demo");
int originalIdle = realm.getSsoSessionIdleTimeout();
realm.setSsoSessionIdleTimeout(1);
keycloakRule.stopSession(session, true);
Thread.sleep(2000);
// test SSO
driver.navigate().to("http://localhost:8081/product-portal");
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
session = keycloakRule.startSession();
realm = session.getRealmByName("demo");
realm.setSsoSessionIdleTimeout(originalIdle);
keycloakRule.stopSession(session, true);
}
@Test
public void testLoginSSOMax() throws Exception {
// test login to customer-portal which does a bearer request to customer-db
driver.navigate().to("http://localhost:8081/customer-portal");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
loginPage.login("bburke@redhat.com", "password");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8081/customer-portal");
String pageSource = driver.getPageSource();
System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("Bill Burke") && pageSource.contains("Stian Thorgersen"));
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.getRealmByName("demo");
int original = realm.getSsoSessionMaxLifespan();
realm.setSsoSessionMaxLifespan(1);
keycloakRule.stopSession(session, true);
Thread.sleep(2000);
// test SSO
driver.navigate().to("http://localhost:8081/product-portal");
Assert.assertTrue(driver.getCurrentUrl().startsWith(LOGIN_URL));
session = keycloakRule.startSession();
realm = session.getRealmByName("demo");
realm.setSsoSessionMaxLifespan(original);
keycloakRule.stopSession(session, true);
}
} }

View file

@ -60,9 +60,9 @@ public class CompositeRoleTest {
RealmModel realm = manager.createRealm("Test"); RealmModel realm = manager.createRealm("Test");
manager.generateRealmKeys(realm); manager.generateRealmKeys(realm);
realmPublicKey = realm.getPublicKey(); realmPublicKey = realm.getPublicKey();
realm.setCentralLoginLifespan(3000); realm.setSsoSessionIdleTimeout(3000);
realm.setAccessTokenLifespan(10000); realm.setAccessTokenLifespan(10000);
realm.setRefreshTokenLifespan(10000); realm.setSsoSessionMaxLifespan(10000);
realm.setAccessCodeLifespanUserAction(1000); realm.setAccessCodeLifespanUserAction(1000);
realm.setAccessCodeLifespan(1000); realm.setAccessCodeLifespan(1000);
realm.setSslNotRequired(true); realm.setSslNotRequired(true);

View file

@ -29,8 +29,14 @@ import org.keycloak.OAuth2Constants;
import org.keycloak.audit.Details; import org.keycloak.audit.Details;
import org.keycloak.audit.Errors; import org.keycloak.audit.Errors;
import org.keycloak.audit.Event; import org.keycloak.audit.Event;
import org.keycloak.models.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.ProviderSession;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken; import org.keycloak.representations.RefreshToken;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
@ -41,9 +47,7 @@ import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.Time; import org.keycloak.util.Time;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
@ -93,7 +97,8 @@ public class RefreshTokenTest {
Assert.assertEquals("bearer", tokenResponse.getTokenType()); Assert.assertEquals("bearer", tokenResponse.getTokenType());
Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300))); Assert.assertThat(token.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
Assert.assertThat(refreshToken.getExpiration() - Time.currentTime(), allOf(greaterThanOrEqualTo(35950), lessThanOrEqualTo(36000))); int actual = refreshToken.getExpiration() - Time.currentTime();
Assert.assertThat(actual, allOf(greaterThanOrEqualTo(559), lessThanOrEqualTo(600)));
Assert.assertEquals(sessionId, refreshToken.getSessionState()); Assert.assertEquals(sessionId, refreshToken.getSessionState());
@ -161,4 +166,131 @@ public class RefreshTokenTest {
events.clear(); events.clear();
} }
@Test
public void testUserSessionRefreshAndIdle() throws Exception {
oauth.doLogin("test-user@localhost", "password");
Event loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
events.poll();
String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.getRealmByName("test");
UserSessionModel userSession = realm.getUserSession(sessionId);
int last = userSession.getLastSessionRefresh();
keycloakRule.stopSession(session, false);
Thread.sleep(2000);
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
AccessToken refreshedToken = oauth.verifyToken(tokenResponse.getAccessToken());
RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(tokenResponse.getRefreshToken());
Assert.assertEquals(200, tokenResponse.getStatusCode());
session = keycloakRule.startSession();
realm = session.getRealmByName("test");
userSession = realm.getUserSession(sessionId);
int next = userSession.getLastSessionRefresh();
keycloakRule.stopSession(session, false);
// should not update last refresh because the access token interval is way less than idle timeout
Assert.assertEquals(last, next);
session = keycloakRule.startSession();
realm = session.getRealmByName("test");
int lastAccessTokenLifespan = realm.getAccessTokenLifespan();
realm.setAccessTokenLifespan(100000);
keycloakRule.stopSession(session, true);
Thread.sleep(2000);
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
session = keycloakRule.startSession();
realm = session.getRealmByName("test");
userSession = realm.getUserSession(sessionId);
next = userSession.getLastSessionRefresh();
keycloakRule.stopSession(session, false);
// lastSEssionRefresh should be updated because access code lifespan is higher than sso idle timeout
Assert.assertThat(next, allOf(greaterThan(last), lessThan(last + 6)));
session = keycloakRule.startSession();
realm = session.getRealmByName("test");
int originalIdle = realm.getSsoSessionIdleTimeout();
realm.setSsoSessionIdleTimeout(1);
keycloakRule.stopSession(session, true);
events.clear();
Thread.sleep(2000);
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
// test idle timeout
assertEquals(400, tokenResponse.getStatusCode());
assertNull(tokenResponse.getAccessToken());
assertNull(tokenResponse.getRefreshToken());
events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
session = keycloakRule.startSession();
realm = session.getRealmByName("test");
realm.setSsoSessionIdleTimeout(originalIdle);
realm.setAccessTokenLifespan(lastAccessTokenLifespan);
keycloakRule.stopSession(session, true);
events.clear();
}
@Test
public void refreshTokenUserSessionMaxLifespan() throws Exception {
oauth.doLogin("test-user@localhost", "password");
Event loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
events.poll();
String refreshId = oauth.verifyRefreshToken(tokenResponse.getRefreshToken()).getId();
KeycloakSession session = keycloakRule.startSession();
RealmModel realm = session.getRealmByName("test");
int maxLifespan = realm.getSsoSessionMaxLifespan();
realm.setSsoSessionMaxLifespan(1);
keycloakRule.stopSession(session, true);
Thread.sleep(1000);
tokenResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
assertEquals(400, tokenResponse.getStatusCode());
assertNull(tokenResponse.getAccessToken());
assertNull(tokenResponse.getRefreshToken());
session = keycloakRule.startSession();
realm = session.getRealmByName("test");
realm.setSsoSessionMaxLifespan(maxLifespan);
keycloakRule.stopSession(session, true);
events.expectRefresh(refreshId, sessionId).error(Errors.INVALID_TOKEN);
events.clear();
}
} }

View file

@ -133,4 +133,17 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
byte[] bytes = os.toByteArray(); byte[] bytes = os.toByteArray();
return JsonSerialization.readValue(bytes, RealmRepresentation.class); return JsonSerialization.readValue(bytes, RealmRepresentation.class);
} }
public KeycloakSession startSession() {
KeycloakSession session = server.getKeycloakSessionFactory().createSession();
session.getTransaction().begin();
return session;
}
public void stopSession(KeycloakSession session, boolean commit) {
if (commit) {
session.getTransaction().commit();
}
session.close();
}
} }

View file

@ -81,19 +81,6 @@ public class KeycloakRule extends AbstractKeycloakRule {
} }
} }
public KeycloakSession startSession() {
KeycloakSession session = server.getKeycloakSessionFactory().createSession();
session.getTransaction().begin();
return session;
}
public void stopSession(KeycloakSession session, boolean commit) {
if (commit) {
session.getTransaction().commit();
}
session.close();
}
public void removeUserSession(String sessionId) { public void removeUserSession(String sessionId) {
KeycloakSession keycloakSession = startSession(); KeycloakSession keycloakSession = startSession();
RealmModel realm = keycloakSession.getRealm("test"); RealmModel realm = keycloakSession.getRealm("test");