Keycloak: 'State cookie was missing' if user started login in Keycloak and is waiting for 10 minutes or more.
Author: AliakseiPaseishviliSyntheticabioCreated Feb 6, 2024Updated Jul 17, 2026
Labelsbugproviderstriage
Provider type
Keycloak
Environment
My package.json:
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prepare": "husky install",
"test": "jest"
},
"dependencies": {
"@floating-ui/react": "^0.26.2",
"@headlessui/react": "^1.7.16",
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.2.0",
"@lexical/react": "^0.12.5",
"@tanstack/react-query": "^5.4.3",
"@tanstack/react-query-devtools": "^5.4.3",
"@types/node": "20.5.0",
"@types/react": "18.2.20",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"classnames": "^2.3.2",
"dayjs": "^1.11.10",
"eslint": "8.47.0",
"eslint-config-next": "13.4.13",
"eslint-plugin-json-es": "^1.5.7",
"i18next": "^23.4.5",
"jwt-decode": "^4.0.0",
"lexical": "^0.12.5",
"lodash": "^4.17.21",
"next": "14.1.0",
"next-auth": "^4.24.5",
"postcss": "8.4.31",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.11",
"react-hook-form": "^7.45.4",
"react-i18next": "^13.2.0",
"react-toastify": "^9.1.3",
"react-virtualized-auto-sizer": "^1.0.20",
"react-window": "^1.8.9",
"reactflow": "^11.8.1",
"sass": "^1.67.0",
"tailwindcss": "3.3.3",
"typescript": "5.1.6",
"yup": "^1.2.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/lodash": "^4.14.199",
"@types/react-window": "^1.8.6",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-import": "^2.28.0",
"eslint-plugin-no-loops": "^0.3.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-react-hooks": "^5.0.0-canary-7118f5dd7-20230705",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^14.0.0",
"prettier": "3.0.1",
"ts-node": "^10.9.2"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json}": [
"npx eslint --quiet --fix"
]
}
}
config for next-auth:
import dayjs from 'dayjs';
import { jwtDecode } from 'jwt-decode';
import { AuthOptions } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import KeycloakProvider from 'next-auth/providers/keycloak';
declare module 'next-auth/jwt' {
interface JWT {
access_token: string;
id_token: string;
expires_at: number;
refresh_token: string;
error?: string;
roles: string[];
}
}
declare module 'jwt-decode' {
export interface JwtPayload {
realm_access: { roles: string[] };
}
}
declare module 'next-auth' {
interface Session {
error?: string;
roles: string[];
sub?: string;
}
}
const refreshAccessToken = async (token: JWT) => {
const url = `${process.env.KEYCLOACK_ISSUER}/protocol/openid-connect/token`;
const resp = await fetch(url, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: process.env.KEYCLOACK_CLIENT_ID,
client_secret: process.env.KEYCLOACK_CLIENT_SECRET,
grant_type: 'refresh_token',
refresh_token: token.refresh_token,
}),
method: 'POST',
});
const refreshToken = await resp.json();
if (!resp.ok) throw refreshToken;
const tokenData = jwtDecode(refreshToken.access_token);
return {
...token,
access_token: refreshToken.access_token,
id_token: refreshToken.id_token,
expires_at: Math.floor(Date.now() / 1000) + refreshToken.expires_in,
refresh_token: refreshToken.refresh_token,
roles: tokenData.realm_access.roles,
};
};
export const authOptions: AuthOptions = {
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOACK_CLIENT_ID,
clientSecret: process.env.KEYCLOACK_CLIENT_SECRET,
issuer: process.env.KEYCLOACK_ISSUER,
}),
],
callbacks: {
async jwt({ token, account, trigger }) {
const nowTimeStamp = dayjs();
try {
// we refresh token on update action
if (trigger === 'update') {
return await refreshAccessToken(token);
}
// we get intial token on sign in
if (
account &&
account.access_token &&
account.id_token &&
account.expires_at &&
account.refresh_token
) {
const tokenData = jwtDecode(account.access_token);
token.roles = tokenData.realm_access.roles;
token.access_token = account.access_token;
token.id_token = account.id_token;
token.expires_at = account.expires_at;
token.refresh_token = account.refresh_token;
return token;
// if time of expires is more then current date then we return existing token.
} else if (
token &&
nowTimeStamp.isBefore(dayjs.unix(token.expires_at))
) {
return token;
// this happens after user session is expired, so in this case we try to update user session
} else {
return await refreshAccessToken(token);
}
// if session update is failed we return error and on client we are doing logout(TokenExpireController).
} catch (error) {
return { ...token, error: 'RefreshAccessTokenError' };
}
},
async session({ session, token }) {
session.error = token.error;
session.roles = token.roles;
session.sub = token.sub;
return session;
},
},
events: {
async signOut({ token }) {
const logOutUrl = new URL(
`${process.env.KEYCLOACK_ISSUER}/protocol/openid-connect/logout`,
);
logOutUrl.searchParams.set('id_token_hint', token.id_token!);
await fetch(logOutUrl);
},
},
secret: process.env.NEXTAUTH_SECRET,
pages: {
signIn: '/auth/signin', // we added this page to manually cover issue with missed state
},
};
Reproduction URL
https://github.com/AliakseiPaseishviliSyntheticabio/test-repo
Describe the issue
Sometime user doing login to our app for too long and in this case we are navigated to sign-in page with next error, instead of giving access to the app.:
Pressing on sign in with keycloak will navigate our user to app without any keycloak login pages.
UPD: repo is not here. This is private code and I can't share env details to you.
How to reproduce
Steps to reproduce:
- Open your app with keycloak.
- Start login with keycloak
- wait for 10+ minutes.
- finish login with keycloak.
- App will redirect you to api/signin?error='OAuthCallback'
- pressing on keycloak button will do fast signin without doing step 2. in logs we got error: 'State cookie was missing'.
Expected behavior
It does login without any issues even in 10 or 15 minutes, because default login time in keycloak is set to 30 minutes.
Source: nextauthjs/next-auth