justauth/JustAuth · error · AuthException

5009

5009

Error message

Illegal state

What it means

Thrown by AuthChecker.checkState: the state echoed back by the provider is empty, or it is not present in the configured AuthStateCache. The state parameter is JustAuth's CSRF protection — checkState sees exactly two failure modes: the state was already consumed and evicted, or it was never issued by this server (forged). Code 5009 (ILLEGAL_STATUS). Twitter is exempt.

Source

Thrown at src/main/java/me/zhyd/oauth/utils/AuthChecker.java:108

    /**
     * 校验回调传回的{@code state},为空或者不存在
     * <p>
     * {@code state}不存在的情况只有两种:
     * 1. {@code state}已使用,被正常清除
     * 2. {@code state}为前端伪造,本身就不存在
     *
     * @param state          {@code state}一定不为空
     * @param source         {@code source}当前授权平台
     * @param authStateCache {@code authStateCache} state缓存实现
     */
    public static void checkState(String state, AuthSource source, AuthStateCache authStateCache) {
        // 推特平台不支持回调 code 和 state
        if (source == AuthDefaultSource.TWITTER) {
            return;
        }
        if (StringUtils.isEmpty(state) || !authStateCache.containsKey(state)) {
            throw new AuthException(AuthResponseStatus.ILLEGAL_STATUS, source);
        }
    }
}

View on GitHub (pinned to 694bbf1b01)

Solutions

  1. If you run more than one instance or restart often, supply a shared/durable AuthStateCache (Redis-backed) instead of the default in-memory one, and set the timeout comfortably longer than a realistic authorize->callback round trip.
  2. On failure, do not retry with the same state — re-issue request.authorize(state) to start a fresh round trip.
  3. Ensure state survives the full redirect: check it is not stripped by your frontend router, proxy, or callback URL normalization.
  4. Guard the callback handler: if state is absent from the request, redirect the user to a new authorize URL rather than calling into the SDK.

Example fix

// before
AuthRequest request = new AuthGithubRequest(config);
// default in-memory AuthStateCache is used; restarts/scaling break state

// after
AuthStateCache redisCache = new AuthDefaultStateCache() {
    // replace with Redis/DB-backed implementation for multi-instance deploys
};
AuthRequest request = new AuthGithubRequest(config, redisCache);
Defensive patterns

Strategy: validation

Validate before calling

String state = callback.getState();
if (StringUtils.isEmpty(state) || !stateCache.containsKey(state)) {
    // expired, consumed, or forged — restart the flow instead of calling login()
    return redirect(authRequest.authorize(StateUtils.createState()));
}

Try / catch

catch (AuthException e) { if (e.getErrorCode() == AuthResponseStatus.ILLEGAL_STATUS.getCode()) { /* re-issue authorize URL; never reuse the state */ } }

Prevention

When it happens

Trigger: Calling request.login(callback)/getAccessToken after the user's authorize redirect lands, when: the state query param is missing/mangled; the cached state expired between authorize and callback; the app restarted and the default in-memory AuthDefaultStateCache lost everything; the app runs multiple instances behind a load balancer and the callback hit an instance that never stored the state.

Common situations: Local dev with frequent restarts (every pending OAuth round-trip breaks); horizontal scaling without a shared cache implementation; browser back-button or double-click re-using an already-consumed state; authStateCache implementations with aggressive TTL; long user dwell time on the provider consent page exceeding cache expiry.

Related errors


AI-assisted analysis of justauth/JustAuth@694bbf1b01 (2026-08-14). Data as JSON: /api/errors/58ee5d67f2fe2178. Report an issue: GitHub.