2021-09-28 11:30:14 +00:00
|
|
|
let page s b =
|
|
|
|
Printf.sprintf {|
|
|
|
|
<html>
|
|
|
|
<head>
|
|
|
|
<title>WebAuthn Demo</title>
|
|
|
|
<script type="text/javascript" src="/static/base64.js"></script>
|
|
|
|
<script>
|
|
|
|
function bufferEncode(value) {
|
|
|
|
return base64js.fromByteArray(value)
|
|
|
|
.replace(/\+/g, "-")
|
|
|
|
.replace(/\//g, "_")
|
|
|
|
.replace(/=/g, "");
|
|
|
|
}
|
2021-10-07 09:53:38 +00:00
|
|
|
function bufferDecode(value) {
|
|
|
|
return base64js.toByteArray(value);
|
|
|
|
}
|
2021-09-28 11:30:14 +00:00
|
|
|
</script>
|
|
|
|
<script>%s</script>
|
|
|
|
</head><body>%s</body></html>|} s b
|
|
|
|
|
|
|
|
let overview notes authenticated_as users =
|
|
|
|
let authenticated_as =
|
|
|
|
match authenticated_as with
|
|
|
|
| None -> "<h2>Not authenticated</h2>"
|
|
|
|
| Some user -> Printf.sprintf {|<h2>Authenticated as %s</h2>
|
|
|
|
<form action="/logout" method="post"><input type="submit" value="Log out"/></form>
|
|
|
|
|} user
|
|
|
|
and links =
|
|
|
|
{|<h2>Register</h2><ul>
|
|
|
|
<li><a href="/register">register</a></li>
|
|
|
|
</ul>
|
|
|
|
|}
|
|
|
|
and users =
|
|
|
|
String.concat ""
|
|
|
|
("<h2>Users</h2><ul>" ::
|
2021-10-05 13:09:35 +00:00
|
|
|
Hashtbl.fold (fun id (name, keys) acc ->
|
2021-10-04 08:42:04 +00:00
|
|
|
let credentials = List.map (fun (_, cid, _) ->
|
|
|
|
Base64.encode_string ~pad:false ~alphabet:Base64.uri_safe_alphabet cid)
|
|
|
|
keys
|
|
|
|
in
|
2021-10-05 13:09:35 +00:00
|
|
|
(Printf.sprintf "<li>%s [<a href=/authenticate/%s>authenticate</a>] (%s)</li>" name id (String.concat ", " credentials)) :: acc)
|
2021-09-28 11:30:14 +00:00
|
|
|
users [] @ [ "</ul>" ])
|
|
|
|
in
|
|
|
|
page "" (String.concat "" (notes @ [authenticated_as;links;users]))
|
|
|
|
|
2021-10-04 08:43:32 +00:00
|
|
|
let register_view origin user =
|
2021-09-28 11:30:14 +00:00
|
|
|
let script = Printf.sprintf {|
|
2021-10-04 15:19:34 +00:00
|
|
|
function makePublicKey(challengeData, attestation) {
|
2021-10-07 09:53:38 +00:00
|
|
|
let challenge = bufferDecode(challengeData.challenge);
|
|
|
|
let user_id = bufferDecode(challengeData.user.id);
|
2021-10-04 08:43:32 +00:00
|
|
|
return {
|
|
|
|
challenge: challenge,
|
|
|
|
rp: {
|
|
|
|
id: "%s",
|
|
|
|
name: "WebAuthn Demo from robur.coop"
|
|
|
|
},
|
|
|
|
user: {
|
|
|
|
id: user_id,
|
|
|
|
displayName: challengeData.user.displayName,
|
|
|
|
name: challengeData.user.name
|
|
|
|
},
|
|
|
|
pubKeyCredParams: [
|
|
|
|
{
|
|
|
|
type: "public-key",
|
|
|
|
alg: -7
|
|
|
|
}
|
|
|
|
],
|
2021-10-04 15:19:34 +00:00
|
|
|
attestation: attestation,
|
2021-10-04 14:36:00 +00:00
|
|
|
excludeCredentials: challengeData.excludeCredentials.map(id => ({ type: "public-key",
|
2021-10-07 09:53:38 +00:00
|
|
|
id: bufferDecode(id)}))
|
2021-10-04 08:43:32 +00:00
|
|
|
};
|
|
|
|
}
|
2021-10-04 15:19:34 +00:00
|
|
|
function do_register(username, attestation) {
|
2021-10-04 14:36:00 +00:00
|
|
|
fetch("/registration-challenge/"+username)
|
2021-10-04 08:43:32 +00:00
|
|
|
.then(response => response.json())
|
|
|
|
.then(function (challengeData) {
|
2021-10-04 15:19:34 +00:00
|
|
|
let publicKey = makePublicKey(challengeData, attestation);
|
2021-10-04 08:43:32 +00:00
|
|
|
navigator.credentials.create({ publicKey })
|
|
|
|
.then(function (credential) {
|
|
|
|
let response = credential.response;
|
|
|
|
let attestationObject = new Uint8Array(response.attestationObject);
|
|
|
|
let clientDataJSON = new Uint8Array(response.clientDataJSON);
|
2021-09-28 11:30:14 +00:00
|
|
|
|
2021-10-04 14:36:00 +00:00
|
|
|
let body =
|
2021-10-04 08:43:32 +00:00
|
|
|
JSON.stringify({
|
2021-10-05 15:56:20 +00:00
|
|
|
attestationObject: bufferEncode(attestationObject),
|
|
|
|
clientDataJSON: bufferEncode(clientDataJSON),
|
2021-10-04 08:43:32 +00:00
|
|
|
});
|
2021-09-28 11:30:14 +00:00
|
|
|
|
2021-10-04 08:43:32 +00:00
|
|
|
let headers = {'Content-type': "application/json; charset=utf-8"};
|
2021-09-28 11:30:14 +00:00
|
|
|
|
2021-10-05 13:09:35 +00:00
|
|
|
let request = new Request('/register_finish/'+challengeData.user.id, { method: 'POST', body: body, headers: headers } );
|
2021-10-04 08:43:32 +00:00
|
|
|
fetch(request)
|
|
|
|
.then(function (response) {
|
|
|
|
if (!response.ok && response.status != 403) {
|
2021-10-04 14:36:00 +00:00
|
|
|
alert("bad response: " + response.status);
|
2021-10-04 08:43:32 +00:00
|
|
|
return
|
|
|
|
};
|
|
|
|
response.json().then(function (success) {
|
|
|
|
alert(success ? "Successfully registered!" : "Failed to register :(");
|
|
|
|
window.location = "/";
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}).catch(function (err) {
|
2021-10-13 10:59:33 +00:00
|
|
|
// XXX: only if the exception came from navigator.credentials.create()
|
|
|
|
if (err.name === "InvalidStateError") {
|
|
|
|
alert("authenticator already registered");
|
|
|
|
} else {
|
|
|
|
alert("exception: " + err);
|
|
|
|
}
|
2021-10-05 13:09:35 +00:00
|
|
|
window.location = "/";
|
2021-10-04 08:43:32 +00:00
|
|
|
});
|
2021-09-28 11:30:14 +00:00
|
|
|
});
|
2021-10-04 08:43:32 +00:00
|
|
|
}
|
|
|
|
function doit() {
|
|
|
|
let username = document.getElementById("username").value;
|
2021-10-04 15:19:34 +00:00
|
|
|
let attestation = document.getElementById("attestation").value;
|
|
|
|
return do_register(username, attestation);
|
2021-10-04 08:43:32 +00:00
|
|
|
}
|
|
|
|
|} origin
|
2021-09-28 11:30:14 +00:00
|
|
|
and body =
|
|
|
|
Printf.sprintf {|
|
2021-10-04 08:43:32 +00:00
|
|
|
<p>Welcome.</p>
|
|
|
|
<form method="post" id="form" onsubmit="return false;">
|
|
|
|
<label for="username" >Desired username</label><input name="username" id="username" value="%s"/>
|
2021-10-04 15:19:34 +00:00
|
|
|
<label for="attestation">Attestation type</label><select name="attestation" id="attestation">
|
|
|
|
<option value="direct">direct</option>
|
|
|
|
<option value="indirect">indirect</option>
|
|
|
|
<option value="none">none</option>
|
|
|
|
</select>
|
2021-10-07 09:53:38 +00:00
|
|
|
<button id="button" type="button" onmousedown="doit()">Register</button>
|
2021-10-04 08:43:32 +00:00
|
|
|
</form>
|
2021-09-28 11:30:14 +00:00
|
|
|
|} user
|
|
|
|
in
|
|
|
|
page script body
|
|
|
|
|
2021-09-29 14:34:09 +00:00
|
|
|
let authenticate_view challenge credentials user =
|
2021-09-28 11:30:14 +00:00
|
|
|
let script =
|
|
|
|
Printf.sprintf {|
|
2021-10-04 14:36:00 +00:00
|
|
|
let request_options = {
|
2021-10-07 09:53:38 +00:00
|
|
|
challenge: bufferDecode("%s"),
|
|
|
|
allowCredentials: %s.map(x => { x.id = bufferDecode(x.id); return x }),
|
2021-09-29 14:34:09 +00:00
|
|
|
};
|
|
|
|
navigator.credentials.get({ publicKey: request_options })
|
|
|
|
.then(function (assertion) {
|
|
|
|
let response = assertion.response;
|
|
|
|
let authenticatorData = new Uint8Array(assertion.response.authenticatorData);
|
|
|
|
let clientDataJSON = new Uint8Array(assertion.response.clientDataJSON);
|
|
|
|
let signature = new Uint8Array(assertion.response.signature);
|
|
|
|
let userHandle = assertion.response.userHandle ? new Uint8Array(assertion.response.userHandle) : null;
|
|
|
|
|
2021-10-04 14:36:00 +00:00
|
|
|
let body =
|
2021-09-29 14:34:09 +00:00
|
|
|
JSON.stringify({
|
2021-10-05 15:56:20 +00:00
|
|
|
authenticatorData: bufferEncode(authenticatorData),
|
|
|
|
clientDataJSON: bufferEncode(clientDataJSON),
|
|
|
|
signature: bufferEncode(signature),
|
|
|
|
userHandle: userHandle ? bufferEncode(userHandle) : null,
|
2021-09-29 14:34:09 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
let headers = {'Content-type': "application/json; charset=utf-8"};
|
2021-10-04 15:19:34 +00:00
|
|
|
let username = window.location.pathname.substring("/authenticate/".length);
|
2021-10-05 15:56:20 +00:00
|
|
|
let request = new Request('/authenticate_finish/'+assertion.id+'/'+username, { method: 'POST', body: body, headers: headers } );
|
2021-09-29 14:34:09 +00:00
|
|
|
fetch(request)
|
|
|
|
.then(function (response) {
|
|
|
|
if (!response.ok) {
|
2021-10-04 14:36:00 +00:00
|
|
|
alert("bad response: " + response.status);
|
|
|
|
window.location = "/";
|
|
|
|
return
|
2021-09-29 14:34:09 +00:00
|
|
|
};
|
2021-10-04 14:36:00 +00:00
|
|
|
response.json().then(function (success) {
|
|
|
|
alert(success ? "Successfully authenticated!" : "Failed to authenticate :(");
|
|
|
|
window.location = "/";
|
|
|
|
});
|
2021-09-29 14:34:09 +00:00
|
|
|
});
|
|
|
|
}).catch(function (err) {
|
2021-10-04 14:36:00 +00:00
|
|
|
alert("exception: " + err);
|
2021-10-05 13:09:35 +00:00
|
|
|
window.location = "/";
|
2021-09-29 14:34:09 +00:00
|
|
|
});
|
|
|
|
|} challenge
|
|
|
|
(Yojson.to_string (`List
|
|
|
|
(List.map (fun credential_id ->
|
|
|
|
(`Assoc ["id", `String credential_id; "type", `String "public-key"]))
|
|
|
|
credentials)))
|
2021-09-28 11:30:14 +00:00
|
|
|
and body =
|
|
|
|
Printf.sprintf {|
|
2021-09-29 14:34:09 +00:00
|
|
|
<p>Touch your token to authenticate as %S.</p>
|
2021-09-28 11:30:14 +00:00
|
|
|
|} user
|
|
|
|
in
|
|
|
|
page script body
|