Skip to content

Commit 62e5f4b

Browse files
authored
Merge pull request #5133 from gchq/gh-5132-empty-session-list
PR for #5132 - /stroom/sessionList is empty on 7.10.2
2 parents 1e133be + 0e5e503 commit 62e5f4b

File tree

19 files changed

+235
-75
lines changed

19 files changed

+235
-75
lines changed

stroom-dropwizard-common/src/main/java/stroom/dropwizard/common/AuthenticationBypassCheckerImpl.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ void registerUnauthenticatedApiPath(final String path) {
3333
}
3434

3535
@Override
36-
public boolean isUnauthenticated(final String servletName, final String servletPath, final String fullPath) {
36+
public boolean isUnauthenticated(final String servletName,
37+
final String servletPath,
38+
final String fullPath) {
3739
if (servletPath == null) {
3840
return false;
3941
} else {
@@ -49,8 +51,8 @@ public boolean isUnauthenticated(final String servletName, final String servletP
4951
canBypassAuth = unauthenticatedServletNames.contains(servletName);
5052
}
5153

52-
LOGGER.debug("servletName: {}, servletPath: {}, canBypassAuth: {}",
53-
servletName, servletPath, canBypassAuth);
54+
LOGGER.debug("isUnauthenticated() - servletName: {}, servletPath: {}, fullPath: {}, canBypassAuth: {}",
55+
servletName, servletPath, fullPath, canBypassAuth);
5456
return canBypassAuth;
5557
}
5658
}

stroom-security/stroom-security-common-impl/src/main/java/stroom/security/common/impl/HasJwtClaims.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package stroom.security.common.impl;
22

3+
import stroom.util.authentication.HasExpiry;
34
import stroom.util.exception.ThrowingFunction;
45
import stroom.util.shared.NullSafe;
56

67
import org.jose4j.jwt.JwtClaims;
78

9+
import java.time.Instant;
810
import java.util.Optional;
911

10-
public interface HasJwtClaims {
12+
public interface HasJwtClaims extends HasExpiry {
1113

1214
JwtClaims getJwtClaims();
1315

@@ -24,4 +26,12 @@ default <T> Optional<T> getClaimValue(final String claim, final Class<T> clazz)
2426
ThrowingFunction.unchecked(jwtClaims ->
2527
jwtClaims.getClaimValue(claim, clazz)));
2628
}
29+
30+
default Instant getExpireTime() {
31+
return NullSafe.getOrElse(
32+
getJwtClaims(),
33+
ThrowingFunction.unchecked(JwtClaims::getExpirationTime),
34+
numericDate -> Instant.ofEpochMilli(numericDate.getValueInMillis()),
35+
Instant.MAX);
36+
}
2737
}

stroom-security/stroom-security-common-impl/src/main/java/stroom/security/common/impl/JwtContextFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,5 @@ public interface JwtContextFactory {
2929
* like audience and subject only if doVerification is true.
3030
*/
3131
Optional<JwtContext> getJwtContext(final String jwt, final boolean doVerification);
32+
3233
}

stroom-security/stroom-security-common-impl/src/main/java/stroom/security/common/impl/UpdatableToken.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ public String getJwt() {
9292
/**
9393
* @return The time the token will expire (with a small buffer before the actual expiry included)
9494
*/
95+
@Override
9596
public Instant getExpireTime() {
96-
return Instant.ofEpochMilli(mutableState.expireTimeWithBufferEpochMs);
97+
return Instant.ofEpochMilli(mutableState.expireTimeEpochMs);
9798
}
9899

99100
@Override
@@ -199,7 +200,8 @@ public String toString() {
199200
", preferredUsername=" + NullSafe.get(mutableState.jwtClaims, claims ->
200201
JwtUtil.getClaimValue(claims, OpenId.CLAIM__PREFERRED_USERNAME).orElse(null)) +
201202
", expireTimeWithBuffer=" + Instant.ofEpochMilli(mutableState.expireTimeWithBufferEpochMs) +
202-
", timeTilExpire=" + Duration.between(Instant.now(), getExpireTime()) +
203+
", timeTilExpire=" + Duration.between(Instant.now(), Instant.ofEpochMilli(
204+
mutableState.expireTimeWithBufferEpochMs())) +
203205
", session=" + SessionUtil.getSessionId(session) +
204206
'}';
205207
}

stroom-security/stroom-security-common-impl/src/main/java/stroom/security/common/impl/UserIdentitySessionUtil.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import jakarta.servlet.http.HttpSession;
1111

12+
import java.util.Objects;
1213
import java.util.Optional;
1314

1415
public final class UserIdentitySessionUtil {
@@ -23,15 +24,16 @@ private UserIdentitySessionUtil() {
2324
/**
2425
* Set the {@link UserIdentity} on the session
2526
*/
26-
public static void set(final HttpSession session, final UserIdentity userIdentity) {
27+
public static void setUserInSession(final HttpSession session, final UserIdentity userIdentity) {
28+
Objects.requireNonNull(session);
2729
LOGGER.debug(() -> LogUtil.message("Setting userIdentity of type {} in session {}, userIdentity: {}",
2830
LogUtil.getSimpleClassName(userIdentity),
2931
NullSafe.get(session, HttpSession::getId),
3032
userIdentity));
3133
session.setAttribute(SESSION_USER_IDENTITY, userIdentity);
3234
}
3335

34-
public static Optional<UserIdentity> get(final HttpSession session) {
36+
public static Optional<UserIdentity> getUserFromSession(final HttpSession session) {
3537
final Optional<UserIdentity> optUserIdentity = Optional.ofNullable(session)
3638
.map(session2 ->
3739
(UserIdentity) session2.getAttribute(SESSION_USER_IDENTITY));

stroom-security/stroom-security-common-impl/src/test/java/stroom/security/common/impl/TestRefreshManager.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ public boolean refresh(final Consumer<Refreshable> onRefreshAction) {
148148
return true;
149149
}
150150

151+
@Override
152+
public Instant getExpireTime() {
153+
return expiry;
154+
}
155+
151156
@Override
152157
public long getExpireTimeEpochMs() {
153158
return expiry.toEpochMilli();

stroom-security/stroom-security-impl/src/main/java/stroom/security/impl/SecurityFilter.java

Lines changed: 68 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import stroom.security.common.impl.UserIdentitySessionUtil;
2424
import stroom.security.impl.OpenIdManager.RedirectUrl;
2525
import stroom.security.openid.api.OpenId;
26+
import stroom.util.authentication.HasExpiry;
2627
import stroom.util.logging.LambdaLogger;
2728
import stroom.util.logging.LambdaLoggerFactory;
2829
import stroom.util.logging.LogUtil;
@@ -136,25 +137,42 @@ private void filter(final HttpServletRequest request,
136137
chain.doFilter(request, response);
137138
} else if (isStaticResource(fullPath, servletPath, servletName)) {
138139
chain.doFilter(request, response);
140+
} else if (shouldBypassAuthentication(request, fullPath, servletPath, servletName)) {
141+
LOGGER.debug("Running as proc user for unauthenticated resource, servletName: {}, " +
142+
"fullPath: {}, servletPath: {}", servletName, fullPath, servletPath);
143+
// Some paths don't need authentication. If that is the case then proceed as proc user.
144+
securityContext.asProcessingUser(() ->
145+
process(request, response, chain));
139146
} else {
140-
// Api requests that are not from the front-end should have a token.
141-
// Also request from an AWS ALB will have an ALB signed token containing the claims
142-
// Need to do this first, so we get a fresh token from AWS ALB rather than using a stale
143-
// one from session.
144-
Optional<UserIdentity> optUserIdentity = openIdManager.loginWithRequestToken(request);
145-
146-
// Log current user.
147-
if (LOGGER.isDebugEnabled()) {
148-
logUserIdentityToDebug(
149-
optUserIdentity, fullPath, "after trying to login with request token");
150-
}
147+
// First see if a previous call has placed a userIdentity in session
148+
Optional<UserIdentity> optUserIdentity = UserIdentitySessionUtil.getUserFromSession(
149+
SessionUtil.getExistingSession(request));
150+
logUserIdentityToDebug(optUserIdentity, fullPath, servletPath, "from session");
151+
152+
// Check if the underlying claims/token have expired. The expiry time of some impls
153+
// may get refreshed over time, so we may never hit it. When code flow is handled by
154+
// AWS ALB we will expire, so will just get the latest token from headers which the
155+
// ALB will be refreshing.
156+
optUserIdentity = optUserIdentity.map(userIdentity -> {
157+
if (userIdentity instanceof final HasExpiry hasExpiry) {
158+
if (hasExpiry.hasExpired()) {
159+
LOGGER.info("UserIdentity {} has expired, expiry: {}",
160+
userIdentityToString(userIdentity), hasExpiry.getExpireTime());
161+
// Clear the identity, so we have to re-acquire it from headers or code flow
162+
return null;
163+
} else {
164+
LOGGER.debug(() -> LogUtil.message("UserIdentity {} expires in {}",
165+
userIdentityToString(userIdentity), hasExpiry.getTimeTilExpired()));
166+
}
167+
}
168+
return userIdentity;
169+
});
151170

152-
// If no user from header token, see if we have one in session already.
171+
// API requests that are not from the front-end should have a token.
172+
// Also requests from an AWS ALB will have an ALB signed token containing the claims
153173
if (optUserIdentity.isEmpty()) {
154-
optUserIdentity = UserIdentitySessionUtil.get(SessionUtil.getExistingSession(request));
155-
if (LOGGER.isDebugEnabled()) {
156-
logUserIdentityToDebug(optUserIdentity, fullPath, "from session");
157-
}
174+
optUserIdentity = openIdManager.loginWithRequestToken(request);
175+
logUserIdentityToDebug(optUserIdentity, fullPath, servletPath, "from request token");
158176
}
159177

160178
if (optUserIdentity.isPresent()) {
@@ -163,19 +181,20 @@ private void filter(final HttpServletRequest request,
163181
// Now we have the session make note of the user-agent for logging and sessionListServlet duties
164182
UserAgentSessionUtil.setUserAgentInSession(request);
165183

184+
// If OIDC code flow has been handled by the AWS ALB then the session won't have been
185+
// created by our code flow code. Thus, ensure we have a session with the user in it
186+
if (isStroomUIServlet(servletName)) {
187+
SessionUtil.getOrCreateSession(request, aSession -> {
188+
LOGGER.info("Creating session {} for user {}, fullPath: {}, servlet: {}",
189+
aSession.getId(), userIdentity, fullPath, servletName);
190+
UserIdentitySessionUtil.setUserInSession(aSession, userIdentity);
191+
});
192+
}
193+
166194
// Now handle the request as this user
167195
securityContext.asUser(userIdentity, () ->
168196
process(request, response, chain));
169-
170-
} else if (shouldBypassAuthentication(request, fullPath, servletPath, servletName)) {
171-
LOGGER.debug("Running as proc user for unauthenticated servletName: {}, " +
172-
"fullPath: {}, servletPath: {}", servletName, fullPath, servletPath);
173-
// Some paths don't need authentication. If that is the case then proceed as proc user.
174-
securityContext.asProcessingUser(() ->
175-
process(request, response, chain));
176-
177-
// } else if (isApiRequest(servletPath)) {
178-
} else if (Objects.equals(ResourcePaths.STROOM_SERVLET_NAME, servletName)) {
197+
} else if (isStroomUIServlet(servletName)) {
179198
doOpenIdFlow(request, response, fullPath);
180199
} else {
181200
// If we couldn't log in with a token or couldn't get a token then error as this is an API call
@@ -187,6 +206,11 @@ private void filter(final HttpServletRequest request,
187206
}
188207
}
189208

209+
private boolean isStroomUIServlet(final String servletName) {
210+
return Objects.equals(ResourcePaths.STROOM_SERVLET_NAME, servletName)
211+
|| Objects.equals(ResourcePaths.SESSION_LIST_SERVLET_NAME, servletName);
212+
}
213+
190214
private void doOpenIdFlow(final HttpServletRequest request,
191215
final HttpServletResponse response,
192216
final String fullPath) throws IOException {
@@ -245,20 +269,27 @@ private void doOpenIdFlow(final HttpServletRequest request,
245269
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
246270
private void logUserIdentityToDebug(final Optional<UserIdentity> optUserIdentity,
247271
final String fullPath,
272+
final String servletName,
248273
final String msg) {
249-
LOGGER.debug("User identity ({}): {} path: {}",
274+
LOGGER.debug(() -> LogUtil.message("User identity ({}): {}, fullPath: {}, servletName: {}",
250275
msg,
251-
optUserIdentity.map(
252-
identity -> {
253-
final String id = identity.getDisplayName() != null
254-
? identity.getSubjectId() + " (" + identity.getDisplayName() + ")"
255-
: identity.getSubjectId();
256-
return LogUtil.message("'{}' {}",
257-
id,
258-
identity.getClass().getSimpleName());
259-
})
276+
optUserIdentity.map(this::userIdentityToString)
260277
.orElse("<empty>"),
261-
fullPath);
278+
fullPath,
279+
servletName));
280+
}
281+
282+
private String userIdentityToString(final UserIdentity userIdentity) {
283+
if (userIdentity == null) {
284+
return "";
285+
} else {
286+
final String id = userIdentity.getDisplayName() != null
287+
? userIdentity.getSubjectId() + " (" + userIdentity.getDisplayName() + ")"
288+
: userIdentity.getSubjectId();
289+
return LogUtil.message("'{}' {}",
290+
id,
291+
userIdentity.getClass().getSimpleName());
292+
}
262293
}
263294

264295
private String getPostAuthRedirectUri(final HttpServletRequest request) {
@@ -298,17 +329,6 @@ private boolean shouldBypassAuthentication(final HttpServletRequest servletReque
298329
} else {
299330
shouldBypass = authenticationBypassChecker.isUnauthenticated(servletName, servletPath, fullPath);
300331
}
301-
302-
if (LOGGER.isDebugEnabled()) {
303-
if (shouldBypass) {
304-
LOGGER.debug("Bypassing authentication for servletName: {}, fullPath: {}, servletPath: {}",
305-
NullSafe.get(
306-
servletRequest.getHttpServletMapping(),
307-
HttpServletMapping::getServletName),
308-
fullPath,
309-
servletPath);
310-
}
311-
}
312332
return shouldBypass;
313333
}
314334

stroom-security/stroom-security-impl/src/main/java/stroom/security/impl/SessionListListener.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void sessionDestroyed(final HttpSessionEvent event) {
100100
try {
101101
// In case the session has been destroyed due to it expiring rather than an explicit
102102
// logout, we need to stop the refresh manager from trying to refresh tokens.
103-
UserIdentitySessionUtil.get(httpSession)
103+
UserIdentitySessionUtil.getUserFromSession(httpSession)
104104
.ifPresent(stroomUserIdentityFactory::logoutUser);
105105

106106
final UserRef userRef = getUserRefFromSession(httpSession);
@@ -205,7 +205,7 @@ private SessionListResponse listSessionsOnThisNode() {
205205
}
206206

207207
private static UserRef getUserRefFromSession(final HttpSession httpSession) {
208-
return UserIdentitySessionUtil.get(httpSession)
208+
return UserIdentitySessionUtil.getUserFromSession(httpSession)
209209
.filter(uid -> uid instanceof HasUserRef)
210210
.map(uid -> ((HasUserRef) uid).getUserRef())
211211
.orElse(null);

stroom-security/stroom-security-impl/src/main/java/stroom/security/impl/SessionListServlet.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,9 @@ private void writeCell(final Writer writer, final String value) throws IOExcepti
167167
public Set<String> getPathSpecs() {
168168
return PATH_SPECS;
169169
}
170+
171+
@Override
172+
public String getName() {
173+
return ResourcePaths.SESSION_LIST_SERVLET_NAME;
174+
}
170175
}

stroom-security/stroom-security-impl/src/main/java/stroom/security/impl/SessionResourceImpl.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public UrlResponse logout(final String redirectUri) {
5454
// Get the session.
5555
final HttpSession session = SessionUtil.getExistingSession(request);
5656
if (session != null) {
57-
final UserIdentity userIdentity = UserIdentitySessionUtil.get(session)
57+
final UserIdentity userIdentity = UserIdentitySessionUtil.getUserFromSession(session)
5858
.orElse(null);
5959
LOGGER.info(() -> LogUtil.message(
6060
"logout() - Logout called for {}, userIdentity: {} {} ({}), session: {}, redirectUri: {}",
@@ -70,7 +70,7 @@ public UrlResponse logout(final String redirectUri) {
7070
// Create an event for logout
7171
authenticationEventLogProvider.get().logoff(userIdentity.getSubjectId());
7272
// Remove the user identity from the current session.
73-
UserIdentitySessionUtil.set(session, null);
73+
UserIdentitySessionUtil.setUserInSession(session, null);
7474
}
7575
session.invalidate();
7676
} else {

0 commit comments

Comments
 (0)