# Passkey Registration and Validation Example

Passkeys let users authenticate with their fingerprint, face scan, or device PIN instead of passwords. This guide walks you through implementing the two essential flows:

- **Passkey registration** - Saving a user’s passkey credential
- **Passkey authentication** - Validating that passkey to log the user in

PropelAuth BYO handles the backend WebAuthn complexity, but you’ll need a frontend library to interact with the user’s device. This guide uses [@simplewebauthn/browser](https://simplewebauthn.dev/docs/packages/browser) for client-side operations.

## Registering a User’s Passkey

Start by creating a backend endpoint that generates registration options. The [Start Passkey Registration](https://docs.byo.propelauth.com/passkeys/reference#start-passkey-registration) API returns `registrationOptions` that tell the browser how to create the passkey (whether to use Google Password Manager, iCloud Keychain, or the device’s built-in authenticator).

### Node
```
app.post("/api/passkey/register/start", async (req: Request, res: Response) => {
    const user = getUserInfoFromRequest(req);
    const result = await auth.passkeys.startRegistration({
        userId: user.userId,
        emailOrUsername: user.email,
    });
    if (result.ok) {
        res.json({ registrationOptions: result.data.registrationOptions });
    } else {
        console.error("Error starting registration:", result.error);
        res.status(500).json({ error: "Failed to start passkey registration" });
    }
});
```

### Python
```
@app.post("/api/passkey/register/start")
async def passkey_register_start(request: Request):
    user = get_user_info_from_request(request)
    result = await client.passkeys.start_registration(
        user_id=user.user_id,
        email_or_username=user.email,
    )
    if is_ok(result):
        return {"registrationOptions": result.data.registration_options}
    else:
        print("Error starting registration:", result.error)
        raise HTTPException(status_code=500, detail="Failed to start passkey registration")
```

### Go
```
mux.HandleFunc("POST /api/passkey/register/start", func(w http.ResponseWriter, r *http.Request) {
    user := getUserInfoFromRequest(r)
    result, err := client.Passkeys.StartRegistration(r.Context(), byo.StartPasskeyRegistrationCommand{
        UserID:          user.UserID,
        EmailOrUsername: user.Email,
    })
    if err != nil {
        log.Println("Error starting registration:", err)
        http.Error(w, "Failed to start passkey registration", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(map[string]any{"registrationOptions": result.RegistrationOptions})
})
```

Next, create a frontend component that triggers the registration. When clicked, it fetches the `registrationOptions` from your backend and passes them to [@simplewebauthn/browser](https://simplewebauthn.dev/docs/packages/browser), which handles the device prompt.

```
import { startRegistration } from '@simplewebauthn/browser';

const EnrollPasskey = () => {
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        try {
            const startResponse = await fetch('/api/passkey/register/start', {
                method: 'POST',
                headers: {'Content-Type': 'application/json',}
            });
            const { registrationOptions } = await startResponse.json();
            const publicKey = await startRegistration({ optionsJSON: registrationOptions, });
        } catch (error) {
            console.error('Error creating passkey:', error);
        }
    };
    return (<div><button onClick={handleSubmit}>Enroll a Passkey</button></div>);
};
export default EnrollPasskey;
```

Clicking the button prompts the browser to create a passkey:

After the user creates the passkey, you need to save it. Add a backend endpoint that accepts the credential and calls [Finish Passkey Registration](https://docs.byo.propelauth.com/passkeys/reference#finish-passkey-registration) to complete the process.

### Node
```
app.post("/api/passkey/register/finish", async (req, res) => {
    const user = getUserInfoFromRequest(req);
    const result = await auth.passkeys.finishRegistration({
        userId: user.userId,
        publicKey: req.body.publicKey,
    });
    if (result.ok) {
        res.json({ success: true, data: result.data });
    } else {
        console.error("Error finishing registration:", result.error);
        res.status(500).json({ error: "Failed to complete passkey registration" });
    }
});
```

### Python
```
@app.post("/api/passkey/register/finish")
async def passkey_register_finish(request: Request):
    user = get_user_info_from_request(request)
    body = await request.json()
    result = await client.passkeys.finish_registration(
        user_id=user.user_id,
        public_key=body["publicKey"],
    )
    if is_ok(result):
        return {"success": True, "data": result.data}
    else:
        print("Error finishing registration:", result.error)
        raise HTTPException(status_code=500, detail="Failed to complete passkey registration")
```

### Go
```
mux.HandleFunc("POST /api/passkey/register/finish", func(w http.ResponseWriter, r *http.Request) {
    user := getUserInfoFromRequest(r)
    var body struct {
        PublicKey json.RawMessage `json:"publicKey"`
    }
    if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    result, err := client.Passkeys.FinishRegistration(r.Context(), byo.FinishPasskeyRegistrationCommand{
        UserID:    user.UserID,
        PublicKey: body.PublicKey,
    })
    if err != nil {
        log.Println("Error finishing registration:", err)
        http.Error(w, "Failed to complete passkey registration", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(map[string]any{"success": true, "data": result})
})
```

The last step is updating our frontend to send the credential to this new endpoint. Once successful, users can register passkeys for use during MFA, login, and more.

```
import { startRegistration } from '@simplewebauthn/browser';

const EnrollPasskey = () => {
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        try {
            const startResponse = await fetch('/api/passkey/register/start', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', },
            });
            const { registrationOptions } = await startResponse.json();
            const publicKey = await startRegistration({ optionsJSON: registrationOptions, });
            const finishResponse = await fetch("/api/passkey/register/finish", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ publicKey: publicKey, }),
            });
            if (!finishResponse.ok) {
                const error = await finishResponse.json();
                throw new Error(error.error || "Failed to finish registration");
            }
            console.log('Passkey registered successfully!');
        } catch (error) {
            console.error('Error creating passkey:', error);
        }
    };
    return (<div><button onClick={handleSubmit}>Enroll a Passkey</button></div>);
};
export default EnrollPasskey;
```

Now let’s implement passkey authentication.

## Authenticating with Passkeys

With registration complete, let’s build the authentication flow. Create a backend endpoint that uses [Start Passkey Authentication](https://docs.byo.propelauth.com/passkeys/reference#start-passkey-authentication) to generate `authenticationOptions` for the frontend:

### Node
```
app.post("/api/passkey/authenticate/start", async (req: Request, res: Response) => {
    const user = getUserInfoFromRequest(req);
    const result = await auth.passkeys.startAuthentication({
        userId: user.userId
    });
    if (result.ok) {
        res.json({ authenticationOptions: result.data.authenticationOptions });
    } else {
        console.error("Error starting authentication:", result.error);
        res.status(500).json({ error: "Failed to start passkey authentication" });
    }
});
```

### Python
```
@app.post("/api/passkey/authenticate/start")
async def passkey_authenticate_start(request: Request):
    user = get_user_info_from_request(request)
    result = await client.passkeys.start_authentication(
        user_id=user.user_id
    )
    if is_ok(result):
        return {"authenticationOptions": result.data.authentication_options}
    else:
        print("Error starting authentication:", result.error)
        raise HTTPException(status_code=500, detail="Failed to start passkey authentication")
```

### Go
```
mux.HandleFunc("POST /api/passkey/authenticate/start", func(w http.ResponseWriter, r *http.Request) {
    user := getUserInfoFromRequest(r)
    result, err := client.Passkeys.StartAuthentication(r.Context(), byo.StartPasskeyAuthenticationCommand{
        UserID: user.UserID,
    })
    if err != nil {
        log.Println("Error starting authentication:", err)
        http.Error(w, "Failed to start passkey authentication", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(map[string]any{"authenticationOptions": result.AuthenticationOptions})
})
```

On the frontend, let’s create a button that triggers the passkey authentication flow. First, we’ll request the `authenticationOptions` from our backend, then pass these to [@simplewebauthn/browser](https://simplewebauthn.dev/docs/packages/browser) which prompts the user to authenticate with their passkey.

```
import { startAuthentication } from '@simplewebauthn/browser';

const ValidatePasskey = () => {
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        try {
            const startResponse = await fetch('/api/passkey/authenticate/start', {
                method: 'POST',
                headers: {'Content-Type': 'application/json',}
            });
            const { authenticationOptions } = await startResponse.json();
            const publicKey = await startAuthentication({ optionsJSON: authenticationOptions, });
        } catch (error) {
            console.error('Error validating passkey:', error);
        }
    };
    return (<div><button onClick={handleSubmit}>Validate a Passkey</button></div>);
};
export default ValidatePasskey;
```

The last step is updating our frontend to send the credential to this new endpoint. If successful, your users can authenticate with passkeys for MFA, login, and more.

```
import { startAuthentication } from '@simplewebauthn/browser';

const AuthenticateWithPasskey = () => {
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        try {
            const startResponse = await fetch('/api/passkey/authenticate/start', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', },
            });
            const { authenticationOptions } = await startResponse.json();
            const publicKey = await startAuthentication({ optionsJSON: authenticationOptions, });
            const finishResponse = await fetch("/api/passkey/authenticate/finish", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ publicKey: publicKey, }),
            });
            if (!finishResponse.ok) {
                const error = await finishResponse.json();
                throw new Error(error.error || "Failed to finish authentication");
            }
            const result = await finishResponse.json();
            console.log("Authentication successful:", result);
        } catch (error) {
            console.error('Error authenticating with passkey:', error);
        }
    };
    return (<div><button onClick={handleSubmit}>Authenticate with Passkey</button></div>);
};
export default AuthenticateWithPasskey;
```

And we’re done! We’ve successfully implemented both passkey registration and authentication flows.
