The Passkey API is designed for frontend developers to easily implement and test passkey authentication using the SimpleWebAuthn library. It’s a quick solution for trying out passkey auth with reliable APIs but is not intended for production use.
Under the hood this project uses @simplewebauthn/server package. For implementation on the frontend client sdk is available from the same package.
- nestjs - A Node.js framework for building server-side applications.
- @simplewebauthn/server - For passkey implementation.
- jsonwebtoken - For JWT generation and signing.
Step 1: Clone the repository
git clone https://github.com/yalturkey/passkey-api.git
Step 2: Install dependencies
npm install --legacy-peer-deps
Step 3: Run the application
npm run start
Runs on port 2000.
RP_ID is a short for 'relying party id', it is basically root level FQDN for which the passkey is registered. It is required at the time of challenge generation (for registration & signup) and verification. The RP_ID must match the root frontend domain name from which the passkey requests are originating. For example with RP_ID set as 'acme.com', frontends hosted on any subdomains '*.acme.com' can be used to initiate passkeys.
CLIENT_URL is the full frontend url from which the passkey requests are initiated (including https://).
Passkey API.postman_collection.json
Step 1: Register a new user email
POST
Request Endpoint
POST /api/auth/register?RP_ID=localhost
POST
Request Body
{
"email": "[email protected]"
}
POST
Request Response
{
"success": true,
"status": 200,
"data": {
"challenge": "Bkp7E3xuVEBe8bc-7X2-oF_oDwkU_DHK70PKRu5ubEo",
"rp": {
"name": "acme",
"id": "localhost"
},
"user": {
"id": "aif-X2DcI0pMpkTrvReYj5622eV8B28kQDs-oAq0qH8",
"name": "[email protected]",
"displayName": ""
},
"pubKeyCredParams": [
{
"alg": -8,
"type": "public-key"
},
{
"alg": -7,
"type": "public-key"
},
{
"alg": -257,
"type": "public-key"
}
],
"timeout": 60000,
"attestation": "none",
"excludeCredentials": [],
"authenticatorSelection": {
"residentKey": "preferred",
"userVerification": "preferred",
"requireResidentKey": false
},
"extensions": {
"credProps": true
}
}
}
Step 2: Pass the response data to the @simplewebauthn/browser
client sdk
import { startRegistration } from '@simplewebauthn/browser'
const optionsJSON = {
challenge: 'Bkp7E3xuVEBe8bc-7X2-oF_oDwkU_DHK70PKRu5ubEo',
rp: {
name: 'acme',
id: 'localhost',
},
user: {
id: 'aif-X2DcI0pMpkTrvReYj5622eV8B28kQDs-oAq0qH8',
name: '[email protected]',
displayName: '',
},
pubKeyCredParams: [
{
alg: -8,
type: 'public-key',
},
{
alg: -7,
type: 'public-key',
},
{
alg: -257,
type: 'public-key',
},
],
timeout: 60000,
attestation: 'none',
excludeCredentials: [],
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
requireResidentKey: false,
},
extensions: {
credProps: true,
},
}
await startRegistration({ optionsJSON })
Step 3: Verify the passkey with the server
POST
Request Endpoint
POST /api/auth/register/verify?RP_ID=localhost&CLIENT_URL=http://localhost:9000
POST
Request Body
{
"email": "[email protected]",
"userId": "1093444839",
"challenge": "IOHpI3SCo6OnBOA_Zyj0jV9VXC5pS2qdle46Bsu253A",
"id": "bkWG-cWnltO7jjm1qgvx_3QCHvKj8jyno-grTi-4OEY",
"rawId": "bkWG-cWnltO7jjm1qgvx_3QCHvKj8jyno-grTi-4OEY",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG5FhvnFp5bTu445taoL8f90Ah7yo_I8p6PoK04vuDhGpQECAyYgASFYIAQKWaLi0pPQTEH7WDj5l5FHgBrK7yFnI9U_Fadtc0i8IlggdmF0zzcjy9R6iSS2fFpk51DcxgRSkxOeRFRuyvGE8Wo",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiSU9IcEkzU0NvNk9uQk9BX1p5ajBqVjlWWEM1cFMycWRsZTQ2QnN1MjUzQSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"transports": [
"internal"
],
"publicKeyAlgorithm": -7,
"publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBApZouLSk9BMQftYOPmXkUeAGsrvIWcj1T8Vp21zSLx2YXTPNyPL1HqJJLZ8WmTnUNzGBFKTE55EVG7K8YTxag",
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAIG5FhvnFp5bTu445taoL8f90Ah7yo_I8p6PoK04vuDhGpQECAyYgASFYIAQKWaLi0pPQTEH7WDj5l5FHgBrK7yFnI9U_Fadtc0i8IlggdmF0zzcjy9R6iSS2fFpk51DcxgRSkxOeRFRuyvGE8Wo"
},
"type": "public-key",
"clientExtensionResults": {
"credProps": {
"rk": true
}
},
"authenticatorAttachment": "platform"
}
POST
Request Response
{
"success": true,
"status": 200,
"verified": true
}
Step 4: Request login with the passkey from the server
POST
Request Endpoint
POST /api/auth/login?RP_ID=localhost
POST
Request Body
{
"email": "[email protected]"
}
POST
Request Response
{
"success": true,
"status": 200,
"data": {
"rpId": "localhost",
"challenge": "R5g601iRnobEVaY-I2j7VwgkDzcr1cbiVD3dsp11DYk",
"allowCredentials": [
{
"id": "bkWG-cWnltO7jjm1qgvx_3QCHvKj8jyno-grTi-4OEY",
"type": "public-key"
}
],
"timeout": 60000,
"userVerification": "preferred"
}
}
Step 5: Pass the response data to the @simplewebauthn/browser
client sdk
import { startAuthentication } from '@simplewebauthn/browser'
const optionsJSON = {
rpId: 'localhost',
challenge: 'R5g601iRnobEVaY-I2j7VwgkDzcr1cbiVD3dsp11DYk',
allowCredentials: [
{
id: 'bkWG-cWnltO7jjm1qgvx_3QCHvKj8jyno-grTi-4OEY',
type: 'public-key',
},
],
timeout: 60000,
userVerification: 'preferred',
}
await startAuthentication({ optionsJSON })
Step 6: Verify the passkey with the server and get the auth tokens
POST
Request Endpoint
POST /api/auth/login/verify?RP_ID=localhost&CLIENT_URL=http://localhost:9000
POST
Request Body
{
"email": "[email protected]",
"challenge": "gKVUXDLxR9ev-KTDOWTlQb0cr57WT5HhC7DWkNWFakk",
"response": {
"id": "bkWG-cWnltO7jjm1qgvx_3QCHvKj8jyno-grTi-4OEY",
"rawId": "bkWG-cWnltO7jjm1qgvx_3QCHvKj8jyno-grTi-4OEY",
"response": {
"authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAAAA",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiZ0tWVVhETHhSOWV2LUtURE9XVGxRYjBjcjU3V1Q1SGhDN0RXa05XRmFrayIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6OTAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
"signature": "MEQCIGZUx4F3l_wq3Pfad1arrPvJ65EpKv_WEnm3dWX2EArlAiBORz44aVuy1vxRObv3L4qJTkWccxjunkSvdkAJYrdvVw",
"userHandle": "_fWJ_BkgyCWfHq57LDr0QpzvP-ZpCTISiE8z7mzx_tA"
},
"type": "public-key",
"clientExtensionResults": {},
"authenticatorAttachment": "platform"
}
}
POST
Request Response
{
"success": true,
"status": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjo0NTE3OTU3MzcsInZlcmlmaWVkIjpmYWxzZSwic291cmNlIjoiZGFzaGJvYXJkIiwiZmluZ2VycHJpbnQiOiI0OGFiMzYxY2NlZWIyZWY2NWIxYjBiYmRhNWY2ZmYzNCIsImVtYWlsIjoiZW1haWxAZXhhbXBsZS5jb20iLCJpYXQiOjE3MzY2MjQwNDksImV4cCI6MTczNjYyNzY0OX0.WhUwsUMzHUp5nNL2qVhkT07yi3z5l1sMnkYUSeROApw",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjo0NTE3OTU3MzcsInZlcmlmaWVkIjpmYWxzZSwic291cmNlIjoiZGFzaGJvYXJkIiwiZmluZ2VycHJpbnQiOiI0OGFiMzYxY2NlZWIyZWY2NWIxYjBiYmRhNWY2ZmYzNCIsImVtYWlsIjoiZW1haWxAZXhhbXBsZS5jb20iLCJpYXQiOjE3MzY2MjQwNDksImV4cCI6MTczNzIyODg0OX0.sEocXcWvgzQ9rcSN5WiAu7dDIZVVNcQvlYxFWzXEwpI",
"expires_at": "2025-01-25T19:34:09.983Z"
}
}
OPTIONAL Step 7: Verify all users data
GET
Request Endpoint
GET /api/auth/users
OPTIONAL Step 8: Purge all users data
DELETE
Request Endpoint
DELETE /api/auth/users/purge