From ae9bdd3fdb01c7f891b39537d3fa1a75122fd330 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Thu, 1 Feb 2024 10:07:06 -0500 Subject: [PATCH 01/16] add passkeys backend functions and some test --- package.json | 3 +- passkey-backend/.env.example | 3 + passkey-backend/.owners | 4 + passkey-backend/CHANGELOG.md | 8 ++ passkey-backend/README.md | 64 ++++++++++++++ .../.well-know/apple-app-site-association | 7 ++ .../assets/.well-know/assetlinks.json | 64 ++++++++++++++ passkey-backend/assets/index.html | 88 +++++++++++++++++++ .../assets/services/helpers.private.js | 10 +++ .../functions/authentication/start.js | 34 +++++++ .../functions/authentication/verification.js | 60 +++++++++++++ .../functions/registration/start.js | 79 +++++++++++++++++ .../functions/registration/verification.js | 60 +++++++++++++ passkey-backend/package.json | 8 ++ .../tests/authentication-verification.test.js | 51 +++++++++++ .../tests/registration-start.test.js | 30 +++++++ .../tests/registration-verification.test.js | 51 +++++++++++ templates.json | 5 ++ 18 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 passkey-backend/.env.example create mode 100644 passkey-backend/.owners create mode 100644 passkey-backend/CHANGELOG.md create mode 100644 passkey-backend/README.md create mode 100644 passkey-backend/assets/.well-know/apple-app-site-association create mode 100644 passkey-backend/assets/.well-know/assetlinks.json create mode 100644 passkey-backend/assets/index.html create mode 100644 passkey-backend/assets/services/helpers.private.js create mode 100644 passkey-backend/functions/authentication/start.js create mode 100644 passkey-backend/functions/authentication/verification.js create mode 100644 passkey-backend/functions/registration/start.js create mode 100644 passkey-backend/functions/registration/verification.js create mode 100644 passkey-backend/package.json create mode 100644 passkey-backend/tests/authentication-verification.test.js create mode 100644 passkey-backend/tests/registration-start.test.js create mode 100644 passkey-backend/tests/registration-verification.test.js diff --git a/package.json b/package.json index 7663d676a..e27834022 100644 --- a/package.json +++ b/package.json @@ -138,6 +138,7 @@ "voice-javascript-sdk", "voicemail", "verify-sna", - "flex-dialpad" + "flex-dialpad", + "passkey-backend" ] } diff --git a/passkey-backend/.env.example b/passkey-backend/.env.example new file mode 100644 index 000000000..a0730e761 --- /dev/null +++ b/passkey-backend/.env.example @@ -0,0 +1,3 @@ +API_URL= +SERVICE_SID= +RELYING_PARTY= \ No newline at end of file diff --git a/passkey-backend/.owners b/passkey-backend/.owners new file mode 100644 index 000000000..9ff1ceb7e --- /dev/null +++ b/passkey-backend/.owners @@ -0,0 +1,4 @@ +dkundel +alisontanu +pthirumurthi +nicolas-camacho diff --git a/passkey-backend/CHANGELOG.md b/passkey-backend/CHANGELOG.md new file mode 100644 index 000000000..3982d4615 --- /dev/null +++ b/passkey-backend/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [Unreleased] + +## [1.0.0] +### Added +- Initial release. + diff --git a/passkey-backend/README.md b/passkey-backend/README.md new file mode 100644 index 000000000..6bbd6f48b --- /dev/null +++ b/passkey-backend/README.md @@ -0,0 +1,64 @@ +# passkey-backend + +Connect appliactions with the passkey service + +## Pre-requisites + +### Environment variables + +This project requires some environment variables to be set. A file named `.env` is used to store the values for those environment variables. To keep your tokens and secrets secure, make sure to not commit the `.env` file in git. When setting up the project with `twilio serverless:init ...` the Twilio CLI will create a `.gitignore` file that excludes `.env` from the version history. + +In your `.env` file, set the following values: + +| Variable | Description | Required | +| :------- | :---------- | :------- | + + +### Function Parameters + +`/blank` expects the following parameters: + +| Parameter | Description | Required | +| :-------- | :---------- | :------- | + + +`/hello-messaging` is protected and requires a valid Twilio signature as well as the following parameters: + +| Parameter | Description | Required | +| :-------- | :---------- | :------- | + + +## Create a new project with the template + +1. Install the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart#install-twilio-cli) +2. Install the [serverless toolkit](https://www.twilio.com/docs/labs/serverless-toolkit/getting-started) + +```shell +twilio plugins:install @twilio-labs/plugin-serverless +``` + +3. Initiate a new project + +``` +twilio serverless:init example --template=passkey-backend && cd example +``` + +4. Start the server with the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): + +``` +twilio serverless:start +``` + +5. Open the web page at https://localhost:3000/index.html and enter your phone number to test + +ℹ️ Check the developer console and terminal for any errors, make sure you've set your environment variables. + +## Deploying + +Deploy your functions and assets with either of the following commands. Note: you must run these commands from inside your project folder. [More details in the docs.](https://www.twilio.com/docs/labs/serverless-toolkit) + +With the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): + +``` +twilio serverless:deploy +``` diff --git a/passkey-backend/assets/.well-know/apple-app-site-association b/passkey-backend/assets/.well-know/apple-app-site-association new file mode 100644 index 000000000..2cb576d39 --- /dev/null +++ b/passkey-backend/assets/.well-know/apple-app-site-association @@ -0,0 +1,7 @@ +{ + "webcredentials": { + "apps": [ + "9EVH78F4V4.com.passkeys.twilio.sampleCode" + ] + } +} \ No newline at end of file diff --git a/passkey-backend/assets/.well-know/assetlinks.json b/passkey-backend/assets/.well-know/assetlinks.json new file mode 100644 index 000000000..0d0bd70b1 --- /dev/null +++ b/passkey-backend/assets/.well-know/assetlinks.json @@ -0,0 +1,64 @@ +[ + { + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "web", + "site": "https://passkey-sample-9652.twil.io" + } + }, + { + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "android_app", + "package_name": "com.twilio.passkeys.android", + "sha256_cert_fingerprints": [ + "AF:E0:6F:5F:BF:5A:C4:E2:A0:89:22:95:B8:1C:05:4B:29:5C:80:7A:3B:69:4B:B1:0A:7B:A6:CF:1E:17:4F:A1" + ] + } + }, + { + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "android_app", + "package_name": "com.twilio.passkeys.android", + "sha256_cert_fingerprints": [ + "50:5C:D6:3D:A5:1F:19:8F:3F:B1:C2:95:0B:6B:46:B6:06:FE:C4:13:57:4B:96:7F:3D:86:A3:CF:73:FE:05:53" + ] + } + }, + { + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "android_app", + "package_name": "com.twilio.passkeys.android", + "sha256_cert_fingerprints": [ + "E1:1B:1C:EA:3B:D8:22:EE:73:3A:26:CF:54:AF:66:0E:08:2B:CF:9A:49:7C:34:6B:D8:02:0B:6A:BA:8D:16:EF" + ] + } + }, + { + "relation": [ + "delegate_permission/common.handle_all_urls", + "delegate_permission/common.get_login_creds" + ], + "target": { + "namespace": "android_app", + "package_name": "com.twilio.passkey_manager", + "sha256_cert_fingerprints": [ + "AF:E0:6F:5F:BF:5A:C4:E2:A0:89:22:95:B8:1C:05:4B:29:5C:80:7A:3B:69:4B:B1:0A:7B:A6:CF:1E:17:4F:A1" + ] + } + } +] diff --git a/passkey-backend/assets/index.html b/passkey-backend/assets/index.html new file mode 100644 index 000000000..14f4606fe --- /dev/null +++ b/passkey-backend/assets/index.html @@ -0,0 +1,88 @@ + + + + + + + Get started with your Twilio Functions! + + + + + + + +
+
+ + +
+
+
+
+

+ +
+

Welcome!

+

Your live application with Twilio is ready to use!

+
+

+
+

Get started with your application

+

+ Follow these steps to try out your new app: +

+

+ This app will return the + TwiML + required to respond "Hello World" to incoming SMS messages. +

+
    +
  1. Text any message to your Twilio phone number
  2. +
  3. You should receive a response saying "Hello World"
  4. +
+
+
+ +
+
+

Troubleshooting

+
    +
  • + Check the + + phone number configuration + + and make sure the Twilio phone number you want for your app has a SMS webhook + configured to point at the following URL +
    + + +
    +
  • +
+
+
+
+ + + diff --git a/passkey-backend/assets/services/helpers.private.js b/passkey-backend/assets/services/helpers.private.js new file mode 100644 index 000000000..51fab6616 --- /dev/null +++ b/passkey-backend/assets/services/helpers.private.js @@ -0,0 +1,10 @@ +const detectMissingParams = (paramNames, event) => { + const missingParams = paramNames.filter( + (param) => !event.hasOwnProperty(param) + ); + return missingParams.length > 0 ? missingParams : null; +}; + +module.exports = { + detectMissingParams, +}; diff --git a/passkey-backend/functions/authentication/start.js b/passkey-backend/functions/authentication/start.js new file mode 100644 index 000000000..486d57004 --- /dev/null +++ b/passkey-backend/functions/authentication/start.js @@ -0,0 +1,34 @@ +const axios = require('axios'); + +// eslint-disable-next-line consistent-return +exports.handler = async (context, _, callback) => { + const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = + context; + + const requestBody = { + details: { + rpId: RELYING_PARTY, + }, + }; + + const challengeURL = `${API_URL}Services/${SERVICE_SID}/Challenges`; + + try { + const response = await axios.post(challengeURL, requestBody, { + auth: { + username: ACCOUNT_SID, + password: AUTH_TOKEN, + }, + }); + return callback(null, response.data.details); + } catch (error) { + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } + return callback('Something went wrong'); + } +}; diff --git a/passkey-backend/functions/authentication/verification.js b/passkey-backend/functions/authentication/verification.js new file mode 100644 index 000000000..88fcb52b2 --- /dev/null +++ b/passkey-backend/functions/authentication/verification.js @@ -0,0 +1,60 @@ +const axios = require('axios'); + +const assets = Runtime.getAssets(); +const { detectMissingParams } = require(assets['/services/helpers.js'].path); + +// eslint-disable-next-line consistent-return +exports.handler = async (context, event, callback) => { + const missingParams = detectMissingParams( + [ + 'id', + 'rawId', + 'type', + 'clientDataJson', + 'authenticatorData', + 'signature', + 'userHandle', + ], + event + ); + if (missingParams) + return callback( + `Missing parameters; please provide: '${missingParams.join(', ')}'.` + ); + + const requestBody = { + rawId: event.rawId, + id: event.id, + authenticatorAttachment: 'platform', + type: 'public-key', + response: { + clientDataJSON: event.clientDataJson, + authenticatorData: event.authenticatorData, + signature: event.signature, + userHandle: event.userHandle, + }, + }; + + const verifyChallengeURL = `${context.API_URL}Services/${context.SERVICE_SID}/Challenges/Verify`; + + try { + const response = await axios.post(verifyChallengeURL, requestBody, { + auth: { + username: ACCOUNT_SID, + password: AUTH_TOKEN, + }, + }); + return callback(null, { + status: response.data.status, + }); + } catch (error) { + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } + return callback('Something went wrong'); + } +}; diff --git a/passkey-backend/functions/registration/start.js b/passkey-backend/functions/registration/start.js new file mode 100644 index 000000000..ca3829a64 --- /dev/null +++ b/passkey-backend/functions/registration/start.js @@ -0,0 +1,79 @@ +const axios = require('axios'); + +const assets = Runtime.getAssets(); +const { detectMissingParams } = require(assets['/services/helpers.js'].path); + +exports.handler = async (context, event, callback) => { + /* + * Constants set as enviroment varibales + * ------------------------------------- + * Constants from the twilio account: + * SERVICE_SID, + * ACCOUNT_SID, + * SERVICE_SID, + * AUTH_TOKEN + * + * Constanst get by services: + * API_URL: passkey verify URL + * RELYING_PARTY: self URL of twilio function + */ + const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = + context; + + // Verify request comes with username + const missingParams = detectMissingParams(['username'], event); + if (missingParams) + return callback( + `Missing parameters; please provide: '${missingParams.join(', ')}'.` + ); + + // Request body sent to passkey verify URL call + /* eslint-disable camelcase */ + const requestBody = { + friendly_name: 'TouchID', + factory_type: 'passkeys', + entity: { + identity: event.username, + display_name: event.username, + }, + config: { + relying_party: { + id: RELYING_PARTY, + name: 'PasskeySample', + origins: [ + `https://${RELYING_PARTY}`, + 'android:apk-key-hash:r-BvX79axOKgiSKVuBwFSylcgHo7aUuxCnumzx4XT6E', + 'android:apk-key-hash:UFzWPaUfGY8_scKVC2tGtgb-xBNXS5Z_PYajz3P-BVM', + ], + }, + authenticator_criteria: { + authenticator_attachment: 'platform', + discoverable_credentials: 'preferred', + user_verification: 'preferred', + }, + }, + }; + + // Factor URL of the passkey service + const factorURL = `${API_URL}Services/${SERVICE_SID}/Factors`; + + // Call made to the passkey service + try { + const response = await axios.post(factorURL, requestBody, { + auth: { + username: ACCOUNT_SID, + password: AUTH_TOKEN, + }, + }); + return callback(null, response.data.config.creation_request); + } catch (error) { + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } + return callback('Something went wrong'); + } +}; diff --git a/passkey-backend/functions/registration/verification.js b/passkey-backend/functions/registration/verification.js new file mode 100644 index 000000000..e18012a24 --- /dev/null +++ b/passkey-backend/functions/registration/verification.js @@ -0,0 +1,60 @@ +const axios = require('axios'); + +const assets = Runtime.getAssets(); +const { detectMissingParams } = require(assets['/services/helpers.js'].path); + +// eslint-disable-next-line consistent-return +exports.handler = async (context, event, callback) => { + const { API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; + + const missingParams = detectMissingParams( + [ + 'id', + 'attestationObject', + 'rawId', + 'type', + 'clientDataJson', + 'transports', + ], + event + ); + if (missingParams) + return callback( + `Missing parameters; please provide: '${missingParams.join(', ')}'.` + ); + + const requestBody = { + id: event.id, + rawId: event.rawId, + authenticatorAttachment: 'platform', + type: event.type, + response: { + attestationObject: event.attestationObject, + clientDataJSON: event.clientDataJson, + transports: event.transports, + }, + }; + + const verifyFactorURL = `${API_URL}Services/${SERVICE_SID}/Factors/Verify`; + + try { + const response = await axios.post(verifyFactorURL, requestBody, { + auth: { + username: ACCOUNT_SID, + password: AUTH_TOKEN, + }, + }); + return callback(null, { + status: response.data.status, + }); + } catch (error) { + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } + return callback('Something went wrong'); + } +}; diff --git a/passkey-backend/package.json b/passkey-backend/package.json new file mode 100644 index 000000000..16b8df2f5 --- /dev/null +++ b/passkey-backend/package.json @@ -0,0 +1,8 @@ +{ + "name": "passkey-backend", + "version": "1.0.0", + "private": true, + "dependencies": { + "@twilio-labs/runtime-helpers": "^0.1.2" + } +} diff --git a/passkey-backend/tests/authentication-verification.test.js b/passkey-backend/tests/authentication-verification.test.js new file mode 100644 index 000000000..8b2fae3f0 --- /dev/null +++ b/passkey-backend/tests/authentication-verification.test.js @@ -0,0 +1,51 @@ +// const { handler } = require('../functions/registration/start'); +const helpers = require('../../test/test-helper'); + +describe('registration/start', () => { + beforeAll(() => { + jest.clearAllMocks(); + const runtime = new helpers.MockRuntime(); + runtime._addAsset( + '/services/helpers.js', + '../assets/services/helpers.private.js' + ); + helpers.setup({}, runtime); + }); + afterAll(() => { + helpers.teardown(); + }); + beforeEach(() => jest.resetModules()); + + describe('when multiple required parameters are missing', () => { + it('returns an error indicating multiple missing parameters', (done) => { + const { handler } = require('../functions/authentication/verification'); + const callback = (_err) => { + expect(_err).toBeDefined(); + expect(_err).toEqual( + `Missing parameters; please provide: 'id, rawId, type, clientDataJson, authenticatorData, signature, userHandle'.` + ); + done(); + }; + handler({}, {}, callback); + }); + + it('returns an error indicating specific missing parameters', (done) => { + const { handler } = require('../functions/authentication/verification'); + const callback = (_err) => { + expect(_err).toBeDefined(); + expect(_err).toEqual( + `Missing parameters; please provide: 'type, clientDataJson, authenticatorData, signature, userHandle'.` + ); + done(); + }; + handler( + {}, + { + id: '123', + rawId: '123', + }, + callback + ); + }); + }); +}); diff --git a/passkey-backend/tests/registration-start.test.js b/passkey-backend/tests/registration-start.test.js new file mode 100644 index 000000000..862ad0f81 --- /dev/null +++ b/passkey-backend/tests/registration-start.test.js @@ -0,0 +1,30 @@ +// const { handler } = require('../functions/registration/start'); +const helpers = require('../../test/test-helper'); + +describe('registration/start', () => { + beforeAll(() => { + jest.clearAllMocks(); + const runtime = new helpers.MockRuntime(); + runtime._addAsset( + '/services/helpers.js', + '../assets/services/helpers.private.js' + ); + helpers.setup({}, runtime); + }); + afterAll(() => { + helpers.teardown(); + }); + beforeEach(() => jest.resetModules()); + + describe('when required username parameter is missing', () => { + it('returns an error response indicating the missing parameters', (done) => { + const { handler } = require('../functions/registration/start'); + const callback = (_err) => { + expect(_err).toBeDefined(); + expect(_err).toEqual(`Missing parameters; please provide: 'username'.`); + done(); + }; + handler({}, {}, callback); + }); + }); +}); diff --git a/passkey-backend/tests/registration-verification.test.js b/passkey-backend/tests/registration-verification.test.js new file mode 100644 index 000000000..49c52b8f9 --- /dev/null +++ b/passkey-backend/tests/registration-verification.test.js @@ -0,0 +1,51 @@ +// const { handler } = require('../functions/registration/start'); +const helpers = require('../../test/test-helper'); + +describe('registration/start', () => { + beforeAll(() => { + jest.clearAllMocks(); + const runtime = new helpers.MockRuntime(); + runtime._addAsset( + '/services/helpers.js', + '../assets/services/helpers.private.js' + ); + helpers.setup({}, runtime); + }); + afterAll(() => { + helpers.teardown(); + }); + beforeEach(() => jest.resetModules()); + + describe('when multiple required parameters are missing', () => { + it('returns an error indicating multiple missing parameters', (done) => { + const { handler } = require('../functions/registration/verification'); + const callback = (_err) => { + expect(_err).toBeDefined(); + expect(_err).toEqual( + `Missing parameters; please provide: 'id, attestationObject, rawId, type, clientDataJson, transports'.` + ); + done(); + }; + handler({}, {}, callback); + }); + + it('returns an error indicating specific missing parameters', (done) => { + const { handler } = require('../functions/registration/verification'); + const callback = (_err) => { + expect(_err).toBeDefined(); + expect(_err).toEqual( + `Missing parameters; please provide: 'attestationObject, type, clientDataJson, transports'.` + ); + done(); + }; + handler( + {}, + { + id: '123', + rawId: '123', + }, + callback + ); + }); + }); +}); diff --git a/templates.json b/templates.json index b294ae764..91d831cd6 100644 --- a/templates.json +++ b/templates.json @@ -339,6 +339,11 @@ "id": "transfers", "name": "Transfers", "description": "Transfers a call to another number" + }, + { + "id": "passkey-backend", + "name": "Backend for passkey app", + "description": "Connect appliactions with the passkey service" } ] } From d178bb5b7717ea31a2bc87eff2738f913d28ae58 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Mon, 5 Feb 2024 11:31:24 -0500 Subject: [PATCH 02/16] update test with response on unsuccessfull --- .../functions/authentication/verification.js | 5 +- .../functions/registration/start.js | 2 +- .../functions/registration/verification.js | 2 +- .../tests/authentication-verification.test.js | 45 +++++++++++++++--- .../tests/registration-start.test.js | 47 ++++++++++++++----- .../tests/registration-verification.test.js | 43 ++++++++++++++--- 6 files changed, 114 insertions(+), 30 deletions(-) diff --git a/passkey-backend/functions/authentication/verification.js b/passkey-backend/functions/authentication/verification.js index 88fcb52b2..61df16ca9 100644 --- a/passkey-backend/functions/authentication/verification.js +++ b/passkey-backend/functions/authentication/verification.js @@ -5,6 +5,7 @@ const { detectMissingParams } = require(assets['/services/helpers.js'].path); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { + const { API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; const missingParams = detectMissingParams( [ 'id', @@ -35,7 +36,7 @@ exports.handler = async (context, event, callback) => { }, }; - const verifyChallengeURL = `${context.API_URL}Services/${context.SERVICE_SID}/Challenges/Verify`; + const verifyChallengeURL = `${API_URL}Services/${SERVICE_SID}/Challenges/Verify`; try { const response = await axios.post(verifyChallengeURL, requestBody, { @@ -55,6 +56,6 @@ exports.handler = async (context, event, callback) => { } else { console.log(error); } - return callback('Something went wrong'); + return callback(null, error); } }; diff --git a/passkey-backend/functions/registration/start.js b/passkey-backend/functions/registration/start.js index ca3829a64..fdb1bb5ec 100644 --- a/passkey-backend/functions/registration/start.js +++ b/passkey-backend/functions/registration/start.js @@ -74,6 +74,6 @@ exports.handler = async (context, event, callback) => { } else { console.log(error); } - return callback('Something went wrong'); + return callback(null, error); } }; diff --git a/passkey-backend/functions/registration/verification.js b/passkey-backend/functions/registration/verification.js index e18012a24..a5e49df24 100644 --- a/passkey-backend/functions/registration/verification.js +++ b/passkey-backend/functions/registration/verification.js @@ -55,6 +55,6 @@ exports.handler = async (context, event, callback) => { } else { console.log(error); } - return callback('Something went wrong'); + return callback(null, error); } }; diff --git a/passkey-backend/tests/authentication-verification.test.js b/passkey-backend/tests/authentication-verification.test.js index 8b2fae3f0..d3c676092 100644 --- a/passkey-backend/tests/authentication-verification.test.js +++ b/passkey-backend/tests/authentication-verification.test.js @@ -1,7 +1,19 @@ -// const { handler } = require('../functions/registration/start'); +const axios = require('axios'); const helpers = require('../../test/test-helper'); -describe('registration/start', () => { +jest.mock('axios'); + +const testEvent = { + id: '12345', + rawId: 'randomRawId', + type: 'test-type', + clientDataJson: {}, + authenticatorData: {}, + signature: 'test-signature', + userHandle: {}, +}; + +describe('authentication/verification', () => { beforeAll(() => { jest.clearAllMocks(); const runtime = new helpers.MockRuntime(); @@ -10,15 +22,19 @@ describe('registration/start', () => { '../assets/services/helpers.private.js' ); helpers.setup({}, runtime); + handlerFunction = + require('../functions/authentication/verification').handler; }); afterAll(() => { helpers.teardown(); }); - beforeEach(() => jest.resetModules()); + beforeEach(() => { + jest.resetModules(); + axios.post.mockClear(); + }); describe('when multiple required parameters are missing', () => { it('returns an error indicating multiple missing parameters', (done) => { - const { handler } = require('../functions/authentication/verification'); const callback = (_err) => { expect(_err).toBeDefined(); expect(_err).toEqual( @@ -26,11 +42,10 @@ describe('registration/start', () => { ); done(); }; - handler({}, {}, callback); + handlerFunction({}, {}, callback); }); it('returns an error indicating specific missing parameters', (done) => { - const { handler } = require('../functions/authentication/verification'); const callback = (_err) => { expect(_err).toBeDefined(); expect(_err).toEqual( @@ -38,7 +53,7 @@ describe('registration/start', () => { ); done(); }; - handler( + handlerFunction( {}, { id: '123', @@ -48,4 +63,20 @@ describe('registration/start', () => { ); }); }); + + describe('When response are unsuccesfull', () => { + it('returns error with unsuccesfull request', (done) => { + const expectedError = new Error('something bad happened'); + axios.post = jest.fn(() => Promise.reject(expectedError)); + + const callback = (_err, result) => { + expect(result).toBeDefined(); + expect(axios.post).toHaveBeenCalledTimes(1); + expect(result).toEqual(expectedError); + done(); + }; + + handlerFunction({}, testEvent, callback); + }); + }); }); diff --git a/passkey-backend/tests/registration-start.test.js b/passkey-backend/tests/registration-start.test.js index 862ad0f81..45ec1fa75 100644 --- a/passkey-backend/tests/registration-start.test.js +++ b/passkey-backend/tests/registration-start.test.js @@ -1,6 +1,8 @@ -// const { handler } = require('../functions/registration/start'); +const axios = require('axios'); const helpers = require('../../test/test-helper'); +jest.mock('axios'); + describe('registration/start', () => { beforeAll(() => { jest.clearAllMocks(); @@ -10,21 +12,42 @@ describe('registration/start', () => { '../assets/services/helpers.private.js' ); helpers.setup({}, runtime); + handlerFunction = require('../functions/registration/start').handler; }); afterAll(() => { helpers.teardown(); }); - beforeEach(() => jest.resetModules()); + beforeEach(() => { + jest.resetModules(); + axios.post.mockClear(); + }); + + it('returns an error response indicating the missing parameters', (done) => { + const callback = (_err) => { + expect(_err).toBeDefined(); + expect(_err).toEqual(`Missing parameters; please provide: 'username'.`); + done(); + }; + handlerFunction({}, {}, callback); + }); - describe('when required username parameter is missing', () => { - it('returns an error response indicating the missing parameters', (done) => { - const { handler } = require('../functions/registration/start'); - const callback = (_err) => { - expect(_err).toBeDefined(); - expect(_err).toEqual(`Missing parameters; please provide: 'username'.`); - done(); - }; - handler({}, {}, callback); - }); + it('returns error with unsuccesfull request', (done) => { + const expectedError = new Error('something bad happened'); + axios.post = jest.fn(() => Promise.reject(expectedError)); + + const callback = (_err, result) => { + expect(result).toBeDefined(); + expect(axios.post).toHaveBeenCalledTimes(1); + expect(result).toEqual(expectedError); + done(); + }; + + handlerFunction( + {}, + { + username: 'test-username', + }, + callback + ); }); }); diff --git a/passkey-backend/tests/registration-verification.test.js b/passkey-backend/tests/registration-verification.test.js index 49c52b8f9..b78c43401 100644 --- a/passkey-backend/tests/registration-verification.test.js +++ b/passkey-backend/tests/registration-verification.test.js @@ -1,7 +1,18 @@ -// const { handler } = require('../functions/registration/start'); +const axios = require('axios'); const helpers = require('../../test/test-helper'); -describe('registration/start', () => { +jest.mock('axios'); + +const testEvent = { + id: '12345', + attestationObject: {}, + rawId: 'randomRawId', + type: 'test-type', + clientDataJson: {}, + transports: 'test-transport', +}; + +describe('registration/verification', () => { beforeAll(() => { jest.clearAllMocks(); const runtime = new helpers.MockRuntime(); @@ -10,15 +21,18 @@ describe('registration/start', () => { '../assets/services/helpers.private.js' ); helpers.setup({}, runtime); + handlerFunction = require('../functions/registration/verification').handler; }); afterAll(() => { helpers.teardown(); }); - beforeEach(() => jest.resetModules()); + beforeEach(() => { + jest.resetModules(); + axios.post.mockClear(); + }); describe('when multiple required parameters are missing', () => { it('returns an error indicating multiple missing parameters', (done) => { - const { handler } = require('../functions/registration/verification'); const callback = (_err) => { expect(_err).toBeDefined(); expect(_err).toEqual( @@ -26,11 +40,10 @@ describe('registration/start', () => { ); done(); }; - handler({}, {}, callback); + handlerFunction({}, {}, callback); }); it('returns an error indicating specific missing parameters', (done) => { - const { handler } = require('../functions/registration/verification'); const callback = (_err) => { expect(_err).toBeDefined(); expect(_err).toEqual( @@ -38,7 +51,7 @@ describe('registration/start', () => { ); done(); }; - handler( + handlerFunction( {}, { id: '123', @@ -48,4 +61,20 @@ describe('registration/start', () => { ); }); }); + + describe('When response are unsuccesfull', () => { + it('returns error with unsuccesfull request', (done) => { + const expectedError = new Error('something bad happened'); + axios.post = jest.fn(() => Promise.reject(expectedError)); + + const callback = (_err, result) => { + expect(result).toBeDefined(); + expect(axios.post).toHaveBeenCalledTimes(1); + expect(result).toEqual(expectedError); + done(); + }; + + handlerFunction({}, testEvent, callback); + }); + }); }); From 3757470c509b362b65d819f9b18e8769f068feca Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Wed, 28 Feb 2024 09:25:29 -0500 Subject: [PATCH 03/16] passkey web demo view with functionality --- passkey-backend/README.md | 61 ++- .../assets/.well-know/assetlinks.json | 2 +- passkey-backend/assets/index.html | 428 +++++++++++++++--- .../assets/services/helpers.private.js | 10 + .../functions/authentication/start.js | 11 +- .../functions/authentication/verification.js | 15 +- .../functions/registration/start.js | 30 +- .../functions/registration/verification.js | 12 +- 8 files changed, 419 insertions(+), 150 deletions(-) diff --git a/passkey-backend/README.md b/passkey-backend/README.md index 6bbd6f48b..1d1780591 100644 --- a/passkey-backend/README.md +++ b/passkey-backend/README.md @@ -8,57 +8,54 @@ Connect appliactions with the passkey service This project requires some environment variables to be set. A file named `.env` is used to store the values for those environment variables. To keep your tokens and secrets secure, make sure to not commit the `.env` file in git. When setting up the project with `twilio serverless:init ...` the Twilio CLI will create a `.gitignore` file that excludes `.env` from the version history. +You can find a `.env.example` file to copy for creating your own `.env` file + In your `.env` file, set the following values: | Variable | Description | Required | | :------- | :---------- | :------- | +| API_URL | Passkey API to point at | yes | +| SERVICE_SID | Service used to call twilio services | yes | +| RELYING_PARTY | Customer app or client | yes +| ACCOUNT_SID | Twilio account where the service belong | yes | +| AUTH_TOKEN | Authentication token for twilio account | yes | ### Function Parameters -`/blank` expects the following parameters: +`/registration/start` expects the following parameters: | Parameter | Description | Required | | :-------- | :---------- | :------- | +| username | user identification name | yes -`/hello-messaging` is protected and requires a valid Twilio signature as well as the following parameters: +`/registration/verification` expects the following parameters: | Parameter | Description | Required | | :-------- | :---------- | :------- | +| id | A base64url encoded representation of `rawId`. | yes | +| rawId | The globally unique identifier for this `PublicKeyCredential`. | yes | +| type | `public-key` | yes | +| attestationObject | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | +| clientDataJson | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | +| transports | An Array with the transport methods given by the `AuthenticatorAttestationResponse` | yes | -## Create a new project with the template - -1. Install the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart#install-twilio-cli) -2. Install the [serverless toolkit](https://www.twilio.com/docs/labs/serverless-toolkit/getting-started) - -```shell -twilio plugins:install @twilio-labs/plugin-serverless -``` - -3. Initiate a new project - -``` -twilio serverless:init example --template=passkey-backend && cd example -``` - -4. Start the server with the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): +`/authentication/start` a GET request, does not expect parameters -``` -twilio serverless:start -``` +`/registration/verification` expects the following parameters: -5. Open the web page at https://localhost:3000/index.html and enter your phone number to test - -ℹ️ Check the developer console and terminal for any errors, make sure you've set your environment variables. - -## Deploying - -Deploy your functions and assets with either of the following commands. Note: you must run these commands from inside your project folder. [More details in the docs.](https://www.twilio.com/docs/labs/serverless-toolkit) +| Parameter | Description | Required | +| :-------- | :---------- | :------- | +| id | A base64url encoded representation of `rawId`. | yes | +| rawId | The globally unique identifier for this `PublicKeyCredential`. | yes | +| type | `public-key` | yes | +| authenticatorData | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | +| clientDataJson | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | +| signature | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | +| userHandle | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | -With the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): +## Test this project locally -``` -twilio serverless:deploy -``` +Follow the steps in the [Twilio CLI Test](https://github.com/AuthyApps/twilio-cli-test) repository diff --git a/passkey-backend/assets/.well-know/assetlinks.json b/passkey-backend/assets/.well-know/assetlinks.json index 0d0bd70b1..105e89bd1 100644 --- a/passkey-backend/assets/.well-know/assetlinks.json +++ b/passkey-backend/assets/.well-know/assetlinks.json @@ -6,7 +6,7 @@ ], "target": { "namespace": "web", - "site": "https://passkey-sample-9652.twil.io" + "site": "https://6dcc8b2b3620.ngrok.app" } }, { diff --git a/passkey-backend/assets/index.html b/passkey-backend/assets/index.html index 14f4606fe..c81f83057 100644 --- a/passkey-backend/assets/index.html +++ b/passkey-backend/assets/index.html @@ -8,81 +8,367 @@ - - + + + -
-
-
+ + ― or ― + +
-
-
-

- -
-

Welcome!

-

Your live application with Twilio is ready to use!

-
-

-
-

Get started with your application

-

- Follow these steps to try out your new app: -

-

- This app will return the - TwiML - required to respond "Hello World" to incoming SMS messages. -

-
    -
  1. Text any message to your Twilio phone number
  2. -
  3. You should receive a response saying "Hello World"
  4. -
-
-
- -
-
-

Troubleshooting

-
    -
  • - Check the - - phone number configuration - - and make sure the Twilio phone number you want for your app has a SMS webhook - configured to point at the following URL -
    - - -
    -
  • -
-
+
-
- We can't wait to see what you build. -
+ + + diff --git a/passkey-backend/assets/services/helpers.private.js b/passkey-backend/assets/services/helpers.private.js index 51fab6616..4014bdfc4 100644 --- a/passkey-backend/assets/services/helpers.private.js +++ b/passkey-backend/assets/services/helpers.private.js @@ -5,6 +5,16 @@ const detectMissingParams = (paramNames, event) => { return missingParams.length > 0 ? missingParams : null; }; +const errorLogger = (error) => { + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } +}; + module.exports = { detectMissingParams, }; diff --git a/passkey-backend/functions/authentication/start.js b/passkey-backend/functions/authentication/start.js index 486d57004..d781b4d0e 100644 --- a/passkey-backend/functions/authentication/start.js +++ b/passkey-backend/functions/authentication/start.js @@ -1,5 +1,8 @@ const axios = require('axios'); +const assets = Runtime.getAssets(); +const { errorLogger } = require(assets['/services/helpers.js'].path); + // eslint-disable-next-line consistent-return exports.handler = async (context, _, callback) => { const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = @@ -22,13 +25,7 @@ exports.handler = async (context, _, callback) => { }); return callback(null, response.data.details); } catch (error) { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } + errorLogger(error); return callback('Something went wrong'); } }; diff --git a/passkey-backend/functions/authentication/verification.js b/passkey-backend/functions/authentication/verification.js index 61df16ca9..dce1e3c82 100644 --- a/passkey-backend/functions/authentication/verification.js +++ b/passkey-backend/functions/authentication/verification.js @@ -1,7 +1,9 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { detectMissingParams } = require(assets['/services/helpers.js'].path); +const { detectMissingParams, errorLogger } = require(assets[ + '/services/helpers.js' +].path); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { @@ -27,7 +29,7 @@ exports.handler = async (context, event, callback) => { rawId: event.rawId, id: event.id, authenticatorAttachment: 'platform', - type: 'public-key', + type: event.type, response: { clientDataJSON: event.clientDataJson, authenticatorData: event.authenticatorData, @@ -47,15 +49,10 @@ exports.handler = async (context, event, callback) => { }); return callback(null, { status: response.data.status, + identity: response.data.entity_identity, }); } catch (error) { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } + errorLogger(error); return callback(null, error); } }; diff --git a/passkey-backend/functions/registration/start.js b/passkey-backend/functions/registration/start.js index fdb1bb5ec..d22e2d1f7 100644 --- a/passkey-backend/functions/registration/start.js +++ b/passkey-backend/functions/registration/start.js @@ -1,22 +1,11 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { detectMissingParams } = require(assets['/services/helpers.js'].path); +const { detectMissingParams, errorLogger } = require(assets[ + '/services/helpers.js' +].path); exports.handler = async (context, event, callback) => { - /* - * Constants set as enviroment varibales - * ------------------------------------- - * Constants from the twilio account: - * SERVICE_SID, - * ACCOUNT_SID, - * SERVICE_SID, - * AUTH_TOKEN - * - * Constanst get by services: - * API_URL: passkey verify URL - * RELYING_PARTY: self URL of twilio function - */ const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; @@ -65,15 +54,12 @@ exports.handler = async (context, event, callback) => { password: AUTH_TOKEN, }, }); - return callback(null, response.data.config.creation_request); + return callback(null, { + ...response.data.config.creation_request, + factor_sid: response.data.sid, + }); } catch (error) { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } + errorLogger(error); return callback(null, error); } }; diff --git a/passkey-backend/functions/registration/verification.js b/passkey-backend/functions/registration/verification.js index a5e49df24..8bb2edef3 100644 --- a/passkey-backend/functions/registration/verification.js +++ b/passkey-backend/functions/registration/verification.js @@ -1,7 +1,9 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { detectMissingParams } = require(assets['/services/helpers.js'].path); +const { detectMissingParams, errorLogger } = require(assets[ + '/services/helpers.js' +].path); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { @@ -48,13 +50,7 @@ exports.handler = async (context, event, callback) => { status: response.data.status, }); } catch (error) { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } + errorLogger(error); return callback(null, error); } }; From eafbe8c91e4ec316f907ec7177b2348478a5f5a7 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Wed, 28 Feb 2024 09:35:51 -0500 Subject: [PATCH 04/16] remove unnecesary console logs --- passkey-backend/assets/index.html | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/passkey-backend/assets/index.html b/passkey-backend/assets/index.html index c81f83057..2913a4620 100644 --- a/passkey-backend/assets/index.html +++ b/passkey-backend/assets/index.html @@ -246,18 +246,6 @@

const { id, rawId, response, type, authenticatorAttachment } = publicKeyCredential const { authenticatorData, clientDataJSON, signature, userHandle } = response - console.log(publicKeyCredential) - - console.log("data", { - id: id, - rawId: ArrayBufferToBase64(rawId), - type: type, - clientDataJson: ArrayBufferToBase64(clientDataJSON), - authenticatorData: ArrayBufferToBase64(authenticatorData), - signature: ArrayBufferToBase64(signature), - userHandle: ArrayBufferToBase64(userHandle) - }); - const authentication = await fetch('./authentication/verification', { method: "POST", headers: { @@ -337,8 +325,6 @@

const attestationObjectString = ArrayBufferToBase64(attestationObject); const clientDataJSONString = ArrayBufferToBase64(clientDataJSON); - console.log(credential) - const verificationResponse = await fetch(`./registration/verification`, { method: "POST", headers: { From e03aa33a6d8c7d1068c54875ebc23475d4c60050 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Wed, 28 Feb 2024 15:17:12 -0500 Subject: [PATCH 05/16] fix user id creation --- passkey-backend/assets/index.html | 28 +++++++++++++------ .../assets/services/helpers.private.js | 1 + 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/passkey-backend/assets/index.html b/passkey-backend/assets/index.html index 2913a4620..248643ce0 100644 --- a/passkey-backend/assets/index.html +++ b/passkey-backend/assets/index.html @@ -4,7 +4,7 @@ - Get started with your Twilio Functions! + Passkey Demo @@ -172,11 +172,11 @@

const loadApp = (username) => { document.getElementById("welcome").innerHTML = `Welcome ${username}` document.getElementById("modal").classList.add("invisible"); + document.getElementById("container").classList.add("invisible"); document.getElementById("app").classList.remove("invisible"); } if (sessionUsername) { - document.getElementById("container").classList.add("invisible"); loadApp(sessionUsername) } @@ -221,6 +221,12 @@

return buffer; } + function stringToArrayBuffer(str) { + const encoder = new TextEncoder(); + const uint8Array = encoder.encode(str); + return uint8Array.buffer; + } + const login = () => { const authenticationCard = document.getElementById("container"); const passkeyCard = document.getElementById("modal"); @@ -232,13 +238,12 @@

try { const response = await fetch(`./authentication/start`); const responseJSON = await response.json(); - - const { challenge, rpId, allowCredentials, timeout, useVerification } = responseJSON.publicKey + const { challenge, rpId, allowCredentials, timeout, userVerification } = responseJSON.publicKey const publicKey = { - challenge: strToArrayBuffer(challenge), + challenge: Uint8Array.from(atob(challenge), c => c.charCodeAt(0)), rpId, allowCredentials, - useVerification + userVerification } navigator.credentials.get({ publicKey }) @@ -265,6 +270,7 @@

const authenticationJSON = await authentication.json(); const {status, identity} = authenticationJSON if(status === "approved") { + sessionStorage.setItem('session', identity); loadApp(identity); } else { console.log(status); @@ -304,14 +310,20 @@

id: rp.id }, user: { - id: new Uint8Array(16), + id: strToArrayBuffer(user.id), name: user.name, displayName: user.displayName }, pubKeyCredParams: [{ type: "public-key", alg: -7 }], attestation: "none", timeout: 600000, - challenge: strToArrayBuffer(challenge) + challenge: strToArrayBuffer(challenge), + authenticatorSelection: { + residentKey: "preferred" + }, + extensions: { + credProps: true, + } } } diff --git a/passkey-backend/assets/services/helpers.private.js b/passkey-backend/assets/services/helpers.private.js index 4014bdfc4..a8da0dd25 100644 --- a/passkey-backend/assets/services/helpers.private.js +++ b/passkey-backend/assets/services/helpers.private.js @@ -17,4 +17,5 @@ const errorLogger = (error) => { module.exports = { detectMissingParams, + errorLogger, }; From 01479d2095e0c68a3bd11d18dc9f666e01e460e2 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Thu, 7 Mar 2024 09:16:26 -0500 Subject: [PATCH 06/16] variables, names and type parameter modified --- passkey-backend/.env.example | 4 ++- passkey-backend/README.md | 25 ++++++++++--- .../.well-know/apple-app-site-association | 2 +- .../assets/.well-know/assetlinks.json | 36 ++----------------- passkey-backend/assets/index.html | 4 +-- .../functions/authentication/verification.js | 3 +- .../functions/registration/start.js | 6 ++-- .../functions/registration/verification.js | 11 ++---- passkey-backend/package.json | 2 +- 9 files changed, 35 insertions(+), 58 deletions(-) diff --git a/passkey-backend/.env.example b/passkey-backend/.env.example index a0730e761..1bec353a0 100644 --- a/passkey-backend/.env.example +++ b/passkey-backend/.env.example @@ -1,3 +1,5 @@ API_URL= SERVICE_SID= -RELYING_PARTY= \ No newline at end of file +RELYING_PARTY= +ACCOUNT_SID= +AUTH_TOKEN= \ No newline at end of file diff --git a/passkey-backend/README.md b/passkey-backend/README.md index 1d1780591..e2d8d37c1 100644 --- a/passkey-backend/README.md +++ b/passkey-backend/README.md @@ -1,6 +1,6 @@ -# passkey-backend +# passkeys-backend -Connect appliactions with the passkey service +Verify enables developers to easily add Passkeys into their existing authentication flows, similar to Verify TOTP and Push. The Verify API supports passkey registration, public key storage, and auth flows. On the client-side, developers can optionally embed an open-source library (SDK) that handles interactions with operating systems and customizable UI widgets that maximize conversion. ## Pre-requisites @@ -20,6 +20,23 @@ In your `.env` file, set the following values: | ACCOUNT_SID | Twilio account where the service belong | yes | | AUTH_TOKEN | Authentication token for twilio account | yes | +### Service customization + +Besides the enviroment variables files, the project also contain two files called `assetlink.json` and `apple-app-site-association` inside `./assets/.well-know/`, that is a public file that contains the identificators for the apps that will be connecting the service. + +`apple-app-site-association` contains identificator hash for the origin app in iOS: + +| Variable | Description | Required | +| :------- | :---------- | :------- | +| ORIGIN_IOS_APP_HASH | Replace it with the identificator of the iOS app | yes | + +`assetlink.json` contains identificator hash for the origin apps in android and web: + +| Variable | Description | Required | +| :------- | :---------- | :------- | +| RELYING_PARTY | Replace it with the value of the relaying party | yes | +| FINGERPRINT_CERTIFICATION_HASH | Replace it with the hash fingerprint given by android app in format SHA256 | yes | + ### Function Parameters @@ -36,7 +53,6 @@ In your `.env` file, set the following values: | :-------- | :---------- | :------- | | id | A base64url encoded representation of `rawId`. | yes | | rawId | The globally unique identifier for this `PublicKeyCredential`. | yes | -| type | `public-key` | yes | | attestationObject | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | | clientDataJson | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | | transports | An Array with the transport methods given by the `AuthenticatorAttestationResponse` | yes | @@ -44,13 +60,12 @@ In your `.env` file, set the following values: `/authentication/start` a GET request, does not expect parameters -`/registration/verification` expects the following parameters: +`/authentication/verification` expects the following parameters: | Parameter | Description | Required | | :-------- | :---------- | :------- | | id | A base64url encoded representation of `rawId`. | yes | | rawId | The globally unique identifier for this `PublicKeyCredential`. | yes | -| type | `public-key` | yes | | authenticatorData | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | | clientDataJson | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | | signature | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | diff --git a/passkey-backend/assets/.well-know/apple-app-site-association b/passkey-backend/assets/.well-know/apple-app-site-association index 2cb576d39..8f9626249 100644 --- a/passkey-backend/assets/.well-know/apple-app-site-association +++ b/passkey-backend/assets/.well-know/apple-app-site-association @@ -1,7 +1,7 @@ { "webcredentials": { "apps": [ - "9EVH78F4V4.com.passkeys.twilio.sampleCode" + "{ORIGIN_IOS_APP_HASH}" ] } } \ No newline at end of file diff --git a/passkey-backend/assets/.well-know/assetlinks.json b/passkey-backend/assets/.well-know/assetlinks.json index 105e89bd1..9a034df28 100644 --- a/passkey-backend/assets/.well-know/assetlinks.json +++ b/passkey-backend/assets/.well-know/assetlinks.json @@ -6,7 +6,7 @@ ], "target": { "namespace": "web", - "site": "https://6dcc8b2b3620.ngrok.app" + "site": "{RELYING_PARTY}" } }, { @@ -17,35 +17,7 @@ "target": { "namespace": "android_app", "package_name": "com.twilio.passkeys.android", - "sha256_cert_fingerprints": [ - "AF:E0:6F:5F:BF:5A:C4:E2:A0:89:22:95:B8:1C:05:4B:29:5C:80:7A:3B:69:4B:B1:0A:7B:A6:CF:1E:17:4F:A1" - ] - } - }, - { - "relation": [ - "delegate_permission/common.handle_all_urls", - "delegate_permission/common.get_login_creds" - ], - "target": { - "namespace": "android_app", - "package_name": "com.twilio.passkeys.android", - "sha256_cert_fingerprints": [ - "50:5C:D6:3D:A5:1F:19:8F:3F:B1:C2:95:0B:6B:46:B6:06:FE:C4:13:57:4B:96:7F:3D:86:A3:CF:73:FE:05:53" - ] - } - }, - { - "relation": [ - "delegate_permission/common.handle_all_urls", - "delegate_permission/common.get_login_creds" - ], - "target": { - "namespace": "android_app", - "package_name": "com.twilio.passkeys.android", - "sha256_cert_fingerprints": [ - "E1:1B:1C:EA:3B:D8:22:EE:73:3A:26:CF:54:AF:66:0E:08:2B:CF:9A:49:7C:34:6B:D8:02:0B:6A:BA:8D:16:EF" - ] + "sha256_cert_fingerprints": ["{FINGERPRINT_CERTIFICATION_HASH}"] } }, { @@ -56,9 +28,7 @@ "target": { "namespace": "android_app", "package_name": "com.twilio.passkey_manager", - "sha256_cert_fingerprints": [ - "AF:E0:6F:5F:BF:5A:C4:E2:A0:89:22:95:B8:1C:05:4B:29:5C:80:7A:3B:69:4B:B1:0A:7B:A6:CF:1E:17:4F:A1" - ] + "sha256_cert_fingerprints": ["{FINGERPRINT_CERTIFICATION_HASH}"] } } ] diff --git a/passkey-backend/assets/index.html b/passkey-backend/assets/index.html index 248643ce0..8a0be9c4b 100644 --- a/passkey-backend/assets/index.html +++ b/passkey-backend/assets/index.html @@ -4,7 +4,7 @@ - Passkey Demo + Passkeys Demo @@ -259,7 +259,6 @@

body: JSON.stringify({ id: id, rawId: ArrayBufferToBase64(rawId), - type: type, clientDataJson: ArrayBufferToBase64(clientDataJSON), authenticatorData: ArrayBufferToBase64(authenticatorData), signature: ArrayBufferToBase64(signature), @@ -346,7 +345,6 @@

id: id, attestationObject: attestationObjectString, rawId: rawIdString, - type: type, clientDataJson: clientDataJSONString, transports: ["internal"] }) diff --git a/passkey-backend/functions/authentication/verification.js b/passkey-backend/functions/authentication/verification.js index dce1e3c82..606452b2c 100644 --- a/passkey-backend/functions/authentication/verification.js +++ b/passkey-backend/functions/authentication/verification.js @@ -12,7 +12,6 @@ exports.handler = async (context, event, callback) => { [ 'id', 'rawId', - 'type', 'clientDataJson', 'authenticatorData', 'signature', @@ -29,7 +28,7 @@ exports.handler = async (context, event, callback) => { rawId: event.rawId, id: event.id, authenticatorAttachment: 'platform', - type: event.type, + type: 'public-key', response: { clientDataJSON: event.clientDataJson, authenticatorData: event.authenticatorData, diff --git a/passkey-backend/functions/registration/start.js b/passkey-backend/functions/registration/start.js index d22e2d1f7..f4d1fcbb1 100644 --- a/passkey-backend/functions/registration/start.js +++ b/passkey-backend/functions/registration/start.js @@ -16,7 +16,7 @@ exports.handler = async (context, event, callback) => { `Missing parameters; please provide: '${missingParams.join(', ')}'.` ); - // Request body sent to passkey verify URL call + // Request body sent to passkeys verify URL call /* eslint-disable camelcase */ const requestBody = { friendly_name: 'TouchID', @@ -43,10 +43,10 @@ exports.handler = async (context, event, callback) => { }, }; - // Factor URL of the passkey service + // Factor URL of the passkeys service const factorURL = `${API_URL}Services/${SERVICE_SID}/Factors`; - // Call made to the passkey service + // Call made to the passkeys service try { const response = await axios.post(factorURL, requestBody, { auth: { diff --git a/passkey-backend/functions/registration/verification.js b/passkey-backend/functions/registration/verification.js index 8bb2edef3..856ca3863 100644 --- a/passkey-backend/functions/registration/verification.js +++ b/passkey-backend/functions/registration/verification.js @@ -10,14 +10,7 @@ exports.handler = async (context, event, callback) => { const { API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; const missingParams = detectMissingParams( - [ - 'id', - 'attestationObject', - 'rawId', - 'type', - 'clientDataJson', - 'transports', - ], + ['id', 'attestationObject', 'rawId', 'clientDataJson', 'transports'], event ); if (missingParams) @@ -29,7 +22,7 @@ exports.handler = async (context, event, callback) => { id: event.id, rawId: event.rawId, authenticatorAttachment: 'platform', - type: event.type, + type: 'public-key', response: { attestationObject: event.attestationObject, clientDataJSON: event.clientDataJson, diff --git a/passkey-backend/package.json b/passkey-backend/package.json index 16b8df2f5..88f97b292 100644 --- a/passkey-backend/package.json +++ b/passkey-backend/package.json @@ -1,5 +1,5 @@ { - "name": "passkey-backend", + "name": "passkeys-backend", "version": "1.0.0", "private": true, "dependencies": { From a1d80b427350f252dd9e69338e9e6c50adcb0661 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Thu, 7 Mar 2024 09:19:24 -0500 Subject: [PATCH 07/16] replaced passkey to passkeys --- package.json | 2 +- {passkey-backend => passkeys-backend}/.env.example | 0 {passkey-backend => passkeys-backend}/.owners | 0 {passkey-backend => passkeys-backend}/CHANGELOG.md | 0 {passkey-backend => passkeys-backend}/README.md | 0 .../assets/.well-know/apple-app-site-association | 0 .../assets/.well-know/assetlinks.json | 0 {passkey-backend => passkeys-backend}/assets/index.html | 0 .../assets/services/helpers.private.js | 0 .../functions/authentication/start.js | 0 .../functions/authentication/verification.js | 0 .../functions/registration/start.js | 0 .../functions/registration/verification.js | 0 {passkey-backend => passkeys-backend}/package.json | 0 .../tests/authentication-verification.test.js | 0 .../tests/registration-start.test.js | 0 .../tests/registration-verification.test.js | 0 templates.json | 6 +++--- 18 files changed, 4 insertions(+), 4 deletions(-) rename {passkey-backend => passkeys-backend}/.env.example (100%) rename {passkey-backend => passkeys-backend}/.owners (100%) rename {passkey-backend => passkeys-backend}/CHANGELOG.md (100%) rename {passkey-backend => passkeys-backend}/README.md (100%) rename {passkey-backend => passkeys-backend}/assets/.well-know/apple-app-site-association (100%) rename {passkey-backend => passkeys-backend}/assets/.well-know/assetlinks.json (100%) rename {passkey-backend => passkeys-backend}/assets/index.html (100%) rename {passkey-backend => passkeys-backend}/assets/services/helpers.private.js (100%) rename {passkey-backend => passkeys-backend}/functions/authentication/start.js (100%) rename {passkey-backend => passkeys-backend}/functions/authentication/verification.js (100%) rename {passkey-backend => passkeys-backend}/functions/registration/start.js (100%) rename {passkey-backend => passkeys-backend}/functions/registration/verification.js (100%) rename {passkey-backend => passkeys-backend}/package.json (100%) rename {passkey-backend => passkeys-backend}/tests/authentication-verification.test.js (100%) rename {passkey-backend => passkeys-backend}/tests/registration-start.test.js (100%) rename {passkey-backend => passkeys-backend}/tests/registration-verification.test.js (100%) diff --git a/package.json b/package.json index e27834022..799e91bbf 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,6 @@ "voicemail", "verify-sna", "flex-dialpad", - "passkey-backend" + "passkeys-backend" ] } diff --git a/passkey-backend/.env.example b/passkeys-backend/.env.example similarity index 100% rename from passkey-backend/.env.example rename to passkeys-backend/.env.example diff --git a/passkey-backend/.owners b/passkeys-backend/.owners similarity index 100% rename from passkey-backend/.owners rename to passkeys-backend/.owners diff --git a/passkey-backend/CHANGELOG.md b/passkeys-backend/CHANGELOG.md similarity index 100% rename from passkey-backend/CHANGELOG.md rename to passkeys-backend/CHANGELOG.md diff --git a/passkey-backend/README.md b/passkeys-backend/README.md similarity index 100% rename from passkey-backend/README.md rename to passkeys-backend/README.md diff --git a/passkey-backend/assets/.well-know/apple-app-site-association b/passkeys-backend/assets/.well-know/apple-app-site-association similarity index 100% rename from passkey-backend/assets/.well-know/apple-app-site-association rename to passkeys-backend/assets/.well-know/apple-app-site-association diff --git a/passkey-backend/assets/.well-know/assetlinks.json b/passkeys-backend/assets/.well-know/assetlinks.json similarity index 100% rename from passkey-backend/assets/.well-know/assetlinks.json rename to passkeys-backend/assets/.well-know/assetlinks.json diff --git a/passkey-backend/assets/index.html b/passkeys-backend/assets/index.html similarity index 100% rename from passkey-backend/assets/index.html rename to passkeys-backend/assets/index.html diff --git a/passkey-backend/assets/services/helpers.private.js b/passkeys-backend/assets/services/helpers.private.js similarity index 100% rename from passkey-backend/assets/services/helpers.private.js rename to passkeys-backend/assets/services/helpers.private.js diff --git a/passkey-backend/functions/authentication/start.js b/passkeys-backend/functions/authentication/start.js similarity index 100% rename from passkey-backend/functions/authentication/start.js rename to passkeys-backend/functions/authentication/start.js diff --git a/passkey-backend/functions/authentication/verification.js b/passkeys-backend/functions/authentication/verification.js similarity index 100% rename from passkey-backend/functions/authentication/verification.js rename to passkeys-backend/functions/authentication/verification.js diff --git a/passkey-backend/functions/registration/start.js b/passkeys-backend/functions/registration/start.js similarity index 100% rename from passkey-backend/functions/registration/start.js rename to passkeys-backend/functions/registration/start.js diff --git a/passkey-backend/functions/registration/verification.js b/passkeys-backend/functions/registration/verification.js similarity index 100% rename from passkey-backend/functions/registration/verification.js rename to passkeys-backend/functions/registration/verification.js diff --git a/passkey-backend/package.json b/passkeys-backend/package.json similarity index 100% rename from passkey-backend/package.json rename to passkeys-backend/package.json diff --git a/passkey-backend/tests/authentication-verification.test.js b/passkeys-backend/tests/authentication-verification.test.js similarity index 100% rename from passkey-backend/tests/authentication-verification.test.js rename to passkeys-backend/tests/authentication-verification.test.js diff --git a/passkey-backend/tests/registration-start.test.js b/passkeys-backend/tests/registration-start.test.js similarity index 100% rename from passkey-backend/tests/registration-start.test.js rename to passkeys-backend/tests/registration-start.test.js diff --git a/passkey-backend/tests/registration-verification.test.js b/passkeys-backend/tests/registration-verification.test.js similarity index 100% rename from passkey-backend/tests/registration-verification.test.js rename to passkeys-backend/tests/registration-verification.test.js diff --git a/templates.json b/templates.json index 91d831cd6..24b63772b 100644 --- a/templates.json +++ b/templates.json @@ -341,9 +341,9 @@ "description": "Transfers a call to another number" }, { - "id": "passkey-backend", - "name": "Backend for passkey app", - "description": "Connect appliactions with the passkey service" + "id": "passkeys-backend", + "name": "Backend for passkeys app", + "description": "Connect appliactions with the passkeys service" } ] } From fd8b8464ddc02f819737e39b7a4cfbc6a8188290 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Mon, 11 Mar 2024 08:17:41 -0500 Subject: [PATCH 08/16] removed unused relation --- passkeys-backend/assets/.well-know/assetlinks.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/passkeys-backend/assets/.well-know/assetlinks.json b/passkeys-backend/assets/.well-know/assetlinks.json index 9a034df28..04a4e0bbe 100644 --- a/passkeys-backend/assets/.well-know/assetlinks.json +++ b/passkeys-backend/assets/.well-know/assetlinks.json @@ -19,16 +19,5 @@ "package_name": "com.twilio.passkeys.android", "sha256_cert_fingerprints": ["{FINGERPRINT_CERTIFICATION_HASH}"] } - }, - { - "relation": [ - "delegate_permission/common.handle_all_urls", - "delegate_permission/common.get_login_creds" - ], - "target": { - "namespace": "android_app", - "package_name": "com.twilio.passkey_manager", - "sha256_cert_fingerprints": ["{FINGERPRINT_CERTIFICATION_HASH}"] - } } ] From d93cbe5014efbf1b6897107106b99590fc7b4903 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Wed, 13 Mar 2024 09:48:08 -0500 Subject: [PATCH 09/16] add usage and deploy section and fix typos --- passkeys-backend/README.md | 20 +++++++++++++++++--- templates.json | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/passkeys-backend/README.md b/passkeys-backend/README.md index e2d8d37c1..f9910d83c 100644 --- a/passkeys-backend/README.md +++ b/passkeys-backend/README.md @@ -2,6 +2,14 @@ Verify enables developers to easily add Passkeys into their existing authentication flows, similar to Verify TOTP and Push. The Verify API supports passkey registration, public key storage, and auth flows. On the client-side, developers can optionally embed an open-source library (SDK) that handles interactions with operating systems and customizable UI widgets that maximize conversion. +## How to use the template + +The best way to use the Function templates is through the Twilio CLI as described below. If you'd like to use the template without the Twilio CLI, [check out our usage docs](../docs/USING_FUNCTIONS.md). + +Make sure befores you use the template you have to set up your enviroment variables and +customize the associated files with your client applications origins you can find this +customization [here](#service-customization). + ## Pre-requisites ### Environment variables @@ -14,7 +22,7 @@ In your `.env` file, set the following values: | Variable | Description | Required | | :------- | :---------- | :------- | -| API_URL | Passkey API to point at | yes | +| API_URL | Passkeys API to point at | yes | | SERVICE_SID | Service used to call twilio services | yes | | RELYING_PARTY | Customer app or client | yes | ACCOUNT_SID | Twilio account where the service belong | yes | @@ -71,6 +79,12 @@ Besides the enviroment variables files, the project also contain two files calle | signature | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | | userHandle | A base64url encoded object given by the `AuthenticatorAttestationResponse` | yes | -## Test this project locally +## Deploying + +Deploy your functions and assets with either of the following commands. Note: you must run these commands from inside your project folder. [More details in the docs.](https://www.twilio.com/docs/labs/serverless-toolkit) + +With the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): -Follow the steps in the [Twilio CLI Test](https://github.com/AuthyApps/twilio-cli-test) repository +``` +twilio serverless:deploy +``` diff --git a/templates.json b/templates.json index 24b63772b..164d4351a 100644 --- a/templates.json +++ b/templates.json @@ -343,7 +343,7 @@ { "id": "passkeys-backend", "name": "Backend for passkeys app", - "description": "Connect appliactions with the passkeys service" + "description": "Connect applications with the passkeys service" } ] } From 6a6cfb50fddd45fd76a5c32c6209039c5a18425a Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Tue, 11 Jun 2024 04:09:05 -0500 Subject: [PATCH 10/16] last functional sample backend --- .../functions/authentication/start.js | 11 ++++++++--- .../functions/authentication/verification.js | 14 ++++++++++---- passkeys-backend/functions/registration/start.js | 15 +++++++++++---- .../functions/registration/verification.js | 12 +++++++++++- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/passkeys-backend/functions/authentication/start.js b/passkeys-backend/functions/authentication/start.js index d781b4d0e..03ee43408 100644 --- a/passkeys-backend/functions/authentication/start.js +++ b/passkeys-backend/functions/authentication/start.js @@ -1,10 +1,9 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { errorLogger } = require(assets['/services/helpers.js'].path); // eslint-disable-next-line consistent-return -exports.handler = async (context, _, callback) => { +exports.handler = async (context, event, callback) => { const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; @@ -25,7 +24,13 @@ exports.handler = async (context, _, callback) => { }); return callback(null, response.data.details); } catch (error) { - errorLogger(error); + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } return callback('Something went wrong'); } }; diff --git a/passkeys-backend/functions/authentication/verification.js b/passkeys-backend/functions/authentication/verification.js index 606452b2c..475e6d259 100644 --- a/passkeys-backend/functions/authentication/verification.js +++ b/passkeys-backend/functions/authentication/verification.js @@ -1,9 +1,7 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { detectMissingParams, errorLogger } = require(assets[ - '/services/helpers.js' -].path); +const { detectMissingParams } = require(assets['/services/helpers.js'].path); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { @@ -12,6 +10,7 @@ exports.handler = async (context, event, callback) => { [ 'id', 'rawId', + // 'type', 'clientDataJson', 'authenticatorData', 'signature', @@ -28,6 +27,7 @@ exports.handler = async (context, event, callback) => { rawId: event.rawId, id: event.id, authenticatorAttachment: 'platform', + // type: event.type, type: 'public-key', response: { clientDataJSON: event.clientDataJson, @@ -51,7 +51,13 @@ exports.handler = async (context, event, callback) => { identity: response.data.entity_identity, }); } catch (error) { - errorLogger(error); + if (error.response) { + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } return callback(null, error); } }; diff --git a/passkeys-backend/functions/registration/start.js b/passkeys-backend/functions/registration/start.js index f4d1fcbb1..28fe04f21 100644 --- a/passkeys-backend/functions/registration/start.js +++ b/passkeys-backend/functions/registration/start.js @@ -1,9 +1,7 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { detectMissingParams, errorLogger } = require(assets[ - '/services/helpers.js' -].path); +const { detectMissingParams } = require(assets['/services/helpers.js'].path); exports.handler = async (context, event, callback) => { const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = @@ -33,6 +31,8 @@ exports.handler = async (context, event, callback) => { `https://${RELYING_PARTY}`, 'android:apk-key-hash:r-BvX79axOKgiSKVuBwFSylcgHo7aUuxCnumzx4XT6E', 'android:apk-key-hash:UFzWPaUfGY8_scKVC2tGtgb-xBNXS5Z_PYajz3P-BVM', + 'android:apk-key-hash:V9oDo6qGAoQG3r3vk7JJBAFBVrpSPvsp-QTlyttftAw', + 'android:apk-key-hash:yOXmgJgVThpM_CUPlnaG4fEiFA0PpR1MCa-FbWfeiDM', ], }, authenticator_criteria: { @@ -59,7 +59,14 @@ exports.handler = async (context, event, callback) => { factor_sid: response.data.sid, }); } catch (error) { - errorLogger(error); + if (error.response) { + console.log(error.response.data); + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } return callback(null, error); } }; diff --git a/passkeys-backend/functions/registration/verification.js b/passkeys-backend/functions/registration/verification.js index 856ca3863..ece9214cd 100644 --- a/passkeys-backend/functions/registration/verification.js +++ b/passkeys-backend/functions/registration/verification.js @@ -30,6 +30,9 @@ exports.handler = async (context, event, callback) => { }, }; + console.log('requestBody', requestBody); + console.log('response object', requestBody.response); + const verifyFactorURL = `${API_URL}Services/${SERVICE_SID}/Factors/Verify`; try { @@ -43,7 +46,14 @@ exports.handler = async (context, event, callback) => { status: response.data.status, }); } catch (error) { - errorLogger(error); + if (error.response) { + console.log(error.response.data); + console.log('Client has given an error', error); + } else if (error.request) { + console.log('Runtime error', error); + } else { + console.log(error); + } return callback(null, error); } }; From 61d974f10ea6fb12cf80350c5828e834fc76bf1f Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Thu, 13 Jun 2024 14:00:33 -0500 Subject: [PATCH 11/16] migrate passkeys backend to comms --- .../functions/authentication/start.js | 12 ++++---- .../functions/authentication/verification.js | 28 ++++++++++--------- .../functions/registration/start.js | 18 ++++-------- .../functions/registration/verification.js | 25 +++++++++-------- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/passkeys-backend/functions/authentication/start.js b/passkeys-backend/functions/authentication/start.js index 03ee43408..09855a7d0 100644 --- a/passkeys-backend/functions/authentication/start.js +++ b/passkeys-backend/functions/authentication/start.js @@ -4,16 +4,16 @@ const assets = Runtime.getAssets(); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { - const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = - context; + const { RELYING_PARTY, API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; const requestBody = { - details: { - rpId: RELYING_PARTY, + content: { + // eslint-disable-next-line camelcase + rp_id: RELYING_PARTY, }, }; - const challengeURL = `${API_URL}Services/${SERVICE_SID}/Challenges`; + const challengeURL = `${API_URL}/Verifications`; try { const response = await axios.post(challengeURL, requestBody, { @@ -22,7 +22,7 @@ exports.handler = async (context, event, callback) => { password: AUTH_TOKEN, }, }); - return callback(null, response.data.details); + return callback(null, response.data.next_step); } catch (error) { if (error.response) { console.log('Client has given an error', error); diff --git a/passkeys-backend/functions/authentication/verification.js b/passkeys-backend/functions/authentication/verification.js index 475e6d259..91eaf4ce9 100644 --- a/passkeys-backend/functions/authentication/verification.js +++ b/passkeys-backend/functions/authentication/verification.js @@ -5,7 +5,7 @@ const { detectMissingParams } = require(assets['/services/helpers.js'].path); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { - const { API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; + const { API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; const missingParams = detectMissingParams( [ 'id', @@ -24,20 +24,22 @@ exports.handler = async (context, event, callback) => { ); const requestBody = { - rawId: event.rawId, - id: event.id, - authenticatorAttachment: 'platform', - // type: event.type, - type: 'public-key', - response: { - clientDataJSON: event.clientDataJson, - authenticatorData: event.authenticatorData, - signature: event.signature, - userHandle: event.userHandle, + content: { + rawId: event.rawId, + id: event.id, + authenticatorAttachment: 'platform', + // type: event.type, + type: 'public-key', + response: { + clientDataJSON: event.clientDataJson, + authenticatorData: event.authenticatorData, + signature: event.signature, + userHandle: event.userHandle, + }, }, }; - const verifyChallengeURL = `${API_URL}Services/${SERVICE_SID}/Challenges/Verify`; + const verifyChallengeURL = `${API_URL}/Verifications/Check`; try { const response = await axios.post(verifyChallengeURL, requestBody, { @@ -48,7 +50,7 @@ exports.handler = async (context, event, callback) => { }); return callback(null, { status: response.data.status, - identity: response.data.entity_identity, + identity: response.data.to.user_identifier, }); } catch (error) { if (error.response) { diff --git a/passkeys-backend/functions/registration/start.js b/passkeys-backend/functions/registration/start.js index 28fe04f21..f719c5c93 100644 --- a/passkeys-backend/functions/registration/start.js +++ b/passkeys-backend/functions/registration/start.js @@ -4,8 +4,7 @@ const assets = Runtime.getAssets(); const { detectMissingParams } = require(assets['/services/helpers.js'].path); exports.handler = async (context, event, callback) => { - const { RELYING_PARTY, API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = - context; + const { RELYING_PARTY, API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; // Verify request comes with username const missingParams = detectMissingParams(['username'], event); @@ -18,12 +17,10 @@ exports.handler = async (context, event, callback) => { /* eslint-disable camelcase */ const requestBody = { friendly_name: 'TouchID', - factory_type: 'passkeys', - entity: { - identity: event.username, - display_name: event.username, + to: { + user_identifier: event.username, }, - config: { + content: { relying_party: { id: RELYING_PARTY, name: 'PasskeySample', @@ -44,7 +41,7 @@ exports.handler = async (context, event, callback) => { }; // Factor URL of the passkeys service - const factorURL = `${API_URL}Services/${SERVICE_SID}/Factors`; + const factorURL = `${API_URL}/Factors`; // Call made to the passkeys service try { @@ -54,10 +51,7 @@ exports.handler = async (context, event, callback) => { password: AUTH_TOKEN, }, }); - return callback(null, { - ...response.data.config.creation_request, - factor_sid: response.data.sid, - }); + return callback(null, response.data.next_step); } catch (error) { if (error.response) { console.log(error.response.data); diff --git a/passkeys-backend/functions/registration/verification.js b/passkeys-backend/functions/registration/verification.js index ece9214cd..bb3ed2dcb 100644 --- a/passkeys-backend/functions/registration/verification.js +++ b/passkeys-backend/functions/registration/verification.js @@ -7,7 +7,7 @@ const { detectMissingParams, errorLogger } = require(assets[ // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { - const { API_URL, SERVICE_SID, ACCOUNT_SID, AUTH_TOKEN } = context; + const { API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; const missingParams = detectMissingParams( ['id', 'attestationObject', 'rawId', 'clientDataJson', 'transports'], @@ -19,21 +19,23 @@ exports.handler = async (context, event, callback) => { ); const requestBody = { - id: event.id, - rawId: event.rawId, - authenticatorAttachment: 'platform', - type: 'public-key', - response: { - attestationObject: event.attestationObject, - clientDataJSON: event.clientDataJson, - transports: event.transports, + content: { + id: event.id, + rawId: event.rawId, + authenticatorAttachment: 'platform', + type: 'public-key', + response: { + attestationObject: event.attestationObject, + clientDataJSON: event.clientDataJson, + transports: event.transports, + }, }, }; console.log('requestBody', requestBody); console.log('response object', requestBody.response); - const verifyFactorURL = `${API_URL}Services/${SERVICE_SID}/Factors/Verify`; + const verifyFactorURL = `${API_URL}/Factors/Approve`; try { const response = await axios.post(verifyFactorURL, requestBody, { @@ -43,7 +45,8 @@ exports.handler = async (context, event, callback) => { }, }); return callback(null, { - status: response.data.status, + status: + response.data.status === 'approved' ? 'verified' : response.data.status, }); } catch (error) { if (error.response) { From 0de51a4bb8246b44a8c213d9ac69b3f78755cac6 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Thu, 20 Jun 2024 06:03:31 -0500 Subject: [PATCH 12/16] Setup ready for PR --- passkeys-backend/.env.example | 1 - passkeys-backend/README.md | 1 - .../assets/services/helpers.private.js | 11 ----- .../functions/authentication/start.js | 24 ++++++----- .../functions/authentication/verification.js | 39 ++++++++++-------- .../functions/registration/start.js | 31 ++++++++------ .../functions/registration/verification.js | 41 ++++++++++--------- .../tests/authentication-start.test.js | 38 +++++++++++++++++ .../tests/authentication-verification.test.js | 27 ++++++------ .../tests/registration-start.test.js | 14 ++++--- .../tests/registration-verification.test.js | 27 ++++++------ 11 files changed, 150 insertions(+), 104 deletions(-) create mode 100644 passkeys-backend/tests/authentication-start.test.js diff --git a/passkeys-backend/.env.example b/passkeys-backend/.env.example index 1bec353a0..1b0b34d66 100644 --- a/passkeys-backend/.env.example +++ b/passkeys-backend/.env.example @@ -1,5 +1,4 @@ API_URL= -SERVICE_SID= RELYING_PARTY= ACCOUNT_SID= AUTH_TOKEN= \ No newline at end of file diff --git a/passkeys-backend/README.md b/passkeys-backend/README.md index f9910d83c..6365713a4 100644 --- a/passkeys-backend/README.md +++ b/passkeys-backend/README.md @@ -23,7 +23,6 @@ In your `.env` file, set the following values: | Variable | Description | Required | | :------- | :---------- | :------- | | API_URL | Passkeys API to point at | yes | -| SERVICE_SID | Service used to call twilio services | yes | | RELYING_PARTY | Customer app or client | yes | ACCOUNT_SID | Twilio account where the service belong | yes | | AUTH_TOKEN | Authentication token for twilio account | yes | diff --git a/passkeys-backend/assets/services/helpers.private.js b/passkeys-backend/assets/services/helpers.private.js index a8da0dd25..51fab6616 100644 --- a/passkeys-backend/assets/services/helpers.private.js +++ b/passkeys-backend/assets/services/helpers.private.js @@ -5,17 +5,6 @@ const detectMissingParams = (paramNames, event) => { return missingParams.length > 0 ? missingParams : null; }; -const errorLogger = (error) => { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } -}; - module.exports = { detectMissingParams, - errorLogger, }; diff --git a/passkeys-backend/functions/authentication/start.js b/passkeys-backend/functions/authentication/start.js index 09855a7d0..bd41c236e 100644 --- a/passkeys-backend/functions/authentication/start.js +++ b/passkeys-backend/functions/authentication/start.js @@ -3,9 +3,12 @@ const axios = require('axios'); const assets = Runtime.getAssets(); // eslint-disable-next-line consistent-return -exports.handler = async (context, event, callback) => { +exports.handler = async (context, _, callback) => { const { RELYING_PARTY, API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; + const response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + const requestBody = { content: { // eslint-disable-next-line camelcase @@ -16,21 +19,20 @@ exports.handler = async (context, event, callback) => { const challengeURL = `${API_URL}/Verifications`; try { - const response = await axios.post(challengeURL, requestBody, { + const APIResponse = await axios.post(challengeURL, requestBody, { auth: { username: ACCOUNT_SID, password: AUTH_TOKEN, }, }); - return callback(null, response.data.next_step); + + response.setStatusCode(200); + response.setBody(APIResponse.data.next_step); } catch (error) { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } - return callback('Something went wrong'); + const statusCode = error.status || 400; + response.setStatusCode(statusCode); + response.setBody(error.message); } + + return callback(null, response); }; diff --git a/passkeys-backend/functions/authentication/verification.js b/passkeys-backend/functions/authentication/verification.js index 91eaf4ce9..230102696 100644 --- a/passkeys-backend/functions/authentication/verification.js +++ b/passkeys-backend/functions/authentication/verification.js @@ -3,14 +3,16 @@ const axios = require('axios'); const assets = Runtime.getAssets(); const { detectMissingParams } = require(assets['/services/helpers.js'].path); -// eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { const { API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; + + const response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + const missingParams = detectMissingParams( [ 'id', 'rawId', - // 'type', 'clientDataJson', 'authenticatorData', 'signature', @@ -18,17 +20,21 @@ exports.handler = async (context, event, callback) => { ], event ); - if (missingParams) - return callback( + + if (missingParams) { + response.setStatusCode(400); + response.setBody( `Missing parameters; please provide: '${missingParams.join(', ')}'.` ); + return callback(null, response); + } + const requestBody = { content: { rawId: event.rawId, id: event.id, authenticatorAttachment: 'platform', - // type: event.type, type: 'public-key', response: { clientDataJSON: event.clientDataJson, @@ -42,24 +48,23 @@ exports.handler = async (context, event, callback) => { const verifyChallengeURL = `${API_URL}/Verifications/Check`; try { - const response = await axios.post(verifyChallengeURL, requestBody, { + const APIresponse = await axios.post(verifyChallengeURL, requestBody, { auth: { username: ACCOUNT_SID, password: AUTH_TOKEN, }, }); - return callback(null, { - status: response.data.status, - identity: response.data.to.user_identifier, + + response.setStatusCode(200); + response.setBody({ + status: APIresponse.data.status, + identity: APIresponse.data.to.user_identifier, }); } catch (error) { - if (error.response) { - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } - return callback(null, error); + const statusCode = error.status || 400; + response.setStatusCode(statusCode); + response.setBody(error.message); } + + return callback(null, response); }; diff --git a/passkeys-backend/functions/registration/start.js b/passkeys-backend/functions/registration/start.js index f719c5c93..9fc8c1f98 100644 --- a/passkeys-backend/functions/registration/start.js +++ b/passkeys-backend/functions/registration/start.js @@ -6,13 +6,20 @@ const { detectMissingParams } = require(assets['/services/helpers.js'].path); exports.handler = async (context, event, callback) => { const { RELYING_PARTY, API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; + const response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + // Verify request comes with username const missingParams = detectMissingParams(['username'], event); - if (missingParams) - return callback( + if (missingParams) { + response.setStatusCode(400); + response.setBody( `Missing parameters; please provide: '${missingParams.join(', ')}'.` ); + return callback(null, response); + } + // Request body sent to passkeys verify URL call /* eslint-disable camelcase */ const requestBody = { @@ -45,22 +52,20 @@ exports.handler = async (context, event, callback) => { // Call made to the passkeys service try { - const response = await axios.post(factorURL, requestBody, { + const APIResponse = await axios.post(factorURL, requestBody, { auth: { username: ACCOUNT_SID, password: AUTH_TOKEN, }, }); - return callback(null, response.data.next_step); + + response.setStatusCode(200); + response.setBody(APIResponse.data.next_step); } catch (error) { - if (error.response) { - console.log(error.response.data); - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } - return callback(null, error); + const statusCode = error.status || 400; + response.setStatusCode(statusCode); + response.setBody(error.message); } + + return callback(null, response); }; diff --git a/passkeys-backend/functions/registration/verification.js b/passkeys-backend/functions/registration/verification.js index bb3ed2dcb..5ffc8b184 100644 --- a/passkeys-backend/functions/registration/verification.js +++ b/passkeys-backend/functions/registration/verification.js @@ -1,23 +1,28 @@ const axios = require('axios'); const assets = Runtime.getAssets(); -const { detectMissingParams, errorLogger } = require(assets[ - '/services/helpers.js' -].path); +const { detectMissingParams } = require(assets['/services/helpers.js'].path); // eslint-disable-next-line consistent-return exports.handler = async (context, event, callback) => { const { API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; + const response = new Twilio.Response(); + response.appendHeader('Content-Type', 'application/json'); + const missingParams = detectMissingParams( ['id', 'attestationObject', 'rawId', 'clientDataJson', 'transports'], event ); - if (missingParams) - return callback( + if (missingParams) { + response.setStatusCode(400); + response.setBody( `Missing parameters; please provide: '${missingParams.join(', ')}'.` ); + return callback(null, response); + } + const requestBody = { content: { id: event.id, @@ -32,31 +37,27 @@ exports.handler = async (context, event, callback) => { }, }; - console.log('requestBody', requestBody); - console.log('response object', requestBody.response); - const verifyFactorURL = `${API_URL}/Factors/Approve`; try { - const response = await axios.post(verifyFactorURL, requestBody, { + const APIResponse = await axios.post(verifyFactorURL, requestBody, { auth: { username: ACCOUNT_SID, password: AUTH_TOKEN, }, }); - return callback(null, { + response.setStatusCode(200); + response.setBody({ status: - response.data.status === 'approved' ? 'verified' : response.data.status, + APIResponse.data.status === 'approved' + ? 'verified' + : APIResponse.data.status, }); } catch (error) { - if (error.response) { - console.log(error.response.data); - console.log('Client has given an error', error); - } else if (error.request) { - console.log('Runtime error', error); - } else { - console.log(error); - } - return callback(null, error); + const statusCode = error.status || 400; + response.setStatusCode(statusCode); + response.setBody(error.message); } + + return callback(null, response); }; diff --git a/passkeys-backend/tests/authentication-start.test.js b/passkeys-backend/tests/authentication-start.test.js new file mode 100644 index 000000000..c746b2835 --- /dev/null +++ b/passkeys-backend/tests/authentication-start.test.js @@ -0,0 +1,38 @@ +const axios = require('axios'); +const helpers = require('../../test/test-helper'); + +jest.mock('axios'); + +describe('authentication/start', () => { + beforeAll(() => { + jest.clearAllMocks(); + const runtime = new helpers.MockRuntime(); + runtime._addAsset( + '/services/helpers.js', + '../assets/services/helpers.private.js' + ); + helpers.setup({}, runtime); + handlerFunction = require('../functions/authentication/start').handler; + }); + afterAll(() => { + helpers.teardown(); + }); + beforeEach(() => { + jest.resetModules(); + axios.post.mockClear(); + }); + + it('returns error with unsuccesfull request', (done) => { + const expectedError = new Error('something bad happened'); + axios.post = jest.fn(() => Promise.reject(expectedError)); + + const callback = (_, { _body }) => { + expect(_body).toBeDefined(); + expect(axios.post).toHaveBeenCalledTimes(1); + expect(_body).toEqual(expectedError.message); + done(); + }; + + handlerFunction({}, {}, callback); + }); +}); diff --git a/passkeys-backend/tests/authentication-verification.test.js b/passkeys-backend/tests/authentication-verification.test.js index d3c676092..fb80eceda 100644 --- a/passkeys-backend/tests/authentication-verification.test.js +++ b/passkeys-backend/tests/authentication-verification.test.js @@ -6,7 +6,6 @@ jest.mock('axios'); const testEvent = { id: '12345', rawId: 'randomRawId', - type: 'test-type', clientDataJson: {}, authenticatorData: {}, signature: 'test-signature', @@ -35,10 +34,12 @@ describe('authentication/verification', () => { describe('when multiple required parameters are missing', () => { it('returns an error indicating multiple missing parameters', (done) => { - const callback = (_err) => { - expect(_err).toBeDefined(); - expect(_err).toEqual( - `Missing parameters; please provide: 'id, rawId, type, clientDataJson, authenticatorData, signature, userHandle'.` + const callback = (_, { _body, _statusCode }) => { + expect(_statusCode).toBeDefined(); + expect(_body).toBeDefined(); + expect(_statusCode).toEqual(400); + expect(_body).toEqual( + `Missing parameters; please provide: 'id, rawId, clientDataJson, authenticatorData, signature, userHandle'.` ); done(); }; @@ -46,10 +47,12 @@ describe('authentication/verification', () => { }); it('returns an error indicating specific missing parameters', (done) => { - const callback = (_err) => { - expect(_err).toBeDefined(); - expect(_err).toEqual( - `Missing parameters; please provide: 'type, clientDataJson, authenticatorData, signature, userHandle'.` + const callback = (_, { _body, _statusCode }) => { + expect(_statusCode).toBeDefined(); + expect(_body).toBeDefined(); + expect(_statusCode).toEqual(400); + expect(_body).toEqual( + `Missing parameters; please provide: 'clientDataJson, authenticatorData, signature, userHandle'.` ); done(); }; @@ -69,10 +72,10 @@ describe('authentication/verification', () => { const expectedError = new Error('something bad happened'); axios.post = jest.fn(() => Promise.reject(expectedError)); - const callback = (_err, result) => { - expect(result).toBeDefined(); + const callback = (_, { _body }) => { + expect(_body).toBeDefined(); expect(axios.post).toHaveBeenCalledTimes(1); - expect(result).toEqual(expectedError); + expect(_body).toEqual(expectedError.message); done(); }; diff --git a/passkeys-backend/tests/registration-start.test.js b/passkeys-backend/tests/registration-start.test.js index 45ec1fa75..3918d4cbb 100644 --- a/passkeys-backend/tests/registration-start.test.js +++ b/passkeys-backend/tests/registration-start.test.js @@ -23,9 +23,11 @@ describe('registration/start', () => { }); it('returns an error response indicating the missing parameters', (done) => { - const callback = (_err) => { - expect(_err).toBeDefined(); - expect(_err).toEqual(`Missing parameters; please provide: 'username'.`); + const callback = (_, { _body, _statusCode }) => { + expect(_statusCode).toBeDefined(); + expect(_body).toBeDefined(); + expect(_statusCode).toEqual(400); + expect(_body).toEqual(`Missing parameters; please provide: 'username'.`); done(); }; handlerFunction({}, {}, callback); @@ -35,10 +37,10 @@ describe('registration/start', () => { const expectedError = new Error('something bad happened'); axios.post = jest.fn(() => Promise.reject(expectedError)); - const callback = (_err, result) => { - expect(result).toBeDefined(); + const callback = (_, { _body }) => { + expect(_body).toBeDefined(); expect(axios.post).toHaveBeenCalledTimes(1); - expect(result).toEqual(expectedError); + expect(_body).toEqual(expectedError.message); done(); }; diff --git a/passkeys-backend/tests/registration-verification.test.js b/passkeys-backend/tests/registration-verification.test.js index b78c43401..feea7aaf5 100644 --- a/passkeys-backend/tests/registration-verification.test.js +++ b/passkeys-backend/tests/registration-verification.test.js @@ -7,7 +7,6 @@ const testEvent = { id: '12345', attestationObject: {}, rawId: 'randomRawId', - type: 'test-type', clientDataJson: {}, transports: 'test-transport', }; @@ -33,10 +32,12 @@ describe('registration/verification', () => { describe('when multiple required parameters are missing', () => { it('returns an error indicating multiple missing parameters', (done) => { - const callback = (_err) => { - expect(_err).toBeDefined(); - expect(_err).toEqual( - `Missing parameters; please provide: 'id, attestationObject, rawId, type, clientDataJson, transports'.` + const callback = (_, { _body, _statusCode }) => { + expect(_statusCode).toBeDefined(); + expect(_body).toBeDefined(); + expect(_statusCode).toEqual(400); + expect(_body).toEqual( + `Missing parameters; please provide: 'id, attestationObject, rawId, clientDataJson, transports'.` ); done(); }; @@ -44,10 +45,12 @@ describe('registration/verification', () => { }); it('returns an error indicating specific missing parameters', (done) => { - const callback = (_err) => { - expect(_err).toBeDefined(); - expect(_err).toEqual( - `Missing parameters; please provide: 'attestationObject, type, clientDataJson, transports'.` + const callback = (_, { _body, _statusCode }) => { + expect(_statusCode).toBeDefined(); + expect(_body).toBeDefined(); + expect(_statusCode).toEqual(400); + expect(_body).toEqual( + `Missing parameters; please provide: 'attestationObject, clientDataJson, transports'.` ); done(); }; @@ -67,10 +70,10 @@ describe('registration/verification', () => { const expectedError = new Error('something bad happened'); axios.post = jest.fn(() => Promise.reject(expectedError)); - const callback = (_err, result) => { - expect(result).toBeDefined(); + const callback = (_, { _body }) => { + expect(_body).toBeDefined(); expect(axios.post).toHaveBeenCalledTimes(1); - expect(result).toEqual(expectedError); + expect(_body).toEqual(expectedError.message); done(); }; From 845f2c28a2fcbef5fe7b8a740357642e32f7fa29 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Thu, 20 Jun 2024 06:04:25 -0500 Subject: [PATCH 13/16] unused variabe removed --- passkeys-backend/functions/authentication/start.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/passkeys-backend/functions/authentication/start.js b/passkeys-backend/functions/authentication/start.js index bd41c236e..e5009b12e 100644 --- a/passkeys-backend/functions/authentication/start.js +++ b/passkeys-backend/functions/authentication/start.js @@ -1,7 +1,5 @@ const axios = require('axios'); -const assets = Runtime.getAssets(); - // eslint-disable-next-line consistent-return exports.handler = async (context, _, callback) => { const { RELYING_PARTY, API_URL, ACCOUNT_SID, AUTH_TOKEN } = context; From fc93745a679fd426efad5b3b0b7d7e94c61e1955 Mon Sep 17 00:00:00 2001 From: Nicolas Camacho Date: Mon, 24 Jun 2024 05:20:16 -0500 Subject: [PATCH 14/16] using webauthn-json packacge --- passkeys-backend/assets/index.html | 64 +++++++----------------------- 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/passkeys-backend/assets/index.html b/passkeys-backend/assets/index.html index 8a0be9c4b..54a33b240 100644 --- a/passkeys-backend/assets/index.html +++ b/passkeys-backend/assets/index.html @@ -10,6 +10,7 @@ +