# Overview This is a very basic demo of passwordless authentication in the web, also known as "WebAuthn". # Technical Details Note that for convenience, the client always sends the base64URL encoded user name from the UI to the server. This encoded version is used as the `Fido2User.Id` while the decoded version is stored as the `Fido2User.DisplayName` and the `Fido2User.Name` is then derived from the display name by converting all characters to lower case and replacing "offending" file name characters with "_". ## Registration ### Building a Challange for Creating a new Credential `CredentialCreateOptions` generated by the program looks like this: - The algorithm identifiers are defined [in the IANA registry](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) - The create options have to be post-processed on the client before they can be used with `navigator.credentials.create` by converting base64URL encoded strings to byte arrays (`Uint8Array`) ```json { "rp": { "id": "localhost", "name": "FIDO2 Test" }, "user": { "name": "test osteron", "id": "VGVzdCBPc3Rlcm9u", "displayName": "Test Osteron" }, "challenge": "UkTN1q5kjoWcHOFTB6AZWQ", "pubKeyCredParams": [ { "type": "public-key", "alg": -7 }, { "type": "public-key", "alg": -257 }, { "type": "public-key", "alg": -37 }, { "type": "public-key", "alg": -35 }, { "type": "public-key", "alg": -258 }, { "type": "public-key", "alg": -38 }, { "type": "public-key", "alg": -36 }, { "type": "public-key", "alg": -259 }, { "type": "public-key", "alg": -39 }, { "type": "public-key", "alg": -8 } ], "timeout": 60000, "attestation": "none", "authenticatorSelection": { "requireResidentKey": false, "userVerification": "discouraged" }, "excludeCredentials": [], "extensions": { "exts": true, "uvm": false }, "status": "ok", "errorMessage": "" } ``` ### Creating the Credential The result of calling `navigator.credentials.create` with the options & challange from the server is an object that mostly contains binary data: - `id` and `rawId` fields represent the same **Credential ID**, the raw version is an array buffer while the "plain" version is a base64URL encoded representation. The (plain / non-raw) ID of the credential looks like: `BKbtxxiJoPWfT8x_3fUwlzXYIR6OwRXSQGH-FMykKcthocRhAznj8DMNY-2YZw7By-HNnEJa1CxjTPK0WyzjwQ` - `authenticatorAttachment` declares what type of authenticator provided the credential ("cross-platform" meaning a _roaming_ authenticator) - `type` being the type of the credential that was generated (generally "public-key") - `response.attestationObject` is a [CBOR encoded](https://datatracker.ietf.org/doc/html/rfc8949) `ArrayBuffer` which contains the _authenticator data_ and _attestation_ (if present) - `response.clientDataJSON` contains some... client data including the challenge value that was used and the RP origin ```json { "type": "webauthn.create", "challenge": "NwZKS4GobKzOqa5YvPPD2g", "origin": "http://localhost:5172", "crossOrigin": false } ``` ### Storing the Credentials We again need to do some reformatting, this time from byte arrays to base64URL encoded strings and then the data from the authenticator is directly sent to the server for verification. ```json { "id": "BKbtxxiJoPWfT8x_3fUwlzXYIR6OwRXSQGH-FMykKcthocRhAznj8DMNY-2YZw7By-HNnEJa1CxjTPK0WyzjwQ", "rawId": "BKbtxxiJoPWfT8x/3fUwlzXYIR6OwRXSQGH+FMykKcthocRhAznj8DMNY+2YZw7By+HNnEJa1CxjTPK0WyzjwQ==", "type": "public-key", "extensions": {}, "response": { "AttestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjESZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFAAAABAAAAAAAAAAAAAAAAAAAAAAAQASm7ccYiaD1n0/Mf931MJc12CEejsEV0kBh/hTMpCnLYaHEYQM54/AzDWPtmGcOwcvhzZxCWtQsY0zytFss48GlAQIDJiABIVggrEeUH2MVMs5oI0dZOGu9Sm9w/5iMFMRXczBtsDrmSOgiWCBO1F75pFRnZS6wRC3LIvt2U7C10i0gQd73NRG3A38bZA==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTndaS1M0R29iS3pPcWE1WXZQUEQyZyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTE3MiIsImNyb3NzT3JpZ2luIjpmYWxzZX0=", "transports": [] } } ``` The server has to compare this response with the challenge from the creation options (the creation options are cached on the server or persisted in the session). Once successful, the credential is persisted in a file "[username].json". ## Login ### Assertion Options from Login For the login, we create a `AssertionOptions` instance from a registered login name (if that login actually exists) which looks like this: ```json { "challenge": "VlXKhoI7x3T5LxsKP_WPnw", "timeout": 60000, "rpId": "demo.larcanum.net", "allowCredentials": [ { "type": "public-key", "id": "vHJFHVmoY37hrUHFObgkw7QkBB1p44uhYlLc4i9r1BHde5XEVObMB8QOKWKpcf4eWEODN2kK5x84pRlNn8etlQ" } ], "userVerification": "discouraged", "extensions": { "exts": true, "uvm": false }, "status": "ok", "errorMessage": "" } ``` The options again need to be post-processed on the client before they can be used with `navigator.credentials.get` by converting base64URL encoded strings to byte arrays (`Uint8Array`). ### Verifying the Credentials The result of `navigator.credentials.get` looks very similar to the one from `navigator.credentials.create` but with the addition of a "signature" - the `signature` field contains the signed challenge from the request object and is the critical piece of information that the server needs to verify The raw verification request looks like this: ```json { "id": "vHJFHVmoY37hrUHFObgkw7QkBB1p44uhYlLc4i9r1BHde5XEVObMB8QOKWKpcf4eWEODN2kK5x84pRlNn8etlQ", "rawId": "vHJFHVmoY37hrUHFObgkw7QkBB1p44uhYlLc4i9r1BHde5XEVObMB8QOKWKpcf4eWEODN2kK5x84pRlNn8etlQ==", "type": "public-key", "extensions": {}, "response": { "authenticatorData": "oRsUvdxlEqdT6Qnbe1LQ/aqF8rHBT3MzNqBH6nFOcf0BAAAACA==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiVmxYS2hvSTd4M1Q1THhzS1BfV1BudyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby5sYXJjYW51bS5uZXQiLCJjcm9zc09yaWdpbiI6ZmFsc2UsIm90aGVyX2tleXNfY2FuX2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgifQ==", "signature": "MEUCIBanHNIJS6ozJe0Nzf0fMJDEeqr/R2J33izif54zuGH1AiEAyOxPUhYfd/7vWnIDh1a4Dind3dTPPjE5O02olEhLbUE=" } } ``` Once the credentials have been verified on the server, it generates a JWT for those credentials which can be used to access a simple API protected with JWT bearer authentication. ## Stored Data Currently, we directly store the `AttestationVerificationSuccess` that the client sends to complete the registration process. [my username.json](./data/my username.json) ## Notes - Credentials are **bound** to a specific domain. If the credentials presented to the passkey are from a different domain, then the message "This security key doesn't look familiar. Please try a different one" appears in the passkey selection window.