cleanup authentication flow, similar to registration flow
This commit is contained in:
parent
1587e76169
commit
3436722505
2 changed files with 32 additions and 36 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue