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
136
bin/template.ml
136
bin/template.ml
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue