import { unreachable } from "./lib.js";
const WEBAUTHN_TIMEOUT = 60 * 1000;
interface KittyboxWebauthnPreRegistrationData {
challenge: string,
rp: PublicKeyCredentialRpEntity,
user: {
cred_id: string,
name: string,
displayName: string
}
}
async function webauthn_create_credential() {
const response = await fetch("/.kittybox/webauthn/pre_register");
const { challenge, rp, user }: KittyboxWebauthnPreRegistrationData = await response.json();
return await navigator.credentials.create({
publicKey: {
challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)),
rp: rp,
user: {
id: Uint8Array.from(user.cred_id, (c) => c.charCodeAt(0)),
name: user.name,
displayName: user.displayName
},
pubKeyCredParams: [{alg: -7, type: "public-key"}],
authenticatorSelection: {},
timeout: WEBAUTHN_TIMEOUT,
attestation: "none"
}
});
}
interface KittyboxWebauthnCredential {
id: string,
type: "public-key"
}
interface KittyboxWebauthnPreAuthenticationData {
challenge: string,
credentials: KittyboxWebauthnCredential[]
}
async function webauthn_authenticate() {
const response = await fetch("/.kittybox/webauthn/pre_auth");
const { challenge, credentials } = await response.json() as unknown as KittyboxWebauthnPreAuthenticationData;
try {
return await navigator.credentials.get({
publicKey: {
challenge: Uint8Array.from(challenge, (c) => c.charCodeAt(0)),
allowCredentials: credentials.map(cred => ({
id: Uint8Array.from(cred.id, c => c.charCodeAt(0)),
type: cred.type
})),
timeout: WEBAUTHN_TIMEOUT
}
})
} catch (e) {
console.error("WebAuthn authentication failed:", e);
alert("Using your authenticator failed. (Check the DevTools for details)");
throw e;
}
}
export async function submit_handler(e: SubmitEvent) {
e.preventDefault();
if (e.target != null && e.target instanceof HTMLFormElement) {
const form = e.target as HTMLFormElement;
let scopes: Array<string>;
let scope_elem = form.elements.namedItem("scope");
if (scope_elem == null) {
scopes = []
} else if (scope_elem instanceof Element) {
scopes = ([scope_elem] as Array<HTMLInputElement>)
.filter((e: HTMLInputElement) => e.checked)
.map((e: HTMLInputElement) => e.value);
} else if (scope_elem instanceof RadioNodeList) {
scopes = (Array.from(scope_elem) as Array<HTMLInputElement>)
.filter((e: HTMLInputElement) => e.checked)
.map((e: HTMLInputElement) => e.value);
} else {
unreachable("HTMLFormControlsCollection returned something that's not null, Element or RadioNodeList")
}
const authorization_request = {
response_type: (form.elements.namedItem("response_type") as HTMLInputElement).value,
client_id: (form.elements.namedItem("client_id") as HTMLInputElement).value,
redirect_uri: (form.elements.namedItem("redirect_uri") as HTMLInputElement).value,
state: (form.elements.namedItem("state") as HTMLInputElement).value,
code_challenge: (form.elements.namedItem("code_challenge") as HTMLInputElement).value,
code_challenge_method: (form.elements.namedItem("code_challenge_method") as HTMLInputElement).value,
// I would love to leave that as a list, but such is the form of
// IndieAuth. application/x-www-form-urlencoded doesn't have
// lists, so scopes are space-separated instead. It is annoying.
scope: scopes.length > 0 ? scopes.join(" ") : undefined,
};
let credential = null;
switch ((form.elements.namedItem("auth_method") as HTMLInputElement).value) {
case "password":
credential = (form.elements.namedItem("user_password") as HTMLInputElement).value;
if (credential.length == 0) {
alert("Please enter a password.")
return
}
break;
case "webauthn":
// credential = await webauthn_authenticate();
alert("WebAuthn isn't implemented yet!")
return
break
default:
alert("Please choose an authentication method.")
return
}
console.log("Authorization request:", authorization_request);
console.log("Authentication method:", credential);
const body = JSON.stringify({
request: authorization_request,
authorization_method: credential
});
console.log(body);
const response = await fetch(form.action, {
method: form.method,
body: body,
headers: {
"Content-Type": "application/json"
}
});
if (response.ok) {
let location = response.headers.get("Location");
if (location != null) {
window.location.href = location
} else {
throw "Error: didn't return a location"
}
}
} else {
return
}
}