Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions passkeys-backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# description: The URL of the API for passkeys
# format: url
# required: true
API_URL=https://comms.twilio.com/preview
API_URL=https://verify.twilio.com/v2/Services

# description: [Optional] Comma separated domains for Android application
# format: list(text)
# description: [Optional] Namespace UUID for generating deterministic UUIDs with the uuid library
# format: text
# required: false
ANDROID_APP_KEYS=
NAMESPACE=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this value? What will be the format of the value? Could there be a default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose is to create a uuid based on the username, this is described in the readme, I think the UUID as is described is self explanatory, but yes we can add a default value


# description: [Optional] SID of the service created in Twilio verify
# format: sid
# required: false
Copy link
Contributor

@yafuquen yafuquen Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if I set this to true the code exchange will take it as a mandatory when creating the function, but this is created with one of the endpoints

SERVICE_SID=
8 changes: 6 additions & 2 deletions passkeys-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ In your `.env` file, set the following values:
| `API_URL` | Passkeys API to point at | Yes |
| `ACCOUNT_SID` | Find in the [console](https://www.twilio.com/console) | Yes |
| `AUTH_TOKEN` | Find in the [console](https://www.twilio.com/console) | Yes |
| `ANDROID_APP_KEYS` | The domain of the Android identity providers hash | No |
| `NAMESPACE` | UUID for generating deterministic UUIDs with the uuid library for username conversion | No |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is not required, why is it needed? Can the function manage it by itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be required, change it right now

| `SERVICE_SID` | Service created in Twilio verify | No |

## Create a new project with the template

Expand Down Expand Up @@ -89,8 +90,12 @@ Besides the enviroment variables files, the project also contain two files calle
| 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 |

`origins.js` contains the origins from where passkeys creation and authentication will be allowed

### Function Parameters

`/registration/service` a POST request, does not expect parameters

`/registration/start` expects the following parameters:

| Parameter | Description | Required |
Expand Down Expand Up @@ -121,4 +126,3 @@ Besides the enviroment variables files, the project also contain two files calle
| 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 |

12 changes: 12 additions & 0 deletions passkeys-backend/assets/origins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const origins = (context) => {
const { DOMAIN_NAME } = context;
const origins = [
`https://${DOMAIN_NAME}`,
'android:apk-key-hash:{base64_hash}',
];
return origins;
};

module.exports = {
origins
};
11 changes: 11 additions & 0 deletions passkeys-backend/functions/.well-known/webauthn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const assets = Runtime.getAssets();
const { origins } = require(assets['/origins.js'].path);

exports.handler = function(context, event, callback) {
const response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');

response.setBody({origins : origins(context)});

return callback(null, response);
};
18 changes: 7 additions & 11 deletions passkeys-backend/functions/authentication/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,28 @@ const axios = require('axios');

// eslint-disable-next-line consistent-return
exports.handler = async (context, _, callback) => {
const { DOMAIN_NAME, API_URL } = context;
const { API_URL, SERVICE_SID } = context;

const response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

const { username, password } = context.getTwilioClient();

const requestBody = {
content: {
// eslint-disable-next-line camelcase
rp_id: DOMAIN_NAME,
},
};

const challengeURL = `${API_URL}/Verifications`;
const challengeURL = `${API_URL}/${SERVICE_SID}/Passkeys/Challenges`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a sense of when these endpoints will be available in the Node Helper Library?


try {
const APIResponse = await axios.post(challengeURL, requestBody, {
const APIResponse = await axios.post(challengeURL, {}, {
auth: {
username,
password,
},
});

response.setStatusCode(200);
response.setBody(APIResponse.data.next_step);
response.setBody(APIResponse.data.options);
} catch (error) {
const statusCode = error.status || 400;
response.setStatusCode(statusCode);
Expand Down
32 changes: 21 additions & 11 deletions passkeys-backend/functions/authentication/verification.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ const assets = Runtime.getAssets();
const { isEmpty } = require(assets['/services/helpers.js'].path);

exports.handler = async (context, event, callback) => {
const { API_URL } = context;
const { API_URL, SERVICE_SID } = context;

const response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

if (isEmpty(event)) {
response.setStatusCode(400);
Expand All @@ -19,17 +22,24 @@ exports.handler = async (context, event, callback) => {

const { username, password } = context.getTwilioClient();

const responseData = event.response
? event.response
: {
clientDataJSON: event.clientDataJSON,
authenticatorData: event.authenticatorData,
signature: event.signature,
userHandle: event.userHandle,
};

const requestBody = {
content: {
rawId: event.rawId,
id: event.id,
authenticatorAttachment: event.authenticatorAttachment,
type: event.type,
response: event.response,
},
};
id: event.id,
rawId: event.rawId,
authenticatorAttachment: event.authenticatorAttachment || 'platform',
type: event.type || 'public-key',
response: responseData,
}

const verifyChallengeURL = `${API_URL}/Verifications/Check`;
const verifyChallengeURL = `${API_URL}/${SERVICE_SID}/Passkeys/ApproveChallenge`;

try {
const APIresponse = await axios.post(verifyChallengeURL, requestBody, {
Expand All @@ -42,7 +52,7 @@ exports.handler = async (context, event, callback) => {
response.setStatusCode(200);
response.setBody({
status: APIresponse.data.status,
identity: APIresponse.data.to.user_identifier,
identity: APIresponse.data.identity
});
} catch (error) {
const statusCode = error.status || 400;
Expand Down
47 changes: 47 additions & 0 deletions passkeys-backend/functions/registration/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const axios = require('axios');
const assets = Runtime.getAssets();
const { origins } = require(assets['/origins.js'].path);

exports.handler = async function(context, event, callback) {
const { DOMAIN_NAME, API_URL } = context;

const response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

const { username, password } = context.getTwilioClient();

const data = new URLSearchParams();
data.append('FriendlyName', 'Passkeys Sample Backend');
data.append('Passkeys.RelyingParty.Id', DOMAIN_NAME);
data.append('Passkeys.RelyingParty.Name', 'Passkeys Sample Backend');
data.append('Passkeys.RelyingParty.Origins', origins(context).join(","));
data.append('Passkeys.AuthenticatorAttachment', 'platform');
data.append('Passkeys.DiscoverableCredentials', 'preferred');
data.append('Passkeys.UserVerification', 'preferred');

const createServiceURL = `${API_URL}`;

try {
const APIResponse = await axios.post(createServiceURL, data, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to include documentation in the README to explain why and how to use this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
auth: {
username,
password,
},
});

response.setStatusCode(200);
response.setBody(APIResponse.data);
} catch (error) {
const statusCode = error.status || 400;
response.setStatusCode(statusCode);
response.setBody(error.message);
}

return callback(null, response)
};
49 changes: 16 additions & 33 deletions passkeys-backend/functions/registration/start.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
const axios = require('axios');
const { v5 } = require('uuid');

const assets = Runtime.getAssets();
const { detectMissingParams } = require(assets['/services/helpers.js'].path);

exports.handler = async (context, event, callback) => {
const { DOMAIN_NAME, API_URL, ANDROID_APP_KEYS } = context;
const { API_URL, SERVICE_SID, NAMESPACE } = context;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the NAMESPACE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed


const response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

// Verify request comes with username
const missingParams = detectMissingParams(['username'], event);
Expand All @@ -22,40 +26,20 @@ exports.handler = async (context, event, callback) => {

const { username, password } = context.getTwilioClient();

const androidOrigins = (keys) => {
if (!keys || keys.trim() === '""') return [];
return keys.split(',');
};
const uuidIdentity = v5(event.username, NAMESPACE)

// Request body sent to passkeys verify URL call
/* eslint-disable camelcase */
const requestBody = {
friendly_name: 'Passkey Example',
to: {
user_identifier: event.username,
},
content: {
relying_party: {
id: DOMAIN_NAME,
name: 'PasskeySample',
origins: [
`https://${DOMAIN_NAME}`,
...androidOrigins(ANDROID_APP_KEYS),
],
},
user: {
display_name: event.username,
},
authenticator_criteria: {
authenticator_attachment: 'platform',
discoverable_credentials: 'preferred',
user_verification: 'preferred',
},
},
};
friendly_name: event.username,
identity: uuidIdentity,
config: {
authenticator_attachment: "platform",
discoverable_credentials: "preferred",
user_verification: "preferred"
}
}

// Factor URL of the passkeys service
const factorURL = `${API_URL}/Factors`;
const factorURL = `${API_URL}/${SERVICE_SID}/Passkeys/Factors`;

// Call made to the passkeys service
try {
Expand All @@ -67,9 +51,8 @@ exports.handler = async (context, event, callback) => {
});

response.setStatusCode(200);
response.setBody(APIResponse.data.next_step);
response.setBody({...APIResponse.data.options.publicKey, "identity": uuidIdentity});
} catch (error) {
console.error('Error in passkeys registration start:', error.message);
const statusCode = error.status || 400;
response.setStatusCode(statusCode);
response.setBody(error.message);
Expand Down
31 changes: 20 additions & 11 deletions passkeys-backend/functions/registration/verification.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ const { isEmpty } = require(assets['/services/helpers.js'].path);

// eslint-disable-next-line consistent-return
exports.handler = async (context, event, callback) => {
const { API_URL } = context;
const { API_URL, SERVICE_SID } = context;

const response = new Twilio.Response();
response.appendHeader('Content-Type', 'application/json');
response.appendHeader('Access-Control-Allow-Origin', '*');
response.appendHeader('Access-Control-Allow-Methods', 'OPTIONS, POST, GET');
response.appendHeader('Access-Control-Allow-Headers', 'Content-Type');

if (isEmpty(event)) {
response.setStatusCode(400);
Expand All @@ -21,17 +24,23 @@ exports.handler = async (context, event, callback) => {

const { username, password } = context.getTwilioClient();

const responseData = event.response
? event.response
: {
attestationObject: event.attestationObject,
clientDataJSON: event.clientDataJSON,
transports: event.transports,
};

const requestBody = {
content: {
id: event.id,
rawId: event.rawId,
authenticatorAttachment: event.authenticatorAttachment,
type: event.type,
response: event.response,
},
};
id: event.id,
rawId: event.rawId,
authenticatorAttachment: event.authenticatorAttachment || 'platform',
type: event.type || 'public-key',
response: responseData,
}

const verifyFactorURL = `${API_URL}/Factors/Approve`;
const verifyFactorURL = `${API_URL}/${SERVICE_SID}/Passkeys/VerifyFactor`;

try {
const APIResponse = await axios.post(verifyFactorURL, requestBody, {
Expand All @@ -50,7 +59,7 @@ exports.handler = async (context, event, callback) => {
} catch (error) {
const statusCode = error.status || 400;
response.setStatusCode(statusCode);
response.setBody(error.message);
response.setBody(error.response.headers);
}

return callback(null, response);
Expand Down
3 changes: 2 additions & 1 deletion passkeys-backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"dependencies": {
"twilio": "^5.3.3",
"axios": "^1.7.7"
"axios": "^1.7.7",
"uuid": "^11.0.4"
}
}
Loading