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
- 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.
- On failure, do not retry with the same state — re-issue request.authorize(state) to start a fresh round trip.
- Ensure state survives the full redirect: check it is not stripped by your frontend router, proxy, or callback URL normalization.
- 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
- Use a shared Redis-backed AuthStateCache in any multi-instance or restart-prone deployment.
- Set state TTL well above the worst-case user dwell time on the consent page.
- Treat a failed state as a security event (possible CSRF/forgery), log it, then restart the flow.
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
- 5006
- 5008
- object.getString("error_description") / object.getString("er
- object.getString("msg")
- object.getString("error")
AI-assisted analysis of justauth/JustAuth@694bbf1b01 (2026-08-14).
Data as JSON: /api/errors/58ee5d67f2fe2178.
Report an issue: GitHub.