Cordra supports an authenticate
JavaScript lifecycle hook that provides complete control
how a request is authenticated. This section provides a complete example of one way in which
this hook can be used to authenticate a JWT (Json Web Token) that have been supplied by a 3rd
party authentication service.
Below is an example of the decoded payload section of such a token:
{
"username": "fred",
"groupIds": [
"test/group1"
],
"iss": "https://example.com/",
"sub": "60afe42cebfb9c0068924e85",
"aud": [
"https://cordra.org/",
"https://example.com/userinfo"
],
"iat": 1630609934,
"exp": 1630696334,
"azp": "0s5qpVcOmDHuzowxcmI2fU7fRFXNyOWZ",
"scope": "openid profile email address phone",
"gty": "password"
}
The below example authenticate
hook implementation on the design object roughly follows
these steps:
Load the providers public key in JWK format from the cordra data directory.
Check if authInfo contains an correctly formatted token from the external provider.
This is done by first checking if the token is a JWT.
Then decoding the JWT and checking that the iss is who we expect it to be.
If it is not an authentication attempt with a suitable token we return null.
If we have an suitable token we verify it against the public key of the provider.
Check the values of the claims stored in the token to make sure they are correct.
Assuming those tests pass, return an object containing the userId, username, groupIds and the expiration from the token.
const cordra = require('cordra');
const cordraUtil = require('cordraUtil');
let providerPublicKey = null;
exports.authenticate = authenticate;
function authenticate(authInfo, context) {
cacheKeyIfNeeded();
if (isTokenAuthentication(authInfo)) {
return checkCredentials(authInfo);
} else {
return null;
}
}
function isTokenAuthentication(authInfo) {
if (authInfo.token) {
if (isJwtFromProvider(authInfo.token)) {
return true;
}
}
return false;
}
function isJwtFromProvider(token) {
if (!token.includes(".")) {
return false;
}
try {
const claims = cordraUtil.extractJwtPayload(token);
return "https://example.com/" === claims.iss;
} catch (error) {
return false;
}
}
function checkCredentials(authInfo) {
const token = authInfo.token;
const payload = cordraUtil.extractJwtPayload(token);
const isVerified = cordraUtil.verifyWithKey(token, providerPublicKey);
const claimsCheck = checkClaims(payload);
const active = isVerified && claimsCheck;
const result = {
active: active
};
if (active) {
result.userId = payload.sub;
if (payload.username) {
result.username = payload.username;
}
if (payload.groupIds) {
result.groupIds = payload.groupIds;
}
if (payload.exp) {
result.exp = payload.exp;
}
result.grantAuthenticatedAccess = true;
}
return result;
}
function isBasicAuth(authHeader) {
return authHeader.startsWith("Basic ");
}
function isBearerTokenAuth(authHeader) {
return authHeader.startsWith("Bearer ");
}
function getTokenFromAuthHeader(authHeader) {
return authHeader.substring(authHeader.indexOf(" ") + 1);
}
function checkClaims(claims) {
if (!claims.iss || !claims.exp || !claims.aud) {
return false;
}
if ("https://example.com/" !== claims.iss) {
return false;
}
const nowInSeconds = Math.floor(Date.now() / 1000);
if (nowInSeconds > claims.exp) {
return false;
}
const aud = claims.aud;
if (!checkAudience(aud)) {
return false;
}
return true;
}
function checkAudience(audElement) {
let aud = [];
if (typeof audElement === "string") {
aud.push(audElement);
} else if (Array.isArray(audElement)) {
aud = audElement;
} else {
return false;
}
if (aud.includes("https://cordra.org/")) {
return true;
} else {
return false;
}
}
function cacheKeyIfNeeded() {
if (!providerPublicKey) {
const configDir = getDataDir();
const File = Java.type('java.io.File');
const keyPath = configDir + File.separator + "publicKey.jwk";
providerPublicKey = readFileToJsonAndParse(keyPath);
}
}
function getDataDir() {
const System = Java.type('java.lang.System');
const cordraDataDir = System.getProperty('cordra.data');
return cordraDataDir;
}
function readFileToString(pathToFile) {
const path = Java.type('java.nio.file.Paths').get(pathToFile);
const string = Java.type('java.nio.file.Files').readString(path);
return string;
}
function readFileToJsonAndParse(pathToFile) {
const jsonString = readFileToString(pathToFile);
const result = JSON.parse(jsonString);
return result;
}
The previous section describes how to verify an access token provided by an external authentication provider on the server side. You may also customize the Cordra UI authentication dialog to sign in with a 3rd party. This is done by embedding in an iframe an html page that you need to write. The content of that html page is entirely under your control. A typical example of such a page would be to load a 3rd party JavaScript authentication library, render a sign in button and acquire the access token. This html page communicates with the parent Cordra UI using postMessage. In particular when your custom page wants to send the newly acquired access token to the Cordra UI it should send the following via postMessage:
let message = {
type: "customAuthentication",
token: accessToken
};
window.parent.postMessage(message, '*');
Your custom page should also listen for a sign out message:
window.addEventListener("message", (event) => {
let message = event.data;
if (message && message.type === "signOut");
// handle sign out with 3rd party provider
}
);
A complete example of a Cordra UI customization html page using the Sign in with Google JavaScript API is shown below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test google sign in</title>
<div id="buttonDiv"></div>
</head>
<body>
<script src="https://accounts.google.com/gsi/client" async defer></script>
<script>
const CLIENT_ID = "your client id provided by google";
window.onload = function () {
window.addEventListener("message", (event) => {
let message = event.data;
if (message && message.type === "signOut");
google.accounts.id.disableAutoSelect();
}
);
google.accounts.id.initialize({
client_id: CLIENT_ID,
callback: handleCredentialResponse
});
google.accounts.id.renderButton(
document.getElementById("buttonDiv"),
{ theme: "outline", size: "large" }
);
};
function handleCredentialResponse(response) {
let message = {
type: "customAuthentication",
token: response.credential
};
window.parent.postMessage(message, '*');
}
</script>
</body>
</html>
Once written your custom authentication page should be stored as a payload on a Cordra object. Typically the design Cordra object is used to store this payload with the payload name “customAuthentication”.
To configure the UI to use this custom authentication page add the follow JSON object to the Cordra UI config as a top level property:
"customAuthentication": {
"url": "/objects/design?payload=customAuthentication",
"tabName": "Google",
"height": 100
}
The url points at the custom html page that is stored as a paylaod on the design object.
The iframe will be created in a new tab in the authentication dialog. You can customize the name of that tab with the tabName property.
The height property allows you to control the height of the iframe in the new tab.