code.gitea.io/gitea@v1.21.7/web_src/js/features/user-auth-webauthn.js (about)

     1  import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
     2  import {showElem} from '../utils/dom.js';
     3  import {GET, POST} from '../modules/fetch.js';
     4  
     5  const {appSubUrl} = window.config;
     6  
     7  export async function initUserAuthWebAuthn() {
     8    const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
     9    if (!elPrompt) {
    10      return;
    11    }
    12  
    13    if (!detectWebAuthnSupport()) {
    14      return;
    15    }
    16  
    17    const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
    18    if (res.status !== 200) {
    19      webAuthnError('unknown');
    20      return;
    21    }
    22    const options = await res.json();
    23    options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
    24    for (const cred of options.publicKey.allowCredentials) {
    25      cred.id = decodeURLEncodedBase64(cred.id);
    26    }
    27    try {
    28      const credential = await navigator.credentials.get({
    29        publicKey: options.publicKey
    30      });
    31      await verifyAssertion(credential);
    32    } catch (err) {
    33      if (!options.publicKey.extensions?.appid) {
    34        webAuthnError('general', err.message);
    35        return;
    36      }
    37      delete options.publicKey.extensions.appid;
    38      try {
    39        const credential = await navigator.credentials.get({
    40          publicKey: options.publicKey
    41        });
    42        await verifyAssertion(credential);
    43      } catch (err) {
    44        webAuthnError('general', err.message);
    45      }
    46    }
    47  }
    48  
    49  async function verifyAssertion(assertedCredential) {
    50    // Move data into Arrays in case it is super long
    51    const authData = new Uint8Array(assertedCredential.response.authenticatorData);
    52    const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
    53    const rawId = new Uint8Array(assertedCredential.rawId);
    54    const sig = new Uint8Array(assertedCredential.response.signature);
    55    const userHandle = new Uint8Array(assertedCredential.response.userHandle);
    56  
    57    const res = await POST(`${appSubUrl}/user/webauthn/assertion`, {
    58      data: {
    59        id: assertedCredential.id,
    60        rawId: encodeURLEncodedBase64(rawId),
    61        type: assertedCredential.type,
    62        clientExtensionResults: assertedCredential.getClientExtensionResults(),
    63        response: {
    64          authenticatorData: encodeURLEncodedBase64(authData),
    65          clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
    66          signature: encodeURLEncodedBase64(sig),
    67          userHandle: encodeURLEncodedBase64(userHandle),
    68        },
    69      },
    70    });
    71    if (res.status === 500) {
    72      webAuthnError('unknown');
    73      return;
    74    } else if (res.status !== 200) {
    75      webAuthnError('unable-to-process');
    76      return;
    77    }
    78    const reply = await res.json();
    79  
    80    window.location.href = reply?.redirect ?? `${appSubUrl}/`;
    81  }
    82  
    83  async function webauthnRegistered(newCredential) {
    84    const attestationObject = new Uint8Array(newCredential.response.attestationObject);
    85    const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
    86    const rawId = new Uint8Array(newCredential.rawId);
    87  
    88    const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, {
    89      data: {
    90        id: newCredential.id,
    91        rawId: encodeURLEncodedBase64(rawId),
    92        type: newCredential.type,
    93        response: {
    94          attestationObject: encodeURLEncodedBase64(attestationObject),
    95          clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
    96        },
    97      },
    98    });
    99  
   100    if (res.status === 409) {
   101      webAuthnError('duplicated');
   102      return;
   103    } else if (res.status !== 201) {
   104      webAuthnError('unknown');
   105      return;
   106    }
   107  
   108    window.location.reload();
   109  }
   110  
   111  function webAuthnError(errorType, message) {
   112    const elErrorMsg = document.getElementById(`webauthn-error-msg`);
   113  
   114    if (errorType === 'general') {
   115      elErrorMsg.textContent = message || 'unknown error';
   116    } else {
   117      const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
   118      if (elTypedError) {
   119        elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`;
   120      } else {
   121        elErrorMsg.textContent = `unknown error type: ${errorType}${message ? ` ${message}` : ''}`;
   122      }
   123    }
   124  
   125    showElem('#webauthn-error');
   126  }
   127  
   128  function detectWebAuthnSupport() {
   129    if (!window.isSecureContext) {
   130      webAuthnError('insecure');
   131      return false;
   132    }
   133  
   134    if (typeof window.PublicKeyCredential !== 'function') {
   135      webAuthnError('browser');
   136      return false;
   137    }
   138  
   139    return true;
   140  }
   141  
   142  export function initUserAuthWebAuthnRegister() {
   143    const elRegister = document.getElementById('register-webauthn');
   144    if (!elRegister) {
   145      return;
   146    }
   147    if (!detectWebAuthnSupport()) {
   148      elRegister.disabled = true;
   149      return;
   150    }
   151    elRegister.addEventListener('click', async (e) => {
   152      e.preventDefault();
   153      await webAuthnRegisterRequest();
   154    });
   155  }
   156  
   157  async function webAuthnRegisterRequest() {
   158    const elNickname = document.getElementById('nickname');
   159  
   160    const formData = new FormData();
   161    formData.append('name', elNickname.value);
   162  
   163    const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
   164      data: formData,
   165    });
   166  
   167    if (res.status === 409) {
   168      webAuthnError('duplicated');
   169      return;
   170    } else if (res.status !== 200) {
   171      webAuthnError('unknown');
   172      return;
   173    }
   174  
   175    const options = await res.json();
   176    elNickname.closest('div.field').classList.remove('error');
   177  
   178    options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
   179    options.publicKey.user.id = decodeURLEncodedBase64(options.publicKey.user.id);
   180    if (options.publicKey.excludeCredentials) {
   181      for (const cred of options.publicKey.excludeCredentials) {
   182        cred.id = decodeURLEncodedBase64(cred.id);
   183      }
   184    }
   185  
   186    try {
   187      const credential = await navigator.credentials.create({
   188        publicKey: options.publicKey
   189      });
   190      await webauthnRegistered(credential);
   191    } catch (err) {
   192      webAuthnError('unknown', err);
   193    }
   194  }