From 31c6b659ae6faa5fb03a62e39f13c25b4b536fb2 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Sat, 8 Aug 2026 12:56:34 +0200 Subject: [PATCH] JCR-5095: retry token creation on concurrent modification of the token parent Concurrent logins of the same user each use their own session but add their token node below the same .tokens parent. A concurrent commit below that parent can invalidate this session's pending changes, so that the token node can neither be saved (InvalidItemStateException from validateTransientItems) nor resolved afterwards (ItemNotFoundException while building its path). Either one failed the whole login. Wrap the token node creation in a bounded retry that discards the doomed transient state via session.refresh(false) and re-reads the token parent, mirroring the conflict handling that getTokenParent already performs for the concurrent creation of the token store itself. The original exception is rethrown once the attempts are exhausted, so behaviour on persistent failures is unchanged. This makes token creation tolerate the conflict but does not remove the underlying race in the transient state handling. --- .../authentication/token/TokenProvider.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java index 1b32a3312f4..ae7dc400c72 100644 --- a/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java +++ b/jackrabbit-core/src/main/java/org/apache/jackrabbit/core/security/authentication/token/TokenProvider.java @@ -31,6 +31,8 @@ import java.util.Map; import java.util.Set; import javax.jcr.AccessDeniedException; +import javax.jcr.InvalidItemStateException; +import javax.jcr.ItemNotFoundException; import javax.jcr.NamespaceRegistry; import javax.jcr.Node; import javax.jcr.Property; @@ -76,6 +78,13 @@ public class TokenProvider extends ProtectedItemModifier { private static final char DELIM = '_'; + /** + * Number of attempts to persist a new token node before giving up. Concurrent logins + * of the same user write below a shared token parent and may invalidate each other's + * pending changes (JCR-5095). + */ + private static final int CREATE_TOKEN_MAX_ATTEMPTS = 3; + private static final Set RESERVED_ATTRIBUTES = new HashSet(3); static { RESERVED_ATTRIBUTES.add(TOKEN_ATTRIBUTE); @@ -146,6 +155,39 @@ public TokenInfo createToken(User user, SimpleCredentials sc) throws RepositoryE */ private TokenInfo createToken(User user, Map attributes) throws RepositoryException { String error = "Failed to create login token. "; + // Concurrent logins of the same user add token nodes below the very same token + // parent. A concurrent commit below that parent may invalidate the pending changes + // of this session, so that the token node can neither be saved + // (InvalidItemStateException) nor resolved afterwards (ItemNotFoundException while + // building its path). Both are transient, so retry with a refreshed session, + // analogous to the conflict handling in getTokenParent (JCR-5095). + for (int attempt = 1; ; attempt++) { + try { + return createTokenNode(user, attributes, error); + } catch (InvalidItemStateException | ItemNotFoundException e) { + if (attempt >= CREATE_TOKEN_MAX_ATTEMPTS) { + throw e; + } + log.debug("Conflict while creating login token (attempt {}) -> retrying", attempt, e); + // discard the token node that could not be persisted before retrying + session.refresh(false); + } + } + } + + /** + * Creates and persists a single token node below the token parent of the given user. + * + * @param user The user for which a new token should be created. + * @param attributes The attributes associated with the new token. + * @param error Prefix used for log messages. + * @return A new {@code TokenInfo} or {@code null} if the token could not be created. + * @throws InvalidItemStateException If the token node could not be persisted because + * the token parent was modified concurrently. + * @throws ItemNotFoundException If the token node could not be resolved after saving + * because the token parent was modified concurrently. + */ + private TokenInfo createTokenNode(User user, Map attributes, String error) throws RepositoryException { NodeImpl tokenParent = getTokenParent(user); if (tokenParent != null) { try {