about summary refs log tree commit diff
path: root/kittybox-rs/src/frontend/indieauth.js
blob: 1762bddf1e294edb780cca27e65805f28b6393a4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
const WEBAUTHN_TIMEOUT = 60 * 1000;

async function webauthn_create_credential() {
  const response = await fetch("/.kittybox/webauthn/pre_register");
  const { challenge, rp, user } = 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),
        name: user.name,
        displayName: user.displayName
      },
      pubKeyCredParams: [{alg: -7, type: "public-key"}],
      authenticatorSelection: {},
      timeout: WEBAUTHN_TIMEOUT,
      attestation: "none"
    }
  });
}

async function webauthn_authenticate() {
  const response = await fetch("/.kittybox/webauthn/pre_auth");
  const { challenge, credentials } = await response.json();

  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;
  }
}

async function submit_handler(e) {
  e.preventDefault();
  const form = e.target;

  let scopes = form.elements.scope === undefined ? [] : Array.from(form.elements.scope)
      .filter(e => e.checked)
      .map(e => e.value);

  const authorization_request = {
    response_type: form.elements.response_type.value,
    client_id: form.elements.client_id.value,
    redirect_uri: form.elements.redirect_uri.value,
    state: form.elements.state.value,
    code_challenge: form.elements.code_challenge.value,
    code_challenge_method: form.elements.code_challenge_method.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.auth_method.value) {
  case "password":
    credential = form.elements.user_password.value;
    if (credential.length == 0) {
      alert("Please enter a password.")
      return
    }
    break;
  case "webauthn":
    credential = await webauthn_authenticate();
    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) {
    window.location.href = response.headers.get("Location")
  }
}

document.getElementById("indieauth_page")
  .addEventListener("submit", submit_handler);