4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-15 05:36:37 +00:00

Further enhancements to the WebAuthn libraries.

This commit is contained in:
Buster Neece 2023-11-27 20:50:07 -06:00
parent bbaea5905b
commit dfbd02de93
No known key found for this signature in database
3 changed files with 121 additions and 51 deletions

View File

@ -120,7 +120,7 @@ const {form, resetForm, v$, validate} = useVuelidateOnForm(
createResponse: {required}
},
{
name: '',
name: '',
createResponse: null
}
);
@ -138,34 +138,30 @@ const create = () => {
show();
};
const {isSupported, doRegister, cancel} = useWebAuthn();
const onHidden = () => {
clearContents();
emit('relist');
clearContents();
cancel();
emit('relist');
};
const {axios} = useAxios();
const {isSupported, processServerArgs, processRegisterResponse} = useWebAuthn();
const selectPasskey = async () => {
// GET registration options from the endpoint that calls
const registerArgs = await axios.get(registerWebAuthnUrl.value).then(r => processServerArgs(r.data));
const registerArgs = await axios.get(registerWebAuthnUrl.value).then(r => r.data);
let attResp;
try {
// Pass the options to the authenticator and wait for a response
attResp = await navigator.credentials.create(registerArgs);
form.value.createResponse = processRegisterResponse(attResp);
} catch (error) {
// Some basic error handling
if (error.name === 'InvalidStateError') {
error.value = 'Error: Authenticator was probably already registered by user';
} else {
error.value = error;
try {
form.value.createResponse = await doRegister(registerArgs);
} catch (err) {
if (err.name === 'InvalidStateError') {
error.value = 'Error: Authenticator was probably already registered by user';
} else {
error.value = err;
}
throw err;
}
throw error;
}
};
const doSubmit = async () => {
@ -174,14 +170,14 @@ const doSubmit = async () => {
return;
}
error.value = null;
error.value = null;
axios({
method: 'PUT',
url: registerWebAuthnUrl.value,
method: 'PUT',
url: registerWebAuthnUrl.value,
data: form.value
}).then(() => {
hide();
hide();
}).catch((error) => {
error.value = error.response.data.message;
});

View File

@ -164,7 +164,11 @@ const props = defineProps({
}
});
const {isSupported: passkeySupported, processServerArgs, processValidateResponse} = useWebAuthn();
const {
isSupported: passkeySupported,
isConditionalSupported: passkeyConditionalSupported,
doValidate
} = useWebAuthn();
const {axios} = useAxios();
@ -173,39 +177,39 @@ const $webAuthnForm = ref<HTMLFormElement | null>(null);
const validateArgs = ref<object | null>(null);
const validateData = ref<string | null>(null);
const handleValidationResponse = async (attResp) => {
validateData.value = JSON.stringify(processValidateResponse(attResp));
const handleValidationResponse = async (validateResp) => {
validateData.value = JSON.stringify(validateResp);
await nextTick();
$webAuthnForm.value?.submit();
}
const logInWithPasskey = async () => {
if (null === validateArgs.value) {
validateArgs.value = await axios.get(props.webAuthnUrl).then(r => processServerArgs(r.data));
if (validateArgs.value === null) {
validateArgs.value = await axios.get(props.webAuthnUrl).then(r => r.data);
}
const attResp = await navigator.credentials.get(validateArgs.value);
await handleValidationResponse(attResp);
try {
const validateResp = await doValidate(validateArgs.value, false);
await handleValidationResponse(validateResp);
} catch (e) {
console.error(e);
}
};
onMounted(async () => {
if (passkeySupported && window.PublicKeyCredential
&& PublicKeyCredential.isConditionalMediationAvailable) {
// Check if conditional mediation is available.
const isCMA = await PublicKeyCredential.isConditionalMediationAvailable();
if (!isCMA) {
return;
}
const isConditionalSupported = await passkeyConditionalSupported();
if (!isConditionalSupported) {
return;
}
// Call WebAuthn authentication
validateArgs.value = await axios.get(props.webAuthnUrl).then(r => processServerArgs(r.data));
// Call WebAuthn authentication
validateArgs.value = await axios.get(props.webAuthnUrl).then(r => r.data);
const attResp = await navigator.credentials.get({
...validateArgs.value,
mediation: 'conditional'
});
await handleValidationResponse(attResp);
try {
const validateResp = await doValidate(validateArgs.value, true);
await handleValidationResponse(validateResp);
} catch (e) {
console.error(e);
}
});
</script>

View File

@ -1,6 +1,26 @@
import {cloneDeep} from "lodash";
export default function useWebAuthn() {
let abortController = null;
const abortAndCreateNew = (message: string) => {
if (abortController) {
const abortError = new Error(message);
abortError.name = 'AbortError';
abortController.abort(abortError);
}
abortController = new AbortController();
return abortController.signal;
};
const cancel = () => {
if (abortController) {
const abortError = new Error('Operation cancelled.');
abortError.name = 'AbortError';
abortController.abort(abortError);
}
}
const recursiveBase64StrToArrayBuffer = (obj) => {
const prefix = '=?BINARY?B?';
const suffix = '?=';
@ -36,7 +56,21 @@ export default function useWebAuthn() {
return window.btoa(binary);
}
const isSupported: boolean = !!window.fetch && !!navigator.credentials && !!navigator.credentials.create;
const isSupported: boolean =
window?.PublicKeyCredential !== undefined &&
typeof window.PublicKeyCredential === 'function';
const isConditionalSupported = async (): Promise<boolean> => {
if (!isSupported) {
return false;
}
if (!window.PublicKeyCredential || !PublicKeyCredential.isConditionalMediationAvailable) {
return false;
}
return await PublicKeyCredential.isConditionalMediationAvailable();
};
const processServerArgs = (serverArgs) => {
const newArgs = cloneDeep(serverArgs);
@ -44,6 +78,7 @@ export default function useWebAuthn() {
return newArgs;
};
// Registration (private creation)
const processRegisterResponse = (cred) => {
return {
transports: cred.response.getTransports ? cred.response.getTransports() : null,
@ -52,6 +87,21 @@ export default function useWebAuthn() {
};
}
const doRegister = async (rawArgs: object) => {
const registerArgs = processServerArgs(rawArgs);
const signal = abortAndCreateNew('New registration started.');
const options = {
...registerArgs,
signal: signal
};
const rawResp = await navigator.credentials.create(options);
return processRegisterResponse(rawResp);
};
// Validation (public login)
const processValidateResponse = (cred) => {
return {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
@ -62,10 +112,30 @@ export default function useWebAuthn() {
};
};
const doValidate = async (rawArgs: object, isConditional: boolean = false) => {
const validateArgs = processServerArgs(rawArgs);
const mediation = (isConditional) ? {
mediation: 'conditional'
} : {};
const signal = abortAndCreateNew('New validation started.');
const options = {
...validateArgs,
...mediation,
signal: signal
};
const rawResp = await navigator.credentials.get(options);
return processValidateResponse(rawResp);
};
return {
isSupported,
processServerArgs,
processRegisterResponse,
processValidateResponse,
isConditionalSupported,
doValidate,
doRegister,
cancel
};
}