3.1. Optional: Configure preferred login option
With the configuration in place so far Cloud Pak foundational services will always present a screen to select your login option, such as Enterprise LDAP, OpenID Connect or others.
In an environment, where all your users always use the identity provider for authentication, you can set the preferred login options.
oc patch cm platform-auth-idp -p '{"data": {"PREFERRED_LOGIN": "OIDC"}}'
The change should take effect immediately and skip the login option selection in future login attempts.
[top]
4. Set up Liberty to use Keycloak for authentication
Just as Cloud Pak foundational service IM, we must register Liberty as a OIDC RP of Keycloak. In the Keycloak admin console in realm ocp-sample, create a new client and "clientapp" and specify a redirect URI following step 2 of Configuring an OpenID Connect Client in Liberty , e.g. https://localhost:9443/oidcclient/redirect/keycloaksample
.
The liberty server will also require a truststore with certificate of all the systems it connects to: Keycloak, IM, and Zen. In a development set up that builds the sample app using gradle, the Liberty server "defaultServer" was located in build/wlp. With that, the following script can create a keystore, obtain the signer certificates from remote machines and import them into the same keystore. Copying the keystore to a different location was only required in the gradle set up:
keystore=build/wlp/usr/servers/defaultServer/resources/security/key.p12
rm $keystore
rm src/main/liberty/config/resources/security/key.p12
build/wlp/bin/securityUtility createSSLCertificate --server=defaultserver --password=passw0rd
keytool -printcert -sslserver unframed1.fyre.ibm.com:8443 -rfc > keycloak.pem
keytool -printcert -sslserver cp-console-fips2301.apps.jefips.cp.fyre.ibm.com:443 -rfc > iam.pem
keytool -printcert -sslserver cpd-fips2301.apps.jefips.cp.fyre.ibm.com:443 -rfc > zen.pem
keytool -import -keystore $keystore -storepass passw0rd -file keycloak.pem -alias keycloak -noprompt
keytool -import -keystore $keystore -storepass passw0rd -file iam.pem -alias iam -noprompt
keytool -import -keystore $keystore -storepass passw0rd -file zen.pem -alias zen -noprompt
cp $keystore src/main/liberty/config/resources/security/key.p12
The final Liberty configuration
- configures features for the sample app (webProfile-8.0, passwordUtilities1.0, jca-1.7 allowing to read credentials from server config)
- configures the openidConnectClient-1.0 feature for interacting with Keycloak
- configures a httpEndpoint to listen to port 9443 on all hostnames
- lines 12-19: configures Liberty as a client of Keycloak. The most important item is the discoveryEndpointUrl to download most of the options of the Keycloak server itself. In addition to that, Liberty needs client specific settings, such as clientId and clientSecret.
- The audiences claim is set by Keycloak to restrict the set of servers that accept the token.
- The realmName and userIdentifier settings are residuals from experiments to find the best configuration matching the token contents.
- The signatureAlgorithm claim tells Liberty to obtain a public key from Keycloak's JWK endpoint to verify identity tokens.
- lines 23-29: configures the web application with a set of protected URLs that require authentication.
- line 31: configures an authentication alias with the client_id and client_secret to be used by the sample app interacting with Cloud Pak foundational service IM, not Keycloak. The application will use this configuration to exchange the authorization code for tokens.
With this configuration in place, a user who attempts to access a protected URL of the sample app is redirected to Keycloak for authentication.
[top]
5. Sample app
The clientapp is available in the library of this community. It is not fully self-contained as it assumes a sample OCP environment and Keycloak, but it can help you getting started.
unzip clientapp.zip
rm clientapp.zip
chmod +x prepare-certs.sh gradlew
./gradlew build deploy && ./prepare-certs.sh && ./gradlew libertyStart
The sample app implements three use cases to allow walking through the sequence more easily. All serious Java code is collected in com.ibm.test.tokensample.clientapp.TokenHelper for easier editing and reuse.
index.html is the anonymous landing page that allows navigating to dedicated pages for the three use cases.
src/main/liberty/config is the copied by the liberty gradle plugin into a dev server. build.gradle mostly follows the tutorial Building a web application with Gradle
[top]
5.1 who-am-I - Liberty's view on the user
This page is not actually related to interacting with CP4BA, however, it is a great utility to learn about Keycloak tokens and Liberty APIs for accessing the security context. The three methods in use are
<h1>Welcome <%=TokenHelper.getCurrentUsername(request)%></h1>
This is just a wrapper around the JEE standard method for obtaining a username from context: request.getUserPrincipal().getName()
<% for (String key : TokenHelper.getIdTokenClaims().keySet()) { %>
The getIdTokenClaims()
methods extracts the id_token, which was obtained during OIDC authorization code flow from Keycloak, from the security context and provides all claims of the token in a Map.
<%=TokenHelper.getSecurityContext().toString() %>
This is a wrapper around a Liberty API to obtain the current user's security context:
WSSubject.getRunAsSubject()
, see
javadoc.
[top]
5.2 Authorization_code flow: Redirecting the user to IM
The following sections of this article deal with the same code that is either invoked "lazily" (by the click of a button) by cp4ba-on-demand.jsp or "eagerly" (automatically) by cp4ba-client.jsp.
When you go through the spec of the OAuth 2.0 authorization code flow, the first step is to redirect the browser to the /authorize endpoint of the OAuth 2.0 authorization server (Cloud Pak foundational service IM in this case). A number of parameters have to be passed by the browser with redirect.
The sample app obtains the IM URL prefix from configuration in web.xml and appends the parameters as follows:
public static final String calculateIamAuthorizeUrl(HttpServletRequest request) throws Exception {
final String METHOD_NAME = "calculateIamAuthorizeUrl";
LOG.entering(CLASS_NAME, METHOD_NAME);
// 1. calculate random state and store it in session for later comparision
String state = UUID.randomUUID().toString();
request.getSession().removeAttribute("state");
request.getSession().setAttribute("state", state);
StringBuilder authorizeUrl = new StringBuilder(request.getServletContext().getInitParameter("iam-urlprefix"));
authorizeUrl.append("/idprovider/v1/auth/authorize");
authorizeUrl.append("?response_type=code");
authorizeUrl.append("&client_id=").append(getIamClientId(request));
authorizeUrl.append("&scope=openid");
authorizeUrl.append("&state=").append(state);
authorizeUrl.append("&redirect_uri=").append(
URLEncoder.encode(calculateRedirectUri(request), java.nio.charset.StandardCharsets.UTF_8.toString()));
LOG.exiting(CLASS_NAME, METHOD_NAME, authorizeUrl.toString());
return authorizeUrl.toString();
}
The redirect_uri
parameter is must be a pre-registered value as described in Configuring an OpenID Connect Client in Liberty step 2. It is the callback URL implemented by token-consumer.jsp.
The state
parameter allows a client to verify that that a callback received at the redirect_uri is actually a "response" to a request for authentication that the application has initiated. It can also help single page applications to restore state after redirecting back and force. The value should be random and verified upon receiving the callback, hence it is stored in the session by this sample app.
The resulting URL may look like this sample: https://cp-console-fips2301.apps.jefips.cp.fyre.ibm.com/idprovider/v1/auth/authorize?response_type=code&client_id=qp4kwb51tg31f5zjd33bq4p16gp9nnf2&scope=openid&state=c2e67e40-17d7-4095-a448-1a8f73e75bae&redirect_uri=https%3A%2F%2Flocalhost%3A9443%2Fclientapp%2Fprotected%2Ftoken-consumer.jsp
The task at hand is now to let the browser visit that URL and ultimately come back to the pre-registered and provided redirect_uri. There are two common patterns to achieve that:
- You may choose to store relevant navigational information (the page that the user actually attempted to go to) in the user's session, a cookie or a dedicated cache using the state parameter value as key. You can then use the current browser tab to visit the authorize endpoint by sending a 302 redirect from the server or using window.location from client side script code. Upon receiving the response, retrieving and storing all relevant tokens in the user's session, your token consumer code must redirect the user to the target page.
- You may choose to keep all state of your current browser tab and open the authorize URL in a dedicated tab. This approach is detailed in the following section. Upon receiving the response, retrieving and storing all relevant tokens in the user's session, your client side code must close the additional tab and notify the originating tab about the successful completion of the authorization code flow.
[top]
5.3 Client-side code: opening and closing browser tab, notify
You can open another browser tab using the following client side JavaScript:
window.open("<%=TokenHelper.calculateIamAuthorizeUrl(request)%>", '_blank').focus();
This approach works well upon user interaction (e.g. the click of a button). However, for security reasons, browsers may block this automatic tab opening without user interaction unless explicitly enabled by the user (or policy).
The token-consumer page later needs to close its own tab, which work without any special permissions in my observation:
window.close();
If you choose the multi-tab approach, you need to somehow communicate the presence of required tokens in the user's session back to the opening page. The browser feature for that is postMessage().
The sending page can send an event which includes the real payload as well as an origin (typically, the server's URL prefix):
window.opener.postMessage("zen token available", "https://localhost:9443");
The receiving page must register an event handler to consume the event:
//register an event listener, so that the token-consumer.jsp page can notify once a token is available in the session.
function init() {
console.log("registering event listener");
window.addEventListener("message",(event) => {
if (event.origin !== "https://localhost:9443") {
console.log("received event for " + event.origin + " ... ignoring." );
return;
}
console.log("event received: " + event.data);
window.location.reload();
}, false);
}
The sample code above ignores events in case the origin is unexpected. For an expected event, the page just reloads using window.location.reload();
without even looking at the event's payload. In a single page application, you may choose to instead set some state that enables buttons and use cases, which require the CP4BA token in the session.
[top]
5.4 Authorization_code flow: Exchanging code for tokens
As defined in OAuth 2.0 authorization code flow, there is a callback to the pre-registered and provided redirect_uri - https://localhost:9443/clientapp/protected/token-consumer.jsp
in the sample. The server-side code, which serves this URL can now validate the state
parameter, extract the code
from a request parameter and call out to IM's token endpoint to exchange code for token.
You can of course use your favorite REST client framework for this task, but the sample app avoids introducing additional dependencies. There is also no need to store all this debugging information in the session, but it may help following the flow during your own implementation.
[top]
5.5 Zen: Exchange IM token for Zen token
There is a Zen REST API to exchange IM tokens for Zen tokens. It expects the username and a matching IM token to be passed as request headers and returns a Zen token in response.
Again, use the REST client framework of your choice and the only data you actually want to store in the session is the Zen token itself.
[top]
5.6 Invoke CP4BA API
Now, with the required token in place, the final step is to invoke a CP4BA REST API and pass the token as in a HTTP request header Authorization: Bearer <token>
.
[top]