Initialise

To initialise the SDK, there are some pre-requisites:

  1. A configured API space.
  2. An authentication provider that can generate a JWT that matches the auth scheme configured for your API space.
  3. 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:

  1. Issuer
  2. Audience
  3. Shared Secret
  4. 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);
    });