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

  1. Update the account's location regex in the JSON resource to include the client's current IP/subnet
  2. Check ClientInfoHolder-reported IP (proxy headers) matches what the regex expects
  3. Make the regex family-agnostic if both IPv4/IPv6 are expected (e.g. 10\..*|fc00:.* )
  4. 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

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


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)