Simplifying Online Security: My Journey with Passkey Signup — Part-II

Khayrul Alam
4 min readJun 8, 2024

--

PASSKEY APP

Introduction: In the first part of this series, we explored the intricacies of implementing signup functionality using passkeys, known as the Attestation Phase. Now, let’s delve into the next crucial step: the login implementation, or the Assertion Phase. While the signup process ensures secure user registration, the login process is equally critical in maintaining the integrity and security of user access. In this post, we’ll cover the essential components and best practices for implementing a secure and seamless login experience using passkeys.

Application Diagram

Assertion Phase Diagram

Sign-in (Assertion Phase)

To sign in to the application, the user is prompted to provide their email address. Once the user inputs their email address, the application sends this information to the server. The server then verifies the existence of the provided email address in its database. If the email address exists, the server responds with the corresponding user information and a randomly generated challenge.

Sign-in UI Screen
Login Step One

During the login challenge process, the server checks if the user exists based on the provided email address. Once verified, the server responds with the user’s information and credential details, retrieved using a join query from the user and public_key_credential tables, along with the most important value: a randomly generated challenge.

export const LoginChallenge = async (req: Request, res: Response) => {
// check request data
const { email } = req.body;
// check email
if (!email) {
return res.status(400).json({ error: "Email address require" });
}
// get user by email
const user = await getUserInfoByEmail(email);
// check user credential
if (!user.PublicKeyCred?.credentialID) {
return res.status(404).json({ error: "User not found" });
}
// create login challenge
const challenge = await Challenge.create();
// return challenge and user data
return res.json({ challenge, user });
};

The Response looks like this:

Login Challenge Response

After receiving the random challenge, the next step is to generate credentials using the web API: navigator.credentials.get

Step two (navigator credential get API)
export const GetCredential = async (
challenge: string,
credentialID: string
) => {
const challengeBuffer = Base64ToUnit8Array(challenge);
const credentialId = Base64ToUnit8Array(credentialID);
try {
const credential = await navigator.credentials.get({
mediation: "required",
publicKey: {
rpId: RPID,
challenge: challengeBuffer,
allowCredentials: [
{
type: "public-key",
id: credentialId,
transports: ["internal"],
},
],
userVerification: "required",
},
});
return credential as ILoginPublicKeyCredential;
} catch (e) {
console.log("error >>", e);
}
};

This navigator.credentials.get browser API will be prompted to use Touch ID or enter your password.

Touch ID Prompted

The credentials get call returns a promise that resolves with the signature and authenticator data.

Browser Credentials Get API Response

With the credentials response, including the signature and authenticator data, we will call our final API endpoint, verifyChallenge. This endpoint is responsible for verifying the received data against the original challenge and ensuring the authenticity and integrity of the login attempt.

export const VerifyLoginChallenge = async (
challenge: LoginChallengeResponse,
credential: ILoginPublicKeyCredential
) => {
const payload = {
challenge: challenge.challenge,
user: challenge.user,
data: {
id: credential.id,
type: credential.type,
rawId: BufferToBase64Url(credential.rawId),
response: {
signature: BufferToBase64Url(credential.response.signature),
authenticatorData: BufferToBase64Url(
credential.response.authenticatorData
),
clientDataJSON: BufferToBase64Url(credential.response.clientDataJSON),
},
},
};
try {
const response = await axios.post("/verify-login-challenge", payload);
return response.data;
} catch (e) {
console.log("error >>", e);
}
};

Note: Before sending it to the server, convert the array buffer to a base64 string.

Request Payload:

Verify Request Payload

In this process, we utilize the verifyAuthenticationResponse method from the @simplewebauthn/server library. This method is instrumental in verifying the authentication response received from the user, ensuring its validity and integrity before proceeding with the login process.

Server verify method

export const LoginChallengeVerify = async (req: Request, res: Response) => {
const { data, user, challenge } = req.body;
// check request data
if (!data || !user || !challenge) {
return res.status(400).json({ error: "Invalid request" });
}
// verify login challenge
const { verified, authenticationInfo } = await verifyLoginChallenge(
data,
user,
challenge
);
// check if verified
if (!verified) {
return res.status(400).json({ error: "Invalid Authentication" });
}
// return user data
return res.json({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
...authenticationInfo,
});
};

Upon successfully verifying the user’s login challenge, we will respond back to the user to confirm the success of the authentication process.

Login Success Screen

Read: The Registration (Attestation Phase) — Part-I

--

--