Skip to content

Authentication State

BadgerWebAuthTools (commerce-core/.../security/BadgerWebAuthTools.java) is the single place that answers "who is on this request, and what may they do". Controllers, extensions and security handlers should use it rather than reading the SecurityContextHolder directly.

The API

Method Answers
isLoggedIn() Is a real, identified principal signed in?
amIAnAdminUser() Do they hold any role in ADMIN_AUTHORITIES?
amIARegisteredUser() Do they hold ROLE_USER?
hasAnyRole(String...) Do they hold any of these specific roles?
getAdminRoles() The canonical admin role-name list
loggedInUserName() The current principal's name

Per the template rules in CLAUDE.md, call these in Java and pass a boolean to the template. Never enumerate roles in Thymeleaf.

Anonymous is not "logged out"

The single most important thing to know about Spring Security here: an unauthenticated visitor does not have a null Authentication. The filter chain installs an AnonymousAuthenticationToken, and that token reports isAuthenticated() == true.

So this is always wrong:

// ❌ true for EVERY visitor, including anonymous ones
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
boolean loggedIn = auth != null && auth.isAuthenticated();

isLoggedIn() uses Spring's AuthenticationTrustResolver to exclude anonymous tokens, which is the canonical way to draw this distinction:

// ✅
return auth != null && auth.isAuthenticated() && !TRUST_RESOLVER.isAnonymous(auth);

This was a live bug: isLoggedIn() previously used the first form, so it could never return false in a normal web request. Anonymous visitors to /forgot-password were handed the authenticated change-password form instead of the forgot-password form.

No security context at all

Worker threads, scheduled tasks and anything outside the Spring Security filter chain have an empty context and a genuinely null Authentication. The role checks are null-safe and answer false there; they previously dereferenced getAuthentication() directly and threw NullPointerException.

Known gaps

These are understood and deliberately not addressed yet — check before relying on them.

loggedInUserName() returns "anonymousUser", not null

That is the principal name Spring gives the anonymous token. Callers that compare it against a real login name are safe (it will never match). Callers that record it — for example RepositoryChangeDetector, which stamps it onto audit entries — will write the literal string "anonymousUser". Gate on isLoggedIn() first where that matters.

Remember-me counts as logged in

isLoggedIn() returns true for a RememberMeAuthenticationToken, because remember-me is a real identity. It is not the same as "authenticated during this session", which is the stricter question a step-up or re-authentication gate needs to ask before a sensitive action. Nothing draws that distinction today. Use AuthenticationTrustResolver.isRememberMe(...) if you need it.

Note the interaction with AuthenticationExtension, which derives:

boolean isSoftLoggedIn = isRegistered && !BadgerWebAuthTools.isLoggedIn();

While isLoggedIn() was always true this was permanently false and the soft-login UI state was unreachable. It is now live for a registered user whose security context is anonymous — worth checking the rendered states if you touch that extension.

@ConditionalOnBean on component-scanned services

Several services (UserPasswordServiceImpl, SubscriptionDunningServiceImpl, OrderEmailResendServiceImpl, the email pipeline processors and others) carry @ConditionalOnBean(EmailService.class) while being plain @Service components.

@ConditionalOnBean is only reliable inside auto-configuration, where ordering is guaranteed. On a component-scanned bean the condition is evaluated during scanning, when the referenced bean definition may not be registered yet, so the result depends on scan order. It works today because EmailServiceConfiguration is processed first, but it is load-bearing on ordering that nothing enforces.