about summary refs log tree commit diff
path: root/templates/javascript/src/indieauth.ts
blob: 57f075e55df25d253aebebcf7fc255fda579568e (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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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
  }

}