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

  1. Have the user re-request a fresh passwordless token and re-enter it before expiry.
  2. 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).
  3. Check that clock skew or token TTL settings are not expiring tokens before the user can submit them.
  4. 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

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

Related errors


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)