Skip to content

Use E.164 format by default for phone numbers #595

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
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
174 changes: 108 additions & 66 deletions passkeys-backend/assets/index.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Passkeys Demo</title>

<link rel="icon" href="https://twilio-labs.github.io/function-templates/static/v1/favicon.ico">
<link rel="stylesheet" href="https://twilio-labs.github.io/function-templates/static/v1/ce-paste-theme.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@19.5.3/build/css/intlTelInput.css">
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@19.5.3/build/js/intlTelInput.min.js"></script>
<link rel="icon" href="https://twilio-labs.github.io/function-templates/static/v1/favicon.ico" />
<link rel="stylesheet" href="https://twilio-labs.github.io/function-templates/static/v1/ce-paste-theme.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@25.3.1/build/css/intlTelInput.css" />
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@25.3.1/build/js/intlTelInput.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@github/webauthn-json@2.1.1/dist/browser-global/webauthn-json.browser-global.min.js"></script>
<style>
body {
Expand All @@ -22,13 +22,15 @@
background-color: rgb(237, 242, 247);
}

#container, #modal, #app {
#container,
#modal,
#app {
max-width: 1280px;
margin: 0 auto;
padding: 4rem;
text-align: center;
border-radius: 10px;
background-color: #FFFFFF;
background-color: #ffffff;
box-shadow: 100px 100px 69px -29px rgba(0, 0, 0, 0.07);
}

Expand All @@ -46,6 +48,7 @@
border-radius: 4px;
padding: 0.8rem;
font-size: 16px;
padding-left: 36px !important;
}

.invisible {
Expand All @@ -54,7 +57,7 @@

.iti {
display: flex;
gap: 10px
gap: 10px;
}

.iti__arrow {
Expand All @@ -69,7 +72,7 @@
}

.input_container .iti__selected-flag {
background-color: #FFFFFF;
background-color: #ffffff;
}

.input_container {
Expand All @@ -91,14 +94,13 @@

.input_component > span {
font-size: 14px;
color: #D04848;
color: #d04848;
}

.hide {
visibility: hidden;
}


.btn {
padding: 15px 0;
border-radius: 50px;
Expand All @@ -108,32 +110,33 @@

.continue_btn {
border-width: 0;
color: #FFFFFF;
color: #ffffff;
background-color: rgb(205, 210, 216);
}

.enable {
background-color:rgb(2, 99, 224);
background-color: rgb(2, 99, 224);
}

.skip_btn {
margin: 20px 0 0 0;
}

a, button {
a,
button {
cursor: pointer;
}

.separator {
color: #7F8487;
color: #7f8487;
font-size: 10px;
}

.passkey_btn {
border-width: 1px;
border-color: rgb(2, 99, 224);
color: rgb(2, 99, 224);
background-color: #FFFFFF;
background-color: #ffffff;
border-style: solid;
}
</style>
Expand All @@ -143,143 +146,182 @@
<h1 class="title">Sign up or sign in</h1>
<div class="input_container">
<div class="input_component">
<input type="tel" name="username_input" id="usr_input" oninput="checkAvalibility()">
<input
type="tel"
name="username_input"
id="usr_input"
oninput="checkAvalibility()"
/>
</div>
<button type="button" class="btn continue_btn" id="continue" onclick="login()" disabled>Continue</button>
<button
type="button"
class="btn continue_btn"
id="continue"
onclick="login()"
disabled
>
Continue
</button>
<span class="separator">&#8213; or &#8213;</span>
<button type="button" class="btn passkey_btn" onclick="signIn()">Sign in with passkey</button>
<button type="button" class="btn passkey_btn" onclick="signIn()">
Sign in with passkey
</button>
</div>
</div>
<div id="modal" class="invisible">
<h1 class="title">Sign-in with your face,<br> fingerprint or PIN</h1>
<p>Harness your device capabilities for a fast<br> passkey login with maximun security.</p>
<h1 class="title">
Sign-in with your face,<br />
fingerprint or PIN
</h1>
<p>
Harness your device capabilities for a fast<br />
passkey login with maximun security.
</p>
<a>Learn more &rarr;</a>
<div class="input_container modal_input">
<button class="btn continue_btn enable" onclick="signUp()">Continue</button>
<button class="btn continue_btn enable" onclick="signUp()">
Continue
</button>
<a class="skip_btn">Skip</a>
</div>
</div>
<div id="app" class="invisible">
<h1 class="title" id="welcome"></h1>
<div class="input_container">
<button class="btn continue_btn enable" onclick="logOut()">Log out</button>
<button class="btn continue_btn enable" onclick="logOut()">
Log out
</button>
</div>
</div>
</body>
<script>

let sessionUsername = sessionStorage.getItem("session");

const loadApp = (username) => {
document.getElementById("welcome").innerHTML = `Welcome ${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) {
loadApp(sessionUsername)
loadApp(sessionUsername);
}

const usernameElement = document.getElementById("usr_input");
const errorElement = document.getElementById("error");
const continueButton = document.getElementById("continue");
window.intlTelInput(usernameElement, {
const iti = window.intlTelInput(usernameElement, {
initialCountry: "us",
showSelectedDialCode: true,
utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@19.5.3/build/js/utils.js",
loadUtils: () =>
import(
"https://cdn.jsdelivr.net/npm/intl-tel-input@25.3.1/build/js/utils.js"
),
});

const checkAvalibility = () => {
const username = usernameElement.value
if(username) {
const username = usernameElement.value;
if (username) {
continueButton.classList.add("enable");
continueButton.disabled = false;
} else {
continueButton.classList.remove("enable");
continueButton.disabled = true;
}
}
};

const login = () => {
const authenticationCard = document.getElementById("container");
const passkeyCard = document.getElementById("modal");
authenticationCard.classList.add("invisible");
passkeyCard.classList.remove("invisible");
}
};

const signIn = async () => {
try {
const response = await fetch(`./authentication/start`);
const responseJSON = await response.json();

window.webauthnJSON.get(responseJSON)
window.webauthnJSON
.get(responseJSON)
.then(async (publicKeyCredential) => {

const authentication = await fetch('./authentication/verification', {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(publicKeyCredential)
});
const authentication = await fetch(
"./authentication/verification",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(publicKeyCredential),
}
);

const { status, identity } = await authentication.json();
if(status === "approved") {
sessionStorage.setItem('session', identity);
if (status === "approved") {
sessionStorage.setItem("session", identity);
loadApp(identity);
} else {
console.log(status);
}
})
.catch((err) => {
if(err) {
console.error("Something goes wrong or maybe you dont have a passkey for this application yet");
if (err) {
console.error(
"Something went wrong - try registering a new passkey for this application."
);
}
});
} catch (error) {
console.log(err);
}
}
};

const signUp = async () => {
const username = usernameElement.value
const username = iti.getNumber(
window.intlTelInput.utils.numberFormat.E164
);

try {
const response = await fetch(`./registration/start`, {
method: "POST",
headers: {
"Content-Type": "application/json"
"Content-Type": "application/json",
},
body: JSON.stringify({ username })
body: JSON.stringify({ username }),
});

const responseJSON = await response.json();

let credential = await window.webauthnJSON.create({publicKey: responseJSON});

const verificationResponse = await fetch(`./registration/verification`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(credential)
let credential = await window.webauthnJSON.create({
publicKey: responseJSON,
});

const verificationResponse = await fetch(
`./registration/verification`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credential),
}
);

const { status } = await verificationResponse.json();
if (status === 'verified') {
sessionStorage.setItem('session', username);
if (status === "verified") {
sessionStorage.setItem("session", username);
loadApp(username);
}
} catch (error) {
console.log(error);
}
}
};

const logOut = () => {
sessionStorage.removeItem("session");
document.getElementById("app").classList.add("invisible");
document.getElementById("container").classList.remove("invisible");
}
};
</script>
</html>
1 change: 1 addition & 0 deletions passkeys-backend/functions/registration/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ exports.handler = async (context, event, callback) => {
response.setStatusCode(200);
response.setBody(APIResponse.data.next_step);
} 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
30 changes: 30 additions & 0 deletions passkeys-backend/tests/registration-start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,36 @@ describe('registration/start', () => {
handlerFunction(mockContext, { username: 'user001' }, callback);
});

it('works with a phone number as a username', (done) => {
const modifiedBody = structuredClone(mockRequestBody);
modifiedBody.to.user_identifier = '+14151234567';
modifiedBody.content.user.display_name = '+14151234567';

const callback = (_, { _body }) => {
expect(axios.post).toHaveBeenCalledWith(
'https://api.com/Factors',
modifiedBody,
{ auth: { password: 'mockPassword', username: 'mockUsername' } }
);
done();
};

const mockContextWithoutAndroidKeys = {
API_URL: 'https://api.com',
DOMAIN_NAME: 'example.com',
getTwilioClient: () => ({
username: 'mockUsername',
password: 'mockPassword',
}),
};

handlerFunction(
mockContextWithoutAndroidKeys,
{ username: '+14151234567' },
callback
);
});

it('works with empty ANDROID_APP_KEYS', (done) => {
const callback = (_, { _body }) => {
expect(axios.post).toHaveBeenCalledWith(
Expand Down