Add Passkey login support (#31504)
closes #22015
After adding a passkey, you can now simply login with it directly by
clicking `Sign in with a passkey`.

Note for testing. You need to run gitea using `https` to get the full
passkeys experience.
---------
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
parent
5821d22891
commit
91745ae46f
8 changed files with 187 additions and 14 deletions
|
@ -5,25 +5,88 @@ import {GET, POST} from '../modules/fetch.js';
|
|||
const {appSubUrl} = window.config;
|
||||
|
||||
export async function initUserAuthWebAuthn() {
|
||||
const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
|
||||
if (!elPrompt) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!detectWebAuthnSupport()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
|
||||
if (res.status !== 200) {
|
||||
const elSignInPasskeyBtn = document.querySelector('.signin-passkey');
|
||||
if (elSignInPasskeyBtn) {
|
||||
elSignInPasskeyBtn.addEventListener('click', loginPasskey);
|
||||
}
|
||||
|
||||
const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
|
||||
if (elPrompt) {
|
||||
login2FA();
|
||||
}
|
||||
}
|
||||
|
||||
async function loginPasskey() {
|
||||
const res = await GET(`${appSubUrl}/user/webauthn/passkey/assertion`);
|
||||
if (!res.ok) {
|
||||
webAuthnError('unknown');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = await res.json();
|
||||
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
|
||||
for (const cred of options.publicKey.allowCredentials) {
|
||||
for (const cred of options.publicKey.allowCredentials ?? []) {
|
||||
cred.id = decodeURLEncodedBase64(cred.id);
|
||||
}
|
||||
|
||||
try {
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: options.publicKey,
|
||||
});
|
||||
|
||||
// Move data into Arrays in case it is super long
|
||||
const authData = new Uint8Array(credential.response.authenticatorData);
|
||||
const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
|
||||
const rawId = new Uint8Array(credential.rawId);
|
||||
const sig = new Uint8Array(credential.response.signature);
|
||||
const userHandle = new Uint8Array(credential.response.userHandle);
|
||||
|
||||
const res = await POST(`${appSubUrl}/user/webauthn/passkey/login`, {
|
||||
data: {
|
||||
id: credential.id,
|
||||
rawId: encodeURLEncodedBase64(rawId),
|
||||
type: credential.type,
|
||||
clientExtensionResults: credential.getClientExtensionResults(),
|
||||
response: {
|
||||
authenticatorData: encodeURLEncodedBase64(authData),
|
||||
clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
|
||||
signature: encodeURLEncodedBase64(sig),
|
||||
userHandle: encodeURLEncodedBase64(userHandle),
|
||||
},
|
||||
},
|
||||
});
|
||||
if (res.status === 500) {
|
||||
webAuthnError('unknown');
|
||||
return;
|
||||
} else if (!res.ok) {
|
||||
webAuthnError('unable-to-process');
|
||||
return;
|
||||
}
|
||||
const reply = await res.json();
|
||||
|
||||
window.location.href = reply?.redirect ?? `${appSubUrl}/`;
|
||||
} catch (err) {
|
||||
webAuthnError('general', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function login2FA() {
|
||||
const res = await GET(`${appSubUrl}/user/webauthn/assertion`);
|
||||
if (!res.ok) {
|
||||
webAuthnError('unknown');
|
||||
return;
|
||||
}
|
||||
|
||||
const options = await res.json();
|
||||
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
|
||||
for (const cred of options.publicKey.allowCredentials ?? []) {
|
||||
cred.id = decodeURLEncodedBase64(cred.id);
|
||||
}
|
||||
|
||||
try {
|
||||
const credential = await navigator.credentials.get({
|
||||
publicKey: options.publicKey,
|
||||
|
@ -71,7 +134,7 @@ async function verifyAssertion(assertedCredential) {
|
|||
if (res.status === 500) {
|
||||
webAuthnError('unknown');
|
||||
return;
|
||||
} else if (res.status !== 200) {
|
||||
} else if (!res.ok) {
|
||||
webAuthnError('unable-to-process');
|
||||
return;
|
||||
}
|
||||
|
@ -167,7 +230,7 @@ async function webAuthnRegisterRequest() {
|
|||
if (res.status === 409) {
|
||||
webAuthnError('duplicated');
|
||||
return;
|
||||
} else if (res.status !== 200) {
|
||||
} else if (!res.ok) {
|
||||
webAuthnError('unknown');
|
||||
return;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue