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:
parent
4eb1298af7
commit
1587e76169
2 changed files with 114 additions and 66 deletions
|
@ -40,18 +40,21 @@ 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)),
|
||||
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: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)),
|
||||
displayName: "%s",
|
||||
name: "%s"
|
||||
id: user_id,
|
||||
displayName: challengeData.user.displayName,
|
||||
name: challengeData.user.name
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{
|
||||
|
@ -61,6 +64,14 @@ let register_view origin user challenge userid =
|
|||
],
|
||||
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
|
||||
|
@ -91,17 +102,32 @@ let register_view origin user challenge userid =
|
|||
fetch(request)
|
||||
.then(function (response) {
|
||||
console.log(response);
|
||||
if (!response.ok) {
|
||||
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);
|
||||
});
|
||||
|} 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
|
||||
|
|
|
@ -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
|
||||
let challenge = Cstruct.to_string (Mirage_crypto_rng.generate 16)
|
||||
and userid = Base64.encode_string user
|
||||
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)
|
||||
(* [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;
|
||||
|
|
Loading…
Reference in a new issue