apereo/cas · error · FailedLoginException
Passwordless authentication has failed
Error message
Passwordless authentication has failed
What it means
FailedLoginException thrown by PasswordlessTokenAuthenticationHandler.doAuthentication when the submitted OneTimePasswordCredential does not equal (case-insensitively) any of the stored tokens for the user in the passwordless token repository. No principal/handler result is produced, so passwordless login fails.
Solutions
- Have the user re-request a fresh passwordless token and re-enter it before expiry.
- Verify the PasswordlessTokenRepository implementation retains tokens across nodes/restarts (use a shared store like Redis/JDBC in clustered deployments, not the default in-memory map).
- Check that clock skew or token TTL settings are not expiring tokens before the user can submit them.
- Enable debug logging on the handler to confirm whether the user id matched but the token did not.
Example fix
// before (clustered app with default repo)
@Bean public PasswordlessTokenRepository repo() { return new DefaultPasswordlessTokenRepository(new HashMap>()); }
// after
@Bean public PasswordlessTokenRepository repo(RedisTemplate tpl) { return new RedisPasswordlessTokenRepository(tpl, expiration); } Defensive patterns
Strategy: validation
Validate before calling
// Caller-side pre-check in a custom flow
boolean tokenExists = passwordlessTokenRepository.findToken(username)
.map(t -> t.equalsIgnoreCase(submittedOtp)).orElse(false);
if (!tokenExists) { return error("invalid or expired code"); } Try / catch
try { handler.authenticate(credential); } catch (FailedLoginException e) { model.addAttribute("error", "Invalid or expired code; request a new one"); } Prevention
- Use a clustered/shared PasswordlessTokenRepository in multi-node deployments.
- Set token TTL long enough for email/SMS delivery plus user entry.
- Invalidate and reissue tokens cleanly when a new one is requested.
When it happens
Trigger: A user submits a one-time password via the passwordless flow and no token record in PasswordlessTokenRepository matches the user id with a token equal to the submitted credential password.
Common situations: User mistypes the code; token already consumed/deleted or expired in the token repository (in-memory repository lost entries on restart); user copies a token issued for another account; clock/token rotation replaced the token between request pages.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Authentication handler is disabled
- No user can be accepted because none is defined
- not found in backing map.
- Unable to authenticate
- No authentication handlers could be resolved to support the…
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/78e54eb6b6e2b8c3.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-passwordless-api/src/main/java/org/apereo/cas/authentication/PasswordlessTokenAuthenticationHandler.java:46
this.passwordlessTokenRepository = passwordlessTokenRepository;
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(final Credential credential, final Service service) throws Throwable {
val otc = (OneTimePasswordCredential) credential;
val token = passwordlessTokenRepository.findToken(otc.getId());
if (token.isPresent()) {
val passed = token
.map(PasswordlessAuthenticationToken::getToken)
.filter(StringUtils::isNotBlank)
.stream()
.allMatch(tk -> tk.equalsIgnoreCase(otc.getPassword()));
if (passed) {
val principal = principalFactory.createPrincipal(otc.getId());
return createHandlerResult(credential, principal, new ArrayList<>());
}
}
throw new FailedLoginException("Passwordless authentication has failed");
}
@Override
public boolean supports(final Class<? extends Credential> clazz) {
return OneTimePasswordCredential.class.isAssignableFrom(clazz);
}
@Override
public boolean supports(final Credential credential) {
if (!(credential instanceof OneTimePasswordCredential)) {
LOGGER.debug("Credential is not one of one-time password and is not accepted by handler [{}]", getName());
return false;
}
return true;
}
}
View on GitHub (pinned to e7288fc434)