cleanup authentication flow, similar to registration flow

This commit is contained in:
Robur 2021-10-04 14:36:00 +00:00
parent 1587e76169
commit 3436722505
2 changed files with 32 additions and 36 deletions

View file

@ -62,29 +62,24 @@ let register_view origin user =
alg: -7 alg: -7
} }
], ],
attestation: "direct" attestation: "direct",
excludeCredentials: challengeData.excludeCredentials.map(id => ({ type: "public-key",
id: Uint8Array.from(window.atob(id), c=>c.charCodeAt(0))}))
}; };
} }
function do_register(username) { function do_register(username) {
fetch("/challenge/"+username) fetch("/registration-challenge/"+username)
.then(response => response.json()) .then(response => response.json())
.then(function (challengeData) { .then(function (challengeData) {
console.log("got challenge data");
console.log(challengeData);
let publicKey = makePublicKey(challengeData); let publicKey = makePublicKey(challengeData);
navigator.credentials.create({ publicKey }) navigator.credentials.create({ publicKey })
.then(function (credential) { .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 response = credential.response;
let attestationObject = new Uint8Array(response.attestationObject); let attestationObject = new Uint8Array(response.attestationObject);
let clientDataJSON = new Uint8Array(response.clientDataJSON); let clientDataJSON = new Uint8Array(response.clientDataJSON);
let rawId = new Uint8Array(credential.rawId); let rawId = new Uint8Array(credential.rawId);
var body = let body =
JSON.stringify({ JSON.stringify({
id: credential.id, id: credential.id,
rawId: bufferEncode(rawId), rawId: bufferEncode(rawId),
@ -94,16 +89,14 @@ let register_view origin user =
clientDataJSON: bufferEncode(clientDataJSON), 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 } ); 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);
if (!response.ok && response.status != 403) { if (!response.ok && response.status != 403) {
console.log("bad response: " + response.status); alert("bad response: " + response.status);
return return
}; };
response.json().then(function (success) { response.json().then(function (success) {
@ -112,7 +105,7 @@ let register_view origin user =
}); });
}); });
}).catch(function (err) { }).catch(function (err) {
console.error(err); alert("exception: " + err);
}); });
}); });
} }
@ -135,13 +128,12 @@ let register_view origin user =
let authenticate_view challenge credentials user = let authenticate_view challenge credentials user =
let script = let script =
Printf.sprintf {| Printf.sprintf {|
var request_options = { let request_options = {
challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)), challenge: Uint8Array.from(window.atob("%s"), c=>c.charCodeAt(0)),
allowCredentials: %s.map(x => { x.id = Uint8Array.from(window.atob(x.id), c=>c.charCodeAt(0)); return x }), allowCredentials: %s.map(x => { x.id = Uint8Array.from(window.atob(x.id), c=>c.charCodeAt(0)); return x }),
}; };
navigator.credentials.get({ publicKey: request_options }) navigator.credentials.get({ publicKey: request_options })
.then(function (assertion) { .then(function (assertion) {
console.log(assertion);
let response = assertion.response; let response = assertion.response;
let rawId = new Uint8Array(assertion.rawId); let rawId = new Uint8Array(assertion.rawId);
let authenticatorData = new Uint8Array(assertion.response.authenticatorData); let authenticatorData = new Uint8Array(assertion.response.authenticatorData);
@ -149,7 +141,7 @@ let authenticate_view challenge credentials user =
let signature = new Uint8Array(assertion.response.signature); let signature = new Uint8Array(assertion.response.signature);
let userHandle = assertion.response.userHandle ? new Uint8Array(assertion.response.userHandle) : null; let userHandle = assertion.response.userHandle ? new Uint8Array(assertion.response.userHandle) : null;
var body = let body =
JSON.stringify({ JSON.stringify({
id: assertion.id, id: assertion.id,
rawId: bufferEncode(rawId), rawId: bufferEncode(rawId),
@ -161,20 +153,24 @@ let authenticate_view challenge credentials user =
userHandle: userHandle ? bufferEncode(userHandle) : null, userHandle: userHandle ? bufferEncode(userHandle) : null,
} }
}); });
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('/authenticate_finish', { method: 'POST', body: body, headers: headers } ); let request = new Request('/authenticate_finish', { method: 'POST', body: body, headers: headers } );
fetch(request) fetch(request)
.then(function (response) { .then(function (response) {
console.log(response);
if (!response.ok) { if (!response.ok) {
console.log("bad response: " + response.status); alert("bad response: " + response.status);
window.location = "/";
return
}; };
response.json().then(function (success) {
alert(success ? "Successfully authenticated!" : "Failed to authenticate :(");
window.location = "/";
});
}); });
}).catch(function (err) { }).catch(function (err) {
console.error(err); alert("exception: " + err);
}); });
|} challenge |} challenge
(Yojson.to_string (`List (Yojson.to_string (`List

View file

@ -37,26 +37,25 @@ 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
(* | Some username -> username *) | Some username -> username
in
let _key_handles = match Hashtbl.find_opt users user with
| None -> []
| Some keys -> List.map (fun (_, kh, _) -> kh) keys
in in
Dream.html (Template.register_view (Webauthn.rpid t) user) Dream.html (Template.register_view (Webauthn.rpid t) user)
in in
(* XXX: should we distinguish between register and authenticate challenges? *) let registration_challenge req =
let challenge req =
let user = Dream.param "user" req in 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)
(* [userid] should be a random value *) (* [userid] should be a random value *)
and userid = Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet user 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 credentials = match Hashtbl.find_opt users user with
| None -> []
| Some credentials -> List.map (fun (_, cid, _) -> cid) credentials
in
let challenge_b64 = (Base64.encode_string challenge) in let challenge_b64 = (Base64.encode_string challenge) in
let json = `Assoc [ let json = `Assoc [
"challenge", `String challenge_b64 ; "challenge", `String challenge_b64 ;
@ -65,6 +64,7 @@ let add_routes t =
"name", `String user ; "name", `String user ;
"displayName", `String user ; "displayName", `String user ;
] ; ] ;
"excludeCredentials", `List (List.map (fun s -> `String (Base64.encode_string s)) credentials) ;
] ]
in in
Logs.info (fun m -> m "produced challenge for user %s: %s" user challenge_b64); Logs.info (fun m -> m "produced challenge for user %s: %s" user challenge_b64);
@ -119,7 +119,7 @@ let add_routes t =
Dream.json "true" Dream.json "true"
end else end else
(Logs.info (fun m -> m "session_user %s, user %s" session_user user); (Logs.info (fun m -> m "session_user %s, user %s" session_user user);
Dream.respond ~status:`Forbidden "Forbidden.") Dream.json ~status:`Forbidden "false")
| None, Some _keys -> | None, Some _keys ->
Logs.app (fun m -> m "no session user"); Logs.app (fun m -> m "no session user");
Dream.json ~status:`Forbidden "false" Dream.json ~status:`Forbidden "false"
@ -163,19 +163,19 @@ let add_routes t =
Flash_message.put_flash "" "Successfully authenticated" req; Flash_message.put_flash "" "Successfully authenticated" req;
Dream.put_session "user" user req >>= fun () -> Dream.put_session "user" user req >>= fun () ->
Dream.put_session "authenticated_as" user req >>= fun () -> Dream.put_session "authenticated_as" user req >>= fun () ->
Dream.redirect req "/" Dream.json "true"
end else begin end else begin
Logs.warn (fun m -> m "credential %S for user %S: counter not strictly increasing! \ Logs.warn (fun m -> m "credential %S for user %S: counter not strictly increasing! \
Got %ld, expected >%ld. webauthn device compromised?" Got %ld, expected >%ld. webauthn device compromised?"
(fst credential) user counter (KhPubHashtbl.find counters credential)); (fst credential) user counter (KhPubHashtbl.find counters credential));
Flash_message.put_flash "" "Authentication failure: key compromised?" req; Flash_message.put_flash "" "Authentication failure: key compromised?" req;
Dream.redirect req "/" Dream.json "false"
end end
| Error e -> | Error e ->
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 "" ("Authentication failure: " ^ err) req; Flash_message.put_flash "" ("Authentication failure: " ^ err) req;
Dream.redirect req "/" Dream.json "false"
in in
let logout req = let logout req =
@ -191,7 +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.get "/registration-challenge/:user" registration_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;