diff --git a/changelog.md b/changelog.md index b0dad39..6249824 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog][keep-a-changelog] ## [Unreleased] - Nothing right now +## [0.1.7] (2020-09-02) + +### Added +- Added IDKollen API provider (idkbankid) + +### Fixed +- Added error handling on internal errors when following requests, should create more stable library (all modules). +- Verified API and updated settings for Funktionstjänster (CGI) Freja eID module (ftfrejaeid) + ## [0.1.5] (2020-09-01) ### Fixed @@ -63,6 +72,7 @@ The format is based on [Keep a Changelog][keep-a-changelog] [keep-a-changelog]: http://keepachangelog.com/en/1.0.0/ [Unreleased]: https://github.com/DSorlov/eid-provider/compare/master...dev +[0.1.7]: https://github.com/DSorlov/eid-provider/releases/tag/v0.1.7 [0.1.5]: https://github.com/DSorlov/eid-provider/releases/tag/v0.1.5 [0.1.4]: https://github.com/DSorlov/eid-provider/releases/tag/v0.1.4 [0.1.3]: https://github.com/DSorlov/eid-provider/releases/tag/v0.1.3 diff --git a/docs/idkbankid.md b/docs/idkbankid.md new file mode 100644 index 0000000..8a4847e --- /dev/null +++ b/docs/idkbankid.md @@ -0,0 +1,37 @@ +## BankID (bankid) + +### Description +This module lets you communicate and use the IDKollen V2 API. + +To use IDKollen API (both test and production) you will need to contact +IDKollen to obtain a key. See (https://idkollen.se/)[https://idkollen.se/] + +Due to the API beeing a call-back API a postback endpoint is created +for each request using https://webhook.site/ and a temporary endpoint, +the endpoint deletes once there response is received. You do not need +Webhook Premium, but if you are using it you can make sure your urls +and data are deleted as appropiate and that no limts are imposed. + +### Inputs and outputs + +**Alternative inputs** + +Also accepts objects with `ssn` (Social Security Number) property. + +**Extra fields on completion** +* `autostart_token` the token used for autostart +* `autostart_url` code for invoking authorization + +### Default Configuration +>**Default production configuration (settings.production)** +``` +endpoint: 'https://api.idkollen.se/v2', +key: '', +webhookkey: '' +``` +>**Default testing configuration (settings.testing)** +``` +endpoint: 'https://stgapi.idkollen.se/v2', +key: '', +webhookkey: '' +``` \ No newline at end of file diff --git a/modules/bankid.js b/modules/bankid.js index 02c101f..5e7e77b 100644 --- a/modules/bankid.js +++ b/modules/bankid.js @@ -152,6 +152,10 @@ async function followRequest(self,initresp, initcallback=undefined, statuscallba // Retreive current status const [error, pollresp] = await to(pollStatus(initresp.id,self)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/frejaeid.js b/modules/frejaeid.js index e392d75..3152b98 100644 --- a/modules/frejaeid.js +++ b/modules/frejaeid.js @@ -269,6 +269,10 @@ async function followRequest(self, pollMethod, initresp, initcallback=undefined, const [error, pollresp] = pollMethod=='auth' ? await to(self.pollAuthStatus(initresp.id,self)) : await to(self.pollSignStatus(initresp.id,self)) + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/frejaorgid.js b/modules/frejaorgid.js index bcf90a8..bb121b1 100644 --- a/modules/frejaorgid.js +++ b/modules/frejaorgid.js @@ -321,6 +321,10 @@ async function followRequest(self, pollMethod, initresp, initcallback=undefined, if (pollMethod=='sign') [error, pollresp] = await to(self.pollSignStatus(initresp.id,self)); if (pollMethod=='add') [error, pollresp] = await to(self.pollAddOrgIdStatus(initresp.id,self)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error'||pollresp.status==='created') { return pollresp; } diff --git a/modules/ftbankid.js b/modules/ftbankid.js index 0cdf65a..5ceec8c 100644 --- a/modules/ftbankid.js +++ b/modules/ftbankid.js @@ -184,6 +184,10 @@ async function followRequest(self,initresp, initcallback=undefined, statuscallba // Retreive current status const [error, pollresp] = await to(pollStatus(initresp.id,self)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/ftfrejaeid.js b/modules/ftfrejaeid.js index 7fe5043..5b9f04c 100644 --- a/modules/ftfrejaeid.js +++ b/modules/ftfrejaeid.js @@ -87,11 +87,11 @@ function createClient(self,uri,method,options) { async function pollStatus(id,self=this) { - if (id.length!=73) { + if (id.length!=102) { return {status: 'error', code: 'request_id_invalid', description: 'The supplied request cannot be found'}; } - var orderRef = id.substring(37,73); + var orderRef = id.substring(37,102); var transactionId = id.substring(0,36); var [error,result] = await to(createClient(self,'GrpServicePortType/CollectRequest', 'Collect', { @@ -138,11 +138,10 @@ async function pollStatus(id,self=this) { ssn: result.userInfo.subjectIdentifier, firstname: result.userInfo.givenName, surname: result.userInfo.sn, - fullname: result.userInfo.displayName + fullname: result.userInfo.givenName + ' ' + result.userInfo.sn }, extra: { - signature: result.validationInfo.signature, - ocspResponse: result.validationInfo.ocspResponse} + signature: result.validationInfo.signature} }; default: return {status: 'error', code: 'api_error', description: 'A communications error occured', details: result.data}; @@ -183,6 +182,10 @@ async function followRequest(self,initresp, initcallback=undefined, statuscallba // Retreive current status const [error, pollresp] = await to(pollStatus(initresp.id,self)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/gbankid.js b/modules/gbankid.js index 8cb4509..09bd7b4 100644 --- a/modules/gbankid.js +++ b/modules/gbankid.js @@ -152,6 +152,10 @@ async function followRequest(self,initresp, initcallback=undefined, statuscallba // Retreive current status const [error, pollresp] = await to(self.pollAuthStatus(initresp.id)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/gfrejaeid.js b/modules/gfrejaeid.js index f80195c..8cbc0e1 100644 --- a/modules/gfrejaeid.js +++ b/modules/gfrejaeid.js @@ -82,6 +82,10 @@ async function followRequest(self, initresp, initcallback=undefined, statuscallb // Retreive current status const [error, pollresp] = await to(pollStatus(initresp.id,self)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/ghsaid.js b/modules/ghsaid.js index a5dc511..eba528c 100644 --- a/modules/ghsaid.js +++ b/modules/ghsaid.js @@ -148,6 +148,10 @@ async function followRequest(self,initresp, initcallback=undefined, statuscallba // Retreive current status const [error, pollresp] = await to(self.pollAuthStatus(initresp.id)); + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + // Check if we we have a definite answer if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } diff --git a/modules/idkbankid.js b/modules/idkbankid.js new file mode 100644 index 0000000..e7b539a --- /dev/null +++ b/modules/idkbankid.js @@ -0,0 +1,260 @@ +const axioslibrary = require('axios').default; +const fs = require('fs'); +const https = require('https'); +const to = require('await-to-js').default; + +const defaultSettings = { + production: { + endpoint: 'https://api.idkollen.se/v2', + key: '', + webhookkey: '' + }, + testing: { + endpoint: 'https://stgapi.idkollen.se/v2', + key: '', + webhookkey: '' + } +} + +var settings = undefined; +var axios = undefined; + +// Lets set some default values for this circus +function initialize(settings) { + //TODO: Validate the incomming object for completeness. + this.settings = settings; + this.axios = axioslibrary.create({ + httpsAgent: new https.Agent(), + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +function unPack(data) { + if (typeof data === 'string') { + return data; + } else { + if (data.ssn) { + return data.ssn; + } else { + return data.toString(); + } + } +} + +// Check the status of an existing request +async function pollStatus(id,self=this) { + + var webhookId = id.substring(37,73); + var orderRef = id.substring(0,36); + + // Check the transaction via API + const [error, response] = await to(self.axios.get(`https://webhook.site/token/${webhookId}/request/latest/raw`, {})); + + + // Since the API is returning error on pending, we consolidate and handle + // it further down + var result = error ? error.response : response; + + if (error && result.status==404) { + return {status: 'pending', code: 'pending_delivered', description: 'Delivered to mobile phone'}; + } + + // Delete our temporary hook + var webhook_data = { data: { } }; + if (self.settings.webhookkey!=='') { + webhook_data.headers = { + 'Api-Key': self.settings.webhookkey + } + } + await to(self.axios.delete(`https://webhook.site/token/${webhookId}`)); + + if (result.message) { + return {status: 'error', code: 'system_error', description: error.status, details: error.message} + } + + // We should actually only get here if we complete but lets handle the unlikely + if (result.data.result==="completed") { + return { + status: 'completed', + user: { + id: result.data.pno, + firstname: '', + surname: '', + fullname: result.data.name + }, + extra: { + checksum: result.data.checksum} + }; + } else { + //This will never happen. Or it should not. Probably gonna happen. + return {status: 'error', code: 'cancelled_by_idp', description: 'The IdP have cancelled the request'}; + } +} + +// Lets set up the followRequest for a singing request +async function signRequest(ssn, text, initcallback=undefined, statuscallback=undefined) { + var initresp = await this.initSignRequest(ssn,text); + return await followRequest(this,initresp,initcallback,statuscallback); +} + +// Lets set up the followRequest for a authentication request +async function authRequest(ssn, initcallback=undefined, statuscallback=undefined) { + var initresp = await this.initAuthRequest(ssn); + return await followRequest(this,initresp,initcallback,statuscallback); +} + +// Start a authRequest and wait for completion +// Callback will be called as long as we are pending definite answer +async function followRequest(self,initresp, initcallback=undefined, statuscallback=undefined) { + + // Since the initAuthRequest will be polite and never throw, we have + // to check for a error in this more civil way. Return if error. + // could do some additional processing here if needed + if (initresp.status==='error') { + return initresp; + } + + // Let the caller know we are starting work + if (initcallback) { initcallback(initresp); } + + // Do the loop thing + while (true) { + + // Retreive current status + const [error, pollresp] = await to(pollStatus(initresp.id,self)); + + if (error) { + return {status: 'error', code: 'system_error', description: 'Internal module error', details: error.message} + } + + // Check if we we have a definite answer + if (pollresp.status==='completed'||pollresp.status==='error') { return pollresp; } + + // Ok, no definite answer yet, check if we have a callback to do and perform that + if (statuscallback) { statuscallback(pollresp); } + + // APIs impose a rate limit so lets wait two seconds before we try again + await new Promise(resolve => setTimeout(resolve, 2000)); + } +} + +// Lets structure a call for a auth request and return the worker +async function initAuthRequest(ssn) { + ssn = unPack(ssn); + return await initRequest(this,'auth', { + ipAddress: '127.0.0.1', + pno: ssn, + callbackUrl: 'https://webhook.site/' + }); +} + +// Lets structure a call for a sign request and return the worker +async function initSignRequest(ssn,text) { + ssn = unPack(ssn); + return await initRequest(this,'sign', { + endUserIp: '127.0.0.1', + pno: ssn, + message: text, + callbackUrl: 'https://webhook.site/' + }); +} + +// Initialize and imediatly return with the id of the request +async function initRequest(self,endpoint,data) { + + // Create a temporary webhook to catch the response + // The IDKollen API is sadly a callback API so workaround is needed. + + var webhook_postdata = { data: { + default_status: 200, + default_content: "Ok", + default_content_type: "text/html", + timeout: 0, + cors: false, + expiry: true + } }; + if (self.settings.webhookkey!=='') { + webhook_postdata.headers = { + 'Api-Key': self.settings.webhookkey + } + } + + const [webhook_error, webhook_response] = await to(self.axios.post(`https://webhook.site/token`, webhook_postdata)); + + // Make sure we generated a URI before continuing + if(webhook_error) { + return {status: 'error', code: 'system_error', description: webhook_error.code, details: error.message} + } + var responseUiid = webhook_response.data.uuid; + data.callbackUrl = data.callbackUrl + responseUiid; + + // Call BankID. We are using a fake remote address as most APIs do not have this + const [error, response] = await to(self.axios.post(`${self.settings.endpoint}/${self.settings.key}/${endpoint}`, data)); + + // Since the API is returning error on pending, we consolidate and handle + // it further down + var result = error ? error.response : response; + + // Check if we get a success message or a failure (http) from the api, return standard response structure + if(!error) { + return {status: 'initialized', id: result.data.orderRef+'-'+responseUiid, extra: { + autostart_token: result.data.autoStartToken, + autostart_url: "bankid:///?autostarttoken="+result.data.autoStartToken+"&redirect=null" + }}; + } else { + if (!error.response && error.isAxiosError) { + return {status: 'error', code: 'system_error', description: error.code, details: error.message} + } + + if (result.data.message) { + switch(result.data.message) { + case "alreadyInProgress": + return {status: 'error', code: 'already_in_progress', description: 'A transaction was already pending for this SSN'}; + case "invalidParameters": + if (result.data.details==='Incorrect personalNumber') { + return {status: 'error', code: 'request_ssn_invalid', description: 'The supplied SSN is not valid'}; + } else if (result.data.details==='Invalid userVisibleData') { + return {status: 'error', code: 'request_text_invalid', description: 'The supplied agreement text is not valid'}; + } else { + return {status: 'error', code: 'api_error', description: 'A communications error occured', details: result.data.details}; + } + default: + return {status: 'error', code: 'api_error', description: 'A communications error occured', details: result.data.details}; + } + } else { + return {status: 'error', code: "api_error", description: 'A communications error occured', details: result.data}; + } + } +} + +// Lets cancel pending requests, dont really care about the results here +async function cancelRequest(id) { + + var webhookId = id.substring(37,73); + var orderRef = id.substring(0,36); + + var webhook_data = { data: { } }; + if (self.settings.webhookkey!=='') { + webhook_data.headers = { + 'Api-Key': self.settings.webhookkey + } + } + + await to(this.axios.delete(`https://webhook.site/token/${webhookId}`, webhook_data)); + return true; +} + +module.exports = { + settings: defaultSettings, + initialize: initialize, + pollAuthStatus: pollStatus, + pollSignStatus: pollStatus, + signRequest: signRequest, + authRequest: authRequest, + initAuthRequest: initAuthRequest, + initSignRequest: initSignRequest, + cancelSignRequest: cancelRequest, + cancelAuthRequest: cancelRequest +} \ No newline at end of file diff --git a/package.json b/package.json index d6d6251..e3d30ef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eid-provider", - "version": "0.1.5", + "version": "0.1.7", "description": "Integration module for electronic identification providers", "bundleDependencies": false, "deprecated": false, diff --git a/readme.md b/readme.md index 02e965b..bbd3b26 100644 --- a/readme.md +++ b/readme.md @@ -1,5 +1,5 @@ [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](#) -[![version](https://img.shields.io/badge/version-0.1.5-green.svg)](#) +[![version](https://img.shields.io/badge/version-0.1.7-green.svg)](#) [![maintained](https://img.shields.io/maintenance/yes/2020.svg)](#) [![maintainer](https://img.shields.io/badge/maintainer-daniel%20sörlöv-blue.svg)](https://github.com/DSorlov) [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://img.shields.io/github/license/DSorlov/eid-provider) @@ -31,13 +31,13 @@ There are basically right now two main types of integrations: one is working dir | Freja eID | [frejaeid](docs/frejaeid.md) | Verisec (Freja eID) | :heavy_check_mark: | :heavy_check_mark: | :sweden: :denmark: :norway: :finland: | Production | | Freja eID | [frejaorgid](docs/frejaorgid.md) | Verisec (Freja eID) | :heavy_check_mark: | :heavy_check_mark: | :sweden: :denmark: :norway: :finland: | Production | | Mobilt BankID | [ftbankid](docs/ftbankid.md) | Funktionstjänster (CGI) | :heavy_check_mark: | :heavy_check_mark: | :sweden: | Production | -| Freja eID | [ftfrejaeid](docs/ftfrejaeid.md) | Funktionstjänster (CGI) | :heavy_check_mark: | :heavy_check_mark: | :sweden: :denmark: :norway: :finland: | Not tested* | +| Freja eID | [ftfrejaeid](docs/ftfrejaeid.md) | Funktionstjänster (CGI) | :heavy_check_mark: | :heavy_check_mark: | :sweden: :denmark: :norway: :finland: | Production | | Mobilt BankID | [gbankid](docs/gbankid.md) | Svensk e-Identitet | :heavy_check_mark: | :heavy_check_mark: | :sweden: | Production | -| Freja eID | [gfrejaeid](docs/gfrejaeid.md) | Svensk e-Identitet | :heavy_check_mark:| :heavy_check_mark: | :sweden: | Production** | +| Freja eID | [gfrejaeid](docs/gfrejaeid.md) | Svensk e-Identitet | :heavy_check_mark:| :heavy_check_mark: | :sweden: | Stable* | | SITHS Mobile | [ghsaid](docs/ghsaid.md) | Svensk e-Identitet | :heavy_check_mark:| :heavy_check_mark: | :sweden: | Production | +| Mobilt BankID | [gbankid](docs/idkbankid.md) | IDKollen | :heavy_check_mark: | :heavy_check_mark: | :sweden: | Production | -* The API key we have been supplied with does not allow for freja authentication so largely untested but complies with api.
-** GrandID do not officially support Freja eID for silent logins. Using some ugly workarounds tbh, so not for production I think imho. +* GrandID do not officially support Freja eID for silent logins. Using some ugly workarounds tbh. Evaluate before production! The configuration options should be quite obvious as what they do. If you are unsure your supplier will most probably be able to determine what information you need. Most modules have sane values, certificates etc for most testing services and production services however there is no production credentials and you need to strike an agreement with the services yourself to obtain these.