1. Before you begin
The Web Authentication API, also known as WebAuthn, lets you create and use origin-scoped, public-key credentials to authenticate users.
The API supports the use of BLE, NFC, and USB-roaming U2F or FIDO2 authenticators—also known as security keys—as well as a platform authenticator, which lets users authenticate with their fingerprints or screen locks.
In this codelab, you build a website with a simple reauthentication functionality that uses a fingerprint sensor. Reauthentication protects account data because it requires users who already signed in to a website to authenticate again when they try to enter important sections of the website or revisit the website after a certain amount of time.
Prerequisites
- Basic understanding of how WebAuthn works
- Basic programming skills with JavaScript
What you'll do
- Build a website with a simple reauthentication functionality that uses a fingerprint sensor
What you'll need
- One of the following devices:
- An Android device, preferably with a biometric sensor
- An iPhone or iPad with Touch ID or Face ID on iOS 14 or higher
- A MacBook Pro or Air with Touch ID on macOS Big Sur or higher
- Windows 10 19H1 or higher with Windows Hello set up
- One of the following browsers:
- Google Chrome 67 or higher
- Microsoft Edge 85 or higher
- Safari 14 or higher
2. Get set up
In this codelab, you use a service called glitch . This is where you can edit client and server-side code with JavaScript, and deploy them instantly.
Navigate to https://glitch.com/edit/#!/webauthn-codelab-start .
See how it works
Follow these steps to see the initial state of the website:
- Click
Show>
In a New Windowto see the live website .
- Enter a username of your choice and click Next.
- Enter a password and click Sign-in.
The password is ignored, but you're still authenticated. You land at the home page.
- Click Try reauth, and repeat the second, third, and fourth steps.
- Click Sign out.
Notice that you must enter the password every time that you try to sign in. This emulates a user who needs to reauthenticate before they can access an important section of a website.
Remix the code
- Navigate to WebAuthn / FIDO2 API Codelab .
- Click the name of your project > Remix Project
to fork the project and continue with your own version at a new URL.
3. Register a credential with a fingerprint
You need to register a credential generated by a UVPA, an authenticator that is built into the device and verifies the user's identity. This is typically seen as a fingerprint sensor depending on the user's device.
You add this feature to the /home
page:
Create registerCredential()
function
Create a registerCredential()
function, which registers a new credential.
public/client.js
export
const
registerCredential
=
async
()
=
>
{
};
Obtain the challenge and other options from server endpoint
Before you ask the user to register a new credential, request that the server return parameters to pass in WebAuthn, including a challenge. Luckily, you already have a server endpoint that responds with such parameters.
Add the following code to registerCredential()
.
public/client.js
const
opts
=
{
attestation
:
'none'
,
authenticatorSelection
:
{
authenticatorAttachment
:
'platform'
,
userVerification
:
'required'
,
requireResidentKey
:
false
}
};
const
options
=
await
_fetch
(
'/auth/registerRequest'
,
opts
);
The protocol between a server and a client is not a part of the WebAuthn specification. However, this codelab is designed to align with the WebAuthn specification and the JSON object that you pass to the server is very similar to PublicKeyCredentialCreationOptions
so that it's intuitive for you. The following table contains the important parameters that you can pass to the server and explains what they do:
Parameters
Descriptions
attestation
Preference for attestation conveyance— none
, indirect
, or direct
. Choose none
unless you need one.
excludeCredentials
Array of PublicKeyCredentialDescriptor
so that the authenticator can avoid creating duplicate ones.
authenticatorSelection
authenticatorAttachment
Filter available authenticators. If you want an authenticator attached to the device, use " platform
". For roaming authenticators, use " cross-platform
".
userVerification
Determine whether authenticator local user verification is " required
", " preferred
", or " discouraged
". If you want fingerprint or screen-lock authentication, use " required
".
requireResidentKey
Use true
if the created credential should be available for future account picker UX.
To learn more about these options, see 5.4. Options for Credential Creation (dictionary PublicKeyCredentialCreationOptions
)
.
The following are example options that you receive from the server.
{
"rp"
:
{
"name"
:
"WebAuthn Codelab"
,
"id"
:
"webauthn-codelab.glitch.me"
},
"user"
:
{
"displayName"
:
"User Name"
,
"id"
:
"..."
,
"name"
:
"test"
},
"challenge"
:
"..."
,
"pubKeyCredParams"
:
[
{
"type"
:
"public-key"
,
"alg"
:
-
7
},
{
"type"
:
"public-key"
,
"alg"
:
-
257
}
],
"timeout"
:
1800000
,
"attestation"
:
"none"
,
"excludeCredentials"
:
[
{
"id"
:
"..."
,
"type"
:
"public-key"
,
"transports"
:
[
"internal"
]
}
],
"authenticatorSelection"
:
{
"authenticatorAttachment"
:
"platform"
,
"userVerification"
:
"required"
}
}
Create a credential
- Because these options are delivered encoded to go through HTTP protocol, convert some parameters back to binary, specifically,
user.id
,challenge
and instances ofid
included in theexcludeCredentials
array:
public/client.js
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
- Call the
navigator.credentials.create()
method to create a new credential.
With this call, the browser interacts with the authenticator and tries to verify the user's identity with the UVPA.
public/client.js
const
cred
=
await
navigator
.
credentials
.
create
({
publicKey
:
options
,
});
Once the user verifies their identity, you should receive a credential object that you can send to the server and register the authenticator.
Register the credential to the server endpoint
Here's an example credential object that you should have received.
{
"id"
:
"..."
,
"rawId"
:
"..."
,
"type"
:
"public-key"
,
"response"
:
{
"clientDataJSON"
:
"..."
,
"attestationObject"
:
"..."
}
}
- Like when you received an option object for registering a credential, encode the binary parameters of the credential so that it can be delivered to the server as a string:
public/client.js
const
credential
=
{};
credential
.
id
=
cred
.
id
;
credential
.
rawId
=
base64url
.
encode
(
cred
.
rawId
);
credential
.
type
=
cred
.
type
;
if
(
cred
.
response
)
{
const
clientDataJSON
=
base64url
.
encode
(
cred
.
response
.
clientDataJSON
);
const
attestationObject
=
base64url
.
encode
(
cred
.
response
.
attestationObject
);
credential
.
response
=
{
clientDataJSON
,
attestationObject
,
};
}
- Store the credential ID locally so that you can use it for authentication when the user comes back:
public/client.js
localStorage
.
set
Item
(
`credId`
,
credential
.
id
);
- Send the object to the server and, if it returns
HTTP code 200
, consider the new credential as successfully registered.
public/client.js
return
await
_fetch
('
/
auth
/
registerResponse
'
,
credential
);
You now have the complete registerCredential()
function!
Final code for this section
public/client.js
...
export
const
registerCredential
=
async
()
=
>
{
const
opts
=
{
attestation
:
'none'
,
authenticatorSelection
:
{
authenticatorAttachment
:
'platform'
,
userVerification
:
'required'
,
requireResidentKey
:
false
}
}
;
const
options
=
await
_fetch
(
'/auth/registerRequest'
,
opts
);
options
.
user
.
id
=
base64url
.
decode
(
options
.
user
.
id
);
options
.
challenge
=
base64url
.
decode
(
options
.
challenge
);
if
(
options
.
excludeCredentials
)
{
for
(
let
cred
of
options
.
excludeCredentials
)
{
cred
.
id
=
base64url
.
decode
(
cred
.
id
);
}
}
const
cred
=
await
navigator
.
credentials
.
create
(
{
publicKey
:
options
}
);
const
credential
=
{}
;
credential
.
id
=
cred
.
id
;
credential
.
rawId
=
base64url
.
encode
(
cred
.
rawId
);
credential
.
type
=
cred
.
type
;
if
(
cred
.
response
)
{
const
clientDataJSON
=
base64url
.
encode
(
cred
.
response
.
clientDataJSON
);
const
attestationObject
=
base64url
.
encode
(
cred
.
response
.
attestationObject
);
credential
.
response
=
{
clientDataJSON
,
attestationObject
}
;
}
localStorage
.
set
Item
(
`credId`
,
credential
.
id
);
return
await
_fetch
(
'/auth/registerResponse'
,
credential
);
}
;
...
4. Build the UI to register, get, and remove credentials
It's nice to have a list of registered credentials and buttons to remove them.
Build UI placeholder
Add UI to list credentials and a button to register a new credential. Depending on whether the feature is available or not, you remove the hidden
class from either the warning message or the button to register a new credential. ul#list
is the placeholder for adding a list of registered credentials.
views/home.html
< p
id
=
"uvpa_unavailable"
class
=
"hidden"
>
This
device
does
not
support
User
Verifying
Platform
Authenticator
.
You
can
'
t
register
a
credential
.
< /
p
>
< h3
class
=
"mdc-typography mdc-typography--headline6"
>
Your
registered
credentials:
< /
h3
>
< section
>
< div
id
=
"list"
>< /
div
>
< /
section
>
< mwc
-
button
id
=
"register"
class
=
"hidden"
icon
=
"fingerprint"
raised>Add
a
credential
< /
mwc
-
button
>
Feature detection and UVPA availability
Follow these steps to check the UVPA availability:
- Examine
window.PublicKeyCredential
to check if WebAuthn is available. - Call
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
to check if a UVPA is available . If they're available, you show the button to register a new credential. If either of them are not available, you show the warning message.
views/home.html
const
register
=
document
.
querySelector
(
'#register'
);
if
(
window
.
PublicKeyCredential
)
{
PublicKeyCredential
.
isUserVerifyingPlatformAuthenticatorAvailable
()
.
then
(
uvpaa
=
>
{
if
(
uvpaa
)
{
register
.
classList
.
remove
(
'hidden'
);
}
else
{
document
.
querySelector
(
'#uvpa_unavailable'
)
.
classList
.
remove
(
'hidden'
);
}
});
}
else
{
document
.
querySelector
(
'#uvpa_unavailable'
)
.
classList
.
remove
(
'hidden'
);
}
Get and display a list of credentials
- Create a
getCredentials()
function so that you can get registered credentials and display them in a list. Luckily, you already have a handy endpoint on the server/auth/getKeys
from which you can fetch registered credentials for the signed-in user.
The returned JSON includes credential information, such as id
and publicKey
. You can build HTML to show them to the user.
views/home.html
const
getCredentials
=
async
()
=
>
{
const
res
=
await
_fetch
(
'/auth/getKeys'
);
const
list
=
document
.
querySelector
(
'#list'
);
const
creds
=
html
`
$
{
res
.
credentials
.
length
>
0
?
res
.
credentials
.
map
(
cred
=
>
html
`
< div
class
=
"mdc-card credential"
>
< span
class
=
"mdc-typography mdc-typography--body2"
> $
{
cred
.
credId
}
< /
span
>
< pre
class
=
"public-key"
> $
{
cred
.
publicKey
}
< /
pre
>
< div
class
=
"mdc-card__actions"
>
< mwc
-
button
id
=
"${cred.credId}"
@
click
=
"${removeCredential}"
raised>Remove
< /
mwc
-
button
>
< /
div
>
< /
div
> `
)
:
html
`
< p>No
credentials
found
.</
p
>
`
}
`
;
render
(
creds
,
list
);
};
- Invoke
getCredentials()
to display available credentials as soon as the user lands on the/home
page.
views/home.html
getCredentials();
Remove the credential
In the list of credentials, you added a button to remove each credential. You can send a request to /auth/removeKey
along with the credId
query parameter to remove them.
public/client.js
export
const
unregisterCredential
=
async
(
credId
)
=
>
{
localStorage
.
removeItem
(
'credId'
);
return
_fetch
(
`
/
auth
/
removeKey
?
credId
=$
{
encodeURIComponent
(
credId
)}
`
);
};
- Append
unregisterCredential
to the existingimport
statement.
views/home.html
import
{
_fetch
,
unregisterCredential
}
from
'/client.js'
;
- Add a function to call when the user clicks Remove.
views/home.html
const
removeCredential
=
async
e
=
>
{
try
{
await
unregisterCredential
(
e
.
target
.
id
);
getCredentials
();
}
catch
(
e
)
{
alert
(
e
);
}
};
Register a credential
You can call registerCredential()
to register a new credential when the user clicks Add a credential.
- Append
registerCredential
to the existingimport
statement.
views/home.html
import
{
_fetch
,
registerCredential
,
unregisterCredential
}
from
'/client.js'
;
- Invoke
registerCredential()
with options fornavigator.credentials.create()
.
Don't forget to renew the credential list by calling getCredentials()
after registration.
views/home.html
register
.
addEventListener
('
click
',
e
=
>
{
registerCredential
().
then
(
user
=
>
{
getCredentials
();
}).
catch
(
e
=
>
alert
(
e
));
});
Now you should be able to register a new credential and display information about it. You may try it on your live website.
Final code for this section
views/home.html
...
< p
id
=
"uvpa_unavailable"
class
=
"hidden"
> This
device
does
not
support
User
Verifying
Platform
Authenticator
.
You
can
't register a credential.
< /
p
>
< h3
class
=
"mdc-typography mdc-typography--headline6"
> Your
registered
credentials
:
< /
h3
>
< section
>
< div
id
=
"list"
>< /
div
>
< mwc
-
fab
id
=
"register"
class
=
"hidden"
icon
=
"add"
>< /
mwc
-
fab
>
< /
section
>
< mwc
-
button
raised><a
href
=
"/reauth"
> Try
reauth
< /
a
>< /
mwc
-
button
>
< mwc
-
button><a
href
=
"/auth/signout"
> Sign
out
< /
a
>< /
mwc
-
button
>
< /
main
>
< script
type
=
"module"
> import
{
_fetch
,
registerCredential
,
unregisterCredential
}
from
'/client.js'
;
import
{
html
,
render
}
from
'https://unpkg.com/lit-html@1.0.0/lit-html.js?module'
;
const
register
=
document
.
querySelector
(
'#register'
);
if
(
window
.
PublicKeyCredential
)
{
PublicKeyCredential
.
isUserVerifyingPlatformAuthenticatorAvailable
()
.
then
(
uvpaa
=
> {
if
(
uvpaa
)
{
register
.
classList
.
remove
(
'hidden'
);
}
else
{
document
.
querySelector
(
'#uvpa_unavailable'
)
.
classList
.
remove
(
'hidden'
);
}
});
}
else
{
document
.
querySelector
(
'#uvpa_unavailable'
)
.
classList
.
remove
(
'hidden'
);
}
const
getCredentials
=
async
()
=
> {
const
res
=
await
_fetch
(
'/auth/getKeys'
);
const
list
=
document
.
querySelector
(
'#list'
);
const
creds
=
html
`$
{
res
.
credentials
.
length
> 0
?
res
.
credentials
.
map
(
cred
=
> html
`
< div
class
=
"mdc-card credential"
>
< span
class
=
"mdc-typography mdc-typography--body2"
> $
{
cred
.
credId
}
< /
span
>
< pre
class
=
"public-key"
> $
{
cred
.
publicKey
}
< /
pre
>
< div
class
=
"mdc-card__actions"
>
< mwc
-
button
id
=
"$
{cred.credId}
"
@click
=
"$
{removeCredential}
"
raised>Remove
< /
mwc
-
button
>
< /
div
>
< /
div
> `
)
:
html
`
< p>No
credentials
found
.</
p
> `
}
`
;
render
(
creds
,
list
);
};
getCredentials
();
const
removeCredential
=
async
e
=
> {
try
{
await
unregisterCredential
(
e
.
target
.
id
);
getCredentials
();
}
catch
(
e
)
{
alert
(
e
);
}
};
register
.
addEventListener
(
'click'
,
e
=
> {
registerCredential
({
attestation
:
'none'
,
authenticatorSelection
:
{
authenticatorAttachment
:
'platform'
,
userVerification
:
'required'
,
requireResidentKey
:
false
}
})
.
then
(
user
=
> {
getCredentials
();
})
.
catch
(
e
=
> alert
(
e
));
});
< /
script
> ...
public/client.js
...
export
const
unregisterCredential
=
async
(
credId
)
=
>
{
localStorage
.
removeItem
(
'credId'
);
return
_fetch
(
`
/
auth
/
removeKey
?
credId
=$
{
encodeURIComponent
(
credId
)}
`
);
};
...
5. Authenticate the user with a fingerprint
You now have a credential registered and ready to use as a way to authenticate the user. Now you add reauthentication functionality to the website. Here's the user experience:
When a user lands on the /reauth
page, they see an Authenticatebutton if biometric authentication is possible. Authentication with a fingerprint (UVPA) starts when they tap Authenticate, successfully authenticate, and then land on the /home
page. If biometric authentication is not available or an authentication with biometric fails, the UI falls back to use the existing password form.
Create authenticate()
function
Create a function called authenticate()
, which verifies the user's identity with a fingerprint. You add JavaScript code here:
public/client.js
export
const
authenticate
=
async
()
=
>
{
};
Obtain the challenge and other options from server endpoint
- Before authentication, examine if the user has a stored credential ID and set it as a query parameter if they do.
When you provide a credential ID along with other options, the server can provide relevant allowCredentials
and this makes user verification reliable.
public/client.js
const
opts
=
{}
;
let
url
=
'/auth/signinRequest'
;
const
credId
=
localStorage
.
getItem
(
`credId`
);
if
(
credId
)
{
url
+=
`?credId=${encodeURIComponent(credId)}`
;
}
- Before you ask the user to authenticate, ask the server to send back a challenge and other parameters. Call
_fetch()
withopts
as an argument to send a POST request to the server.
public/client.js
const
options
=
await
_fetch
(
url
,
opts
);
Here are example options you should receive (aligns with PublicKeyCredentialRequestOptions
).
{
"challenge"
:
"..."
,
"timeout"
:
1800000
,
"rpId"
:
"webauthn-codelab.glitch.me"
,
"userVerification"
:
"required"
,
"allowCredentials"
:
[
{
"id"
:
"..."
,
"type"
:
"public-key"
,
"transports"
:
[
"internal"
]
}
]
}
The most important option here is allowCredentials
. When you receive options from the server, allowCredentials
should be either a single object in an array or an empty array depending on whether a credential with the ID in the query parameter is found on the server side.
- Resolve the promise with
null
whenallowCredentials
is an empty array so that the UI falls back to asking for a password.
if
(
options
.
allowCredentials
.
length
===
0
)
{
console
.
info
('
No
registered
credentials
found
.');
return
Promise
.
resolve
(
null
);
}
Locally verify the user and get a credential
- Because these options are delivered encoded in order to go through HTTP protocol, convert some parameters back to binary, specifically
challenge
and instances ofid
included in theallowCredentials
array:
public/client.js
options.challenge = base64url.decode(options.challenge);
for (let cred of options.allowCredentials) {
cred.id = base64url.decode(cred.id);
}
- Call the
navigator.credentials.get()
method to verify the user's identity with a UVPA.
public/client.js
const
cred
=
await
navigator
.
credentials
.
get
({
publicKey
:
options
});
Once the user verifies their identity, you should receive a credential object that you can send to the server and authenticate the user.
Verify the credential
Here's an example PublicKeyCredential
object ( response
is AuthenticatorAssertionResponse
) that you should have received:
{
"id"
:
"..."
,
"type"
:
"public-key"
,
"rawId"
:
"..."
,
"response"
:
{
"clientDataJSON"
:
"..."
,
"authenticatorData"
:
"..."
,
"signature"
:
"..."
,
"userHandle"
:
""
}
}
- Encode the binary parameters of the credential so that it can be delivered to the server as a string:
public/client.js
const
credential
=
{};
credential
.
id
=
cred
.
id
;
credential
.
type
=
cred
.
type
;
credential
.
rawId
=
base64url
.
encode
(
cred
.
rawId
);
if
(
cred
.
response
)
{
const
clientDataJSON
=
base64url
.
encode
(
cred
.
response
.
clientDataJSON
);
const
authenticatorData
=
base64url
.
encode
(
cred
.
response
.
authenticatorData
);
const
signature
=
base64url
.
encode
(
cred
.
response
.
signature
);
const
userHandle
=
base64url
.
encode
(
cred
.
response
.
userHandle
);
credential
.
response
=
{
clientDataJSON
,
authenticatorData
,
signature
,
userHandle
,
};
}
- Send the object to the server and, if it returns
HTTP code 200
, consider the user as successfully signed in:
public/client.js
return await _fetch(`/auth/signinResponse`, credential);
You now have the complete authentication()
function!
Final code for this section
public/client.js
...
export
const
authenticate
=
async
()
=
>
{
const
opts
=
{}
;
let
url
=
'/auth/signinRequest'
;
const
credId
=
localStorage
.
getItem
(
`credId`
);
if
(
credId
)
{
url
+=
`?credId=${encodeURIComponent(credId)}`
;
}
const
options
=
await
_fetch
(
url
,
opts
);
if
(
options
.
allowCredentials
.
length
===
0
)
{
console
.
info
(
'No registered credentials found.'
);
return
Promise
.
resolve
(
null
);
}
options
.
challenge
=
base64url
.
decode
(
options
.
challenge
);
for
(
let
cred
of
options
.
allowCredentials
)
{
cred
.
id
=
base64url
.
decode
(
cred
.
id
);
}
const
cred
=
await
navigator
.
credentials
.
get
(
{
publicKey
:
options
}
);
const
credential
=
{}
;
credential
.
id
=
cred
.
id
;
credential
.
type
=
cred
.
type
;
credential
.
rawId
=
base64url
.
encode
(
cred
.
rawId
);
if
(
cred
.
response
)
{
const
clientDataJSON
=
base64url
.
encode
(
cred
.
response
.
clientDataJSON
);
const
authenticatorData
=
base64url
.
encode
(
cred
.
response
.
authenticatorData
);
const
signature
=
base64url
.
encode
(
cred
.
response
.
signature
);
const
userHandle
=
base64url
.
encode
(
cred
.
response
.
userHandle
);
credential
.
response
=
{
clientDataJSON
,
authenticatorData
,
signature
,
userHandle
,
}
;
}
return
await
_fetch
(
`/auth/signinResponse`
,
credential
);
}
;
...
6. Enable reauthentication experience
Build UI
When the user comes back, you want them to reauthenticate as easily and securely as possible. This is where biometric authentication shines. However, there are cases in which biometric authentication may not work:
- The UVPA is not available.
- The user has not registered any credentials on their device yet.
- The storage is cleared and the device no longer remembers the credential ID.
- The user is unable to verify their identity for some reason, such as when their finger is wet or they're wearing a mask.
That is why it's always important that you provide other sign-in options as fallbacks. In this codelab, you use the form-based password solution.
- Add UI to show an authentication button that invokes the biometric authentication in addition to the password form.
Use the hidden
class to selectively show and hide one of them depending on the user's state.
views/reauth.html
<div id="uvpa_available" class="hidden">
<h2>
Verify your identity
</h2>
<div>
<mwc-button id="reauth" raised>Authenticate</mwc-button>
</div>
<div>
<mwc-button id="cancel">Sign-in with password</mwc-button>
</div>
</div>
- Append
class="hidden"
to the form:
views/reauth.html
<form id="form" method="POST" action="/auth/password" class="hidden">
Feature detection and UVPA availability
Users must sign in with a password if one of these conditions is met:
- WebAuthn is not available.
- UVPA is not available.
- A credential ID for this UVPA is not discoverable.
Selectively show the authentication button or hide it:
views/reauth.html
if
(
window
.
PublicKeyCredential
)
{
PublicKeyCredential
.
isUserVerifyingPlatformAuthenticatorAvailable
()
.
then
(
uvpaa
=
>
{
if
(
uvpaa
&&
localStorage
.
getItem
(
`credId`
))
{
document
.
querySelector
(
'#uvpa_available'
)
.
classList
.
remove
(
'hidden'
);
}
else
{
form
.
classList
.
remove
(
'hidden'
);
}
}
);
}
else
{
form
.
classList
.
remove
(
'hidden'
);
}
Fallback to password form
The user should also be able to choose to sign in with a password.
Show the password form and hide the authentication button when the user clicks Sign in with password:.
views/reauth.html
const
cancel
=
document
.
querySelector
(
'#cancel'
);
cancel
.
addEventListener
(
'click'
,
e
=
>
{
form
.
classList
.
remove
(
'hidden'
);
document
.
querySelector
(
'#uvpa_available'
)
.
classList
.
add
(
'hidden'
);
});
Invoke the biometric authentication
Finally, enable the biometric authentication.
- Append
authenticate
to the existingimport
statement:
views/reauth.html
import
{
_fetch
,
authenticate
}
from
'/client.js'
;
- Invoke
authenticate()
when the user taps Authenticateto start the biometric authentication.
Make sure that a failure on biometric authentication falls back to the password form.
views/reauth.html
const
button
=
document
.
querySelector
(
'#reauth'
);
button
.
addEventListener
(
'click'
,
e
=
>
{
authenticate
()
.
then
(
user
=
>
{
if
(
user
)
{
location
.
href
=
'/home'
;
}
else
{
throw
'User not found.'
;
}
})
.
catch
(
e
=
>
{
console
.
error
(
e
.
message
||
e
);
alert
(
'Authentication failed. Use password to sign-in.'
);
form
.
classList
.
remove
(
'hidden'
);
document
.
querySelector
(
'#uvpa_available'
)
.
classList
.
add
(
'hidden'
);
});
});
Final code for this section
views/reauth.html
...
< main
class
=
"content"
>
< div
id
=
"uvpa_available"
class
=
"hidden"
>
< h2
>
Verify
your
identity
< /
h2
>
< div
>
< mwc
-
button
id
=
"reauth"
raised>Authenticate
< /
mwc
-
button
>
< /
div
>
< div
>
< mwc
-
button
id
=
"cancel"
> Sign
-
in
with
password
< /
mwc
-
button
>
< /
div
>
< /
div
>
< form
id
=
"form"
method
=
"POST"
action
=
"/auth/password"
class
=
"hidden"
>
< h2
>
Enter
a
password
< /
h2
>
< input
type
=
"hidden"
name
=
"username"
value
=
"{{username}}"
/
>
< div
class
=
"mdc-text-field mdc-text-field--filled"
>
< span
class
=
"mdc-text-field__ripple"
>< /
span
>
< label
class
=
"mdc-floating-label"
id
=
"password-label"
> password
< /
label
>
< input
type
=
"password"
class
=
"mdc-text-field__input"
aria
-
labelledby
=
"password-label"
name
=
"password"
/
>
< span
class
=
"mdc-line-ripple"
>< /
span
>
< /
div
>
< input
type
=
"submit"
class
=
"mdc-button mdc-button--raised"
value
=
"Sign-In"
/
>
< p
class
=
"instructions"
> password
will
be
ignored
in
this
demo
.
< /
p
>
< /
form
>
< /
main
>
< script
src
=
"https://unpkg.com/material-components-web@7.0.0/dist/material-components-web.min.js"
>< /
script
>
< script
type
=
"module"
>
new
mdc
.
textField
.
MDCTextField
(
document
.
querySelector
(
'
.
mdc
-
text
-
field
'
));
import
{
_fetch
,
authenticate
}
from
'
/
client
.
js
'
;
const
form
=
document
.
querySelector
(
'#
form
'
);
form
.
addEventListener
(
'
submit
'
,
e
=
>
{
e
.
preventDefault
();
const
form
=
new
FormData
(
e
.
target
);
const
cred
=
{};
form
.
forEach
((
v
,
k
)
=
>
cred
[
k
]
=
v
);
_fetch
(
e
.
target
.
action
,
cred
)
.
then
(
user
=
>
{
location
.
href
=
'
/
home
'
;
})
.
catch
(
e
=
>
alert
(
e
));
});
if
(
window
.
PublicKeyCredential
)
{
PublicKeyCredential
.
isUserVerifyingPlatformAuthenticatorAvailable
()
.
then
(
uvpaa
=
>
{
if
(
uvpaa
&&
localStorage
.
getItem
(
`
credId
`
))
{
document
.
querySelector
(
'#
uvpa_available
'
)
.
classList
.
remove
(
'
hidden
'
);
}
else
{
form
.
classList
.
remove
(
'
hidden
'
);
}
});
}
else
{
form
.
classList
.
remove
(
'
hidden
'
);
}
const
cancel
=
document
.
querySelector
(
'#
cancel
'
);
cancel
.
addEventListener
(
'
click
'
,
e
=
>
{
form
.
classList
.
remove
(
'
hidden
'
);
document
.
querySelector
(
'#
uvpa_available
'
)
.
classList
.
add
(
'
hidden
'
);
});
const
button
=
document
.
querySelector
(
'#
reauth
'
);
button
.
addEventListener
(
'
click
'
,
e
=
>
{
authenticate
().
then
(
user
=
>
{
if
(
user
)
{
location
.
href
=
'
/
home
'
;
}
else
{
throw
'
User
not
found
.
'
;
}
}).
catch
(
e
=
>
{
console
.
error
(
e
.
message
||
e
);
alert
(
'
Authentication
failed
.
Use
password
to
sign
-
in
.
'
);
form
.
classList
.
remove
(
'
hidden
'
);
document
.
querySelector
(
'#
uvpa_available
'
).
classList
.
add
(
'
hidden
'
);
});
});
< /
script
> ...
7. Congratulations!
You finished this codelab!
Learn more
- Web Authentication: An API for accessing Public Key Credentials Level 1
- Introduction to WebAuthn API
- FIDO WebAuthn Workshop
- WebAuthn Guide: DUOSEC
- Your first Android FIDO2 API
Special thanks to Yuriy Ackermann from FIDO Alliance for your help.