To initialise the SDK, there are some pre-requisites:
- A configured API space.
- An authentication provider that can generate a JWT that matches the auth scheme configured for your API space.
- The generated JWT must include the provided nonce as a claim.
ES6
Hes a typescript sample using ES6 import syntax, available from the NPM package.)
// some app specific imports
import { AppSettings } from "../settings";
import { AuthService } from "./auth";
// Comapi class / interface imports
import { Foundation, ComapiConfig, IAuthChallengeOptions } from "@comapi/sdk-js-foundation"
export class ComapiService {
public sdk: Foundation;
private authChallenge(options: IAuthChallengeOptions, answerAuthenticationChallenge) {
this._authService.getToken(options.nonce)
.then((token) => {
answerAuthenticationChallenge(token);
});
}
constructor(private _authService: AuthService) { }
/**
* Public method to encapsulate up the initialisation of Comapi
*/
public initialise(): Promise<Foundation> {
return new Promise((resolve, reject) => {
if (this._authService.isAuthenticated()) {
let comapiConfig = new ComapiConfig()
.withApiSpace(AppSettings.APP_SPACE_ID)
// Note the this pointer binding so I can access this._authService in the authChallenge calllback
.withAuthChallenge(this.authChallenge.bind(this));
Foundation.initialise(comapiConfig)
.then((sdk) => {
this.sdk = sdk;
console.log("foundation interface created");
resolve(sdk);
})
.catch((error) => {
console.error("initialise failed", error);
reject(error);
});
} else {
reject("Not logged in");
}
});
}
}
Classical
function authChallenge(options, answerAuthenticationChallenge) {
authService.getToken(options.nonce)
.then((token) => {
answerAuthenticationChallenge(token);
})
.catch(error=>{
answerAuthenticationChallenge(null);
});
}
var comapiConfig = new COMAPI.ComapiConfig()
.withApiSpace(appConfig.apiSpaceId)
.withAuthChallenge(authChallenge);
COMAPI.Foundation.initialise(comapiConfig)
.then(function (sdk) {
console.log("Foundation interface created", sdk);
})
.catch(function (error) {
$log.error("paragonService: failed to initialise", error);
});
Advanced options
The above examples initialised the SDK with minimal configuration. You can customise the SDK behaviour with the following optional settings.
logRetentionTime
When the SDK uses indexedDB to persist logs, they are purged on SDK initialisation. This value represents the number of hours to keep logs for. The default value is 24.
logLevel
This parameter controls what level of logging to perform.
export enum LogLevels {
None,
Error,
Warn,
Debug
};
The default setting is to only log errors.
logPersistence
This parameter controls whether and where to persist log data. The historic data is available through a call to Foundation.getLogs()
.
export enum LogPersistences {
None,
IndexedDB,
LocalStorage
};
The default setting is to use local storage. IndexedDB is more performant but might require a poly-fill.
Authentication
JWT
The Comapi Auth Challenge needs to generate and return a JWT through the answerAuthenticationChallenge
method.
There are 4 pieces of data that need to be specified in the portal for the ApiSpace auth settings:
- Issuer
- Audience
- Shared Secret
- ID Claim
A cryptographic nonce is used as part of the authentication flow. This is passed into the authChallenge (options.nonce
) and needs to be added as a claim in the generated JWT.
The below sample uses jsrsasign to dynamically create a client side JWT.
function authChallenge (options, answerAuthenticationChallenge) {
// Header
var oHeader = { alg: 'HS256', typ: 'JWT' };
// Payload
var tNow = KJUR.jws.IntDate.get('now');
var tEnd = KJUR.jws.IntDate.get('now + 1day');
var oPayload = {
sub: "john smith",
nonce: options.nonce,
iss: "https://my-issuer.com",
aud: "https://my-audience.com",
iat: tNow,
exp: tEnd,
};
var sHeader = JSON.stringify(oHeader);
var sPayload = JSON.stringify(oPayload);
var sJWT = KJUR.jws.JWS.sign("HS256", sHeader, sPayload, {utf8: "my shared secret"});
answerAuthenticationChallenge(sJWT);
}
Be careful with the secret value being automatically cast to hex
The jsrassign library automatically checks the secret field to see if it is a hexadecimal number being passed. We always use string based secrets, so to stop this unwanted automatic cast you must ensure that the secret value is always cast to an UTF8 string by using the following cast operation
{utf8: <Your secret value>}
.Common indications of this issue are 403 - Invalid JWT errors when trying to start a session with the SDK.
This node express method uses the njwt package and achieves the same as above but server - side
/**
* @Params {string} req.body.username
* @Params {string} req.body.password
* @Params {string} req.body.nonce
*/
app.post('/authenticate', function (req, res, next) {
// TODO: authenticate username & password ...
var claims = {
iss: "https://my-issuer.com",
sub: req.body.username,
nonce: req.body.nonce,
aud: "https://my-audience.com"
}
var jwt = njwt.create(claims, "my shared secret");
var token = jwt.compact();
res.json({ jwt: token });
});
The following auth challenge could be used in conjunction with the above node endpoint.
function authChallenge (options, answerAuthenticationChallenge) {
$http.post("/authenticate", {
username: "johnSmith"
password: "Passw0rd!",
nonce: options.nonce })
.then(function (response) {
answerAuthenticationChallenge(response.data.token);
})
.catch(function (error) {
answerAuthenticationChallenge(null);
});
}
Sessions
To call onto any of our services, a valid session is required. This is what the authChallenge
is for.
Whenever we need a session and there isn’t a currently active one, we run through the auth flow as part of session creation.
You can explicitly start a session, or the SDK creates one on the fly the first time you call a method that requires one.
To start a session manually as part of the initialisation flow:
Foundation.initialise(comapiConfig)
.then(sdk => {
console.log("sdk initialised", sdk);
return sdk.startSession();
})
.then(sessionInfo => {
console.log("session started", sessionInfo);
})
.catch(error => {
console.error("Something went wrong", error);
});