Przeglądaj źródła

More documentation

Lukas Angerer 2 lat temu
rodzic
commit
8a32d6108c
5 zmienionych plików z 85 dodań i 6 usunięć
  1. 1 0
      .gitignore
  2. 1 0
      Program.cs
  3. 61 4
      README.md
  4. 1 1
      data/pixlar.json
  5. 21 1
      wwwroot/index.html

+ 1 - 0
.gitignore

@@ -2,6 +2,7 @@
 ## files generated by popular Visual Studio add-ons.
 ## files generated by popular Visual Studio add-ons.
 ##
 ##
 ## Get latest from `dotnet new gitignore`
 ## Get latest from `dotnet new gitignore`
+data/*
 
 
 # dotenv files
 # dotenv files
 .env
 .env

+ 1 - 0
Program.cs

@@ -76,6 +76,7 @@ if (app.Environment.IsDevelopment())
     app.UseSwaggerUI();
     app.UseSwaggerUI();
 }
 }
 
 
+app.UseDefaultFiles();
 app.UseStaticFiles();
 app.UseStaticFiles();
 app.UseHttpsRedirection();
 app.UseHttpsRedirection();
 app.UseAuthorization();
 app.UseAuthorization();

+ 61 - 4
README.md

@@ -1,6 +1,5 @@
-This is a very basic demo of passwordless authentication in the web, also known as "WebAuthn".
-
-# Overview
+# Overview
+This is a very basic demo of passwordless authentication in the web, also known as "WebAuthn".
 
 
 # Technical Details
 # Technical Details
 Note that for convenience, the client always sends the base64URL encoded user name from the UI to
 Note that for convenience, the client always sends the base64URL encoded user name from the UI to
@@ -133,8 +132,66 @@ persisted in a file "[username].json".
 
 
 ## Login
 ## 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
 ## Stored Data
 Currently, we directly store the `AttestationVerificationSuccess` that the client sends to complete
 Currently, we directly store the `AttestationVerificationSuccess` that the client sends to complete
 the registration process.
 the registration process.
 
 
-[my username.json](./data/my username.json)
+[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.

+ 1 - 1
data/pixlar.json

@@ -10,7 +10,7 @@
   "AttestationCertificate": null,
   "AttestationCertificate": null,
   "AttestationCertificateChain": [],
   "AttestationCertificateChain": [],
   "CredentialId": "AUaVZoW6gV0Z3T3fX9yR2EO49nSwgXphbs67Aqcq/8ttQ6/byO1tW6F9qq6YG08mnluYtsYV9gjI5n0lbKLHeG0=",
   "CredentialId": "AUaVZoW6gV0Z3T3fX9yR2EO49nSwgXphbs67Aqcq/8ttQ6/byO1tW6F9qq6YG08mnluYtsYV9gjI5n0lbKLHeG0=",
-  "Counter": 1,
+  "Counter": 3,
   "status": null,
   "status": null,
   "errorMessage": null
   "errorMessage": null
 }
 }

+ 21 - 1
wwwroot/index.html

@@ -1,13 +1,29 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
 <head>
 <head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>Passwordless Demo</title>
     <title>Passwordless Demo</title>
 </head>
 </head>
 <body>
 <body>
     <h1>Passwordless Demo</h1>
     <h1>Passwordless Demo</h1>
+    <p>
+        This demo uses the FIDO2 browser APIs <code>navigator.credentials.create</code> and
+        <code>navigator.credentials.get</code> to implement a <em>passwordless</em> login with non-resident keys.
+    </p>
+    <p>
+        <strong>Step 1:</strong> Choose a username and click the "Register" button. If the username is still available,
+        your browser will prompt you to select a device to use as a <em>passkey</em>. After authorizing your passkey
+        device your account is created.
+    </p>
+    <p>
+        <strong>Step 2:</strong> With the same username that you used for the registration, click the "Login" button.
+        The same browser prompt will show up to perform the login with your passkey device. If the login was successful,
+        you will receive a JWT from the server that will be shown below.
+    </p>
     <div>
     <div>
         <label for="username">Login:</label>
         <label for="username">Login:</label>
-        <input id="username" type="text" value="My UserName" minlength="3" />
+        <input id="username" type="text" placeholder="Choose Username" minlength="3" />
     </div>
     </div>
     <div>
     <div>
         <button id="register">Register</button>
         <button id="register">Register</button>
@@ -15,6 +31,10 @@
     </div>
     </div>
     <hr />
     <hr />
     <div>
     <div>
+        <p>
+            <strong>Step 3:</strong> Once you are authenticated with the server, you can click the
+            "Access Protected API" button to test the effectiveness of your JWT. You have just used 
+        </p>
         <pre><code id="userState">[not authenticated]</code></pre>
         <pre><code id="userState">[not authenticated]</code></pre>
         <div>
         <div>
             <button id="protected">Access Protected API</button>
             <button id="protected">Access Protected API</button>