Cloud Pak for Business Automation

 View Only

Sample Java CLI REST client invoking Cloud Pak for Automation APIs (authorization code flow)

By Jens Engelke posted Tue January 04, 2022 01:09 PM

  

In API access tokens in Cloud Pak for Automation 21.0.3 I explained various flows how a client can obtain tokens for invoking Cloud Pak for Automation APIs. This follow up provides a Java sample invoking REST APIs for Business Teams Service (BTS) from a command line.

The tool is supposed to mimic typical behavior that your users may be used to when working with a CLI for e.g. github (gh), OpenShift (oc) or IBM cloud (ibmcloud).

CLI functions

This CLI (Command Line Interface) supports two sample APIs of Business Teams Service (BTS):

  • server-status for showing availability of the BTS server
  • current-user for getting information about the current user

Of course, there are typical commands for version information and help. To visualize some of the flows, there is a show-config command to dump the current contents of a config file in the user's home directory to standard out.

The three commands of interest in the context of this blog post are 

  • logout - basically removing all state from the config file
  • login-implicit - using the implicit flow to obtain an API token
  • login-code - using the authorization code flow to obtain an API token

Both login-... methods will open the system's default browser allowing the user to login. Finally, an API token is retrieved and stored in a configuration file in the user's home directory, allowing API access until the token expires.

The demo above shows how the tool refuses to call out to Business Teams Service as the configuration file does not yet contain a suitable token for authentication. Calling btscli login-code invokes the OpenID Connect authorization code flow, opening the default browser with the IAM's /authorize endpoint URL. This particular environment is set up to delegate authentication via SAML to an upstream identity provider. Ultimately, an authorization code is made available to the CLI tool via HTTP callback and exchanged for tokens for API access. The next invocation of btscli server-status successfully displays server status information.

Authorization code flow

The overall scenario involves a number of components:

On the left hand side, there is the user's laptop running a command shell, which interacts with the Java program. Upon invoking btscli login-code, the Java program
  1. creates a random state parameter value
  2. creates a HTTP server listening on a random on localhost
  3. opens a browser to request to IAM's /authorize endpoint with response_type=code and redirect_uri pointing to its own web server at http://localhost:1089/authcallback (in this example)
If there is no existing session with IAM, authentication is required. In this sample set up, the user is redirected to a SAML identity provider for authentication and comes back to IAM's assertion consumer service (ACS) before actually invoking the the /authorize endpoint. Depending on authentication state with the SAML IdP, the user may need to enter a password, provide some one time token or finger print or alike. 
If there is an existing session with IAM, the request for /authorize is processed immediately. Assuming all prerequisites are met (see below), there is no user interaction, but the browser immediately moves on to the requested redirect_uri: https://localhost:1089/authcallback?code=ff333-djdj&state=abcd
It is the same Java program receiving this HTTP request that has also opened the browser. It can now verify that the state parameter has the same value as in its original request to IAM and if so, parse the code from the request URI. It can then call IAM's /token API to exchange the code for access_token, id_token etc. before ultimately calling the Platform UI API that obtains a token for CP4A API access.

Prerequisites

For the above to work, the CLI app must be registered at IAM with a client_id and client_secret that allows authorization code flow and redirects to any http://localhost URL.

$ curl -sku  "$iamreguser:$iamregpwd"  "$iamhost/idauth/oidc/endpoint/OP/registration/d99a3420280b4ea3bb3de788fb9ba5e1" | jq
{
  "client_id_issued_at": 1640853977,
  "registration_client_uri": "https://cp-console.apps.jens.cp.fyre.ibm.com/oidc/endpoint/OP/registration/d99a3420280b4ea3bb3de788fb9ba5e1",
  "client_secret_expires_at": 0,
  "token_endpoint_auth_method": "client_secret_basic",
  "scope": "openid",
  "grant_types": [
    "authorization_code",
    "client_credentials",
    "implicit"
  ],
  "response_types": [
    "code",
    "token"
  ],
  "application_type": "web",
  "post_logout_redirect_uris": [],
  "preauthorized_scope": "openid",
  "introspect_tokens": false,
  "trusted_uri_prefixes": [],
  "resource_ids": [],
  "functional_user_groupIds": [],
  "proofKeyForCodeExchange": false,
  "publicClient": false,
  "appPasswordAllowed": false,
  "appTokenAllowed": false,
  "hash_itr": 0,
  "hash_len": 0,
  "client_id": "d99a3420280b4ea3bb3de788fb9ba5e1",
  "client_secret": "*",
  "client_name": "d99a3420280b4ea3bb3de788fb9ba5e1",
  "redirect_uris": [
    "sample-app://authenticate",
    "regexp:http://localhost:[0-9]*/authcallback"
  ],
  "allow_regexp_redirects": true
}

Pay special attention to grant_types, response_types, allow_regexp_redirects  and redirect_uris.
As the Java program cannot know which HTTP port may be available at the time of executing the authorization code flow, it will pick a random free port. To register any port as possible redirect_uri, IAM allows the use of regular expressions.

Download

Source code is  uploaded to the "Library" section of IBM Automation Community: sample-client-cp4a2103.zip

There is no binary to start executing the utility. The code is intentionally not generalized to run anywhere, but in its original test environment as it is only meant to visualize the concepts and must not be confused with a production ready template.

Code walk-through

The downloadable .zip contains a project that can be built using gradlew build. For convenient invocation, I added a btscli.bat file to my PATH.

@java -jar "c:\git\zenrestclient\app\build\libs\btscli-1.0.0.jar" "%1"

The Java code is in app/src/main/java/com/ibm/sample/btscli/App.java. Its main method basically dispatches to dedicated methods for each of the commands. The config file is nothing more than a java.util.Properties object that is read and stored with built-in Java methods. The interesting logic in the context of this blog post is 

    private static void commandLoginCode() throws IOException {
        commandLogout();

        int port = 8080;
        try (ServerSocket serverSocket = new ServerSocket(0)) {
            port = serverSocket.getLocalPort();
        } catch (IOException e) {
            System.err.println("Unable to determine free network port");
        }

        CONFIG.setProperty(PROP_NAME_REDIRECTURI, "http://localhost:" + port + "/authcallback");
        CONFIG.setProperty(PROP_NAME_STATE, getRandomString(15));
        saveConfig();

        authCallbackHttpServer = HttpServer.create(new InetSocketAddress(port), 0);
        HttpContext context = authCallbackHttpServer.createContext("/");
        context.setHandler(App::receiveHttpAuthCallback);
        authCallbackHttpServer.start();
        System.out.println("Waiting for browser callback.");

        StringBuilder codeUrl = new StringBuilder(IAMHOST + "/idprovider/v1/auth/authorize");
        appendUrlParam(codeUrl, "client_id", CLIENTID);
        appendUrlParam(codeUrl, "response_type", "code");
        appendUrlParam(codeUrl, "scope", "openid");
        appendUrlParam(codeUrl, "redirect_uri", CONFIG.getProperty(PROP_NAME_REDIRECTURI));
        appendUrlParam(codeUrl, "state", CONFIG.getProperty(PROP_NAME_STATE));

        Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + codeUrl.toString());
    }


It creates a HTTP server (requires Java 11) on  a random port and uses Runtime.exec(...) to open the default browser with a calculated URL.
The final callback from the browser is received by the HTTP Server callback handler method :

    private static void receiveHttpAuthCallback(HttpExchange exchange) throws IOException {
        String response = "<html><body onload=\"window.close()\"/></html>";
        URI requestURI = exchange.getRequestURI();
        exchange.getResponseHeaders().put("Content-Type", Collections.singletonList("text/html"));
        exchange.sendResponseHeaders(200, response.getBytes().length);// response code and length
        OutputStream os = exchange.getResponseBody();
        os.write(response.getBytes());
        os.close();
        authCallbackHttpServer.stop(0);
        String code = extractCodeFromCallback(requestURI.toString());

        try {
            exchangeAuthorizationCodeForIamToken(code);
            obtainUsernameFromUserInfo();
            exchangeIamTokenForCp4aToken();
        } catch (KeyManagementException | NoSuchAlgorithmException e) {
            System.err.println("ERROR exchange authorization code for tokens at IAM.");
            e.printStackTrace();
        }

    }


It creates a response to close the browser window and extracts the code before invoking methods for exchanging code for token and IAM token for CP4A token as described in API access tokens in Cloud Pak for Automation 21.0.3 (via cURL).  
These methods as well as the actual invocation of REST APIs using the token for authentication is regular HTTP client and JSON processing for which you'll probably select your framework of choice anyways.

0 comments
51 views

Permalink