main.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. 
  2. function base64UrlEncode(value) {
  3. const encoded = btoa(value);
  4. return encoded
  5. .replace(/\+/g, "-") // '+' => '-'
  6. .replace(/\//g, "_") // '/' => '_'
  7. .replace(/=*$/g, ""); // trim trailing '=' padding
  8. }
  9. function base64UrlDecode(value) {
  10. const base64 = value
  11. .replace(/-/g, "+")
  12. .replace(/_/g, "/");
  13. return atob(base64);
  14. }
  15. function coerceToArrayBuffer(value) {
  16. if (!(typeof value === "string")) {
  17. throw new Error(`Invalid input type. Expected string but got ${typeof(value)}`);
  18. }
  19. // base64url to base64
  20. const str = base64UrlDecode(value);
  21. // base64 to Uint8Array
  22. const bytes = new Uint8Array(str.length);
  23. for (let i = 0; i < str.length; i++) {
  24. bytes[i] = str.charCodeAt(i);
  25. }
  26. return bytes;
  27. };
  28. function coerceToBase64Url(value) {
  29. if (!(value instanceof Uint8Array)) {
  30. throw new Error(`Invalid input type. Expected Uint8Array but got ${typeof(value)}`);
  31. }
  32. let str = "";
  33. for (let i = 0; i < value.byteLength; i++) {
  34. str += String.fromCharCode(value[i]);
  35. }
  36. return window.btoa(str);
  37. }
  38. class Registration {
  39. constructor() {
  40. document.getElementById('register').addEventListener('click', this.registerAccount.bind(this));
  41. this.usernameInput = document.getElementById('username');
  42. }
  43. async registerAccount(event) {
  44. if (this.usernameInput.value.length < 3) {
  45. console.error("Username must at least be 3 characters long");
  46. return;
  47. }
  48. const login = base64UrlEncode(this.usernameInput.value);
  49. let options;
  50. try {
  51. options = await this.buildCredentialOptions(login);
  52. } catch (e) {
  53. console.error("Building create options failed with exception", e);
  54. return;
  55. }
  56. // Create the Credential with navigator.credentials.create API
  57. let newCredential;
  58. try {
  59. newCredential = await navigator.credentials.create({
  60. publicKey: options
  61. });
  62. } catch (e) {
  63. console.error("credentials.create failed with exception", e);
  64. return;
  65. }
  66. console.log("PublicKeyCredential created", newCredential);
  67. let result;
  68. try {
  69. result = this.register(login, newCredential);
  70. } catch (e) {
  71. console.error("Registering new credentials with the server failed with exception", e);
  72. return;
  73. }
  74. console.log("New login created", result);
  75. }
  76. async buildCredentialOptions(login) {
  77. const response = await fetch(`/buildCredentialOptions?login=${login}`, {
  78. method: 'GET',
  79. headers: {
  80. 'Accept': 'application/json'
  81. }
  82. });
  83. const options = await response.json();
  84. if (options.status !== "ok") {
  85. throw new Error(`Error in buildCredentialOptions: ${options.errorMessage}`);
  86. }
  87. console.log("Got options", options);
  88. // Turn the challenge back into the accepted format of padded base64
  89. options.challenge = coerceToArrayBuffer(options.challenge);
  90. // Turn ID into a UInt8Array Buffer for some reason
  91. options.user.id = coerceToArrayBuffer(options.user.id);
  92. options.excludeCredentials = options.excludeCredentials.map((c) => {
  93. c.id = coerceToArrayBuffer(c.id);
  94. return c;
  95. });
  96. console.log("Options Formatted", options);
  97. return options;
  98. }
  99. async register(login, newCredential) {
  100. // Move data into Arrays in case it is super long
  101. // TODO: check!
  102. let attestationObject = new Uint8Array(newCredential.response.attestationObject);
  103. let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
  104. let rawId = new Uint8Array(newCredential.rawId);
  105. const data = {
  106. id: newCredential.id,
  107. rawId: coerceToBase64Url(rawId),
  108. type: newCredential.type,
  109. extensions: newCredential.getClientExtensionResults(),
  110. response: {
  111. AttestationObject: coerceToBase64Url(attestationObject),
  112. clientDataJSON: coerceToBase64Url(clientDataJSON),
  113. transports: newCredential.response.getTransports()
  114. }
  115. };
  116. let response = await fetch(`/registerCredential?login=${login}`, {
  117. method: 'POST',
  118. body: JSON.stringify(data),
  119. headers: {
  120. 'Accept': 'application/json',
  121. 'Content-Type': 'application/json'
  122. }
  123. });
  124. const result = await response.json();
  125. console.log("Credential Object", result);
  126. if (result.status !== "ok") {
  127. throw new Error(`Error creating credential: ${result.errorMessage}`);
  128. }
  129. return result;
  130. }
  131. }
  132. class Login {
  133. constructor() {
  134. document.getElementById('login').addEventListener('click', this.login.bind(this));
  135. this.usernameInput = document.getElementById('username');
  136. }
  137. async login(event) {
  138. const login = base64UrlEncode(this.usernameInput.value);
  139. let assertionOptions;
  140. try {
  141. assertionOptions = await this.buildAssertionOptions(login);
  142. } catch (e) {
  143. console.error("Building assertion options failed with exception", e);
  144. return;
  145. }
  146. // ask browser for credentials (browser will ask connected authenticators)
  147. let credential;
  148. try {
  149. credential = await navigator.credentials.get({ publicKey: assertionOptions })
  150. console.log("credentials.get result", credential);
  151. } catch (e) {
  152. console.error("credentials.get failed with exception", e);
  153. return;
  154. }
  155. let result;
  156. try {
  157. result = await this.verifyCredential(login, credential);
  158. } catch (e) {
  159. console.error("Verifying credentials with the server failed with exception", e);
  160. return;
  161. }
  162. console.log("New login created", result);
  163. }
  164. async buildAssertionOptions(login) {
  165. let assertionOptions;
  166. const response = await fetch(`/buildAssertionOptions?login=${login}`, {
  167. method: 'GET',
  168. headers: {
  169. 'Accept': 'application/json'
  170. }
  171. });
  172. assertionOptions = await response.json();
  173. console.log("Raw Assertion Options", assertionOptions);
  174. assertionOptions.challenge = coerceToArrayBuffer(assertionOptions.challenge);
  175. // fix escaping. Change this to coerce
  176. assertionOptions.allowCredentials.map((listItem) => {
  177. listItem.id = coerceToArrayBuffer(listItem.id);
  178. return listItem;
  179. });
  180. console.log("Assertion Options", assertionOptions);
  181. return assertionOptions;
  182. }
  183. async verifyCredential(login, credential) {
  184. let authData = new Uint8Array(credential.response.authenticatorData);
  185. let clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
  186. let rawId = new Uint8Array(credential.rawId);
  187. let sig = new Uint8Array(credential.response.signature);
  188. const data = {
  189. id: credential.id,
  190. rawId: coerceToBase64Url(rawId),
  191. type: credential.type,
  192. extensions: credential.getClientExtensionResults(),
  193. response: {
  194. authenticatorData: coerceToBase64Url(authData),
  195. clientDataJSON: coerceToBase64Url(clientDataJSON),
  196. signature: coerceToBase64Url(sig)
  197. }
  198. };
  199. let response = await fetch("/verifyCredential", {
  200. method: 'POST',
  201. body: JSON.stringify(data),
  202. headers: {
  203. 'Accept': 'application/json',
  204. 'Content-Type': 'application/json'
  205. }
  206. });
  207. const result = await response.json();
  208. console.log("JWT", result);
  209. window.api.setToken(result);
  210. return result;
  211. }
  212. }
  213. class ApiHandler {
  214. constructor() {
  215. document.getElementById('protected').addEventListener('click', this.access.bind(this));
  216. this.userStateElement = document.getElementById('userState');
  217. this.responseElement = document.getElementById('response');
  218. this.token = null;
  219. }
  220. setToken(value) {
  221. this.token = value;
  222. this.userStateElement.textContent = this.token;
  223. }
  224. async access() {
  225. const response = await fetch("/protected", {
  226. method: 'GET',
  227. headers: {
  228. 'Accept': 'application/json',
  229. 'Content-Type': 'application/json',
  230. 'Authorization': `Bearer ${this.token}`
  231. }
  232. });
  233. const data = response.status === 200 ? await response.json() : await response.text();
  234. if (response.status === 200) {
  235. this.responseElement.textContent = JSON.stringify(data, null, 2);
  236. } else {
  237. this.responseElement.textContent = `${response.status} ${response.statusText} - ${data}`;
  238. }
  239. console.log("/protected returned", response);
  240. console.log("data", data);
  241. }
  242. }
  243. window.registration = new Registration();
  244. window.login = new Login();
  245. window.api = new ApiHandler();