Skip to content

Commit c5bccee

Browse files
authored
Merge pull request #595 from twilio-labs/passkeys-tel-input
Use E.164 format by default for phone numbers
2 parents 7547fc4 + d6f47f1 commit c5bccee

File tree

3 files changed

+139
-66
lines changed

3 files changed

+139
-66
lines changed

passkeys-backend/assets/index.html

Lines changed: 108 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<meta charset="utf-8">
5-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<meta http-equiv="x-ua-compatible" content="ie=edge">
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<meta http-equiv="x-ua-compatible" content="ie=edge" />
77
<title>Passkeys Demo</title>
88

9-
<link rel="icon" href="https://twilio-labs.github.io/function-templates/static/v1/favicon.ico">
10-
<link rel="stylesheet" href="https://twilio-labs.github.io/function-templates/static/v1/ce-paste-theme.css">
11-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@19.5.3/build/css/intlTelInput.css">
12-
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@19.5.3/build/js/intlTelInput.min.js"></script>
9+
<link rel="icon" href="https://twilio-labs.github.io/function-templates/static/v1/favicon.ico" />
10+
<link rel="stylesheet" href="https://twilio-labs.github.io/function-templates/static/v1/ce-paste-theme.css" />
11+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@25.3.1/build/css/intlTelInput.css" />
12+
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@25.3.1/build/js/intlTelInput.min.js"></script>
1313
<script src="https://cdn.jsdelivr.net/npm/@github/[email protected]/dist/browser-global/webauthn-json.browser-global.min.js"></script>
1414
<style>
1515
body {
@@ -22,13 +22,15 @@
2222
background-color: rgb(237, 242, 247);
2323
}
2424

25-
#container, #modal, #app {
25+
#container,
26+
#modal,
27+
#app {
2628
max-width: 1280px;
2729
margin: 0 auto;
2830
padding: 4rem;
2931
text-align: center;
3032
border-radius: 10px;
31-
background-color: #FFFFFF;
33+
background-color: #ffffff;
3234
box-shadow: 100px 100px 69px -29px rgba(0, 0, 0, 0.07);
3335
}
3436

@@ -46,6 +48,7 @@
4648
border-radius: 4px;
4749
padding: 0.8rem;
4850
font-size: 16px;
51+
padding-left: 36px !important;
4952
}
5053

5154
.invisible {
@@ -54,7 +57,7 @@
5457

5558
.iti {
5659
display: flex;
57-
gap: 10px
60+
gap: 10px;
5861
}
5962

6063
.iti__arrow {
@@ -69,7 +72,7 @@
6972
}
7073

7174
.input_container .iti__selected-flag {
72-
background-color: #FFFFFF;
75+
background-color: #ffffff;
7376
}
7477

7578
.input_container {
@@ -91,14 +94,13 @@
9194

9295
.input_component > span {
9396
font-size: 14px;
94-
color: #D04848;
97+
color: #d04848;
9598
}
9699

97100
.hide {
98101
visibility: hidden;
99102
}
100103

101-
102104
.btn {
103105
padding: 15px 0;
104106
border-radius: 50px;
@@ -108,32 +110,33 @@
108110

109111
.continue_btn {
110112
border-width: 0;
111-
color: #FFFFFF;
113+
color: #ffffff;
112114
background-color: rgb(205, 210, 216);
113115
}
114116

115117
.enable {
116-
background-color:rgb(2, 99, 224);
118+
background-color: rgb(2, 99, 224);
117119
}
118120

119121
.skip_btn {
120122
margin: 20px 0 0 0;
121123
}
122124

123-
a, button {
125+
a,
126+
button {
124127
cursor: pointer;
125128
}
126129

127130
.separator {
128-
color: #7F8487;
131+
color: #7f8487;
129132
font-size: 10px;
130133
}
131134

132135
.passkey_btn {
133136
border-width: 1px;
134137
border-color: rgb(2, 99, 224);
135138
color: rgb(2, 99, 224);
136-
background-color: #FFFFFF;
139+
background-color: #ffffff;
137140
border-style: solid;
138141
}
139142
</style>
@@ -143,143 +146,182 @@
143146
<h1 class="title">Sign up or sign in</h1>
144147
<div class="input_container">
145148
<div class="input_component">
146-
<input type="tel" name="username_input" id="usr_input" oninput="checkAvalibility()">
149+
<input
150+
type="tel"
151+
name="username_input"
152+
id="usr_input"
153+
oninput="checkAvalibility()"
154+
/>
147155
</div>
148-
<button type="button" class="btn continue_btn" id="continue" onclick="login()" disabled>Continue</button>
156+
<button
157+
type="button"
158+
class="btn continue_btn"
159+
id="continue"
160+
onclick="login()"
161+
disabled
162+
>
163+
Continue
164+
</button>
149165
<span class="separator">&#8213; or &#8213;</span>
150-
<button type="button" class="btn passkey_btn" onclick="signIn()">Sign in with passkey</button>
166+
<button type="button" class="btn passkey_btn" onclick="signIn()">
167+
Sign in with passkey
168+
</button>
151169
</div>
152170
</div>
153171
<div id="modal" class="invisible">
154-
<h1 class="title">Sign-in with your face,<br> fingerprint or PIN</h1>
155-
<p>Harness your device capabilities for a fast<br> passkey login with maximun security.</p>
172+
<h1 class="title">
173+
Sign-in with your face,<br />
174+
fingerprint or PIN
175+
</h1>
176+
<p>
177+
Harness your device capabilities for a fast<br />
178+
passkey login with maximun security.
179+
</p>
156180
<a>Learn more &rarr;</a>
157181
<div class="input_container modal_input">
158-
<button class="btn continue_btn enable" onclick="signUp()">Continue</button>
182+
<button class="btn continue_btn enable" onclick="signUp()">
183+
Continue
184+
</button>
159185
<a class="skip_btn">Skip</a>
160186
</div>
161187
</div>
162188
<div id="app" class="invisible">
163189
<h1 class="title" id="welcome"></h1>
164190
<div class="input_container">
165-
<button class="btn continue_btn enable" onclick="logOut()">Log out</button>
191+
<button class="btn continue_btn enable" onclick="logOut()">
192+
Log out
193+
</button>
166194
</div>
167195
</div>
168196
</body>
169197
<script>
170-
171198
let sessionUsername = sessionStorage.getItem("session");
172199

173200
const loadApp = (username) => {
174-
document.getElementById("welcome").innerHTML = `Welcome ${username}`
201+
document.getElementById("welcome").innerHTML = `Welcome ${username}`;
175202
document.getElementById("modal").classList.add("invisible");
176203
document.getElementById("container").classList.add("invisible");
177204
document.getElementById("app").classList.remove("invisible");
178-
}
205+
};
179206

180207
if (sessionUsername) {
181-
loadApp(sessionUsername)
208+
loadApp(sessionUsername);
182209
}
183-
210+
184211
const usernameElement = document.getElementById("usr_input");
185212
const errorElement = document.getElementById("error");
186213
const continueButton = document.getElementById("continue");
187-
window.intlTelInput(usernameElement, {
214+
const iti = window.intlTelInput(usernameElement, {
188215
initialCountry: "us",
189216
showSelectedDialCode: true,
190-
utilsScript: "https://cdn.jsdelivr.net/npm/[email protected]/build/js/utils.js",
217+
loadUtils: () =>
218+
import(
219+
"https://cdn.jsdelivr.net/npm/[email protected]/build/js/utils.js"
220+
),
191221
});
192222

193223
const checkAvalibility = () => {
194-
const username = usernameElement.value
195-
if(username) {
224+
const username = usernameElement.value;
225+
if (username) {
196226
continueButton.classList.add("enable");
197227
continueButton.disabled = false;
198228
} else {
199229
continueButton.classList.remove("enable");
200230
continueButton.disabled = true;
201231
}
202-
}
232+
};
203233

204234
const login = () => {
205235
const authenticationCard = document.getElementById("container");
206236
const passkeyCard = document.getElementById("modal");
207237
authenticationCard.classList.add("invisible");
208238
passkeyCard.classList.remove("invisible");
209-
}
239+
};
210240

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

216-
window.webauthnJSON.get(responseJSON)
246+
window.webauthnJSON
247+
.get(responseJSON)
217248
.then(async (publicKeyCredential) => {
218-
219-
const authentication = await fetch('./authentication/verification', {
220-
method: "POST",
221-
headers: {
222-
"Content-Type": "application/json"
223-
},
224-
body: JSON.stringify(publicKeyCredential)
225-
});
249+
const authentication = await fetch(
250+
"./authentication/verification",
251+
{
252+
method: "POST",
253+
headers: {
254+
"Content-Type": "application/json",
255+
},
256+
body: JSON.stringify(publicKeyCredential),
257+
}
258+
);
226259

227260
const { status, identity } = await authentication.json();
228-
if(status === "approved") {
229-
sessionStorage.setItem('session', identity);
261+
if (status === "approved") {
262+
sessionStorage.setItem("session", identity);
230263
loadApp(identity);
231264
} else {
232265
console.log(status);
233266
}
234267
})
235268
.catch((err) => {
236-
if(err) {
237-
console.error("Something goes wrong or maybe you dont have a passkey for this application yet");
269+
if (err) {
270+
console.error(
271+
"Something went wrong - try registering a new passkey for this application."
272+
);
238273
}
239274
});
240275
} catch (error) {
241276
console.log(err);
242277
}
243-
}
244-
278+
};
279+
245280
const signUp = async () => {
246-
const username = usernameElement.value
281+
const username = iti.getNumber(
282+
window.intlTelInput.utils.numberFormat.E164
283+
);
247284

248285
try {
249286
const response = await fetch(`./registration/start`, {
250287
method: "POST",
251288
headers: {
252-
"Content-Type": "application/json"
289+
"Content-Type": "application/json",
253290
},
254-
body: JSON.stringify({ username })
291+
body: JSON.stringify({ username }),
255292
});
256293

257294
const responseJSON = await response.json();
258-
259-
let credential = await window.webauthnJSON.create({publicKey: responseJSON});
260295

261-
const verificationResponse = await fetch(`./registration/verification`, {
262-
method: "POST",
263-
headers: {
264-
"Content-Type": "application/json"
265-
},
266-
body: JSON.stringify(credential)
296+
let credential = await window.webauthnJSON.create({
297+
publicKey: responseJSON,
267298
});
268299

300+
const verificationResponse = await fetch(
301+
`./registration/verification`,
302+
{
303+
method: "POST",
304+
headers: {
305+
"Content-Type": "application/json",
306+
},
307+
body: JSON.stringify(credential),
308+
}
309+
);
310+
269311
const { status } = await verificationResponse.json();
270-
if (status === 'verified') {
271-
sessionStorage.setItem('session', username);
312+
if (status === "verified") {
313+
sessionStorage.setItem("session", username);
272314
loadApp(username);
273315
}
274316
} catch (error) {
275317
console.log(error);
276318
}
277-
}
319+
};
278320

279321
const logOut = () => {
280322
sessionStorage.removeItem("session");
281323
document.getElementById("app").classList.add("invisible");
282324
document.getElementById("container").classList.remove("invisible");
283-
}
325+
};
284326
</script>
285327
</html>

passkeys-backend/functions/registration/start.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ exports.handler = async (context, event, callback) => {
6969
response.setStatusCode(200);
7070
response.setBody(APIResponse.data.next_step);
7171
} catch (error) {
72+
console.error('Error in passkeys registration start:', error.message);
7273
const statusCode = error.status || 400;
7374
response.setStatusCode(statusCode);
7475
response.setBody(error.message);

passkeys-backend/tests/registration-start.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,36 @@ describe('registration/start', () => {
8181
handlerFunction(mockContext, { username: 'user001' }, callback);
8282
});
8383

84+
it('works with a phone number as a username', (done) => {
85+
const modifiedBody = structuredClone(mockRequestBody);
86+
modifiedBody.to.user_identifier = '+14151234567';
87+
modifiedBody.content.user.display_name = '+14151234567';
88+
89+
const callback = (_, { _body }) => {
90+
expect(axios.post).toHaveBeenCalledWith(
91+
'https://api.com/Factors',
92+
modifiedBody,
93+
{ auth: { password: 'mockPassword', username: 'mockUsername' } }
94+
);
95+
done();
96+
};
97+
98+
const mockContextWithoutAndroidKeys = {
99+
API_URL: 'https://api.com',
100+
DOMAIN_NAME: 'example.com',
101+
getTwilioClient: () => ({
102+
username: 'mockUsername',
103+
password: 'mockPassword',
104+
}),
105+
};
106+
107+
handlerFunction(
108+
mockContextWithoutAndroidKeys,
109+
{ username: '+14151234567' },
110+
callback
111+
);
112+
});
113+
84114
it('works with empty ANDROID_APP_KEYS', (done) => {
85115
const callback = (_, { _body }) => {
86116
expect(axios.post).toHaveBeenCalledWith(

0 commit comments

Comments
 (0)