I have been trying to integrate Keycloak SSO as authentication provider for an application I am building. I am using openid-connect provider from keycloak. I have a frontend, an express server and a keycloack service all running as containers behing a caddy reverse HTTPS proxy container. The custom express server helps me check if a user is whitelisted/admin based on the role assigned to them. I have been facing the below issue for some time now.
BadRequestError
at ResponseContext.callback (/app/node_modules/express-openid-connect/lib/context.js:396:15)
Below given is the prototype code which I have used to connect keycloak with my custom express server. Once a user has logged in, I am passing them a custom JWT token for session state management
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const { auth } = require('express-openid-connect');
const jwt = require('jsonwebtoken');
const authCustomRoutes = require('./routes/routes');
const PORT = process.env.EXPRESS_PORT || 3001;
const CLIENT_URL = process.env.REACT_APP_BASE_URL || 'http://localhost:3000';
const CUSTOM_JWT_SECRET = process.env.JWT_SECRET;
const CUSTOM_TOKEN_COOKIE = 'access_token';
const app = express();
app.set('trust proxy', 1);
app.use(cors({
origin: CLIENT_URL,
credentials: true,
}));
app.use(cookieParser());
app.use(express.json());
const config = {
authRequired: false,
auth0Logout: true,
secret: process.env.SESSION_SECRET,
baseURL: process.env.VITE_AUTH_SERVICE_BASE_URL,
clientID: process.env.KEYCLOAK_CLIENT_ID,
issuerBaseURL: process.env.KEYCLOAK_ISSUER_BASE_URL,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
routes: {},
authorizationParams: {
response_type: 'code',
response_mode: 'query',
scope: 'openid profile email roles',
},
session: {
name: 'appSession',
cookie: {
httpOnly: true,
sameSite: 'Lax'
}
},
afterCallback: (req, res, session) => {
try{
console.log('OIDC afterCallback triggered.');
const claims = session.id_token_claims;
const { email, name } = claims;
let role = 'APP_USER';
if (
claims.resource_access &&
claims.resource_access[process.env.KEYCLOAK_CLIENT_ID] &&
claims.resource_access[process.env.KEYCLOAK_CLIENT_ID].roles && claims.resource_access[process.env.KEYCLOAK_CLIENT_ID].roles.includes('APP_ADMIN')
) {
role = 'APP_ADMIN';
}
const isWhitelisted = (role === 'APP_ADMIN' || (claims.resource_access &&
claims.resource_access[process.env.KEYCLOAK_CLIENT_ID] &&
claims.resource_access[process.env.KEYCLOAK_CLIENT_ID].roles && claims.resource_access[process.env.KEYCLOAK_CLIENT_ID].roles.includes('APP_USER')));
if (!isWhitelisted) {
console.warn(`Unauthorized login attempt via OIDC: User ${email} does not have APP_USER or APP_ADMIN role.`);
res.clearCookie('appSession');
res.redirect(`${CLIENT_URL}/login-error?error=unauthorized`);
throw new Error('User not whitelisted based on Keycloak roles.');
}
const customJWTPayload = { email, name, role };
const customToken = jwt.sign(customJWTPayload, CUSTOM_JWT_SECRET, { expiresIn: '1h' });
res.cookie(CUSTOM_TOKEN_COOKIE, customToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Lax',
maxAge: 3600000
});
console.log(`User ${email} logged in successfully via Keycloak with role ${role}. Custom JWT issued.`);
return session;
} catch(error) {
console.log("Error in callback function : ", error);
throw error;
}
}
};
app.use(auth(config));
app.use('/auth', authCustomRoutes);
app.get('/', (req, res) => {
res.send(req.oidc.isAuthenticated() ? 'Logged in via Keycloak' : 'Logged out');
});
app.listen(PORT, "0.0.0.0", () => {
console.log(`Express auth server with express-openid-connect listening on http://0.0.0.0:${PORT}`);
console.log(`Login available at: /login`);
console.log(`Logout available at: /logout`);
});
From what I can understand, the error is thrown before the afterCallback function is executed.
In keycloak, I have set up Valid redirect URI as https[://]localhost:3001/callback and web origin as https[://]localhost:3001 for development
I am sorry in advance if I have made any silly errors as I have just started learning about OAuth and openid-connect protocols recently