Skip to content

Commit 1669d98

Browse files
duttarnabyuriyz
andauthored
feat(jans-config-api): updating Admin UI Session Expiry to Enforce Idle-Based Logout (#13172)
* feat: updating Admin UI Session Expiry to Enforce Idle-Based Logout Signed-off-by: duttarnab <arnab.bdutta@gmail.com> * feat: addressing code-rabbit comments Signed-off-by: duttarnab <arnab.bdutta@gmail.com> * feat: addressing code-rabbit comments Signed-off-by: duttarnab <arnab.bdutta@gmail.com> --------- Signed-off-by: duttarnab <arnab.bdutta@gmail.com> Co-authored-by: YuriyZ <yzabrovarniy@gmail.com>
1 parent f2be9fc commit 1669d98

File tree

4 files changed

+108
-21
lines changed

4 files changed

+108
-21
lines changed

jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/filters/AdminUICookieFilter.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ public class AdminUICookieFilter implements ContainerRequestFilter {
7777
@Override
7878
public void filter(ContainerRequestContext requestContext) {
7979
try {
80-
log.debug("========================================================================");
80+
log.trace("========================================================================");
8181
log.debug("Inside AdminUICookieFilter filter...");
82-
log.debug("========================================================================");
82+
log.trace("========================================================================");
8383
Map<String, Cookie> cookies = requestContext.getCookies();
8484
initializeCaches();
8585
removeExpiredSessionsIfNeeded();
@@ -235,13 +235,14 @@ private Optional<String> fetchUJWTFromAdminUISession(Map<String, Cookie> cookies
235235
log.debug("Found a Admin UI session cookie in request header.");
236236
Cookie adminUISessionCookie = cookies.get(ADMIN_UI_SESSION_ID);
237237
String sessionId = adminUISessionCookie.getValue();
238-
AdminUISession configApiSession = configApiSessionService.getSession(sessionId);
238+
AdminUISession adminUISession = configApiSessionService.getSession(sessionId);
239239
//if config api session does not exist
240-
if (configApiSession == null) {
240+
if (adminUISession == null) {
241241
return Optional.empty();
242242
}
243243
log.debug("Admin UI session exist in persistence.");
244-
String ujwtString = configApiSession.getUjwt();
244+
configApiSessionService.updateSessionExpiryDate(adminUISession);
245+
String ujwtString = adminUISession.getUjwt();
245246
return Optional.ofNullable(ujwtString);
246247
}
247248

jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/adminui/AdminUISessionService.java

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.jans.as.model.config.adminui.AdminConf;
1111
import io.jans.as.model.jwt.Jwt;
1212
import io.jans.as.model.jwt.JwtClaims;
13+
import io.jans.ca.plugin.adminui.service.config.AUIConfigurationService;
1314
import io.jans.ca.plugin.adminui.utils.CommonUtils;
1415
import io.jans.configapi.core.model.adminui.AUIConfiguration;
1516
import io.jans.configapi.core.model.adminui.AdminUISession;
@@ -29,7 +30,12 @@
2930
import org.json.JSONArray;
3031
import org.json.JSONObject;
3132
import org.slf4j.Logger;
33+
3234
import java.util.*;
35+
import java.util.concurrent.TimeUnit;
36+
37+
import static io.jans.ca.plugin.adminui.utils.CommonUtils.addMinutes;
38+
3339
import static io.jans.as.model.util.Util.escapeLog;
3440

3541
@ApplicationScoped
@@ -54,6 +60,9 @@ public class AdminUISessionService {
5460
@Inject
5561
ConfigHttpService httpService;
5662

63+
@Inject
64+
AUIConfigurationService auiConfigurationService;
65+
5766
/**
5867
* Builds the LDAP distinguished name (DN) for a session identifier.
5968
*
@@ -89,14 +98,79 @@ public AdminUISession getSession(String sessionId) {
8998
return configApiSession;
9099
}
91100

101+
/**
102+
* Updates the expiration time of an Admin UI session based on user activity.
103+
* <p>
104+
* After a successful login, an {@link AdminUISession} is persisted in the database
105+
* with an expiration date derived from the configured session timeout. On each
106+
* subsequent request, this method may extend the session expiration to enforce
107+
* an idle-based logout policy.
108+
* </p>
109+
*
110+
* <p>
111+
* The session expiration is refreshed only when:
112+
* <ul>
113+
* <li>The session and its expiration date are not {@code null}</li>
114+
* <li>More than 30 seconds has passed since the session was last updated</li>
115+
* </ul>
116+
* </p>
117+
*
118+
* <p>
119+
* This approach prevents frequent database updates while ensuring that an
120+
* active user session remains valid and a force logout occurs only when the
121+
* application has been idle longer than the configured
122+
* {@code max_idle_time}.
123+
* </p>
124+
*
125+
* @param adminUISession the persisted Admin UI session to be evaluated and updated
126+
*/
127+
public void updateSessionExpiryDate(AdminUISession adminUISession) {
128+
if (adminUISession == null || adminUISession.getExpirationDate() == null) {
129+
return;
130+
}
131+
try {
132+
Date lastUpdated = adminUISession.getLastUpdated();
133+
long nowMillis = System.currentTimeMillis();
134+
135+
// Update expiry date only if last update was more than 30 sec ago : the intent of the 30-second throttle is to reduce database writes
136+
if (lastUpdated != null) {
137+
long secondsSinceLastUpdate =
138+
TimeUnit.MILLISECONDS.toSeconds(nowMillis - lastUpdated.getTime());
139+
140+
if (secondsSinceLastUpdate < 30) {
141+
return;
142+
}
143+
}
144+
145+
AUIConfiguration config = auiConfigurationService.getAUIConfiguration();
146+
if (config == null || config.getSessionTimeoutInMins() == null) {
147+
logger.warn("AUI configuration is null, cannot update session expiry");
148+
return;
149+
}
150+
int sessionTimeoutMins = config.getSessionTimeoutInMins();
151+
152+
Date now = new Date(nowMillis);
153+
// do not update if the sesiion is already expired. AdminUICookieFilter will remove this session.
154+
if (adminUISession.getExpirationDate().before(now)) {
155+
return;
156+
}
157+
adminUISession.setExpirationDate(addMinutes(now, sessionTimeoutMins));
158+
adminUISession.setLastUpdated(now);
159+
persistenceEntryManager.merge(adminUISession);
160+
} catch (Exception e) {
161+
logger.warn("Failed to update session expiry for session {}",
162+
adminUISession.getSessionId(), e);
163+
}
164+
}
165+
92166
/**
93167
* Removes all AdminUISession entries whose expirationDate is earlier than the current time.
94168
* This method queries sessions under the service's session base DN and deletes any persisted
95169
* AdminUISession whose expiration date has already passed.
96170
*/
97171
public void removeAllExpiredSessions() {
98172
final Filter filter = Filter.createPresenceFilter(SID);
99-
List<AdminUISession> adminUISessions = persistenceEntryManager.findEntries(SESSION_DN, AdminUISession.class, filter);
173+
List<AdminUISession> adminUISessions = persistenceEntryManager.findEntries(SESSION_DN, AdminUISession.class, filter);
100174
Date currentDate = new Date();
101175
adminUISessions.stream().filter(ele ->
102176
((ele.getExpirationDate().getTime() - currentDate.getTime()) < 0))
@@ -106,7 +180,7 @@ public void removeAllExpiredSessions() {
106180
/**
107181
* Checks whether a cached token is active by calling the Admin UI introspection endpoint.
108182
*
109-
* @param token the token to introspect; may be null or empty
183+
* @param token the token to introspect; may be null or empty
110184
* @param auiConfiguration configuration holding the introspection endpoint URL
111185
* @return `true` if the introspection response contains `"active": true`, `false` otherwise
112186
* @throws JsonProcessingException if the introspection response body cannot be parsed as JSON
@@ -124,7 +198,7 @@ public boolean isCachedTokenValid(String token, AUIConfiguration auiConfiguratio
124198
.executePost(auiConfiguration.getAuiBackendApiServerIntrospectionEndpoint(),
125199
token, CommonUtils.toUrlEncodedString(body),
126200
ContentType.APPLICATION_FORM_URLENCODED,
127-
"Bearer " );
201+
"Bearer ");
128202
String jsonString = null;
129203
if (httpServiceResponse.getHttpResponse() != null
130204
&& httpServiceResponse.getHttpResponse().getStatusLine() != null) {
@@ -133,15 +207,15 @@ public boolean isCachedTokenValid(String token, AUIConfiguration auiConfiguratio
133207
"httpServiceResponse.getHttpResponse():{}, httpServiceResponse.getHttpResponse().getStatusLine():{}, httpServiceResponse.getHttpResponse().getEntity():{}",
134208
httpServiceResponse.getHttpResponse(), httpServiceResponse.getHttpResponse().getStatusLine(),
135209
httpServiceResponse.getHttpResponse().getEntity());
136-
if(httpServiceResponse.getHttpResponse().getStatusLine().getStatusCode() == 200) {
210+
if (httpServiceResponse.getHttpResponse().getStatusLine().getStatusCode() == 200) {
137211
ObjectMapper mapper = new ObjectMapper();
138212

139213
HttpEntity httpEntity = httpServiceResponse.getHttpResponse().getEntity();
140214
if (httpEntity != null) {
141215
jsonString = httpService.getContent(httpEntity);
142216

143217
HashMap<String, Object> payloadMap = mapper.readValue(jsonString, HashMap.class);
144-
if(payloadMap.containsKey("active")) {
218+
if (payloadMap.containsKey("active")) {
145219
return (boolean) payloadMap.get("active");
146220
}
147221
return false;
@@ -157,7 +231,7 @@ public boolean isCachedTokenValid(String token, AUIConfiguration auiConfiguratio
157231
*
158232
* @param ujwtString the user-info JWT to include in the token request; must be non-null and non-empty to generate a token
159233
* @param auiConfiguration configuration containing the backend token endpoint, client ID, encrypted client secret, and redirect URI
160-
* @return a TokenResponse containing the access token, or `null` if `ujwtString` is null or empty
234+
* @return a TokenResponse containing the access token, or `null` if `ujwtString` is null or empty
161235
* @throws StringEncrypter.EncryptionException if decrypting the client secret fails
162236
* @throws JsonProcessingException if parsing token responses fails
163237
*/
@@ -187,14 +261,14 @@ public TokenResponse getApiProtectionToken(String ujwtString, AUIConfiguration a
187261
}
188262

189263
/**
190-
* Exchange token request parameters with the authorization server and return parsed token response parameters.
191-
*
192-
* @param tokenRequest token request details (grant type, client credentials, redirect URI; may include authorization code and PKCE verifier)
193-
* @param tokenEndpoint the token endpoint URL to call
194-
* @param userInfoJwt optional user-info JWT to include as the `ujwt` parameter
195-
* @return a map of token response parameters (for example `access_token`, `expires_in`) with any `token_type` entry removed
196-
* @throws ConfigApiApplicationException if the HTTP exchange fails or the response cannot be parsed as JSON
197-
*/
264+
* Exchange token request parameters with the authorization server and return parsed token response parameters.
265+
*
266+
* @param tokenRequest token request details (grant type, client credentials, redirect URI; may include authorization code and PKCE verifier)
267+
* @param tokenEndpoint the token endpoint URL to call
268+
* @param userInfoJwt optional user-info JWT to include as the `ujwt` parameter
269+
* @return a map of token response parameters (for example `access_token`, `expires_in`) with any `token_type` entry removed
270+
* @throws ConfigApiApplicationException if the HTTP exchange fails or the response cannot be parsed as JSON
271+
*/
198272
public Map<String, Object> getToken(TokenRequest tokenRequest, String tokenEndpoint, String userInfoJwt) throws ConfigApiApplicationException {
199273

200274
try {
@@ -221,7 +295,7 @@ public Map<String, Object> getToken(TokenRequest tokenRequest, String tokenEndpo
221295

222296
HttpServiceResponse httpServiceResponse = httpService
223297
.executePost(tokenEndpoint, tokenRequest.getEncodedCredentials(), CommonUtils.toUrlEncodedString(body), ContentType.APPLICATION_FORM_URLENCODED,
224-
"Basic " );
298+
"Basic ");
225299
String jsonString = null;
226300
if (httpServiceResponse.getHttpResponse() != null
227301
&& httpServiceResponse.getHttpResponse().getStatusLine() != null) {
@@ -230,7 +304,7 @@ public Map<String, Object> getToken(TokenRequest tokenRequest, String tokenEndpo
230304
" FINAL httpServiceResponse.getHttpResponse():{}, httpServiceResponse.getHttpResponse().getStatusLine():{}, httpServiceResponse.getHttpResponse().getEntity():{}",
231305
httpServiceResponse.getHttpResponse(), httpServiceResponse.getHttpResponse().getStatusLine(),
232306
httpServiceResponse.getHttpResponse().getEntity());
233-
if(httpServiceResponse.getHttpResponse().getStatusLine().getStatusCode() == 200) {
307+
if (httpServiceResponse.getHttpResponse().getStatusLine().getStatusCode() == 200) {
234308
ObjectMapper mapper = new ObjectMapper();
235309

236310
HttpEntity httpEntity = httpServiceResponse.getHttpResponse().getEntity();

jans-config-api/plugins/admin-ui-plugin/src/main/java/io/jans/ca/plugin/adminui/service/auth/OAuth2Service.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ public void setAdminUISession(String sessionId, String ujwt) throws ApplicationE
177177
adminUISession.setUjwt(ujwt);
178178
adminUISession.setJansUsrDN(getDnForUser((String) claims.get("inum")));
179179
adminUISession.setCreationDate(currentDate);
180+
adminUISession.setLastUpdated(currentDate);
180181
adminUISession.setExpirationDate(addMinutes(currentDate, auiConfiguration.getSessionTimeoutInMins()));
181182

182183
entryManager.persist(adminUISession);

jans-config-api/shared/src/main/java/io/jans/configapi/core/model/adminui/AdminUISession.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public class AdminUISession {
2626
private Date creationDate = new Date();
2727
@AttributeName(name = "exp")
2828
private Date expirationDate;
29+
@AttributeName(name = "jansLastAccessTime")
30+
private Date lastUpdated;
2931

3032
/**
3133
* Gets the distinguished name (DN) for this entry.
@@ -155,6 +157,14 @@ public void setJansUsrDN(String jansUsrDN) {
155157
this.jansUsrDN = jansUsrDN;
156158
}
157159

160+
public Date getLastUpdated() {
161+
return lastUpdated;
162+
}
163+
164+
public void setLastUpdated(Date lastUpdated) {
165+
this.lastUpdated = lastUpdated;
166+
}
167+
158168
@Override
159169
public boolean equals(Object o) {
160170
if (!(o instanceof AdminUISession)) return false;
@@ -177,6 +187,7 @@ public String toString() {
177187
", ujwt='" + ujwt + '\'' +
178188
", creationDate=" + creationDate +
179189
", expirationDate=" + expirationDate +
190+
", lastUpdated=" + lastUpdated +
180191
'}';
181192
}
182193
}

0 commit comments

Comments
 (0)