smartid-auth
Version:
Smart-ID authentication client module for Node.JS
162 lines (147 loc) • 4.57 kB
JavaScript
const axios = require('axios');
const crypto = require('crypto');
const x509 = require('@ghaiklor/x509');
const authHash = require('./authhash.js');
const CERT_BEGIN = '-----BEGIN CERTIFICATE-----\n';
const CERT_END = '\n-----END CERTIFICATE-----';
function timeout(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
class Session {
constructor(config, request, id, verificationCode) {
this.config = config;
this._request = request;
this.id = id;
this.verificationCode = verificationCode;
}
async getResponse(rejectNotOK) {
let response;
try {
response = await axios(
Object.assign(
{
method: 'GET',
responseType: 'json',
validateStatus: (status) => status === 200,
url: this.config.host + '/session/' + this.id + '?timeoutMs=10000',
},
this.config.http || {}
)
);
} catch (err) {
response = err.response;
}
const body = response && response.data;
if (typeof body !== 'object') {
throw new Error(`Invalid response: ${body}`);
} else if (!body.state && !body.result) {
throw new Error(`Invalid response: ${JSON.stringify(body)}`);
} else if (body.state && body.state !== 'COMPLETE') {
// not completed yet, retry after 100ms
await timeout(100);
await this.getResponse(rejectNotOK);
} else {
if (body.result.endResult !== 'OK') {
if (rejectNotOK) {
throw new Error(`Invalid result: ${body.result.endResult}`);
} else {
return {
result: body.result.endResult,
data: body,
};
}
} else {
// result.endResult = "OK"
// verify signature:
const verifier = crypto.createVerify(body.signature.algorithm);
verifier.update(this._request.hash.raw);
const cert = CERT_BEGIN + body.cert.value + CERT_END;
if (!verifier.verify(cert, body.signature.value, 'base64')) {
throw new Error('Invalid signature (verify failed)');
}
// check if cert is active and not expired:
const parsedCert = x509.parseCert(cert);
const date = new Date();
if (parsedCert.notBefore > date) {
throw new Error('Certificate is not active yet');
} else if (parsedCert.notAfter < date) {
throw new Error('Certificate has expired');
} else {
return {
result: body.result.endResult,
subject: x509.getSubject(cert),
data: body,
};
}
}
}
}
}
class Authentication {
constructor(config, country, idNumber) {
this.config = config;
this.request = {
idNumber: idNumber,
country: country.toUpperCase(),
};
}
async authenticate(displayText) {
const hash = await authHash.generateRandomHash();
this.request.hash = hash;
let response;
try {
response = await axios(
Object.assign(
{
method: 'post',
url:
this.config.host +
'/authentication/pno/' +
this.request.country +
'/' +
this.request.idNumber,
responseType: 'json',
validateStatus: (status) => status === 200,
data: Object.assign(
{
hash: hash.digest,
hashType: 'SHA512',
displayText:
typeof displayText === 'string' ? displayText : undefined,
},
this.config.requestParams
),
},
this.config.http || {}
)
);
} catch (err) {
response = err.response;
}
const body = response && response.data;
if (typeof body !== 'object' || !body.sessionID) {
throw new Error(`Invalid response: ${JSON.stringify(body)}`);
}
return new Session(
this.config,
this.request,
body.sessionID,
authHash.calculateVerificationCode(hash.digest)
);
}
}
class SmartID {
constructor(config) {
if (!config.host || !config.requestParams)
throw new TypeError('Invalid configuration');
this.config = config;
}
async authenticate(country, idNumber, displayText) {
if (!country || !idNumber)
throw new TypeError('Missing mandatory parameters');
const auth = new Authentication(this.config, country, idNumber);
const response = await auth.authenticate(displayText);
return response;
}
}
module.exports = SmartID;