status:Succes from SAML - code, clientId or tabId was null - IDENTITY_PROVIDER_LOGIN_ERROR

I’ve setup Identity Brokering with external SAML IDP.
It seems as if some state was missing? The error in the log says:

Invalid request. Authorization code, clientId or tabId was null. Code=, clientId=null, tabID=null

I can’t identify what’s missing.
I have set up identical flow for comparison but with external Keycloak as SAML IDP, and I can’t find any cookie or parameter missing.

How can I work around this?

  • change the assertion consumer url to IDP-initiated login? something like

    broker/{idp-name}/endpoint/clients/{client-id}
    
  • write custom authenticator? Then… perform login with external IDP myself?

  • … any other thought?

Here is the tail of the log:

16:49:47,718 DEBUG [org.keycloak.saml.validators.ConditionsValidator] (default task-24) Evaluating Conditions of Assertion _fb47d036740a855071c3efb8c9010d48583d. notBefore=2021-04-19T16:49:17Z, notOnOrAfter=2021-04-19T16:51:17Z, updatedNotBefore: 2021-04-19T16:49:17Z, updatedOnOrAfter=2021-04-19T16:51:17Z, now: 2021-04-19T16:49:47.717Z
16:49:47,718 DEBUG [org.keycloak.saml.validators.ConditionsValidator] (default task-24) Assertion _fb47d036740a855071c3efb8c9010d48583d validity is VALID
16:49:47,718 DEBUG [org.keycloak.services.resources.IdentityBrokerService] (default task-24) Invalid request. Authorization code, clientId or tabId was null. Code=, clientId=null, tabID=null
16:49:47,718 WARN  [org.keycloak.events] (default task-24) type=IDENTITY_PROVIDER_LOGIN_ERROR, realmId=ext-broker, clientId=null, userId=null, ipAddress=10.0.3.104, error=invalidRequestMessage
16:49:47,718 ERROR [org.keycloak.services.resources.IdentityBrokerService] (default task-24) invalidRequestMessage

And the SAML Response:

<Response xmlns="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://kcloak-ext.example.com/auth/realms/ext-broker/broker/kcloak-saml/endpoint" ID="_97b420215dbfab6c91bbd82e0cdc83729f9b" IssueInstant="2021-04-19T16:49:47Z" Version="2.0">
  <ns1:Issuer xmlns:ns1="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">sepmp</ns1:Issuer>
  <Status>
    <StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
  </Status>
  <ns2:Assertion xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_fb47d036740a855071c3efb8c9010d48583d" IssueInstant="2021-04-19T16:49:47Z" Version="2.0">
    <ns2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">sepmp</ns2:Issuer>
    <ns2:Subject>
      <ns2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">sue.jones@ninja.com</ns2:NameID>
      <ns2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
        <ns2:SubjectConfirmationData NotOnOrAfter="2021-04-19T16:51:17Z" Recipient="https://kcloak-ext.example.com/auth/realms/ext-broker/broker/kcloak-saml/endpoint"/>
      </ns2:SubjectConfirmation>
    </ns2:Subject>
    <ns2:Conditions NotBefore="2021-04-19T16:49:17Z" NotOnOrAfter="2021-04-19T16:51:17Z">
      <ns2:AudienceRestriction>
        <ns2:Audience>https://kcloak-ext.example.com/auth/realms/ext-broker</ns2:Audience>
      </ns2:AudienceRestriction>
    </ns2:Conditions>
    <ns2:AuthnStatement AuthnInstant="2021-04-19T16:49:46Z" SessionIndex="ZoKClfN0m3w0ow/kPFaxgX2L8E4=Wk3cog==" SessionNotOnOrAfter="2021-04-19T16:51:17Z">
      <ns2:AuthnContext>
        <ns2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</ns2:AuthnContextClassRef>
      </ns2:AuthnContext>
    </ns2:AuthnStatement>
    <ns2:AttributeStatement>
      <ns2:Attribute Name="Email Address" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
        <ns2:AttributeValue>sue.jones@ninja.com</ns2:AttributeValue>
      </ns2:Attribute>
    </ns2:AttributeStatement>
  </ns2:Assertion>
</Response>

And the trace from SAML Tracer (POST from external IDP):

{
    "method": "POST",
    "url": "https://kcloak-ext.example.com/auth/realms/ext-broker/broker/kcloak-saml/endpoint",
    "requestId": "7994",
    "requestHeaders": [
      {
        "name": "Host",
        "value": "kcloak-ext.example.com"
      },
      {
        "name": "User-Agent",
        "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:87.0) Gecko/20100101 Firefox/87.0"
      },
      {
        "name": "Accept",
        "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
      },
      {
        "name": "Accept-Language",
        "value": "en-US,en;q=0.5"
      },
      {
        "name": "Accept-Encoding",
        "value": "gzip, deflate, br"
      },
      {
        "name": "Referer",
        "value": "https://sfederationpreprod.ninja.com/affwebservices/public/saml2sso?SPID=https%3a%2f%2fkcloak-ext.example.com%2fauth%2frealms%2fext-broker&ProtocolBinding=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST&RelayState="
      },
      {
        "name": "Content-Type",
        "value": "application/x-www-form-urlencoded"
      },
      {
        "name": "Content-Length",
        "value": "6627"
      },
      {
        "name": "Origin",
        "value": "https://sfederationpreprod.ninja.com"
      },
      {
        "name": "DNT",
        "value": "1"
      },
      {
        "name": "Connection",
        "value": "keep-alive"
      },
      {
        "name": "Cookie",
        "value": "AUTH_SESSION_ID=ff9d1688-a260-40df-849d-d04b604502da.keycloak-0; KC_RESTART=eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI5Yzk4ZDFmZi1iYmI1LTQ2YmUtYTBlNi00NzY5MTBjM2Y1NWMifQ.eyJjaWQiOiJnYXRla2VlcGVyIiwicHR5Ijoib3BlbmlkLWNvbm5lY3QiLCJydXJpIjoiaHR0cHM6Ly90YWZmaS5leGFtcGxlLmNvbS9ncmFmYW5hL29hdXRoL2NhbGxiYWNrIiwiYWN0IjoiQVVUSEVOVElDQVRFIiwibm90ZXMiOnsic2NvcGUiOiJvcGVuaWQgZW1haWwgcHJvZmlsZSIsImlzcyI6Imh0dHBzOi8va2Nsb2FrLWV4dC5leGFtcGxlLmNvbS9hdXRoL3JlYWxtcy9leHQtYnJva2VyIiwicmVzcG9uc2VfdHlwZSI6ImNvZGUiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL3RhZmZpLmV4YW1wbGUuY29tL2dyYWZhbmEvb2F1dGgvY2FsbGJhY2siLCJzdGF0ZSI6IjNjMTMzMzk0LTlhN2ItNGM1OC1hMTc2LWQ1ZDhkODQxYTVkZSJ9fQ==.lMgqoLLQWL19wVGuCi1KYNvYKCtJFolp7-FQJZxDSqQ"
      },
      {
        "name": "Upgrade-Insecure-Requests",
        "value": "1"
      }
    ],
    "postData": {
      "RelayState": [
        ""
      ],
      "SAMLResponse": [
        "(omitted)"
      ]
    },
    "post": [
      [
        "RelayState",
        ""
      ],
      [
        "SAMLResponse",
        "(omitted)"
      ]
    ],
    "protocol": "SAML-P",
    "saml": "(omitted)",
    "samlart": null,
    "responseStatus": 400,
    "responseStatusText": "HTTP/1.1 400 Bad Request",
    "responseHeaders": [
      {
        "name": "Content-Language",
        "value": "en"
      },
      {
        "name": "Content-Length",
        "value": "1602"
      },
      {
        "name": "Content-Security-Policy",
        "value": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
      },
      {
        "name": "Content-Type",
        "value": "text/html;charset=utf-8"
      },
      {
        "name": "Date",
        "value": "Mon, 19 Apr 2021 16:49:47 GMT"
      },
      {
        "name": "Strict-Transport-Security",
        "value": "max-age=31536000; includeSubDomains"
      },
      {
        "name": "X-Content-Type-Options",
        "value": "nosniff"
      },
      {
        "name": "X-Frame-Options",
        "value": "SAMEORIGIN"
      },
      {
        "name": "X-Robots-Tag",
        "value": "none"
      },
      {
        "name": "X-Xss-Protection",
        "value": "1; mode=block"
      }
    ]
  }

I have omitted the SAML response, having posted it outside.
Actually the flow looks like: (client app) → (gatekeeper-proxy) -oidc → (keycloak broker) -saml → (external IDP) → ([few redirects between internal systems]) -saml → (keycloak broker endpoint)

I got assured that the external IDP was configured to interact in SP initiated flow.

Any help would be appreciated.

I have found out that in my case the missing RelayState was causing the flow to break.

Authentication flow consisted of 3 SAML interactions:

  1. keycloak-broker → corporate-SSO (RelayState ok)
  2. corporate-SSO → active-directory-gateway (RelayState overwritten to get back to corporate-SSO)
  3. corporate-SSO → keycloak-broker (RelayState reset to empty string)

A quick’n’dirty solution would be to wrap keycloak-broker with ReverseProxy that would restore RelayState.