apereo/cas · error · InvalidLoginLocationException
Unable to login from this location
Error message
Unable to login from this location
What it means
JsonResourceAuthenticationHandler reads account records from a JSON resource that may carry a 'location' regex per account. When ClientInfoHolder has client info and the account's location pattern does not match the requester's IP, it throws InvalidLoginLocationException 'Unable to login from this location', blocking authentication based on network origin.
Solutions
- Update the account's location regex in the JSON resource to include the client's current IP/subnet
- Check ClientInfoHolder-reported IP (proxy headers) matches what the regex expects
- Make the regex family-agnostic if both IPv4/IPv6 are expected (e.g. 10\..*|fc00:.* )
- Remove/blank the location field if IP restriction is not intended
Example fix
// before (JSON account) "location": "^192\\.168\\.1\\.10$" // after "location": "^192\\.168\\.1\\.(10|1[1-9])$"
Defensive patterns
Strategy: try-catch
Validate before calling
String ip = ClientInfoHolder.getClientInfo() != null ? ClientInfoHolder.getClientInfo().getClientIpAddress() : null; // precheck account location regex against ip before submitting
Try / catch
try {
return handler.authenticate(credential);
} catch (InvalidLoginLocationException e) {
// respond 403 with location-based denial message
} Prevention
- Maintain location regexes for VPN/office ranges including both IPv4 and IPv6 forms
- Document IP restrictions per account in the JSON resource
- Account for proxies/NAT when authoring IP rules
When it happens
Trigger: authenticateUsernamePasswordInternal for a JSON account whose location field is non-blank and fails RegexUtils.find(account.getLocation(), clientIpAddress).
Common situations: Account restricted to an office/VPN subnet but user connects from a different network; IPv6 vs IPv4 mismatch against a regex written for the other family; location regex written without correct escaping; NAT/proxy making the client IP different from expected.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Unable to login at this time
- Account password on file does not match the provided…
- Provided regular expression or IP/netmask
- Authentication handler is disabled
- No user can be accepted because none is defined
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/93fa77bb4dda3b2a.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-generic/src/main/java/org/apereo/cas/adaptors/generic/JsonResourceAuthenticationHandler.java:84
val account = map.get(username);
if (!matches(originalPassword, account.getPassword())) {
LOGGER.warn("Account password on file does not match the provided password for [{}]", username);
throw new FailedLoginException();
}
LOGGER.debug("Located account [{}]", account);
switch (account.getStatus()) {
case DISABLED -> throw new AccountDisabledException();
case EXPIRED -> throw new AccountExpiredException();
case LOCKED -> throw new AccountLockedException();
case MUST_CHANGE_PASSWORD -> throw new AccountPasswordMustChangeException();
case OK -> LOGGER.debug("Account status is OK");
}
val clientInfo = ClientInfoHolder.getClientInfo();
if (clientInfo != null && StringUtils.isNotBlank(account.getLocation())
&& !RegexUtils.find(account.getLocation(), clientInfo.getClientIpAddress())) {
throw new InvalidLoginLocationException("Unable to login from this location");
}
if (StringUtils.isNotBlank(account.getAvailability())) {
val range = Splitter.on("~").splitToList(account.getAvailability());
val startDate = DateTimeUtils.convertToZonedDateTime(range.getFirst());
val endDate = DateTimeUtils.convertToZonedDateTime(range.get(1));
val now = ZonedDateTime.now(Clock.systemUTC());
if (now.isBefore(startDate) || now.isAfter(endDate)) {
throw new InvalidLoginTimeException("Unable to login at this time");
}
}
val warnings = new ArrayList<MessageDescriptor>();
if (account.getExpirationDate() != null) {
val now = LocalDate.now(ZoneOffset.UTC);
if (now.isEqual(account.getExpirationDate()) || now.isAfter(account.getExpirationDate())) {
throw new AccountExpiredException();
}View on GitHub (pinned to e7288fc434)