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:
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:
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.