From 1587e7616937911b97b12a5e2fbf57711405d04d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reynir=20Bj=C3=B6rnsson?= Date: Mon, 4 Oct 2021 10:43:32 +0200 Subject: [PATCH] Rework registration flow The registration page comes with a form where the user selects a username. When the user clicks "register" a javascript function is called that reads the user form input value and makes a GET request to /challenge/:user where it receives a challenge and a user object as JSON. The WebAuthn registration process continues using those challenge parameters. The user then receives an alert() with a string explaining the registration status and is then redirected to the front page where a flash message is displayed as well. --- bin/template.ml | 136 ++++++++++++++++++++++++++----------------- bin/webauthn_demo.ml | 44 ++++++++++---- 2 files changed, 114 insertions(+), 66 deletions(-) diff --git a/bin/template.ml b/bin/template.ml index 7940180..6412af4 100644 --- a/bin/template.ml +++ b/bin/template.ml @@ -40,68 +40,94 @@ let overview notes authenticated_as users = in page "" (String.concat "" (notes @ [authenticated_as;links;users])) -let register_view origin user challenge userid = +let register_view origin user = let script = Printf.sprintf {| - var publicKey = { - challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), - rp: { - id: "%s", - name: "WebAuthn Demo from robur.coop" - }, - user: { - id: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), - displayName: "%s", - name: "%s" - }, - pubKeyCredParams: [ - { - type: "public-key", - alg: -7 - } - ], - attestation: "direct" - }; - navigator.credentials.create({ publicKey }) - .then(function (credential) { - // send attestation response and client extensions - // to the server to proceed with the registration - // of the credential - console.log(credential); - // Move data into Arrays incase it is super long - let response = credential.response; - let attestationObject = new Uint8Array(response.attestationObject); - let clientDataJSON = new Uint8Array(response.clientDataJSON); - let rawId = new Uint8Array(credential.rawId); + function makePublicKey(challengeData) { + let challenge = Uint8Array.from(window.atob(challengeData.challenge), c=>c.charCodeAt(0)); + let user_id = Uint8Array.from(window.atob(challengeData.user.id), c=>c.charCodeAt(0)); + return { + challenge: challenge, + rp: { + id: "%s", + name: "WebAuthn Demo from robur.coop" + }, + user: { + id: user_id, + displayName: challengeData.user.displayName, + name: challengeData.user.name + }, + pubKeyCredParams: [ + { + type: "public-key", + alg: -7 + } + ], + attestation: "direct" + }; + } + function do_register(username) { + fetch("/challenge/"+username) + .then(response => response.json()) + .then(function (challengeData) { + console.log("got challenge data"); + console.log(challengeData); + let publicKey = makePublicKey(challengeData); + navigator.credentials.create({ publicKey }) + .then(function (credential) { + // send attestation response and client extensions + // to the server to proceed with the registration + // of the credential + console.log(credential); + // Move data into Arrays incase it is super long + let response = credential.response; + let attestationObject = new Uint8Array(response.attestationObject); + let clientDataJSON = new Uint8Array(response.clientDataJSON); + let rawId = new Uint8Array(credential.rawId); - var body = - JSON.stringify({ - id: credential.id, - rawId: bufferEncode(rawId), - type: credential.type, - response: { - attestationObject: bufferEncode(attestationObject), - clientDataJSON: bufferEncode(clientDataJSON), - }, - }); - console.log(body); + var body = + JSON.stringify({ + id: credential.id, + rawId: bufferEncode(rawId), + type: credential.type, + response: { + attestationObject: bufferEncode(attestationObject), + clientDataJSON: bufferEncode(clientDataJSON), + }, + }); + console.log(body); - let headers = {'Content-type': "application/json; charset=utf-8"}; + let headers = {'Content-type': "application/json; charset=utf-8"}; - let request = new Request('/register_finish', { method: 'POST', body: body, headers: headers } ); - fetch(request) - .then(function (response) { - console.log(response); - if (!response.ok) { - console.log("bad response: " + response.status); - }; + let request = new Request('/register_finish', { method: 'POST', body: body, headers: headers } ); + fetch(request) + .then(function (response) { + console.log(response); + if (!response.ok && response.status != 403) { + console.log("bad response: " + response.status); + return + }; + response.json().then(function (success) { + alert(success ? "Successfully registered!" : "Failed to register :("); + window.location = "/"; + }); + }); + }).catch(function (err) { + console.error(err); + }); }); - }).catch(function (err) { - console.error(err); - }); -|} challenge origin userid user user + } + function doit() { + let username = document.getElementById("username").value; + return do_register(username); + } +|} origin and body = Printf.sprintf {| -

Welcome %s.

+

Welcome.

+
+ + +
|} user in page script body diff --git a/bin/webauthn_demo.ml b/bin/webauthn_demo.ml index 9512c97..e7ae2af 100644 --- a/bin/webauthn_demo.ml +++ b/bin/webauthn_demo.ml @@ -37,7 +37,7 @@ let add_routes t = Dream.html (Template.overview flash authenticated_as users) in - let register req = + let register _req = let user = (* match Dream.session "authenticated_as" req with | None -> *) gen_data ~alphabet:Base64.uri_safe_alphabet 8 @@ -47,12 +47,29 @@ let add_routes t = | None -> [] | Some keys -> List.map (fun (_, kh, _) -> kh) keys in + Dream.html (Template.register_view (Webauthn.rpid t) user) + in + + (* XXX: should we distinguish between register and authenticate challenges? *) + let challenge req = + let user = Dream.param "user" req in let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16) - and userid = Base64.encode_string user - in + (* [userid] should be a random value *) + and userid = Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet user in Hashtbl.replace challenges challenge user; + let challenge_b64 = (Base64.encode_string challenge) in + let json = `Assoc [ + "challenge", `String challenge_b64 ; + "user", `Assoc [ + "id", `String userid ; + "name", `String user ; + "displayName", `String user ; + ] ; + ] + in + Logs.info (fun m -> m "produced challenge for user %s: %s" user challenge_b64); Dream.put_session "challenge" challenge req >>= fun () -> - Dream.html (Template.register_view (Webauthn.rpid t) user (Base64.encode_string challenge) userid) + Dream.json (Yojson.Safe.to_string json) in let register_finish req = @@ -68,7 +85,7 @@ let add_routes t = Logs.warn (fun m -> m "error %a" Webauthn.pp_error e); let err = to_string e in Flash_message.put_flash "" ("Registration failed " ^ err) req; - Dream.redirect req "/" + Dream.json "false" | Ok (_aaguid, credential_id, pubkey, _client_extensions, user_present, user_verified, sig_count, _authenticator_extensions, attestation_cert) -> ignore (check_counter (credential_id, pubkey) sig_count); @@ -79,29 +96,33 @@ let add_routes t = Dream.respond ~status:`Internal_Server_Error "Internal server error: couldn't find user for challenge" | Some user -> + Logs.app (fun m -> m "challenge for user %S" user); Hashtbl.remove challenges challenge; match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with | _, None -> - Logs.app (fun m -> m "registered %s" user); + Logs.app (fun m -> m "registered %s: %S" user credential_id); Hashtbl.replace users user [ (pubkey, credential_id, attestation_cert) ]; Dream.invalidate_session req >>= fun () -> Flash_message.put_flash "" (Printf.sprintf "Successfully registered as %s! [authenticate]" user user) req; - Dream.redirect req "/" + Dream.json "true" | Some session_user, Some keys -> + Logs.app (fun m -> m "user %S session_user %S" user session_user); if String.equal user session_user then begin - Logs.app (fun m -> m "registered %s" user); + Logs.app (fun m -> m "registered %s: %S" user credential_id); Hashtbl.replace users user ((pubkey, credential_id, attestation_cert) :: keys) ; Dream.invalidate_session req >>= fun () -> Flash_message.put_flash "" (Printf.sprintf "Successfully registered as %s! [authenticate]" user user) req; - Dream.redirect req "/" + Dream.json "true" end else - Dream.respond ~status:`Forbidden "Forbidden." + (Logs.info (fun m -> m "session_user %s, user %s" session_user user); + Dream.respond ~status:`Forbidden "Forbidden.") | None, Some _keys -> - Dream.respond ~status:`Forbidden "Forbidden." + Logs.app (fun m -> m "no session user"); + Dream.json ~status:`Forbidden "false" in let authenticate req = @@ -170,6 +191,7 @@ let add_routes t = Dream.router [ Dream.get "/" main; Dream.get "/register" register; + Dream.get "/challenge/:user" challenge; Dream.post "/register_finish" register_finish; Dream.get "/authenticate/:user" authenticate; Dream.post "/authenticate_finish" authenticate_finish;