<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jihed Ben Arfa</title>
    <description>The latest articles on DEV Community by Jihed Ben Arfa (@jihedbfrart).</description>
    <link>https://dev.to/jihedbfrart</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035892%2F7b83a670-a826-4d37-b3ac-467b6cd58bda.jpg</url>
      <title>DEV Community: Jihed Ben Arfa</title>
      <link>https://dev.to/jihedbfrart</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jihedbfrart"/>
    <language>en</language>
    <item>
      <title>Instrumenting a Keycloak SPI provider so the next silent JDBC bug doesn't need a code review to find it</title>
      <dc:creator>Jihed Ben Arfa</dc:creator>
      <pubDate>Sun, 19 Jul 2026 18:08:13 +0000</pubDate>
      <link>https://dev.to/jihedbfrart/instrumenting-a-keycloak-spi-provider-so-the-next-silent-jdbc-bug-doesnt-need-a-code-review-to-563a</link>
      <guid>https://dev.to/jihedbfrart/instrumenting-a-keycloak-spi-provider-so-the-next-silent-jdbc-bug-doesnt-need-a-code-review-to-563a</guid>
      <description>&lt;p&gt;&lt;strong&gt;Project&lt;/strong&gt;: &lt;a href="https://github.com/jihedbfr-art/keycloak-spi-workbench" rel="noopener noreferrer"&gt;keycloak-spi-workbench&lt;/a&gt; — custom Keycloak SPI providers, one of them a read-only user storage federation (&lt;code&gt;legacy-user-storage&lt;/code&gt;) that reads users from an existing JDBC table instead of forcing a big-bang migration into Keycloak's own store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happened first, no Sentry involved&lt;/strong&gt;: &lt;code&gt;LegacyUserStorageProvider#searchForUserByUserAttributeStream&lt;/code&gt; was calling &lt;code&gt;getUserByUsername&lt;/code&gt; twice per invocation — once for a null check, once again to build the returned stream:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;getUserByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;realm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrValue&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
        &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getUserByUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;realm&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrValue&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;empty&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two calls, two separate JDBC round trips, against a repository with no connection pooling (&lt;code&gt;LegacyUserRepository&lt;/code&gt; opens a fresh &lt;code&gt;Connection&lt;/code&gt; per query, on purpose — see the repo's README on why). Every username-attribute lookup, twice the DB load it needed. No exception, no wrong result, just silently double the work on every call. Caught by reading the method, not by any tool — fixed by storing the result once and reusing it, with a regression test that fails if it comes back.&lt;/p&gt;

&lt;p&gt;That's exactly the kind of bug that's invisible until someone happens to read that method, and the reason I added Sentry next: catching this again shouldn't depend on a code review catching it first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where Sentry goes in&lt;/strong&gt;: &lt;code&gt;LegacyUserRepository&lt;/code&gt;'s three query methods (&lt;code&gt;findOneWhere&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;) now open a child span under whatever transaction Keycloak's own request tracing has active — &lt;code&gt;legacy_db.find_one&lt;/code&gt;, &lt;code&gt;legacy_db.search&lt;/code&gt; — and &lt;code&gt;search&lt;/code&gt; records how many rows came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;ISpan&lt;/span&gt; &lt;span class="nf"&gt;startSpan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ISpan&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Sentry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSpan&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startChild&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;NoOpSpan&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Falls back to &lt;code&gt;NoOpSpan&lt;/code&gt; when there's no active transaction — never throws, never blocks a login on Sentry being reachable. Any &lt;code&gt;SQLException&lt;/code&gt; also gets &lt;code&gt;Sentry.captureException(e)&lt;/code&gt; before it's wrapped and rethrown, so a failing legacy database shows up as an event, not just a stack trace in a Keycloak log nobody's tailing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part that mattered more than the instrumentation itself&lt;/strong&gt;: making it opt-in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Scope&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SENTRY_DSN"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isBlank&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;Sentry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEnabled&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Sentry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDsn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dsn&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTracesSampleRate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEnvironment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getOrDefault&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SENTRY_ENVIRONMENT"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;SENTRY_DSN&lt;/code&gt; set, &lt;code&gt;Sentry.init&lt;/code&gt; never runs, every span call resolves to &lt;code&gt;NoOpSpan&lt;/code&gt;, every &lt;code&gt;captureException&lt;/code&gt; call is a no-op. This is a provider other people install into their own Keycloak instance — it doesn't get to assume they want a Sentry account wired into their auth server, and it doesn't get to silently start sending data anywhere without an explicit environment variable making that choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this actually earns its keep&lt;/strong&gt;: a per-user JDBC query duplicating itself is the boring, common case. The interesting one is a legacy database that's slow for one specific customer's data shape, or a driver that starts throwing intermittently under load — the &lt;code&gt;search&lt;/code&gt; span's &lt;code&gt;result_count&lt;/code&gt; and the per-query timing are exactly what would surface a slow query pattern before it shows up as "login is slow" tickets with nothing pointing at the actual repository call underneath.&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>keycloak</category>
      <category>java</category>
    </item>
    <item>
      <title>The bug that only showed up because my test was too fast</title>
      <dc:creator>Jihed Ben Arfa</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:59:34 +0000</pubDate>
      <link>https://dev.to/jihedbfrart/the-bug-that-only-showed-up-because-my-test-was-too-fast-3l8l</link>
      <guid>https://dev.to/jihedbfrart/the-bug-that-only-showed-up-because-my-test-was-too-fast-3l8l</guid>
      <description>&lt;p&gt;A couple hundred milliseconds cost me an hour, and the annoying part is I'd written the fix for it before I even had the bug.&lt;/p&gt;

&lt;p&gt;I was writing a Testcontainers deployment check for a custom Keycloak authenticator — boot a real Keycloak server with the built provider jar dropped into &lt;code&gt;providers/&lt;/code&gt;, hit the admin REST API, confirm the provider shows up. First pass, I picked the wait strategy the way you'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitingFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forLogMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".*Running the server in development mode.*"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keycloak prints that line during &lt;code&gt;start-dev&lt;/code&gt;, so it felt like a reasonable "the server is up" signal. The test ran, and about one time in three it failed with a plain connection reset trying to hit the admin token endpoint. No stack trace pointing anywhere useful, just "connection refused," which is the least helpful error message a network client can give you.&lt;/p&gt;

&lt;p&gt;The instinct is to blame Testcontainers, or Docker networking, or just add a &lt;code&gt;Thread.sleep(2000)&lt;/code&gt; and move on. I've done that before and regretted it — a sleep just moves the flake to a slower CI runner instead of fixing it.&lt;/p&gt;

&lt;p&gt;What was actually happening: that log line prints the moment Keycloak's startup sequence reaches "development mode," which is before the HTTP listener has finished binding and accepting connections. The gap is small — a couple hundred milliseconds most of the time — which is exactly why it only failed sometimes. Fast enough CI runner, you never see it. Slightly loaded runner, the test wins the race against the listener and gets a reset.&lt;/p&gt;

&lt;p&gt;The fix was smaller than the investigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitingFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forHttp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/realms/master"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forStatusCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withStartupTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMinutes&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Poll the actual endpoint you're about to call, instead of trusting a log line that tells you what the server is &lt;em&gt;doing&lt;/em&gt;, not what it's &lt;em&gt;ready for&lt;/em&gt;. Obvious in hindsight. It always is.&lt;/p&gt;

&lt;p&gt;The part that stuck with me: a log-message wait strategy will pass code review without question, because it reads as "wait until the server says it's running" — which sounds exactly right. It just isn't the same claim as "wait until the server is accepting connections," and nothing about the code makes that distinction visible. You only find the gap between those two claims by having the test flake on you, ideally in CI where you can't step through it, which is the least convenient possible place to learn it.&lt;/p&gt;

&lt;p&gt;Same class of bug, one level up: don't trust a stated intention, wait on the actual side effect you depend on. Applies well past Testcontainers — anywhere something logs "starting" before it's actually ready to be used.&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>testing</category>
      <category>java</category>
    </item>
    <item>
      <title>Keycloak lets you pick a login option that's guaranteed to fail — here's the fix</title>
      <dc:creator>Jihed Ben Arfa</dc:creator>
      <pubDate>Sun, 19 Jul 2026 17:59:32 +0000</pubDate>
      <link>https://dev.to/jihedbfrart/keycloak-lets-you-pick-a-login-option-thats-guaranteed-to-fail-heres-the-fix-2jbn</link>
      <guid>https://dev.to/jihedbfrart/keycloak-lets-you-pick-a-login-option-thats-guaranteed-to-fail-heres-the-fix-2jbn</guid>
      <description>&lt;p&gt;&lt;strong&gt;Project&lt;/strong&gt;: &lt;a href="https://github.com/keycloak/keycloak" rel="noopener noreferrer"&gt;keycloak/keycloak&lt;/a&gt; — the identity and access management server. PR: &lt;a href="https://github.com/keycloak/keycloak/pull/51021" rel="noopener noreferrer"&gt;#51021&lt;/a&gt;, fixing &lt;a href="https://github.com/keycloak/keycloak/issues/46481" rel="noopener noreferrer"&gt;#46481&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bug&lt;/strong&gt;: if you write a custom Keycloak &lt;code&gt;Authenticator&lt;/code&gt; that isn't a &lt;code&gt;CredentialValidator&lt;/code&gt; — anything that isn't a password, OTP, WebAuthn, that kind of thing — and it requires a user to already be identified, Keycloak's "Try another way" screen will offer it as an option even when it isn't actually usable for that user. Pick it, and you get an immediate &lt;code&gt;CREDENTIAL_SETUP_REQUIRED&lt;/code&gt; error. The option shouldn't have been on the screen in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;, traced through two files in the &lt;code&gt;services&lt;/code&gt; module:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AuthenticationSelectionResolver#addSimpleAuthenticationExecution&lt;/code&gt; decides what goes on the selection list. It only checks one thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Authenticator&lt;/span&gt; &lt;span class="n"&gt;localAuthenticator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getProvider&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Authenticator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAuthenticator&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;localAuthenticator&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;CredentialValidator&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;nonCredentialExecutions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;execution&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a &lt;code&gt;CredentialValidator&lt;/code&gt;? Straight onto the list, no further questions. Nothing here checks &lt;code&gt;authenticator.configuredFor(...)&lt;/code&gt; or the factory's &lt;code&gt;isUserSetupAllowed()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There's a second, separate instance of the same gap. &lt;code&gt;DefaultAuthenticationFlow.processSingleFlowExecutionModel&lt;/code&gt; has its own &lt;code&gt;configuredFor&lt;/code&gt;/&lt;code&gt;isUserSetupAllowed&lt;/code&gt; guard meant to catch exactly this — but only for &lt;code&gt;requiresUser() == false&lt;/code&gt; authenticators, and only if they're a &lt;code&gt;CredentialValidator&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;authUser&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;authenticator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;configuredFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSession&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRealm&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;authUser&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isUserSetupAllowed&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authenticator&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;CredentialValidator&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationFlowException&lt;/span&gt;&lt;span class="o"&gt;(...);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;instanceof CredentialValidator&lt;/code&gt; at the end means a plain authenticator in that exact shape runs &lt;code&gt;authenticate()&lt;/code&gt; completely unguarded, even as a &lt;code&gt;REQUIRED&lt;/code&gt; step — a code path that never goes through the selection list at all, so fixing the resolver alone wouldn't have caught it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: added &lt;code&gt;isSelectableForCurrentUser&lt;/code&gt; to the resolver — authenticators that don't require a user pass straight through, the rest only get listed if they're configured for the current user or the factory allows self-setup on the spot. And dropped the &lt;code&gt;instanceof CredentialValidator&lt;/code&gt; restriction in the flow's own guard, so it applies the same way the &lt;code&gt;requiresUser() == true&lt;/code&gt; branch right above it already does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test&lt;/strong&gt;: &lt;code&gt;AuthenticationSelectionResolverTest&lt;/code&gt;, five cases covering the full matrix — no user yet, configured, not configured but self-setup allowed, not configured and not allowed, and authenticators that don't require a user at all (always selectable, regardless of the rest).&lt;/p&gt;

&lt;p&gt;This one didn't need any special tooling — just reading the two files the original report pointed at and following the logic all the way to where it actually breaks, which is most of what real bug fixing is.&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>devchallenge</category>
      <category>keycloak</category>
      <category>java</category>
    </item>
    <item>
      <title>Testing Keycloak SPIs with Testcontainers — the part every tutorial skips</title>
      <dc:creator>Jihed Ben Arfa</dc:creator>
      <pubDate>Sat, 18 Jul 2026 23:39:15 +0000</pubDate>
      <link>https://dev.to/jihedbfrart/testing-keycloak-spis-with-testcontainers-the-part-every-tutorial-skips-4loe</link>
      <guid>https://dev.to/jihedbfrart/testing-keycloak-spis-with-testcontainers-the-part-every-tutorial-skips-4loe</guid>
      <description>&lt;p&gt;Most "here's how to write a custom Keycloak authenticator" posts end at &lt;code&gt;mvn package&lt;/code&gt;. You get a class that implements &lt;code&gt;Authenticator&lt;/code&gt;, a factory, a &lt;code&gt;META-INF/services&lt;/code&gt; file, and a "drop the jar in providers/ and restart" instruction — and that's where the tutorial stops. Nobody shows you how to actually test the thing, which is a problem, because SPI development has a genuinely nasty feedback loop: package, copy the jar, restart Keycloak, click through a login flow by hand, repeat. I've spent enough evenings doing that manually on custom authenticators to want it gone.&lt;/p&gt;

&lt;p&gt;The unit-testable part is easy. If your authenticator's logic is a pure function of some inputs (a user attribute, a client ID, a config value), you extract that into a method and mock the Keycloak model objects around it. That's normal JUnit + Mockito, nothing SPI-specific. The part that actually needs a real server is: does the jar &lt;em&gt;deploy&lt;/em&gt;? Does &lt;code&gt;META-INF/services&lt;/code&gt; point at the right class? Did you get a method signature wrong that only shows up as a stack trace on server startup? None of that shows up in a unit test, because a unit test never boots Keycloak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Booting a real Keycloak in a test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Testcontainers makes this less painful than it sounds. You start the official image, copy your built jar straight into &lt;code&gt;providers/&lt;/code&gt; before the container starts, and let Keycloak boot normally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Container&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;GenericContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;keycloak&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GenericContainer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"quay.io/keycloak/keycloak:26.6"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withCopyFileToContainer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MountableFile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forHostPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builtJarPath&lt;/span&gt;&lt;span class="o"&gt;()),&lt;/span&gt; &lt;span class="s"&gt;"/opt/keycloak/providers/my-spi.jar"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withEnv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KEYCLOAK_ADMIN"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withEnv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"KEYCLOAK_ADMIN_PASSWORD"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"admin"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withExposedPorts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withCommand&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start-dev"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitingFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Wait&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forHttp&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/realms/master"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;forStatusCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The wait strategy is the one detail that'll cost you an hour if you get it wrong. My first pass used &lt;code&gt;Wait.forLogMessage(".*Running the server in development mode.*", 1)&lt;/code&gt; — that line prints during startup, but &lt;em&gt;before&lt;/em&gt; the HTTP listener is actually accepting connections. The test would then immediately try to hit the admin API and get a connection reset, because the server said "I'm running" a couple hundred milliseconds before it actually was. Polling the real endpoint with &lt;code&gt;Wait.forHttp(...)&lt;/code&gt; instead of trusting a log line fixed it — obvious in hindsight, and exactly the kind of thing you only learn by having the test flake on you in CI.&lt;/p&gt;

&lt;p&gt;Once the container's up, you hit the admin REST API the same way an admin console would:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;HttpRequest&lt;/span&gt; &lt;span class="n"&gt;providersRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/admin/realms/master/authentication/authenticator-providers"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;accessToken&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;GET&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and assert your provider ID shows up in the list. That's it — that's the check that actually catches packaging mistakes: wrong &lt;code&gt;META-INF/services&lt;/code&gt; entry, a factory that throws in &lt;code&gt;init()&lt;/code&gt;, a missing dependency that only fails at classload time. None of that surfaces in a unit test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same idea, different provider type&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built a second provider — an event listener that publishes login events to Kafka — right after, and the same "don't mock the thing you're trying to prove works" logic applied to it, just with a different target. Mocking the &lt;code&gt;KafkaProducer&lt;/code&gt; in a test would let a subtly wrong producer config (bad serializer, bad &lt;code&gt;acks&lt;/code&gt; setting) pass silently. So instead of mocking, the integration test spins up a real Kafka broker via Testcontainers, constructs the provider with a real producer pointed at it, fires an event, and reads the message back with a real consumer. If the JSON shape is wrong, or the key/value serializers are misconfigured, the test catches it — a mock never would have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Maven wiring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One thing that trips people up: these integration tests need the packaged jar to already exist, so they can't run in the same phase as unit tests. Surefire runs unit tests at &lt;code&gt;test&lt;/code&gt; (excluding anything named &lt;code&gt;*IT.java&lt;/code&gt;), and Failsafe runs the integration tests at &lt;code&gt;verify&lt;/code&gt;, after &lt;code&gt;package&lt;/code&gt; has already produced the jar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-surefire-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;excludes&amp;gt;&amp;lt;exclude&amp;gt;&lt;/span&gt;**/*IT.java&lt;span class="nt"&gt;&amp;lt;/exclude&amp;gt;&amp;lt;/excludes&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-failsafe-plugin&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;executions&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;execution&amp;gt;&amp;lt;goals&amp;gt;&amp;lt;goal&amp;gt;&lt;/span&gt;integration-test&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&amp;lt;goal&amp;gt;&lt;/span&gt;verify&lt;span class="nt"&gt;&amp;lt;/goal&amp;gt;&amp;lt;/goals&amp;gt;&amp;lt;/execution&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/executions&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mvn test&lt;/code&gt; for the fast feedback loop while you're writing the logic, &lt;code&gt;mvn verify&lt;/code&gt; when you actually need to know the thing deploys — which is also what CI runs, so a green build actually means something.&lt;/p&gt;

&lt;p&gt;None of this is complicated once it's set up. It's just work nobody writes about, because "here's a class that implements an interface" is a much shorter blog post than "here's how you prove the class actually works on a real server." The second one's the only one that matters once you're shipping.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>testing</category>
      <category>testcontainers</category>
      <category>java</category>
    </item>
    <item>
      <title>Your Keycloak roles aren't working in Spring Security. Here's the actual reason.</title>
      <dc:creator>Jihed Ben Arfa</dc:creator>
      <pubDate>Sat, 18 Jul 2026 23:39:14 +0000</pubDate>
      <link>https://dev.to/jihedbfrart/your-keycloak-roles-arent-working-in-spring-security-heres-the-actual-reason-8fo</link>
      <guid>https://dev.to/jihedbfrart/your-keycloak-roles-arent-working-in-spring-security-heres-the-actual-reason-8fo</guid>
      <description>&lt;p&gt;If you've wired up a Spring Boot resource server behind Keycloak and your &lt;code&gt;@PreAuthorize("hasRole('ADMIN')")&lt;/code&gt; is silently returning 403 for a user you can see has the ADMIN role in the admin console, you're not going crazy. This happens on basically every first Keycloak + Spring Security integration, and the reason is boring once you see it: Spring Security's default JWT converter has no idea Keycloak exists.&lt;/p&gt;

&lt;p&gt;I've hit this on every Keycloak-secured backend I've built over the past couple of years — most recently on a multi-service Spring Boot 3 platform where I ended up owning the IAM layer, including custom Keycloak SPI authenticators. Same wiring problem, every time, until I finally pulled the fix out into a small library instead of copy-pasting it again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The actual token shape&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spring Security's &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt; expects authorities to come from a &lt;code&gt;scope&lt;/code&gt; (or &lt;code&gt;scp&lt;/code&gt;) claim — a flat, space-separated string, OAuth2-style. That's the generic spec-y default. Keycloak doesn't do that. A Keycloak access token puts roles here instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"realm_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"offline_access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uma_authorization"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource_access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"my-client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ADMIN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VIEWER"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Realm roles under &lt;code&gt;realm_access.roles&lt;/code&gt;, client roles nested under &lt;code&gt;resource_access.&amp;lt;clientId&amp;gt;.roles&lt;/code&gt;. Spring Security never looks there. So &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt; runs, finds no &lt;code&gt;scope&lt;/code&gt; claim, produces zero &lt;code&gt;GrantedAuthority&lt;/code&gt; objects, and every &lt;code&gt;hasRole(...)&lt;/code&gt; check quietly fails — no error, no stack trace, just "access denied" for a user who is very clearly in the right group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix, the manual version&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You write your own &lt;code&gt;Converter&amp;lt;Jwt, AbstractAuthenticationToken&amp;gt;&lt;/code&gt; that reads both claims, flattens them into &lt;code&gt;SimpleGrantedAuthority&lt;/code&gt; with a &lt;code&gt;ROLE_&lt;/code&gt; prefix (Spring Security's convention), and registers it on the &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;KeycloakRealmRoleConverter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Converter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Jwt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GrantedAuthority&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Collection&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;GrantedAuthority&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Jwt&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;realmAccess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClaim&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"realm_access"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;realmAccess&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
                &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;)&lt;/span&gt; &lt;span class="n"&gt;realmAccess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrDefault&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"roles"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SimpleGrantedAuthority&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ROLE_"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it into a &lt;code&gt;JwtAuthenticationConverter&lt;/code&gt;, register that as a bean, and roles start working. That's maybe twenty lines and it's fine for a single service. The annoying part is you'll write this exact converter again on the next service, and the one after that, usually slightly differently each time (some people prefix with &lt;code&gt;ROLE_&lt;/code&gt;, some don't, some forget client roles exist at all and only handle realm roles).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I landed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After the third time writing basically the same class, I pulled it into &lt;a href="https://github.com/jihedbfr-art/spring-keycloak-toolkit" rel="noopener noreferrer"&gt;spring-keycloak-toolkit&lt;/a&gt; — an auto-configuration that registers the converter for you, handles both realm and client roles, and also fixes the second thing that bites you right after the first one: Spring Security's default 401/403 responses are empty bodies, which is useless if anything downstream (a frontend, an API gateway, a support engineer reading logs) needs to know &lt;em&gt;why&lt;/em&gt; a request got rejected. The library adds RFC 7807 &lt;code&gt;problem+json&lt;/code&gt; bodies for both cases instead.&lt;/p&gt;

&lt;p&gt;What it deliberately does &lt;em&gt;not&lt;/em&gt; do is touch your &lt;code&gt;SecurityFilterChain&lt;/code&gt; or guess which endpoints should be public. I thought about auto-wiring that too, and decided against it — guessing your endpoint matchers wrong and failing silently is worse than making you write four lines of config yourself. A library that gets your security posture subtly wrong is more dangerous than one that does less.&lt;/p&gt;

&lt;p&gt;If you're wiring up Keycloak with Spring Security and hitting this, the manual converter above will get you unstuck in five minutes. If you're doing it more than once, the library's on Maven via JitPack — link's in the repo.&lt;/p&gt;

</description>
      <category>keycloak</category>
      <category>spring</category>
      <category>java</category>
      <category>security</category>
    </item>
  </channel>
</rss>
