In order to allow getting started with a client, the CRMLS OP has been configured to allow a couple sample clients. One for Client Credentials grant type and another for Implicit grant type (more on grant/flow types). A sample API resource has also been set up to allow access by both client applications (includes CORS for origin http://localhost:4200).
Regardless of how a client application has been configured, the following steps are required within a client's workflow to make use of protected API resources that are accessible by the client:
- Obtain an
access_token
from CRMLS OP. - Use the
access_token
to interface with an API resource (e.g., calls to ODataApi). - Refresh or re-get
access_token
periodically to continue proper interaction with API resources.
The sample API resource:
"api1" has one protected GET endpoint @ http://soc.crmls.org/sampleApi/protectedApi
All it does is return all the claims that the API resource has visibility of based on the caller.
Using Client Credentials Flow
Requirements | |
---|---|
Token endpoint | "http://soc.crmls.org/connect/token" |
Client ID | "client" |
Client secret | "secret" |
Scope | "api1" |
This client can be demonstrated by creating a simple console application to run through the essential workflow.
The following is a simplified C# example:
public class Program
{
private static async Task Main()
{
var client = new HttpClient();
// Obtain an access_token using the required data.
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = "http://soc.crmls.org/connect/token",
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
// Use the access_token to interface with an API resource
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://soc.crmls.org/sampleApi/protectedApi");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
}
Execution of the above would produce an output similar to this:
[
{
"type": "nbf",
"value": "1567568077"
},
{
"type": "exp",
"value": "1567571677"
},
{
"type": "iss",
"value": "http://soc.crmls.org"
},
{
"type": "aud",
"value": "http://soc.crmls.org/resources"
},
{
"type": "aud",
"value": "api1"
},
{
"type": "client_id",
"value": "client"
},
{
"type": "scope",
"value": "api1"
}
]
If this client was long running (i.e., longer than token lifetime - expires_in
- which is configurable but 60 minutes by default), it would need to re-GET the token. This console app can be re-run to mimic the re-GET and inspect the access_token value upon each run (i.e., that they are different each time with a another 60 minute window of access).
Using Implicit Flow
Almost identical procedure as the quickstart for 'Adding a JavaScript client' but tailored to CRMLS OP.
It shows how to build a browser-based JavaScript client application.
- The user can login
- Call the sample API resource with the access token obtained at login
- Log out
Requirements | |
---|---|
Authentication endpoint | "http://soc.crmls.org/connect/authorize" |
Client ID | "client" |
Redirect URI | "http://localhost:4200/callback.html" |
Scope * | "identity profile api1 CrmlsProfile" |
*identity profile and CrmlsProfile are optional scope values that can be added to the implicit flow sample client.
Additional Requirements
- Ability to host the client on localhost:4200
- NPM to download oidc-client or manual download from git repo
The client will contain four static files to host on localhost port 4200.
- index.html
- app.js
- oidc-client.js
- callback.html
index.html - the main page of the sample application. Simple HTML for login, logout, and calling the sample API. The full contents of our sample index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Sample Web Client</title>
</head>
<body>
<h1>Sample Web Client</h1>
<p>Allows SSO: CRMLS OP + External (Clareity) IDP</p>
<p>Once logged on, can call sample protected API resource.</p>
<button id="login">Login</button> |
<button id="callApi">Call Sample API</button> |
<button id="logout">Logout</button>
<p id="loginStatus"></p>
<pre id="results"></pre>
<script src="oidc-client.js"></script>
<script src="app.js"></script>
</body>
</html>
app.js - contains the main application code. Bindings for user interaction (e.g., hooking up clicking of buttons). Where all the 'Requirements' are set for configuration of the client. First, define a couple variables and a helper function to log the results.
var appBaseUrl = "http://localhost:4200"; // registered in CRMLS OP with this URL!
var sampleApiUrl = "http://soc.crmls.org/sampleApi/protectedApi"; // api resource
function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
Then the code to bind the buttons to the click events. As well as setting up the UserManager from oidc-client library with the 'Requirements' for client settings.
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("callApi").addEventListener("click", callApi, false);
document.getElementById("logout").addEventListener("click", logout, false);
var config = {
authority: "http://soc.crmls.org",
client_id: "js_oidc",
redirect_uri: appBaseUrl + "/callback.html",
response_type: "id_token token",
scope: "openid profile api1 CrmlsProfile",
post_logout_redirect_uri: "https://idp.crmls.org/idp/logout.jsp",
acr_values: "idp:Saml2",
loadUserInfo: true,
};
var mgr = new Oidc.UserManager(config);
Then a few more helper methods to display a better status, to clear stale sessions, and logout/signout event handling. The rest of app.js:
mgr.events.addUserSignedOut(function () {
log("User signed out");
display("#loginStatus", "");
});
// clears any old stale requests from storage
mgr.clearStaleState().then(function () {
console.log("Finished clearing old state");
}).catch(function (e) {
console.error("Error clearing state:", e.message);
});
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
display("#loginStatus", "Logged on as " + user.profile.MemberLoginId + " with access_token: " + user.access_token);
}
else {
log("User not logged in");
display("#loginStatus", "");
}
});
function login() {
display("#results", "Redirecting to login...");
mgr.signinRedirect();
}
function callApi() {
mgr.getUser().then(function (user) {
displayLoading();
var xhr = new XMLHttpRequest();
xhr.onload = function (e) {
if (xhr.status >= 400) {
display("#results", {
status: xhr.status,
statusText: xhr.statusText,
wwwAuthenticate: xhr.getResponseHeader("WWW-Authenticate")
});
}
else {
display("#results", xhr.response);
}
};
xhr.open("GET", sampleApiUrl, true);
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
function displayLoading() {
display("#results", "Loading...");
}
function logout() {
display("#results", "Logging out...");
mgr.signoutRedirect();
}
function display(selector, data) {
if (data && typeof data === 'string') {
try {
data = JSON.parse(data);
}
catch (e) { }
}
if (data && typeof data !== 'string') {
data = JSON.stringify(data, null, 2);
}
document.querySelector(selector).textContent = data;
}
oidc-client.js - the OpenId Connect client library - via NPM and copy to the root html directory where index.html resides.
npm i oidc-client
copy node_modules\oidc-client\dist\oidc-client.js <html-root-dir>
callback.html - once the user has logged in, the redirect_uri
page that CRMLS OP will complete the handshake with. After the sign-in is complete, then redirects the user back to the main index.html page. The full contents of callback.html:
<!DOCTYPE html>
<html>
<head>
<title>Sample web client (callback)</title>
</head>
<body>
<script src="oidc-client.js"></script>
<script>
//new Oidc.UserManager({ response_mode: "query" }).signinRedirectCallback().then(function () {
new Oidc.UserManager().signinRedirectCallback().then(function () {
window.location = "index.html";
}).catch(function (e) {
console.error(e);
});
</script>
</body>
</html>
Once hosted, outputs/results should be similar to these screen shots.
Landing page - without login user: Once 'Login' is selected, the user will log in using member username/password on external-CRMLS (Clareity) login page. |
|
After login user: Claims are shown based on scope configured by the client (and allowed by the OP). In this case, scopes 'identity', 'profile' and 'CrmlsProfile' provide all the claims displayed. |
|
After sample API call: Similar output/results as with the client credentials flow. The difference is the inclusion in this output for claim key/values for 'sub' and 'MemberLoginId' (cropped out of screen shot visibility) because the scope 'api1' allows 'MemberLoginId' user claim. The client credential output did not have this claim because there was no end user involvement. |