Passport Keycloak Bearer Strategy
Passport-Keycloak-Bearer is an HTTP Bearer authentication strategy designed for Passport.js, enabling Node.js applications to authenticate requests against a Keycloak identity provider using OAuth 2.0 bearer tokens. This package, currently at version 2.4.1, integrates seamlessly with Connect-style middleware frameworks like Express.js. It focuses on extracting, validating, and propagating JWT claims from access tokens to a `verify` callback, allowing developers to process user information and attach it to `req.user`. While there isn't an explicit release cadence mentioned, the versioning suggests ongoing maintenance. Its key differentiator is the direct integration with Keycloak's token validation, simplifying the setup for Keycloak-backed applications compared to generic JWT strategies that require manual configuration of Keycloak's public keys and issuer metadata.
Common errors
-
Error: bearer token not found
cause The incoming HTTP request does not contain an `Authorization` header with a `Bearer` token, or the token is malformed.fixEnsure the client is sending a request with an `Authorization: Bearer <your_jwt_token>` header. Verify the token format and that it's present in the request. -
JsonWebTokenError: invalid algorithm
cause The JWT token's signing algorithm does not match any of the algorithms specified in the `algorithms` option passed to `KeycloakBearerStrategy`.fixCheck the `alg` header in your JWT token and ensure it's included in the `algorithms` array option when initializing `KeycloakBearerStrategy`. For Keycloak, this is typically `RS256` or `PS256` for production environments. -
TokenExpiredError: jwt expired
cause The provided JWT bearer token has exceeded its `exp` (expiration) claim and is no longer valid, and `ignoreExpiration` is not set to `true`.fixObtain a new, valid (unexpired) JWT token from Keycloak. Implement a token refresh mechanism on the client side to proactively renew tokens before they expire. If strictly necessary for testing, set `ignoreExpiration: true` (with caution).
Warnings
- breaking Older versions (pre-2.x) of `passport-keycloak-bearer` might have different constructor signatures or option configurations. Always consult the specific version's README for exact usage, especially when upgrading across major versions.
- gotcha The `url` and `realm` options are critically important and must accurately point to your Keycloak server and specific realm. Misconfigurations here will lead to failed token validation or inability to retrieve public keys.
- gotcha By default, `passport-keycloak-bearer` validates the token's expiration. If `ignoreExpiration` is set to `true` (which is generally discouraged in production for security reasons), expired tokens will still be accepted.
- gotcha The `verify` callback is responsible for determining if a user is successfully authenticated and what user object should be attached to `req.user`. Incorrectly calling `done(null, false)` or `done(error)` can lead to unexpected authentication failures or errors.
Install
-
npm install passport-keycloak-bearer -
yarn add passport-keycloak-bearer -
pnpm add passport-keycloak-bearer
Imports
- KeycloakBearerStrategy
import { KeycloakBearerStrategy } from 'passport-keycloak-bearer'import KeycloakBearerStrategy from 'passport-keycloak-bearer'
- passport
const passport = require('passport')import passport from 'passport'
- Strategy Options (Type)
import type { IKeycloakBearerStrategyOptions } from 'passport-keycloak-bearer'
Quickstart
import express from 'express';
import passport from 'passport';
import KeycloakBearerStrategy from 'passport-keycloak-bearer';
const app = express();
// --- IMPORTANT: Replace with your actual Keycloak configuration ---
const KEYCLOAK_URL = process.env.KEYCLOAK_AUTH_URL ?? 'https://your-keycloak-instance.com/auth';
const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM ?? 'your-realm';
// ------------------------------------------------------------------
passport.use(new KeycloakBearerStrategy({
url: KEYCLOAK_URL,
realm: KEYCLOAK_REALM,
passReqToCallback: false // Set to true if your verify callback needs the request object
}, (jwtPayload, done) => {
// In a real application, you would fetch user details from a DB
// or create a user object based on the JWT payload (e.g., jwtPayload.sub)
const user = { id: jwtPayload.sub, username: jwtPayload.preferred_username, roles: jwtPayload.realm_access?.roles };
// Example: only allow users with 'api-user' role
if (user.roles && user.roles.includes('api-user')) {
return done(null, user); // User authenticated successfully
} else {
return done(null, false, { message: 'User does not have required roles.' }); // Authentication failed
}
}));
app.use(passport.initialize());
// A protected route
app.get('/api/protected', passport.authenticate('keycloak-bearer', { session: false }), (req, res) => {
// req.user will contain the user object returned from the verify callback
res.json({ message: 'Access granted!', user: req.user });
});
// Public route
app.get('/api/public', (req, res) => {
res.json({ message: 'This is a public endpoint.' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log(`Test with curl -H "Authorization: Bearer <YOUR_KEYCLOAK_JWT_TOKEN>" http://localhost:${PORT}/api/protected`);
});