AUT-1358 Pass id_token_hint to Keycloak logout to skip confirmation#10
Conversation
Keycloak 26 shows a logout confirmation page unless the request includes id_token_hint + client_id. The middleware now reads the id_token from the grant before unstoring it and forwards it through Keycloak.logoutUrl, so the user is redirected straight to the post-logout URL. Also gates the Jenkins Publish stage on the master branch to avoid publishing from feature branches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| let port = headerHost[1] || ''; | ||
| let redirectUrl = request.query.redirectUrl || request.protocol + '://' + host + (port === '' ? '' : ':' + port) + '/'; | ||
|
|
||
| const idTokenHint = request.kauth.grant.id_token && request.kauth.grant.id_token.token; |
There was a problem hiding this comment.
Critical bug — TypeError when session has no grant
setup.js initializes request.kauth = {} with no grant property. When a logout request arrives for a user whose session has fully expired (both access and refresh tokens expired), grant-attacher.js calls next() without setting request.kauth.grant, leaving it undefined.
The line:
const idTokenHint = request.kauth.grant.id_token && request.kauth.grant.id_token.token;is evaluated before the if (request.kauth.grant) guard below it. Accessing .id_token on undefined throws:
TypeError: Cannot read properties of undefined (reading 'id_token')
This crashes the logout middleware for unauthenticated or expired-session requests — the exact edge case the spec document anticipated and explicitly guarded against (see aut-1358-kc26-logout-redirect-options.md, lines 95–99):
const idToken = request.kauth.grant && request.kauth.grant.id_token && request.kauth.grant.id_token.token;Fix — add the outer guard:
const idTokenHint = request.kauth.grant && request.kauth.grant.id_token && request.kauth.grant.id_token.token;Or with optional chaining (Node 16 supports it):
const idTokenHint = request.kauth.grant?.id_token?.token;A test for this case is also missing — 'does not throw when grant has no id_token' only removes id_token from an existing grant object. The absent-grant scenario should be covered separately:
test('logout middleware: does not throw when kauth has no grant', t => {
const { keycloak, calls } = buildKeycloakStub();
const middleware = logoutMiddleware(keycloak, '/logout');
const req = buildRequest();
delete req.kauth.grant; // simulate expired session
const res = buildResponse();
t.doesNotThrow(() => {
middleware(req, res, () => t.fail('next() should not be called'));
});
t.equal(calls.logoutUrl[0].idTokenHint, undefined, 'idTokenHint should be undefined');
t.end();
});There was a problem hiding this comment.
Above looks important, please review.
There was a problem hiding this comment.
Good catch — fixed in d498ced.
The guard now covers grant itself:
const idTokenHint = request.kauth.grant && request.kauth.grant.id_token && request.kauth.grant.id_token.token;(request.kauth is always truthy because setup.js initializes it to {}; grant is the field that can be absent.)
Added the missing test logout middleware: does not throw when kauth has no grant (expired session) — it deletes req.kauth.grant, asserts the middleware doesn't throw, that logoutUrl is still invoked with idTokenHint === undefined, that deauthenticated is skipped, and that the redirect still happens. All 35 tests pass locally.
Accessing request.kauth.grant.id_token threw a TypeError when the session had no grant (expired session), because the guard one block down was evaluated too late. Guard the grant lookup itself and add a test that covers the expired-session path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
id_token_hintandclient_id.Keycloak.prototype.logoutUrlnow accepts an optionalidTokenHintand appends both query params when present.request.kauth.grant.id_token.tokenbefore the grant is unstored, then passes it intologoutUrlso the user is redirected straight to the post-logout URL.logoutUrlbehavior and a newlogout-middleware-test.jscovering path bypass, id_token capture ordering, missing id_token, redirect URL derivation (with/without port),query.redirectUrloverride, and the final redirect.Publishstage on themasterbranch so feature branches don't trigger publishes.Test plan
npx tape test/unit/keycloak-object-test.js test/unit/logout-middleware-test.js— 30/30 pass🤖 Generated with Claude Code