Search code examples
javaoauth-2.0openid-connectkeycloakidp

Programatic username/password access with KeyCloak using external IDP brokering


I'm using Identity Brokering feature and external IDP. So, user logs in into external IDP UI, then KeyCloak broker client receives JWT token from external IDP and KeyCloak provides JWT with which we access the resources. I've set up Default Identitiy Provider feature, so external IDP login screen is displayed to the user on login. That means that users and their passwords are stored on external IDP.

The problem occurs when I need to log in using "Direct Access Grant" (Resource Owner Password grant) programatically in tests. As password is not stored on KeyCloak, I always get 401 Unauthorized error from KeyCloak on login. When I tried to change user password it started to work, so the problem is that user password is not provisioned on KeyCloak and using "Direct Access Grant" KeyCloak doesn't invoke external IDP on programatic login.

I use the following code to obtain access token, but get 401 error everytime I pass valid username/password.

org.keycloak.authorization.client.util.HttpResponseException: Unexpected response from server: 401 / Unauthorized

Direct access grant is enabled for that client.

public static String login(final Configuration configuration) {
    final AuthzClient authzClient = AuthzClient.create(configuration);
    final AccessTokenResponse accessTokenResponse = authzClient.obtainAccessToken(USERNAME, PASSWORD);
    return accessTokenResponse.getToken();
  }

Is there any way it can be fixed? For example to call identity broker on "Direct Access Grant", so that KeyCloak provides us it's valid token?


Solution

  • The problem was that KeyCloak has no information about passwords from initial identity provider. They have a token exchange feature which should be used for programmatic token exchange.

    External Token to Interanal Token Exchange should be used to achieve it.

    Here is an example code in Python which does it (just place correct values in placeholders):

    def login():
        idp_access_token = idp_login()
        return keycloak_token_exchange(idp_access_token)
    
    def idp_login():
        login_data = {
            "client_id": <IDP-CLIENT-ID>,
            "client_secret": <IDP-CLIENT-SECRET>,
            "grant_type": <IDP-PASSWORD-GRANT-TYPE>,
            "username": <USERNAME>,
            "password": <PASSWORD>,
            "scope": "openid",
            "realm": "Username-Password-Authentication"
        }
        login_headers = {
            "Content-Type": "application/json"
        }
        token_response = requests.post(<IDP-URL>, headers=login_headers, data=json.dumps(login_data))
        return parse_response(token_response)['access_token']
    
    def keycloak_token_exchange(idp_access_token):
        token_exchange_url = <KEYCLOAK-SERVER-URL> + '/realms/master/protocol/openid-connect/token'
        data = {
            'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
            'subject_token': idp_access_token,
            'subject_issuer': <IDP-PROVIDER-ALIAS>,
            'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
            'audience': <KEYCLOAK-CLIENT-ID>
        }
        response = requests.post(token_exchange_url, data=data,
                                 auth=(<KEYCLOAK-CLIENT-ID>, <KEYCLOAK-CLIENT-SECRET>))
        logger.info(response)
        return parse_response(response)['access_token']