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.
This commit is contained in:
Reynir Björnsson 2021-10-04 10:43:32 +02:00
parent 4eb1298af7
commit 1587e76169
2 changed files with 114 additions and 66 deletions

View file

@ -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 {|
<p>Welcome %s.</p>
<p>Welcome.</p>
<form method="post" id="form" onsubmit="return false;">
<label for="username" >Desired username</label><input name="username" id="username" value="%s"/>
<button id="button" type="button" onclick="doit()">Register</button>
</form>
|} user
in
page script body

View file

@ -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! <a href=\"/authenticate/%s\">[authenticate]</a>" 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! <a href=\"/authenticate/%s\">[authenticate]</a>" 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;