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:
parent
bbaea5905b
commit
dfbd02de93
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user