Skip to content

AUT-1358 Pass id_token_hint to Keycloak logout to skip confirmation#10

Open
DmitryMasley wants to merge 2 commits into
masterfrom
AUT-1358--KC26-logout-shows-confirmation-page-instead-of-redirecting-to-post-logout-URL
Open

AUT-1358 Pass id_token_hint to Keycloak logout to skip confirmation#10
DmitryMasley wants to merge 2 commits into
masterfrom
AUT-1358--KC26-logout-shows-confirmation-page-instead-of-redirecting-to-post-logout-URL

Conversation

@DmitryMasley
Copy link
Copy Markdown

Summary

  • Keycloak 26 displays a logout confirmation page unless the logout request includes id_token_hint and client_id. Keycloak.prototype.logoutUrl now accepts an optional idTokenHint and appends both query params when present.
  • The logout middleware captures request.kauth.grant.id_token.token before the grant is unstored, then passes it into logoutUrl so the user is redirected straight to the post-logout URL.
  • Adds unit tests for the new logoutUrl behavior and a new logout-middleware-test.js covering path bypass, id_token capture ordering, missing id_token, redirect URL derivation (with/without port), query.redirectUrl override, and the final redirect.
  • Gates the Jenkins Publish stage on the master branch 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
  • Manual verification against a Keycloak 26 realm: logging out redirects to the post-logout URL without the confirmation page

🤖 Generated with Claude Code

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>
Comment thread middleware/logout.js Outdated
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Above looks important, please review.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants