IBM Verify

IBM Verify

Join this online user group to communicate across Security product users and IBM experts by sharing advice and best practices with peers and staying up to date regarding product enhancements.

 View Only

Using basic form based authentication to obtain a session in IBM Cloud Identity

By Adam Case posted Sun October 20, 2019 07:08 PM

  

OpenID Connect provides us a ton of options for authenticating users and devices from the normal Authorization Code grants, to Client Credentials, to ROPC, and many more. However, in some cases, you are limited with what you can do once you've authenticated. In an Authorization Code grant flow, your session at the identity provider side may have expired but your access tokens may still be valid. In an ROPC grant flow, you have obtained rights to interact with the identity provider, but you never established a session with the browser so if you attempted to go elsewhere, you'd be out of luck.

In this scenario, we will use the ROPC grant flow with IBM Cloud Identity to obtain an access token, and then make an ajax based HTTP call with that token and obtain an established authenticated session with IBM Cloud Identity. Once the session has been established, users are able to traverse other applications, both SAML and OIDC, without re-authenticating directly with the identity provider.
Note: For the time being, CORS bypass needs to be enabled via a browser plugin. I'm working on a workaround at the moment.


ezgif-2-908df307e957.gif

New authentication session endpoint

In a recent release of IBM Cloud Identity, our developers added a new API endpoint that allows for session cookies to be set when performed by in a browser flow.

GET /v1.0/auth/session

In this case, to call this API, a valid token must be provided in the browser header (ex. Authorization: bearer {token}) to receive a successful response.

This new API is pretty simple to call from an ajax call. In the code sample below, I've created a javascript function, providing the access token I received, and upon a successful response from the API call, I send the user to the launchpad (which is in of itself an OIDC application) and I wont be asked for any further authentication (that is if you didn't apply MFA to it).

const hostname = "https://casesecurity.ice.ibmcloud.com"
function redirectUser() {
	window.location.replace(`${hostname}/usc`);
}
function establishSession(token) {
    var xhr = new XMLHttpRequest();
	xhr.withCredentials = true;

	xhr.addEventListener("readystatechange", function() {
		if(this.readyState === 4) {
		    console.log(this.responseText);
				redirectUser();
		  }
		});
	xhr.open("GET", "https://tenant.ice.ibmcloud.com/v1.0/auth/session");
	xhr.setRequestHeader("Authorization", `Bearer ${token}`);
	xhr.setRequestHeader("X-Forwarded-Host", `tenant.ice.ibmcloud.com`);
	xhr.setRequestHeader("Access-Control-Allow-Headers", `x-requested-with`);
	xhr.setRequestHeader("Access-Control-Allow-Origin", `*`);
	xhr.setRequestHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
	xhr.send();
    }

This function can apply to longer lived session based applications that have refresh tokens as well. Any authenticated user-based application access token can use this API to establish an underlying user session for other applications.

Sample form based login flow example

I have put together a sample application, no more than 160 lines of HTML/JS code. This application does one thing, and one thing only: logs a user in to the launchpad from an third party application.

Prerequisite) Create a new OIDC application for ROPC

Using a custom connector in in IBM Cloud Identity, create a new OIDC application, with Resource Owner Password Grant (ROPC) enabled, with a public client ID, and no secret. For this flow, the secret does not really do us much good because this is just a front-end based application. It wouldn't really be private anyway as there isn't a real good option to hide it.

Additionally, because this application is a third party, you will need to add your server URL, whether localhost or a fully-qualified domain, to your Allowed Domains.

Add your domain to allowed domains in IBM Cloud Identity

In the admin portal of IBM CLoud Identity, navigate to the Configuration tab. Under the API Clients tab, select Allowed Domains. Add your domain, port, and anything else necessary to allow CORS. In this case, I am using python to create a HTTP server on port 8000. I've added http://localhost:8000 to my list of allowed domains.

Step 1) Include the third party files for styling and jQuery

Create a single index.html file for this example. Within the <head> tags, include the following snippets. For styling we will be using the Carbon Design framework (an IBM open-sourced UI framework for quick application styling). Read more here: https://www.carbondesignsystem.com/

<link rel="stylesheet" type="text/css" href="https://unpkg.com/carbon-components/css/carbon-components.min.css">
<script src="https://unpkg.com/carbon-components/scripts/carbon-components.min.js"></script>

Step 2) Set the OIDC client variables and createfunctions

Still within the <head> tags, start the following:

2.1 Set the variables

<script>
    const hostname = "https://tenant.ice.ibmcloud.com";
    const client_id = "client_id";

 

2.2 Create the function for establishing the user session

function redirectUser() {
	window.location.replace(`${hostname}/usc`);
}
function establishSession(token) {
    var xhr = new XMLHttpRequest();
	xhr.withCredentials = true;

	xhr.addEventListener("readystatechange", function() {
		if(this.readyState === 4) {
		    console.log(this.responseText);
				redirectUser();
		  }
		});
	xhr.open("GET", "https://tenant.ice.ibmcloud.com/v1.0/auth/session");
	xhr.setRequestHeader("Authorization", `Bearer ${token}`);
	xhr.setRequestHeader("X-Forwarded-Host", `tenant.ice.ibmcloud.com`);
	xhr.setRequestHeader("Access-Control-Allow-Headers", `x-requested-with`);
	xhr.setRequestHeader("Access-Control-Allow-Origin", `*`);
	xhr.setRequestHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
	xhr.send();
    }

 

2.3 Create the function to perform the initial login

This will take in the form values upon submission and input them into the ROPC grant flow. You could easily add reCaptcha support

function login(form) {
        var un = encodeURI(form.Username.value);
        var pw = form.Password.value;
        var data = `grant_type=password&client_id=${client_id}&username=${un}&password=${pw}`;

        var xhr = new XMLHttpRequest();
        xhr.withCredentials = true;

        xhr.addEventListener("readystatechange", function () {
            if (this.readyState === 4) {
                var response = JSON.parse(this.responseText);
                console.log(response);
                loginResults(this.responseText);
                establishSession(response.access_token);
            }
        });

        xhr.open("POST", `${hostname}/oidc/endpoint/default/token`);
        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xhr.send(data);
    }

2.4 Add an event listener to get the data from the login form

window.addEventListener("load", function() {
        var loginForm = document.getElementById("LoginForm");
        loginForm.addEventListener("submit", function() {
             login(loginForm);
         });
     });

 

2.5 Add a show / hide function to change the user view upon login or error

function loginResults(response) {
        var loggedIn = document.getElementById("LoggedIn");
        var badLogin = document.getElementById("BadLogin");
        var form = document.getElementById("LoginForm");
        console.log(response.indexOf("error"));
        if (response.indexOf("error") == -1) {
            loggedIn.style.display = "block";
            form.style.display = "none";
            badLogin.style.display = "none";
        } else {
            badLogin.style.display = "block";
        }
    }
</script>

 

2.6 Add some extra style

<style>
        .tx--tile {
            background-color: #ffffff;
            margin-top:1rem;
        }
        .tx--title {
            
            margin-top:2rem;
        }
        .tx--m-t-1 {
            margin-top:1rem;
        }
</style>

 

Step 3) Create the login form

Add the following code snippet to the HTML file. There's obviously extra styling in here for effect but you get the idea.

<body style="font-family: IBM Plex Sans; background-color: #f4f4f4">
    <div class="bx--grid">
      <div class="bx--row">
        <!-- box --> 
        <div class="bx--offset-lg-4 bx--col-lg-4 bx--offset-md-2 bx--col-md-4">
            <!-- START DISCLAIMER -->
            <div data-notification class="bx--inline-notification bx--inline-notification--info" role="alert">
                <div class="bx--inline-notification__details">
                      <svg focusable="false" preserveAspectRatio="xMidYMid meet" style="will-change: transform;" xmlns="http://www.w3.org/2000/svg" class="bx--inline-notification__icon" width="20" height="20" viewBox="0 0 32 32" aria-hidden="true"><path d="M16,2A14,14,0,1,0,30,16,14,14,0,0,0,16,2Zm0,5a1.5,1.5,0,1,1-1.5,1.5A1.5,1.5,0,0,1,16,7Zm4,17.12H12V21.88h2.88V15.12H13V12.88h4.13v9H20Z"></path></svg>
                    <div class="bx--inline-notification__text-wrapper">
                      <p class="bx--inline-notification__title">For demonstration use only</p>
                      <p class="bx--inline-notification__subtitle">This is a demonstration of using OpenID connect form-based access (ROPC) to obtain an access token and exchange that for a session to be used with single sign on. This should be used for testing purposes <b>only</b>.</p>
                    </div>
                </div>
                <button data-notification-btn class="bx--inline-notification__close-button" type="button" aria-label="close">
                  <svg focusable="false" preserveAspectRatio="xMidYMid meet" style="will-change: transform;" xmlns="http://www.w3.org/2000/svg" class="bx--inline-notification__close-icon" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M12 4.7L11.3 4 8 7.3 4.7 4 4 4.7 7.3 8 4 11.3 4.7 12 8 8.7 11.3 12 12 11.3 8.7 8z"></path></svg>
                </button>
            </div>
            <!-- END DISCLAIMER -->
            <h2 class="tx--title">Welcome, please login!</h2>
            <div class="bx--tile tx--tile">
                <form id="LoginForm" onsubmit="return false">
                    <div class="bx--form-item">
                      <label for="Username" class="bx--label">Username</label>
                      <input
                        required
                        id="Username"
                        type="text"
                        class="bx--text-input"
                        placeholder="Enter username here"
                      />
                    </div>
                    <div class="bx--form-item tx--m-t-1">
                      <label for="Password" class="bx--label">Password</label>
                      <input
                        required
                        id="Password"
                        type="password"
                        class="bx--text-input"
                        placeholder="Enter password here"
                      />
                    </div>
                    <button class="bx--btn bx--btn--primary tx--m-t-1" type="submit">Login</button>
                </form>
                <div data-notification class="bx--inline-notification bx--inline-notification--error" role="alert" id="BadLogin" style="display:none">
                    <div class="bx--inline-notification__details">
                          <svg focusable="false" preserveAspectRatio="xMidYMid meet" style="will-change: transform;" xmlns="http://www.w3.org/2000/svg" class="bx--inline-notification__icon" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M10,1c-5,0-9,4-9,9s4,9,9,9s9-4,9-9S15,1,10,1z M13.5,14.5l-8-8l1-1l8,8L13.5,14.5z"></path><path d="M13.5,14.5l-8-8l1-1l8,8L13.5,14.5z" data-icon-path="inner-path" opacity="0"></path></svg>
                        <div class="bx--inline-notification__text-wrapper">
                          <p class="bx--inline-notification__title">Login error!</p>
                          <p class="bx--inline-notification__subtitle">The login information you entered does not match an account in our records. Please try again.</p>
                        </div>
                    </div>
                </div>  
                <div id="LoggedIn" style="display:none">
                    <div data-inline-loading class="bx--inline-loading" role="alert" aria-live="assertive">
                      <div class="bx--inline-loading__animation">
                        <div data-inline-loading-spinner class="bx--loading bx--loading--small">
                          <svg class="bx--loading__svg" viewBox="-75 -75 150 150">
                            <circle class="bx--loading__background" cx="0" cy="0" r="30" />
                            <circle class="bx--loading__stroke" cx="0" cy="0" r="30"/>
                          </svg>
                        </div>
                        <svg focusable="false" preserveAspectRatio="xMidYMid meet" style="will-change: transform;" xmlns="http://www.w3.org/2000/svg" class="bx--inline-loading__checkmark-container" hidden="" data-inline-loading-finished="" width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7s7-3.1,7-7C15,4.1,11.9,1,8,1z M7,11L4.3,8.3l0.9-0.8L7,9.3l4-3.9l0.9,0.8L7,11z"></path><path d="M7,11L4.3,8.3l0.9-0.8L7,9.3l4-3.9l0.9,0.8L7,11z" data-icon-path="inner-path" opacity="0"></path></svg>
                      </div>
                      <p data-inline-loading-text-active class="bx--inline-loading__text">Logging in...</p>
                    </div>
                </div>
            </div>
        <!-- box --> 
        </div>
      </div>
    </grid>
</body>

Save this file and create a localhost server via php -S localhost:8000 or python -m SimpleHTTPServer on a terminal window while in the directory of the index.html file.
Download the full file here: index.html

0 comments
46 views

Permalink