Просмотр исходного кода

Creating credentials on the client side

Lukas Angerer 2 лет назад
Родитель
Сommit
02013e18b0
4 измененных файлов с 199 добавлено и 33 удалено
  1. 73 0
      CredentialManager.cs
  2. 8 31
      Program.cs
  3. 3 2
      wwwroot/index.html
  4. 115 0
      wwwroot/main.js

+ 73 - 0
CredentialManager.cs

@@ -0,0 +1,73 @@
+using System.Text;
+using System.Text.Json;
+using Fido2NetLib;
+using Fido2NetLib.Objects;
+
+namespace Passwordless;
+
+public class CredentialManager
+{
+    private readonly IFido2 _fido2;
+    private readonly JsonSerializerOptions _jsonOptions;
+
+    public CredentialManager(IFido2 fido2)
+    {
+        _fido2 = fido2;
+
+        _jsonOptions = new JsonSerializerOptions()
+        {
+            WriteIndented = true,
+
+        };
+    }
+    
+    public CredentialCreateOptions BuildCredentialOptions(string login)
+    {
+        var loginDisplay = Encoding.UTF8.GetString(Convert.FromBase64String(login));
+        var loginName = NameTransform.ToFileName(loginDisplay);
+
+        var user = new Fido2User
+        {
+            DisplayName = loginDisplay,
+            Id = Convert.FromBase64String(login),
+            Name = loginName,
+        };
+
+        var authenticatorSelection = new AuthenticatorSelection
+        {
+            UserVerification = UserVerificationRequirement.Discouraged,
+            RequireResidentKey = false,
+        };
+
+        var extensions = new AuthenticationExtensionsClientInputs
+        {
+            Extensions = true,
+            UserVerificationMethod = false,
+        };
+        
+        var options = _fido2.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
+        
+        return options;
+    }
+
+    public async Task<Fido2.CredentialMakeResult> RegisterCredential(string login, AuthenticatorAttestationRawResponse attestationResponse)
+    {
+        // 2. Create callback so that lib can verify credential id is unique to this user
+        static Task<bool> Callback(IsCredentialIdUniqueToUserParams args, CancellationToken cancellationToken)
+        {
+            return Task.FromResult(!File.Exists($"./data/{args.CredentialId}"));
+        }
+
+        var options = BuildCredentialOptions(login);
+
+        var success = await _fido2.MakeNewCredentialAsync(attestationResponse, options, Callback);
+
+        if (success.Status == "ok")
+        {
+            await using var fileStream = File.OpenWrite($"./data/{success.Result!.User.Name}");
+            await JsonSerializer.SerializeAsync(fileStream, success.Result, _jsonOptions);
+        }
+        
+        return success;
+    }
+}

+ 8 - 31
Program.cs

@@ -1,6 +1,4 @@
-using System.Text;
 using Fido2NetLib;
-using Fido2NetLib.Objects;
 using Microsoft.AspNetCore.Mvc;
 using Passwordless;
 
@@ -17,6 +15,7 @@ builder.Services.AddFido2(options =>
     options.Origins = ["http://localhost:5172"];
     options.TimestampDriftTolerance = 300000;
 });
+builder.Services.AddTransient<CredentialManager>();
 
 var app = builder.Build();
 
@@ -30,36 +29,14 @@ if (app.Environment.IsDevelopment())
 app.UseStaticFiles();
 app.UseHttpsRedirection();
 
-app.MapGet("/buildCredentialOptions", ([FromQuery] string login, IFido2 fido2) =>
-    {
-        var data = File.ReadAllText("./data/test.json");
-        var loginDisplay = Encoding.UTF8.GetString(Convert.FromBase64String(login));
-        var loginName = NameTransform.ToFileName(loginDisplay);
-
-        var user = new Fido2User
-        {
-            DisplayName = loginDisplay,
-            Id = Convert.FromBase64String(login),
-            Name = loginName,
-        };
-
-        var authenticatorSelection = new AuthenticatorSelection
-        {
-            UserVerification = UserVerificationRequirement.Discouraged,
-            RequireResidentKey = false,
-        };
-
-        var extensions = new AuthenticationExtensionsClientInputs
-        {
-            Extensions = true,
-            UserVerificationMethod = false,
-        };
-        
-        var options = fido2.RequestNewCredential(user, new List<PublicKeyCredentialDescriptor>(), authenticatorSelection, AttestationConveyancePreference.None, extensions);
-        
-        return options;
-    })
+app.MapGet("/buildCredentialOptions", ([FromQuery] string login, CredentialManager credMan) => 
+        credMan.BuildCredentialOptions(login))
     .WithName("BuildCredentialOptions")
     .WithOpenApi();
 
+app.MapGet("/registerCredential", async ([FromQuery] string login, [FromBody] AuthenticatorAttestationRawResponse attestationResponse, CredentialManager credMan) => 
+        await credMan.RegisterCredential(login, attestationResponse))
+    .WithName("RegisterCredential")
+    .WithOpenApi();
+
 app.Run();

+ 3 - 2
wwwroot/index.html

@@ -4,7 +4,8 @@
     <title>Passwordless Demo</title>
 </head>
 <body>
-<h1>Passwordless Demo</h1>
-<button id="start">Start</button>
+    <h1>Passwordless Demo</h1>
+    <button id="start">Start</button>
+    <script src="main.js"></script>
 </body>
 </html>

+ 115 - 0
wwwroot/main.js

@@ -0,0 +1,115 @@
+
+coerceToArrayBuffer = function (thing, name) {
+    if (typeof thing === "string") {
+        // base64url to base64
+        thing = thing.replace(/-/g, "+").replace(/_/g, "/");
+
+        // base64 to Uint8Array
+        const str = atob(thing);
+        const bytes = new Uint8Array(str.length);
+        for (let i = 0; i < str.length; i++) {
+            bytes[i] = str.charCodeAt(i);
+        }
+        return bytes;
+    }
+};
+
+class Registration {
+    constructor() {
+        document.getElementById('start').addEventListener('click', this.registerAccount.bind(this));
+    }
+    
+    async registerAccount(event) {
+        event.preventDefault();
+        
+        const username = 'Foo! That Bar';
+        const login = btoa(username);
+
+        const response = await fetch(`/buildCredentialOptions?login=${login}`, {
+            method: 'GET',
+            headers: {
+                'Accept': 'application/json'
+            }
+        });
+
+        const options = await response.json();
+
+        if (options.status !== "ok") {
+            console.error("Error in buildCredentialOptions");
+            console.error(options.errorMessage);
+            return;
+        } else {
+            console.log("Got options", options);
+        }
+
+        // Turn the challenge back into the accepted format of padded base64
+        options.challenge = coerceToArrayBuffer(options.challenge);
+        // Turn ID into a UInt8Array Buffer for some reason
+        options.user.id = coerceToArrayBuffer(options.user.id);
+
+        options.excludeCredentials = options.excludeCredentials.map((c) => {
+            c.id = coerceToArrayBuffer(c.id);
+            return c;
+        });
+
+        console.log("Options Formatted", options);
+
+        // Create the Credential with navigator.credentials.create API
+        
+        let newCredential;
+        try {
+            newCredential = await navigator.credentials.create({
+                publicKey: options
+            });
+        } catch (e) {
+            console.error("credentials.create failed with exception", e);
+        }
+
+        console.log("PublicKeyCredential created", newCredential);
+
+        // try {
+        //     register(newCredential);
+        // } catch (e) {
+        //     console.error("Registering new credentials with the server failed with exception", e);
+        // }
+    }
+    
+    async register(newCredential) {
+        // Move data into Arrays in case it is super long
+        // TODO: check!
+        let attestationObject = new Uint8Array(newCredential.response.attestationObject);
+        let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
+        let rawId = new Uint8Array(newCredential.rawId);
+
+        const data = {
+            id: newCredential.id,
+            //rawId: coerceToBase64Url(rawId),
+            type: newCredential.type,
+            extensions: newCredential.getClientExtensionResults(),
+            response: {
+                AttestationObject: coerceToBase64Url(attestationObject),
+                clientDataJSON: coerceToBase64Url(clientDataJSON),
+                transports: newCredential.response.getTransports()
+            }
+        };
+
+        let response = await fetch(`/registerCredential?login=`, {
+            method: 'POST', // or 'PUT'
+            body: JSON.stringify(formData), // data can be `string` or {object}!
+            headers: {
+                'Accept': 'application/json',
+                'Content-Type': 'application/json'
+            }
+        });
+
+        const result = await response.json();
+
+        console.log("Credential Object", result);
+
+        if (result.status !== "ok") {
+            throw new Error(`Error creating credential: ${result.errorMessage}`);
+        }
+    }
+}
+
+window.registration = new Registration();