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 in
page "" (String.concat "" (notes @ [authenticated_as;links;users])) page "" (String.concat "" (notes @ [authenticated_as;links;users]))
let register_view origin user challenge userid = let register_view origin user =
let script = Printf.sprintf {| let script = Printf.sprintf {|
var publicKey = { function makePublicKey(challengeData) {
challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), let challenge = Uint8Array.from(window.atob(challengeData.challenge), c=>c.charCodeAt(0));
rp: { let user_id = Uint8Array.from(window.atob(challengeData.user.id), c=>c.charCodeAt(0));
id: "%s", return {
name: "WebAuthn Demo from robur.coop" challenge: challenge,
}, rp: {
user: { id: "%s",
id: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), name: "WebAuthn Demo from robur.coop"
displayName: "%s", },
name: "%s" user: {
}, id: user_id,
pubKeyCredParams: [ displayName: challengeData.user.displayName,
{ name: challengeData.user.name
type: "public-key", },
alg: -7 pubKeyCredParams: [
} {
], type: "public-key",
attestation: "direct" alg: -7
}; }
navigator.credentials.create({ publicKey }) ],
.then(function (credential) { attestation: "direct"
// send attestation response and client extensions };
// to the server to proceed with the registration }
// of the credential function do_register(username) {
console.log(credential); fetch("/challenge/"+username)
// Move data into Arrays incase it is super long .then(response => response.json())
let response = credential.response; .then(function (challengeData) {
let attestationObject = new Uint8Array(response.attestationObject); console.log("got challenge data");
let clientDataJSON = new Uint8Array(response.clientDataJSON); console.log(challengeData);
let rawId = new Uint8Array(credential.rawId); 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 = var body =
JSON.stringify({ JSON.stringify({
id: credential.id, id: credential.id,
rawId: bufferEncode(rawId), rawId: bufferEncode(rawId),
type: credential.type, type: credential.type,
response: { response: {
attestationObject: bufferEncode(attestationObject), attestationObject: bufferEncode(attestationObject),
clientDataJSON: bufferEncode(clientDataJSON), clientDataJSON: bufferEncode(clientDataJSON),
}, },
}); });
console.log(body); 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 } ); let request = new Request('/register_finish', { method: 'POST', body: body, headers: headers } );
fetch(request) fetch(request)
.then(function (response) { .then(function (response) {
console.log(response); console.log(response);
if (!response.ok) { if (!response.ok && response.status != 403) {
console.log("bad response: " + response.status); 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); function doit() {
}); let username = document.getElementById("username").value;
|} challenge origin userid user user return do_register(username);
}
|} origin
and body = and body =
Printf.sprintf {| 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 |} user
in in
page script body page script body

View file

@ -37,7 +37,7 @@ let add_routes t =
Dream.html (Template.overview flash authenticated_as users) Dream.html (Template.overview flash authenticated_as users)
in in
let register req = let register _req =
let user = let user =
(* match Dream.session "authenticated_as" req with (* match Dream.session "authenticated_as" req with
| None -> *) gen_data ~alphabet:Base64.uri_safe_alphabet 8 | None -> *) gen_data ~alphabet:Base64.uri_safe_alphabet 8
@ -47,12 +47,29 @@ let add_routes t =
| None -> [] | None -> []
| Some keys -> List.map (fun (_, kh, _) -> kh) keys | Some keys -> List.map (fun (_, kh, _) -> kh) keys
in 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) let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16)
and userid = Base64.encode_string user (* [userid] should be a random value *)
in and userid = Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet user in
Hashtbl.replace challenges challenge user; 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.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 in
let register_finish req = let register_finish req =
@ -68,7 +85,7 @@ let add_routes t =
Logs.warn (fun m -> m "error %a" Webauthn.pp_error e); Logs.warn (fun m -> m "error %a" Webauthn.pp_error e);
let err = to_string e in let err = to_string e in
Flash_message.put_flash "" ("Registration failed " ^ err) req; Flash_message.put_flash "" ("Registration failed " ^ err) req;
Dream.redirect req "/" Dream.json "false"
| Ok (_aaguid, credential_id, pubkey, _client_extensions, user_present, | Ok (_aaguid, credential_id, pubkey, _client_extensions, user_present,
user_verified, sig_count, _authenticator_extensions, attestation_cert) -> user_verified, sig_count, _authenticator_extensions, attestation_cert) ->
ignore (check_counter (credential_id, pubkey) sig_count); ignore (check_counter (credential_id, pubkey) sig_count);
@ -79,29 +96,33 @@ let add_routes t =
Dream.respond ~status:`Internal_Server_Error Dream.respond ~status:`Internal_Server_Error
"Internal server error: couldn't find user for challenge" "Internal server error: couldn't find user for challenge"
| Some user -> | Some user ->
Logs.app (fun m -> m "challenge for user %S" user);
Hashtbl.remove challenges challenge; Hashtbl.remove challenges challenge;
match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with match Dream.session "authenticated_as" req, Hashtbl.find_opt users user with
| _, None -> | _, 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) ]; Hashtbl.replace users user [ (pubkey, credential_id, attestation_cert) ];
Dream.invalidate_session req >>= fun () -> Dream.invalidate_session req >>= fun () ->
Flash_message.put_flash "" Flash_message.put_flash ""
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user) (Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
req; req;
Dream.redirect req "/" Dream.json "true"
| Some session_user, Some keys -> | 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 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) ; Hashtbl.replace users user ((pubkey, credential_id, attestation_cert) :: keys) ;
Dream.invalidate_session req >>= fun () -> Dream.invalidate_session req >>= fun () ->
Flash_message.put_flash "" Flash_message.put_flash ""
(Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user) (Printf.sprintf "Successfully registered as %s! <a href=\"/authenticate/%s\">[authenticate]</a>" user user)
req; req;
Dream.redirect req "/" Dream.json "true"
end else 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 -> | None, Some _keys ->
Dream.respond ~status:`Forbidden "Forbidden." Logs.app (fun m -> m "no session user");
Dream.json ~status:`Forbidden "false"
in in
let authenticate req = let authenticate req =
@ -170,6 +191,7 @@ let add_routes t =
Dream.router [ Dream.router [
Dream.get "/" main; Dream.get "/" main;
Dream.get "/register" register; Dream.get "/register" register;
Dream.get "/challenge/:user" challenge;
Dream.post "/register_finish" register_finish; Dream.post "/register_finish" register_finish;
Dream.get "/authenticate/:user" authenticate; Dream.get "/authenticate/:user" authenticate;
Dream.post "/authenticate_finish" authenticate_finish; Dream.post "/authenticate_finish" authenticate_finish;