From 12c4910781fe961848235d8b71d7e22efe5910cf Mon Sep 17 00:00:00 2001 From: Kok Seng Date: Thu, 31 Aug 2023 23:27:22 +0800 Subject: [PATCH 1/3] feat: add rules API --- package-lock.json | 188 ++++++++++++++++++++++++++++++++++++++++------ package.json | 2 + src/SgidClient.ts | 46 +++++++++--- src/constants.ts | 3 + src/error.ts | 4 + src/types.ts | 13 ++++ 6 files changed, 224 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5c2b4e..e692e60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "jose": "4.9.2", + "node-fetch": "2.6.1", "node-rsa": "1.1.1", "openid-client": "5.4.0" }, @@ -17,6 +18,7 @@ "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^9.0.1", "@types/node": "*", + "@types/node-fetch": "^2.6.4", "@types/node-rsa": "^1.1.1", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", @@ -1502,6 +1504,16 @@ "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==", "dev": true }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/node-rsa": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.1.tgz", @@ -2206,6 +2218,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -2747,6 +2765,18 @@ "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -2873,6 +2903,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3931,6 +3970,20 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5940,6 +5993,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6022,6 +6096,26 @@ } } }, + "node_modules/msw/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/msw/node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -6053,23 +6147,11 @@ "dev": true }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", "engines": { "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } } }, "node_modules/node-int64": { @@ -8814,6 +8896,16 @@ "integrity": "sha512-7EIraBEyRHEe7CH+Fm1XvgqU6uwZN8Q7jppJGcqjROMT29qhAuuOxYB1uEY5UMYQKEmA5D+5tBnhdaPXSsLONA==", "dev": true }, + "@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/node-rsa": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.1.tgz", @@ -9272,6 +9364,12 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -9648,6 +9746,15 @@ "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -9742,6 +9849,12 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -10557,6 +10670,17 @@ "is-callable": "^1.1.3" } }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -12059,6 +12183,21 @@ "picomatch": "^2.2.3" } }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -12113,6 +12252,15 @@ "yargs": "^17.3.1" }, "dependencies": { + "node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + } + }, "type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -12140,13 +12288,9 @@ "dev": true }, "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" }, "node-int64": { "version": "0.4.0", diff --git a/package.json b/package.json index f1b63fd..a2af061 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^9.0.1", "@types/node": "*", + "@types/node-fetch": "2.6.4", "@types/node-rsa": "^1.1.1", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", @@ -54,6 +55,7 @@ }, "dependencies": { "jose": "4.9.2", + "node-fetch": "2.6.1", "node-rsa": "1.1.1", "openid-client": "5.4.0" } diff --git a/src/SgidClient.ts b/src/SgidClient.ts index 0d301ee..2848ed4 100644 --- a/src/SgidClient.ts +++ b/src/SgidClient.ts @@ -1,4 +1,5 @@ import { compactDecrypt, importJWK, importPKCS8 } from 'jose' +import fetch from 'node-fetch' import { Client, Issuer } from 'openid-client' import { @@ -6,6 +7,7 @@ import { DEFAULT_SCOPE, DEFAULT_SGID_CODE_CHALLENGE_METHOD, SGID_AUTH_METHOD, + SGID_RULES_ENGINE_URL, SGID_SIGNING_ALG, SGID_SUPPORTED_GRANT_TYPES, } from './constants' @@ -17,6 +19,8 @@ import { CallbackParams, CallbackReturn, ParsedSgidDataValue, + RulesParams, + RulesReturn, SgidClientParams, UserInfoParams, UserInfoReturn, @@ -33,11 +37,11 @@ export class SgidClient { /** * Initialises an SgidClient instance. - * @param params Constructor arguments - * @param params.clientId Client ID provided during client registration - * @param params.clientSecret Client secret provided during client registration - * @param params.privateKey Client private key provided during client registration - * @param params.redirectUri Redirection URI for user to return to your application + * @param params Constructor arguments. + * @param params.clientId Client ID provided during client registration. + * @param params.clientSecret Client secret provided during client registration. + * @param params.privateKey Client private key provided during client registration. + * @param params.redirectUri Redirection URI for user to return to your application. * after login. If not provided in the constructor, this must be provided to the * authorizationUrl and callback functions. * @param params.hostname Hostname of OpenID provider (sgID). Defaults to @@ -99,7 +103,7 @@ export class SgidClient { * param is provided, it will be used instead of the redirect URI provided in the * SgidClient constructor. If not provided in the constructor, the redirect URI * must be provided here. - * @param codeChallenge The code challenge from the code verifier used for PKCE enhancement + * @param codeChallenge The code challenge from the code verifier used for PKCE enhancement. */ authorizationUrl({ state, @@ -136,12 +140,12 @@ export class SgidClient { /** * Exchanges authorization code for access token. - * @param code The authorization code received from the authorization server + * @param code The authorization code received from the authorization server. * @param nonce Nonce passed to authorizationUrl for this request. Specify null * if no nonce was passed to authorizationUrl. * @param redirectUri The redirect URI used in the authorization request. Defaults to the one * passed to the SgidClient constructor. - * @param codeVerifier The code verifier that was used to generate the code challenge that was passed in `authorizationUrl` + * @param codeVerifier The code verifier that was used to generate the code challenge that was passed in `authorizationUrl`. * @returns The sub (subject identifier claim) of the user and access token. The subject * identifier claim is the end-user's unique ID. */ @@ -169,8 +173,8 @@ export class SgidClient { /** * Retrieves verified user info and decrypts it with your private key. - * @param sub The sub returned from the callback function - * @param accessToken The access token returned from the callback function + * @param sub The sub returned from the callback function. + * @param accessToken The access token returned from the callback function. * @returns The sub of the end-user and the end-user's verified data. The sub * returned is the same as the one passed in the params. */ @@ -209,6 +213,28 @@ export class SgidClient { return { sub, data: {} } } + /** + * Generates dynamic derived data based on rules defined on sgID Developer Portal. + * @param rulesParams The parameters for dynamic derived data calculation. + * @param rulesParams.clientId The client ID of the Relying Party. + * @param rulesParams.accessToken The access token returned from the callback function. + * @param rulesParams.ruleNames The space-separated string containing rule names. + * @param rulesParams.userInfoData The end-user's verified data. + * @returns {RulesReturn} A list of rule names, alongside with their corresponding inputs and outputs. + */ + async rules(rulesParams: RulesParams): Promise { + try { + const response = await fetch(SGID_RULES_ENGINE_URL, { + method: 'POST', + body: JSON.stringify(rulesParams), + }) + const data: RulesReturn = await response.json() + return data + } catch (err) { + return Promise.reject((err as Error).message) + } + } + private async decryptPayload( encryptedPayloadKey: string, data: Record, diff --git a/src/constants.ts b/src/constants.ts index a1ee242..9431a22 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,3 +7,6 @@ export const SGID_SUPPORTED_GRANT_TYPES: ResponseType[] = ['code'] export const SGID_AUTH_METHOD: ClientAuthMethod = 'client_secret_post' export const API_VERSION = 2 + +// TODO: Replace with https://rules.id.gov.sg/api/rule/eval once it's up +export const SGID_RULES_ENGINE_URL = 'http://localhost:3001/api/rule/eval' diff --git a/src/error.ts b/src/error.ts index f528f96..f02237d 100644 --- a/src/error.ts +++ b/src/error.ts @@ -21,3 +21,7 @@ export const DECRYPT_BLOCK_KEY_ERROR = export const DECRYPT_PAYLOAD_ERROR = 'Unable to decrypt payload' export const SUB_MISMATCH_ERROR = 'Sub returned by sgID did not match the sub passed to the userinfo method. Check that you passed the correct sub to the userinfo method.' + +// rules errors +export const INVALID_RULES_NAMES_ERROR = + '[ruleNames] is missing/contains invalid rule names' diff --git a/src/types.ts b/src/types.ts index e97ccd2..6260cf0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,3 +36,16 @@ export type SgidClientParams = { redirectUri?: string hostname?: string } + +export type RulesParams = { + clientId: string + accessToken: string + ruleNames: string + userInfoData: Record +} + +export type RulesReturn = { + ruleName: string + input: string + output: string +}[] From 789d92eecd457723008e9c313083de59af0b7b0b Mon Sep 17 00:00:00 2001 From: Kok Seng Date: Thu, 31 Aug 2023 23:49:22 +0800 Subject: [PATCH 2/3] docs: add TODO --- src/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 9431a22..d1efdb5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,4 +9,5 @@ export const SGID_AUTH_METHOD: ClientAuthMethod = 'client_secret_post' export const API_VERSION = 2 // TODO: Replace with https://rules.id.gov.sg/api/rule/eval once it's up -export const SGID_RULES_ENGINE_URL = 'http://localhost:3001/api/rule/eval' +// Was overwritten with local host URL during testing +export const SGID_RULES_ENGINE_URL = 'https://rules-stg.id.gov.sg/api/rule/eval' From 6e9e0ca1445f1e14c59c7364dc0f9e9a382e8623 Mon Sep 17 00:00:00 2001 From: Kok Seng Date: Mon, 25 Sep 2023 14:42:41 +0800 Subject: [PATCH 3/3] fix: add rules engine hostname as optional parameter --- package-lock.json | 36 ++++++++++++++++++------------------ package.json | 2 +- src/SgidClient.ts | 33 +++++++++++++++++++++++++-------- src/constants.ts | 5 ++--- src/error.ts | 4 ---- src/types.ts | 6 +++--- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index e692e60..244683e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "jose": "4.9.2", - "node-fetch": "2.6.1", + "node-fetch": "2.6.4", "node-rsa": "1.1.1", "openid-client": "5.4.0" }, @@ -18,7 +18,7 @@ "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^9.0.1", "@types/node": "*", - "@types/node-fetch": "^2.6.4", + "@types/node-fetch": "2.6.4", "@types/node-rsa": "^1.1.1", "@typescript-eslint/eslint-plugin": "^5.59.2", "@typescript-eslint/parser": "^5.59.2", @@ -6147,9 +6147,12 @@ "dev": true }, "node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-aD1fO+xtLiSCc9vuD+sYMxpIuQyhHscGSkBEo2o5LTV/3bTEAYvdUii29n8LlO5uLCmWdGP7uVUVXFo5SRdkLA==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, "engines": { "node": "4.x || >=6.0.0" } @@ -7283,8 +7286,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-jest": { "version": "29.0.5", @@ -7559,14 +7561,12 @@ "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" @@ -12288,9 +12288,12 @@ "dev": true }, "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-aD1fO+xtLiSCc9vuD+sYMxpIuQyhHscGSkBEo2o5LTV/3bTEAYvdUii29n8LlO5uLCmWdGP7uVUVXFo5SRdkLA==", + "requires": { + "whatwg-url": "^5.0.0" + } }, "node-int64": { "version": "0.4.0", @@ -13116,8 +13119,7 @@ "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "ts-jest": { "version": "29.0.5", @@ -13314,14 +13316,12 @@ "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "requires": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index a2af061..756bd95 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ }, "dependencies": { "jose": "4.9.2", - "node-fetch": "2.6.1", + "node-fetch": "2.6.4", "node-rsa": "1.1.1", "openid-client": "5.4.0" } diff --git a/src/SgidClient.ts b/src/SgidClient.ts index 2848ed4..5cc5f6a 100644 --- a/src/SgidClient.ts +++ b/src/SgidClient.ts @@ -7,7 +7,7 @@ import { DEFAULT_SCOPE, DEFAULT_SGID_CODE_CHALLENGE_METHOD, SGID_AUTH_METHOD, - SGID_RULES_ENGINE_URL, + SGID_RULES_ENGINE_ENDPOINT, SGID_SIGNING_ALG, SGID_SUPPORTED_GRANT_TYPES, } from './constants' @@ -34,6 +34,7 @@ import { export class SgidClient { private privateKey: string private sgID: Client + private rulesEngineEndpoint: string /** * Initialises an SgidClient instance. @@ -46,6 +47,8 @@ export class SgidClient { * authorizationUrl and callback functions. * @param params.hostname Hostname of OpenID provider (sgID). Defaults to * https://api.id.gov.sg. + * @param params.rulesEngineEndpoint API endpoint for sgID Rules Engine. Defaults to + * https://rules.id.gov.sg/api/rule/eval. */ constructor({ clientId, @@ -53,6 +56,7 @@ export class SgidClient { privateKey, redirectUri, hostname = 'https://api.id.gov.sg', + rulesEngineEndpoint = SGID_RULES_ENGINE_ENDPOINT, }: SgidClientParams) { /** * Note that issuer is appended with version number only from v2 onwards @@ -76,6 +80,8 @@ export class SgidClient { token_endpoint_auth_method: SGID_AUTH_METHOD, }) + this.rulesEngineEndpoint = rulesEngineEndpoint + /** * For backward compatibility with pkcs1 */ @@ -216,20 +222,31 @@ export class SgidClient { /** * Generates dynamic derived data based on rules defined on sgID Developer Portal. * @param rulesParams The parameters for dynamic derived data calculation. - * @param rulesParams.clientId The client ID of the Relying Party. * @param rulesParams.accessToken The access token returned from the callback function. - * @param rulesParams.ruleNames The space-separated string containing rule names. + * @param rulesParams.ruleIds The space-separated string containing rule IDs. * @param rulesParams.userInfoData The end-user's verified data. - * @returns {RulesReturn} A list of rule names, alongside with their corresponding inputs and outputs. + * @returns {RulesReturn} A list of rule IDs, alongside with their corresponding inputs and outputs. */ async rules(rulesParams: RulesParams): Promise { try { - const response = await fetch(SGID_RULES_ENGINE_URL, { + const { accessToken, ruleIds, userInfoData } = rulesParams + const response = await fetch(this.rulesEngineEndpoint, { method: 'POST', - body: JSON.stringify(rulesParams), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + ruleIds, + userInfoData, + }), }) - const data: RulesReturn = await response.json() - return data + + if (!response.ok) { + // json() is less helpful if API users log errors with console.error() + throw new Error(await response.text()) + } + return await response.json() } catch (err) { return Promise.reject((err as Error).message) } diff --git a/src/constants.ts b/src/constants.ts index d1efdb5..13f4444 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -8,6 +8,5 @@ export const SGID_AUTH_METHOD: ClientAuthMethod = 'client_secret_post' export const API_VERSION = 2 -// TODO: Replace with https://rules.id.gov.sg/api/rule/eval once it's up -// Was overwritten with local host URL during testing -export const SGID_RULES_ENGINE_URL = 'https://rules-stg.id.gov.sg/api/rule/eval' +export const SGID_RULES_ENGINE_ENDPOINT = + 'https://rules.id.gov.sg/api/rule/eval' diff --git a/src/error.ts b/src/error.ts index f02237d..f528f96 100644 --- a/src/error.ts +++ b/src/error.ts @@ -21,7 +21,3 @@ export const DECRYPT_BLOCK_KEY_ERROR = export const DECRYPT_PAYLOAD_ERROR = 'Unable to decrypt payload' export const SUB_MISMATCH_ERROR = 'Sub returned by sgID did not match the sub passed to the userinfo method. Check that you passed the correct sub to the userinfo method.' - -// rules errors -export const INVALID_RULES_NAMES_ERROR = - '[ruleNames] is missing/contains invalid rule names' diff --git a/src/types.ts b/src/types.ts index 6260cf0..cdcc955 100644 --- a/src/types.ts +++ b/src/types.ts @@ -35,17 +35,17 @@ export type SgidClientParams = { privateKey: string redirectUri?: string hostname?: string + rulesEngineEndpoint?: string } export type RulesParams = { - clientId: string accessToken: string - ruleNames: string + ruleIds: string userInfoData: Record } export type RulesReturn = { - ruleName: string + ruleId: string input: string output: string }[]