You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

user-auth-webauthn.js 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import $ from 'jquery';
  2. import {encode, decode} from 'uint8-to-base64';
  3. import {hideElem, showElem} from '../utils/dom.js';
  4. const {appSubUrl, csrfToken} = window.config;
  5. export function initUserAuthWebAuthn() {
  6. if ($('.user.signin.webauthn-prompt').length === 0) {
  7. return;
  8. }
  9. if (!detectWebAuthnSupport()) {
  10. return;
  11. }
  12. $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {})
  13. .done((makeAssertionOptions) => {
  14. makeAssertionOptions.publicKey.challenge = decodeURLEncodedBase64(makeAssertionOptions.publicKey.challenge);
  15. for (let i = 0; i < makeAssertionOptions.publicKey.allowCredentials.length; i++) {
  16. makeAssertionOptions.publicKey.allowCredentials[i].id = decodeURLEncodedBase64(makeAssertionOptions.publicKey.allowCredentials[i].id);
  17. }
  18. navigator.credentials.get({
  19. publicKey: makeAssertionOptions.publicKey
  20. })
  21. .then((credential) => {
  22. verifyAssertion(credential);
  23. }).catch((err) => {
  24. // Try again... without the appid
  25. if (makeAssertionOptions.publicKey.extensions && makeAssertionOptions.publicKey.extensions.appid) {
  26. delete makeAssertionOptions.publicKey.extensions['appid'];
  27. navigator.credentials.get({
  28. publicKey: makeAssertionOptions.publicKey
  29. })
  30. .then((credential) => {
  31. verifyAssertion(credential);
  32. }).catch((err) => {
  33. webAuthnError('general', err.message);
  34. });
  35. return;
  36. }
  37. webAuthnError('general', err.message);
  38. });
  39. }).fail(() => {
  40. webAuthnError('unknown');
  41. });
  42. }
  43. function verifyAssertion(assertedCredential) {
  44. // Move data into Arrays incase it is super long
  45. const authData = new Uint8Array(assertedCredential.response.authenticatorData);
  46. const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
  47. const rawId = new Uint8Array(assertedCredential.rawId);
  48. const sig = new Uint8Array(assertedCredential.response.signature);
  49. const userHandle = new Uint8Array(assertedCredential.response.userHandle);
  50. $.ajax({
  51. url: `${appSubUrl}/user/webauthn/assertion`,
  52. type: 'POST',
  53. data: JSON.stringify({
  54. id: assertedCredential.id,
  55. rawId: encodeURLEncodedBase64(rawId),
  56. type: assertedCredential.type,
  57. clientExtensionResults: assertedCredential.getClientExtensionResults(),
  58. response: {
  59. authenticatorData: encodeURLEncodedBase64(authData),
  60. clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
  61. signature: encodeURLEncodedBase64(sig),
  62. userHandle: encodeURLEncodedBase64(userHandle),
  63. },
  64. }),
  65. contentType: 'application/json; charset=utf-8',
  66. dataType: 'json',
  67. success: (resp) => {
  68. if (resp && resp['redirect']) {
  69. window.location.href = resp['redirect'];
  70. } else {
  71. window.location.href = '/';
  72. }
  73. },
  74. error: (xhr) => {
  75. if (xhr.status === 500) {
  76. webAuthnError('unknown');
  77. return;
  78. }
  79. webAuthnError('unable-to-process');
  80. }
  81. });
  82. }
  83. // Encode an ArrayBuffer into a URLEncoded base64 string.
  84. function encodeURLEncodedBase64(value) {
  85. return encode(value)
  86. .replace(/\+/g, '-')
  87. .replace(/\//g, '_')
  88. .replace(/=/g, '');
  89. }
  90. // Dccode a URLEncoded base64 to an ArrayBuffer string.
  91. function decodeURLEncodedBase64(value) {
  92. return decode(value
  93. .replace(/_/g, '/')
  94. .replace(/-/g, '+'));
  95. }
  96. function webauthnRegistered(newCredential) {
  97. const attestationObject = new Uint8Array(newCredential.response.attestationObject);
  98. const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
  99. const rawId = new Uint8Array(newCredential.rawId);
  100. return $.ajax({
  101. url: `${appSubUrl}/user/settings/security/webauthn/register`,
  102. type: 'POST',
  103. headers: {'X-Csrf-Token': csrfToken},
  104. data: JSON.stringify({
  105. id: newCredential.id,
  106. rawId: encodeURLEncodedBase64(rawId),
  107. type: newCredential.type,
  108. response: {
  109. attestationObject: encodeURLEncodedBase64(attestationObject),
  110. clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
  111. },
  112. }),
  113. dataType: 'json',
  114. contentType: 'application/json; charset=utf-8',
  115. }).then(() => {
  116. window.location.reload();
  117. }).fail((xhr) => {
  118. if (xhr.status === 409) {
  119. webAuthnError('duplicated');
  120. return;
  121. }
  122. webAuthnError('unknown');
  123. });
  124. }
  125. function webAuthnError(errorType, message) {
  126. hideElem($('#webauthn-error [data-webauthn-error-msg]'));
  127. const $errorGeneral = $(`#webauthn-error [data-webauthn-error-msg=general]`);
  128. if (errorType === 'general') {
  129. showElem($errorGeneral);
  130. $errorGeneral.text(message || 'unknown error');
  131. } else {
  132. const $errorTyped = $(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
  133. if ($errorTyped.length) {
  134. showElem($errorTyped);
  135. } else {
  136. showElem($errorGeneral);
  137. $errorGeneral.text(`unknown error type: ${errorType}`);
  138. }
  139. }
  140. $('#webauthn-error').modal('show');
  141. }
  142. function detectWebAuthnSupport() {
  143. if (!window.isSecureContext) {
  144. $('#register-button').prop('disabled', true);
  145. $('#login-button').prop('disabled', true);
  146. webAuthnError('insecure');
  147. return false;
  148. }
  149. if (typeof window.PublicKeyCredential !== 'function') {
  150. $('#register-button').prop('disabled', true);
  151. $('#login-button').prop('disabled', true);
  152. webAuthnError('browser');
  153. return false;
  154. }
  155. return true;
  156. }
  157. export function initUserAuthWebAuthnRegister() {
  158. if ($('#register-webauthn').length === 0) {
  159. return;
  160. }
  161. $('#webauthn-error').modal({allowMultiple: false});
  162. $('#register-webauthn').on('click', (e) => {
  163. e.preventDefault();
  164. if (!detectWebAuthnSupport()) {
  165. return;
  166. }
  167. webAuthnRegisterRequest();
  168. });
  169. }
  170. function webAuthnRegisterRequest() {
  171. if ($('#nickname').val() === '') {
  172. webAuthnError('empty');
  173. return;
  174. }
  175. $.post(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
  176. _csrf: csrfToken,
  177. name: $('#nickname').val(),
  178. }).done((makeCredentialOptions) => {
  179. $('#nickname').closest('div.field').removeClass('error');
  180. makeCredentialOptions.publicKey.challenge = decodeURLEncodedBase64(makeCredentialOptions.publicKey.challenge);
  181. makeCredentialOptions.publicKey.user.id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.user.id);
  182. if (makeCredentialOptions.publicKey.excludeCredentials) {
  183. for (let i = 0; i < makeCredentialOptions.publicKey.excludeCredentials.length; i++) {
  184. makeCredentialOptions.publicKey.excludeCredentials[i].id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.excludeCredentials[i].id);
  185. }
  186. }
  187. navigator.credentials.create({
  188. publicKey: makeCredentialOptions.publicKey
  189. }).then(webauthnRegistered)
  190. .catch((err) => {
  191. if (!err) {
  192. webAuthnError('unknown');
  193. return;
  194. }
  195. webAuthnError('general', err.message);
  196. });
  197. }).fail((xhr) => {
  198. if (xhr.status === 409) {
  199. webAuthnError('duplicated');
  200. return;
  201. }
  202. webAuthnError('unknown');
  203. });
  204. }