Enhance webauthn error handling at authentication

This commit is contained in:
Bubka 2023-09-16 09:35:43 +02:00
parent acc5d7170c
commit 81bf3afc91
2 changed files with 109 additions and 23 deletions

View File

@ -42,7 +42,8 @@
import Form from './../../components/Form'
import WebauthnService from './../../webauthn/WebauthnService'
import { webauthnAbortService } from '../../webauthn/webauthnAbortService'
import { webauthnAbortService } from './../../webauthn/webauthnAbortService'
import { identifyAuthenticationError } from './../../webauthn/identifyAuthenticationError'
export default {
data(){
@ -55,8 +56,7 @@
}),
isBusy: false,
showWebauthn: this.$root.userPreferences.useWebauthnOnly,
csrfRefresher: null,
webauthn: new WebauthnService()
csrfRefresher: null
}
},
@ -105,6 +105,7 @@
*/
async webauthnLogin() {
this.isBusy = false
let webauthnService = new WebauthnService()
// Check https context
if (!window.isSecureContext) {
@ -113,36 +114,26 @@
}
// Check browser support
if (this.webauthn.doesntSupportWebAuthn) {
if (webauthnService.doesntSupportWebAuthn) {
this.$notify({ type: 'is-danger', text: this.$t('errors.browser_does_not_support_webauthn') })
return false
}
const loginOptions = await this.form.post('/webauthn/login/options').then(res => res.data)
const publicKey = this.webauthn.parseIncomingServerOptions(loginOptions)
const credentials = await navigator.credentials.get({ publicKey: publicKey })
const publicKey = webauthnService.parseIncomingServerOptions(loginOptions)
let options = { publicKey }
options.signal = webauthnAbortService.createNewAbortSignal()
const credentials = await navigator.credentials.get(options)
.catch(error => {
if (error.name == 'AbortError') {
this.$notify({ type: 'is-warning', text: this.$t('errors.aborted_by_user') })
}
else if (error.name == 'SecurityError') {
this.$notify({ type: 'is-danger', text: this.$t('errors.security_error_check_rpid') })
}
else if (error.name == 'NotAllowedError') {
this.$notify({ type: 'is-danger', text: this.$t('errors.not_allowed_operation') })
}
else if (error.name == 'NotSupportedError') {
this.$notify({ type: 'is-danger', text: this.$t('errors.no_authenticator_support_specified_algorithms') })
}
else if (error.name == 'InvalidStateError') {
this.$notify({ type: 'is-danger', text: this.$t('auth.webauthn.unknown_device') })
}
else this.$notify({ type: 'is-danger', text: this.$t('errors.unknown_error') })
const webauthnError = identifyAuthenticationError(error, options)
this.$notify({ type: webauthnError.type, text: this.$t(webauthnError.phrase) })
})
if (!credentials) return false
let publicKeyCredential = this.webauthn.parseOutgoingCredentials(credentials)
let publicKeyCredential = webauthnService.parseOutgoingCredentials(credentials)
publicKeyCredential.email = this.form.email
this.axios.post('/webauthn/login', publicKeyCredential, {returnError: true}).then(response => {

View File

@ -0,0 +1,95 @@
/**
* MIT License
*
* Copyright (c) 2023 Bubka - https://github.com/Bubka/2FAuth
* Copyright (c) 2020 Matthew Miller - https://github.com/MasterKale/SimpleWebAuthn
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
import { isValidDomain } from './isValidDomain';
/**
* Attempt to intuit _why_ an error was raised after calling `navigator.credentials.get()`
*/
export function identifyAuthenticationError(error, options) {
const { publicKey } = options;
if (error.name === 'AbortError') {
if (options.signal instanceof AbortSignal) {
// Authentication ceremony was sent an abort signal
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16)
return {
phrase: 'errors.aborted_by_user',
type: 'is-warning'
}
}
} else if (error.name === 'NotAllowedError') {
/**
* Pass the error directly through. Platforms are overloading this error beyond what the spec
* defines and we don't want to overwrite potentially useful error messages.
*/
return {
phrase: 'errors.not_allowed_operation',
type: 'is-danger'
}
} else if (error.name === 'SecurityError') {
const effectiveDomain = window.location.hostname;
if (!isValidDomain(effectiveDomain)) {
// The current location domain is not a valid domain
// https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 5)
return {
phrase: 'errors.2fauth_has_not_a_valid_domain',
type: 'is-danger'
}
} else if (publicKey.rpId !== effectiveDomain) {
// The RP ID "${publicKey.rpId}" is invalid for this domain
// // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 6)
return {
phrase: 'errors.security_error_check_rpid',
type: 'is-danger'
}
}
} else if (error.name === 'UnknownError') {
// The authenticator was unable to process the specified options, or could not create a new assertion signature
// https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 1)
// https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 12)
return {
phrase: 'errors.unknown_error',
type: 'is-danger'
}
}
return {
phrase: 'errors.unknown_error',
type: 'is-danger'
}
}