mirror of
https://github.com/Athou/commafeed.git
synced 2026-11-05 03:44:47 +00:00
invalidate existing sessions on password change
This commit is contained in:
@@ -10,7 +10,10 @@ import io.quarkus.security.UnauthorizedException;
|
||||
|
||||
import jakarta.annotation.Priority;
|
||||
import jakarta.validation.ValidationException;
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.NewCookie;
|
||||
import jakarta.ws.rs.core.UriInfo;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -49,8 +52,19 @@ public class ExceptionMappers {
|
||||
|
||||
@ServerExceptionMapper(AuthenticationFailedException.class)
|
||||
public RestResponse<AuthenticationFailed> authenticationFailed(
|
||||
AuthenticationFailedException e) {
|
||||
AuthenticationFailedException e, HttpHeaders headers, UriInfo uriInfo) {
|
||||
NewCookie logoutCookie = cookieService.buildLogoutCookie();
|
||||
|
||||
// a browser navigating with an invalidated session would otherwise display the error
|
||||
// instead of the page, retry the request now that the cookie is removed
|
||||
if (headers.getAcceptableMediaTypes().stream()
|
||||
.anyMatch(m -> MediaType.TEXT_HTML_TYPE.isCompatible(m) && !m.isWildcardType())) {
|
||||
return ResponseBuilder.<AuthenticationFailed>create(Status.TEMPORARY_REDIRECT)
|
||||
.location(uriInfo.getRequestUri())
|
||||
.cookie(logoutCookie)
|
||||
.build();
|
||||
}
|
||||
|
||||
return ResponseBuilder.create(Status.UNAUTHORIZED, new AuthenticationFailed(e.getMessage()))
|
||||
.cookie(logoutCookie)
|
||||
.build();
|
||||
|
||||
@@ -42,6 +42,9 @@ public class User extends AbstractModel {
|
||||
@Column(nullable = false)
|
||||
private boolean disabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int sessionVersion;
|
||||
|
||||
@Column private Instant lastLogin;
|
||||
|
||||
@Column private Instant created;
|
||||
|
||||
@@ -155,6 +155,17 @@ public class UserService {
|
||||
userDAO.delete(user);
|
||||
}
|
||||
|
||||
public void changePassword(User user, String password) {
|
||||
user.setPassword(encryptionService.getEncryptedPassword(password, user.getSalt()));
|
||||
userDAO.merge(user);
|
||||
|
||||
invalidateSessions(user);
|
||||
}
|
||||
|
||||
public void invalidateSessions(User user) {
|
||||
user.setSessionVersion(user.getSessionVersion() + 1);
|
||||
}
|
||||
|
||||
public String generateApiKey(User user) {
|
||||
byte[] key =
|
||||
encryptionService.getEncryptedPassword(
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.commafeed.backend.dao.UserRoleDAO;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.PasswordEncryptionService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.model.UserModel;
|
||||
import com.commafeed.frontend.model.request.AdminSaveUserRequest;
|
||||
@@ -56,7 +55,6 @@ public class AdminREST {
|
||||
private final UserDAO userDAO;
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final MetricRegistry metrics;
|
||||
|
||||
@Path("/user/save")
|
||||
@@ -102,10 +100,15 @@ public class AdminREST {
|
||||
User u = userDAO.findById(id);
|
||||
u.setName(req.getName());
|
||||
if (StringUtils.isNotBlank(req.getPassword())) {
|
||||
u.setPassword(
|
||||
encryptionService.getEncryptedPassword(req.getPassword(), u.getSalt()));
|
||||
userService.changePassword(u, req.getPassword());
|
||||
if (StringUtils.isNotBlank(u.getApiKey())) {
|
||||
u.setApiKey(userService.generateApiKey(u));
|
||||
}
|
||||
}
|
||||
u.setEmail(req.getEmail());
|
||||
if (!u.isDisabled() && !req.isEnabled()) {
|
||||
userService.invalidateSessions(u);
|
||||
}
|
||||
u.setDisabled(!req.isEnabled());
|
||||
|
||||
Set<Role> roles = userRoleDAO.findRoles(u);
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.commafeed.backend.model.UserSettings.ReadingMode;
|
||||
import com.commafeed.backend.model.UserSettings.ReadingOrder;
|
||||
import com.commafeed.backend.model.UserSettings.ScrollMode;
|
||||
import com.commafeed.backend.service.MailService;
|
||||
import com.commafeed.backend.service.PasswordEncryptionService;
|
||||
import com.commafeed.backend.service.PushNotificationService;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.backend.service.db.DatabaseStartupService;
|
||||
@@ -86,7 +85,6 @@ public class UserREST {
|
||||
private final UserRoleDAO userRoleDAO;
|
||||
private final UserSettingsDAO userSettingsDAO;
|
||||
private final UserService userService;
|
||||
private final PasswordEncryptionService encryptionService;
|
||||
private final DatabaseStartupService databaseStartupService;
|
||||
private final MailService mailService;
|
||||
private final CommaFeedConfiguration config;
|
||||
@@ -316,10 +314,7 @@ public class UserREST {
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(request.getNewPassword())) {
|
||||
byte[] password =
|
||||
encryptionService.getEncryptedPassword(
|
||||
request.getNewPassword(), user.getSalt());
|
||||
user.setPassword(password);
|
||||
userService.changePassword(user, request.getNewPassword());
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
|
||||
@@ -455,8 +450,7 @@ public class UserREST {
|
||||
return Response.status(Status.UNAUTHORIZED).entity("Token expired.").build();
|
||||
}
|
||||
|
||||
byte[] encryptedPassword = encryptionService.getEncryptedPassword(password, user.getSalt());
|
||||
user.setPassword(encryptedPassword);
|
||||
userService.changePassword(user, password);
|
||||
if (StringUtils.isNotBlank(user.getApiKey())) {
|
||||
user.setApiKey(userService.generateApiKey(user));
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ public class AuthenticationContext {
|
||||
return null;
|
||||
}
|
||||
|
||||
String userId = securityIdentity.getPrincipal().getName();
|
||||
if (userId == null) {
|
||||
String name = securityIdentity.getPrincipal().getName();
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return userDAO.findById(Long.valueOf(userId));
|
||||
return userDAO.findById(SessionPrincipal.parse(name).userId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.commafeed.security;
|
||||
|
||||
import com.commafeed.backend.model.User;
|
||||
|
||||
/** Value of the security principal, which is also what ends up in the encrypted session cookie. */
|
||||
public record SessionPrincipal(long userId, int sessionVersion) {
|
||||
|
||||
private static final String SEPARATOR = ":";
|
||||
|
||||
public static SessionPrincipal of(User user) {
|
||||
return new SessionPrincipal(user.getId(), user.getSessionVersion());
|
||||
}
|
||||
|
||||
public static SessionPrincipal parse(String name) {
|
||||
// cookies created before session versions were introduced only contain the user id
|
||||
if (!name.contains(SEPARATOR)) {
|
||||
return new SessionPrincipal(Long.parseLong(name), 0);
|
||||
}
|
||||
|
||||
String[] parts = name.split(SEPARATOR, 2);
|
||||
return new SessionPrincipal(Long.parseLong(parts[0]), Integer.parseInt(parts[1]));
|
||||
}
|
||||
|
||||
public String toName() {
|
||||
return userId + SEPARATOR + sessionVersion;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.commafeed.backend.dao.UnitOfWork;
|
||||
import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.security.SessionPrincipal;
|
||||
|
||||
import io.quarkus.security.AuthenticationFailedException;
|
||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||
@@ -47,7 +48,7 @@ public class DatabaseApiKeyIdentityProvider
|
||||
|
||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
||||
return QuarkusSecurityIdentity.builder()
|
||||
.setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
|
||||
.setPrincipal(new QuarkusPrincipal(SessionPrincipal.of(user.get()).toName()))
|
||||
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
||||
.build();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.frontend.exception.CommaFeedApplicationException;
|
||||
import com.commafeed.frontend.exception.CommaFeedExceptionType;
|
||||
import com.commafeed.security.SessionPrincipal;
|
||||
|
||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||
import io.quarkus.security.identity.IdentityProvider;
|
||||
@@ -52,7 +53,7 @@ public class DatabaseUsernamePasswordIdentityProvider
|
||||
|
||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
||||
return QuarkusSecurityIdentity.builder()
|
||||
.setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
|
||||
.setPrincipal(new QuarkusPrincipal(SessionPrincipal.of(user.get()).toName()))
|
||||
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
||||
.build();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.commafeed.backend.model.User;
|
||||
import com.commafeed.backend.model.UserRole.Role;
|
||||
import com.commafeed.backend.service.UserService;
|
||||
import com.commafeed.backend.service.internal.PostLoginActivities;
|
||||
import com.commafeed.security.SessionPrincipal;
|
||||
|
||||
import io.quarkus.security.AuthenticationFailedException;
|
||||
import io.quarkus.security.identity.AuthenticationRequestContext;
|
||||
@@ -41,11 +42,17 @@ public class TrustedIdentityProvider implements IdentityProvider<TrustedAuthenti
|
||||
public Uni<SecurityIdentity> authenticate(
|
||||
TrustedAuthenticationRequest request, AuthenticationRequestContext context) {
|
||||
return context.runBlocking(() -> {
|
||||
Long userId = Long.valueOf(request.getPrincipal());
|
||||
User user = unitOfWork.call(() -> userDAO.findById(userId));
|
||||
SessionPrincipal principal = SessionPrincipal.parse(request.getPrincipal());
|
||||
User user = unitOfWork.call(() -> userDAO.findById(principal.userId()));
|
||||
if (user == null) {
|
||||
throw new AuthenticationFailedException("user not found");
|
||||
}
|
||||
if (user.isDisabled()) {
|
||||
throw new AuthenticationFailedException("user disabled");
|
||||
}
|
||||
if (user.getSessionVersion() != principal.sessionVersion()) {
|
||||
throw new AuthenticationFailedException("session invalidated");
|
||||
}
|
||||
|
||||
// execute post login activities manually because we didn't call login() since
|
||||
// we received
|
||||
@@ -54,7 +61,7 @@ public class TrustedIdentityProvider implements IdentityProvider<TrustedAuthenti
|
||||
|
||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user));
|
||||
return QuarkusSecurityIdentity.builder()
|
||||
.setPrincipal(new QuarkusPrincipal(String.valueOf(userId)))
|
||||
.setPrincipal(new QuarkusPrincipal(principal.toName()))
|
||||
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
||||
.build();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user