Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Passkeys. The Why, The What, The How

Avatar for Leeds Mobile Leeds Mobile
October 18, 2025
5

Passkeys. The Why, The What, The How

Avatar for Leeds Mobile

Leeds Mobile

October 18, 2025
Tweet

Transcript

  1. Passkeys @0xTim - brokenhands.io Introduction • Founder of Broken Hands

    • Vapor Core Team • SSWG and SWWG Member • Organise ServerSide.swift, NSManchester, Vapor London • @0xTim
  2. Why

  3. I’d like to borrow this book please Sure, who are

    you? My name is Tim Can you prove who you are? Here is my ID
  4. I’d like to borrow this book please Let me check

    you are allowed to I’m from the iOS course Sorry, this book is only for law students
  5. I’d like to borrow this book please Sure, who are

    you? My name is Tim Can you prove who you are? Here is my ID
  6. 2FA

  7. Passkeys @0xTim - brokenhands.io Something you know Password Security question

    PIN number Memorable Word Something you have RSA Token Authenticator App SMS App Approval Email account Security Key Something you are FaceID TouchID Iris scanner Heartbeat analysis
  8. FIDO Alliance “Passkeys are a replacement for passwords that provide

    faster, easier, and more secure sign-ins to websites and apps across a user’s devices. Unlike passwords, passkeys are always strong and phishing- resistant.”
  9. Passkeys @0xTim - brokenhands.io Message 🔑 Signature Veri fi cation

    ✅ Signature Message Signature Message 🔒
  10. I’d like to register please! Sure send me a public

    key! Let me create a key pair OK, here’s the public key 🔒 Ok let me verify that Ok, all looks good 👍, I’ll save that with your username Great, I’ll save the private key with the domain
  11. I’d like to sign in please! Ok here’s a challenge

    for you to sign Let me create a signature using my private key OK, here’s the signature ✍ Ok let me verify that with
 the public key I have Ok, all looks good 👍,
 you’re signed in Let me ask the user to
 authorise and I’ll check the
 domain
  12. How

  13. let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( relyingPartyIdentifier: domain) let challenge = try

    await getChallengeFromServer() let platformKeyRequest = publicKeyCredentialProvider .createCredentialRegistrationRequest( challenge: challenge, name: "Tim Condon", userID: userID)
  14. let authController = ASAuthorizationController( authorizationRequests: [ platformKeyRequest ] ) authController.delegate

    = self authController.presentationContextProvider = self authController.performRequests()
  15. func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { switch authorization.credential

    { case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: // Send public key to the server try completeRegistration( data: credentialRegistration.rawAttestationObject) // User has been registered didFinishSignIn() case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: // Log in default: // Handle } }
  16. // step 1 for registration authSessionRoutes.get("makeCredential") { req -> PublicKeyCredentialCreationOptions

    in let options = req.webAuthn.beginRegistration(user: userObject) // Store challenge req.session.data["registrationChallenge"] = Data(options.challenge).base64EncodedString() return options }
  17. // step 2 for registration authSessionRoutes.post("makeCredential") { req -> HTTPStatus

    in // Get the user to register and challenge let (user, challenge) = try req.retrieveRegistrationDataFromSession() // Verify the credential the client sent us let credential = try await req.webAuthn.finishRegistration( challenge: challenge, credentialCreationData: req.content.decode(RegistrationCredential.self), confirmCredentialIDNotRegisteredYet: { credentialID in let existingCredential = try await WebAuthnCredential .query(on: req.db).filter(\.$id == credentialID).first() return existingCredential == nil } ) try await WebAuthnCredential( from: credential, userID: user.requireID()).save(on: req.db) return .ok }
  18. let publicKeyCredentialProvider = ASAuthorizationPlatformPublicKeyCredentialProvider( relyingPartyIdentifier: domain) let challenge = try

    await getChallengeFromServer() let platformKeyRequest = publicKeyCredentialProvider .createCredentialAssertionRequest( challenge: challenge)
  19. let authController = ASAuthorizationController( authorizationRequests: [ platformKeyRequest ] ) authController.delegate

    = self authController.presentationContextProvider = self authController.performRequests()
  20. func authorizationController( controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { switch authorization.credential

    { case let credentialRegistration as ASAuthorizationPlatformPublicKeyCredentialRegistration: // Registration case let credentialAssertion as ASAuthorizationPlatformPublicKeyCredentialAssertion: try completeLogIn( signature: credentialAssertion.signature, userID: credentialAssertion.userID, data: credentialAssertion.rawClientDataJSON) default: // Handle } }
  21. // step 1 for authentication authSessionRoutes.get("authenticate") { req -> PublicKeyCredentialRequestOptions

    in let options = try req.webAuthn.beginAuthentication() req.session.data["authChallenge"] = Data(options.challenge).base64EncodedString() return options }
  22. // step 2 for authentication authSessionRoutes.post("authenticate") { req -> HTTPStatus

    in // Obtain the challenge we stored on the server for this session let challenge = try req.getAuthChallenge() // Decode the credential the client sent us let authenticationCredential = try req.content.decode(AuthenticationCredential.self) // find the credential the stranger claims to possess guard let credential = try await WebAuthnCredential .query(on: req.db) .filter(\.$id == authenticationCredential.id.urlDecoded.asString()) .with(\.$user).first() else { throw Abort(.unauthorized) } // … }
  23. // step 2 for authentication continued authSessionRoutes.post("authenticate") { req ->

    HTTPStatus in // ... let verifiedAuthentication = try req.webAuthn.finishAuthentication( credential: authenticationCredential, expectedChallenge: [UInt8](challenge), credentialPublicKey: [UInt8](URLEncodedBase64(credential.publicKey) .urlDecoded.decoded!), credentialCurrentSignCount: credential.currentSignCount ) // if we successfully verified the user, update the sign count credential.currentSignCount = verifiedAuthentication.newSignCount try await credential.save(on: req.db) // finally authenticate the user req.auth.login(credential.user) return .ok }