Help with understanding Keycloak as Identity Broker

Hello, I’m trying to build a small Proof of Concept where Keycloak acts as an Identity Broker for an IBM Identity Provider, named WebSeal.
I have used KC before (simple OICD) but I’m struggling to understand how the auth flow would work, and I was wondering if I could get some pointers here.

WebSeal exposes its own login form and, on successful authentication, it places the username of the logged-user in the response HEADER and redirects to a configured URL (called junction).
My understanding is that I should create a custom authenticator that would use its own authentication flow. Am I on the correct path?

Thanks!

In this example, WebSeal is the Identity Provider. You should look into implementing a custom IdentityProvider and IdentityProviderFactory, since it sounds like WebSeal does not support the OIDC or SAML protocols that Keycloak does. Take a look at org.keycloak.broker.provider.AbstractIdentityProvider and org.keycloak.broker.provider.AbstractIdentityProviderFactory as well as their implementations in the Keycloak source for guidance on implementation.

1 Like

Yes, exactly. WebSeal is not very advanced.
What I’m still struggling to grasp is how does Keycloak redirects to the WebSeal login page? Is it done by implementing a custom IdentityProvider (based on the above-mentioned AbstractIdentityProvider?

I believe it is the performLogin​(AuthenticationRequest request) method, which returns a javax.ws.rs.core.Response, which you can use to redirect to the WebSeal login page. The javadoc for that says:

Initiates the authentication process by sending an authentication request to an identity provider.

From the looks of the social identity providers, they create the remote login URI, and then use a seeOther to redirect. E.g.

  URI authorizationUrl = createAuthorizationUrl(request).build();
  return Response.seeOther(authorizationUrl).build();

Similarly, the handler method for when the request gets handed back from WebSeal is callback​(RealmModel realm, IdentityProvider.AuthenticationCallback callback, EventBuilder event).

JAXRS callback endpoint for when the remote IDP wants to callback to keycloak.

I would start with an IdentityProvider stub and some logging, and then figure out what methods get called where. Sadly, the documentation is going to be thin here, so your best bet is to just tinker.

Yes, I have found some code on Github and I’m going more or less in the direction you are pointing, will report back.

1 Like

Please share your findings, as this is an area I haven’t seen many good examples.

Reporting back on my progress so far. :grimacing:

I have created a simple IdentityProvider which is configured to use the external Provider for the login form.

@Override
    public Response performLogin(AuthenticationRequest request) {
        try {
            URI webSealLogin = new URI("......");
            return Response.seeOther(webSealLogin).build();
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

I have duplicated the standard browser flow and replaced the Identity Provider Redirector step to use my custom provider, which is deployed in Keycloak.

I have created a new client for my realm with an “Authentication flow override”, that uses my duplicated browser flow.
When I try to log in, I’m actually redirected to the login page of the third-party provider and upon login, the request is redirected to the URL specified in my Identity Provider configuration.

At that point, the server gives me this error:

ERROR [org.keycloak.services.resources.IdentityBrokerService] (executor-thread-0) invalidRequestMessage
WARN  [org.keycloak.events] (executor-thread-0) type=IDENTITY_PROVIDER_LOGIN_ERROR, realmId=3aed2837-2aa2-4bf7-a548-9970566035fe, clientId=null, userId=null, ipAddress=127.0.0.1, error=invalidRequestMessage

The code that I wrote is kind of a frankenstein of different bits that I found floating around, the COMPLETE lack of documentation makes me wonder why Keycloak is even allowing to build custom IdP.

Anyway, there are a couple of things that I still do not understand, apart from the above-mentioned error.

  1. when I log in on the third-party provider, the provider responds with a value in the header corresponding to the userid. Do I need to have a user in Keycloak matching that userid? I suppose I don’t.
  2. When I configure the custom Identity Provider, I’m presented with a “General Settings” page, that among other things, forces me to add a client id and a `client secret". Do these value have to match the id and secret of my domain?

Thanks!

For this case, you’re correct. There is almost no documentation other than examples in the code. However, your use case is possible.

Can you post your code for your IdentityProvider implementation, and a capture of the response coming back from WebSeal? It would help us debug if we could see exactly what you are seeing.

  1. when I log in on the third-party provider, the provider responds with a value in the header corresponding to the userid. Do I need to have a user in Keycloak matching that userid? I suppose I don’t.

No. But this should be set in the BrokeredIdentityContext as the brokerUserId.

  1. When I configure the custom Identity Provider, I’m presented with a “General Settings” page, that among other things, forces me to add a client id and a `client secret". Do these value have to match the id and secret of my domain?

I think you can ignore all of that for now. Building a config UI for a custom IdentityProvider is another step that you probably don’t need to take, as you’re not (yet) making it configurable for anyone’s use, so you can hard code anything that is required by WebSeal for now.

Can you post your code for your IdentityProvider implementation

Sure, thanks for your help.

This is the Provider

public class WebSealIdentityProvider extends AbstractIdentityProvider<WebSealIdentityProviderConfig> {

    public WebSealIdentityProvider(KeycloakSession session, WebSealIdentityProviderConfig config) {
        super(session, config);
    }

    @Override
    public Response retrieveToken(KeycloakSession keycloakSession, FederatedIdentityModel identity) {

        return Response.ok(identity.getToken()).type(MediaType.APPLICATION_JSON).build();
    }

    @Override
    public Response performLogin(AuthenticationRequest request) {
        try {
            URI webSealLogin = new URI("http://localhost:80/webseal");
            return Response.seeOther(webSealLogin).build();
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Object callback(RealmModel realm, AuthenticationCallback callback, EventBuilder event) {

        System.out.println("CALLBACK --->  - " + callback.toString());

        return new WebSealEndpoint(this, this.getConfig() , callback);
    }
}

The performLogin is redirecting to a local Docker container running WebSeal, hence the localhost

This is the WebSealEndpoint, where I think the problem is:

public class WebSealEndpoint {


    private WebSealIdentityProvider provider;

    private WebSealIdentityProviderConfig config;
    private IdentityProvider.AuthenticationCallback callback;
   

    @Context
    protected HttpHeaders headers;

    private static final String EFIKEY = "EFIAPPUSERNAME";

    @Context
    protected KeycloakSession session;
    public WebSealEndpoint(
                     final WebSealIdentityProvider provider, 
                     final WebSealIdentityProviderConfig config,
                    final IdentityProvider.AuthenticationCallback callback) {
        this.provider = provider;
        this.config = config;
        this.callback = callback;
    }


    @GET
    public Response done(@Context HttpHeaders headers) {

        Map<String, Cookie> cookies = headers.getCookies();
        String id = getUsername(headers);

        AuthenticationSessionModel authSession = this.callback.getAndVerifyAuthenticationSession(id);
        session.getContext().setAuthenticationSession(authSession);
// The above 2 lines are necessary to avoid -> java.lang.NullPointerException: Cannot invoke "org.keycloak.sessions.AuthenticationSessionModel.getClient()" because "authenticationSession" is null

        try {
            BrokeredIdentityContext identity = new BrokeredIdentityContext(id);
            //identity.setAuthenticationSession(authSession);
            //identity.setBrokerSessionId("239832Aisji");
            identity.setId(id);
            identity.setIdpConfig(config);
            identity.setIdp(this.provider);
            identity.setUsername(id);
            identity.setFirstName("John");
            identity.setLastName("Doe");
            identity.setBrokerUserId(id);
            identity.setModelUsername(id);

            return callback.authenticated(identity);
        } catch ( WebApplicationException e ) {
            e.printStackTrace();
            return e.getResponse();
        } catch (Exception e) {
            throw new RuntimeException("Failed to decode user information.", e);
        }
    }

    private String getUsername(HttpHeaders headers) {

        Map<String, Cookie> cookies = headers.getCookies();
        for (String cookie : cookies.keySet()) {
            if (cookie.equals(EFIKEY)) {
                return cookies.get(cookie).getValue();
            }
        }
        throw new NullPointerException("no user!");
    }
}

As mentioned in a previous comment, I have setup a new client that uses a custom flow that internally uses this Id Provider.
Keycloak correctly redirects me to the WebSeal page, but after login in that page the Keycloak console logs:

DEBUG [org.keycloak.services.resources.IdentityBrokerService] (executor-thread-5) Invalid request. Authorization code, clientId or tabId was null. Code=dddd, clientId=null, tabID=null

ERROR [org.keycloak.services.resources.IdentityBrokerService] (executor-thread-0) invalidRequestMessage
WARN  [org.keycloak.events] (executor-thread-0) type=IDENTITY_PROVIDER_LOGIN_ERROR, realmId=cd8a6a33-93c3-4343-82b5-2261744275a3, clientId=null, userId=null, ipAddress=127.0.0.1, error=invalidRequestMessage

I figured out how to pass the clientId and tabId to Keycloak and the error from my previous reply went away. I feel it’s an unorthodox way (read hack) but for now, it should be ok:

This is the change:

String id = getUsername(headers)
AuthenticationSessionModel authSession = this.callback.getAndVerifyAuthenticationSession(id);

String id = getUsername(headers) + ".1."+"my-client";
AuthenticationSessionModel authSession = this.callback.getAndVerifyAuthenticationSession(id);

Keycloak internally parses the state using . as separator. I have no idea what tabId is but it seems to be happy with 1.

Unfortunately, the flow still fails because Keycloak can not find the AUTH_SESSION_ID cookie that is sent to the client during the initial request to Keycloak:

2022-09-19 16:27:43,028 DEBUG [org.keycloak.services.resources.SessionCodeChecks] (executor-thread-7) Will use client 'my-client' in back-to-application link
2022-09-19 16:27:43,028 DEBUG [org.keycloak.services.util.CookieHelper] (executor-thread-7) Could not find any cookies with name {0}, trying {1}
2022-09-19 16:27:43,028 DEBUG [org.keycloak.services.managers.AuthenticationSessionManager] (executor-thread-7) Not found AUTH_SESSION_ID cookie
2022-09-19 16:27:43,028 DEBUG [org.keycloak.services.util.CookieHelper] (executor-thread-7) Could not find any cookies with name {0}, trying {1}
2022-09-19 16:27:43,028 DEBUG [org.keycloak.services.managers.AuthenticationSessionManager] (executor-thread-7) Not found AUTH_SESSION_ID cookie

Hi @luciano

Did you find a solution to your AUTH_SESSION_ID cookie issue?