mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-04-21 07:58:54 +02:00
Enhance webauthn error handling at registration
This commit is contained in:
parent
935e560552
commit
acc5d7170c
@ -41,7 +41,8 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import Form from './../../components/Form'
|
import Form from './../../components/Form'
|
||||||
import WebAuthn from './../../components/WebAuthn'
|
import WebauthnService from './../../webauthn/WebauthnService'
|
||||||
|
import { webauthnAbortService } from '../../webauthn/webauthnAbortService'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data(){
|
data(){
|
||||||
@ -55,7 +56,7 @@
|
|||||||
isBusy: false,
|
isBusy: false,
|
||||||
showWebauthn: this.$root.userPreferences.useWebauthnOnly,
|
showWebauthn: this.$root.userPreferences.useWebauthnOnly,
|
||||||
csrfRefresher: null,
|
csrfRefresher: null,
|
||||||
webauthn: new WebAuthn()
|
webauthn: new WebauthnService()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -131,7 +132,7 @@
|
|||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.not_allowed_operation') })
|
this.$notify({ type: 'is-danger', text: this.$t('errors.not_allowed_operation') })
|
||||||
}
|
}
|
||||||
else if (error.name == 'NotSupportedError') {
|
else if (error.name == 'NotSupportedError') {
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.unsupported_operation') })
|
this.$notify({ type: 'is-danger', text: this.$t('errors.no_authenticator_support_specified_algorithms') })
|
||||||
}
|
}
|
||||||
else if (error.name == 'InvalidStateError') {
|
else if (error.name == 'InvalidStateError') {
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('auth.webauthn.unknown_device') })
|
this.$notify({ type: 'is-danger', text: this.$t('auth.webauthn.unknown_device') })
|
||||||
|
@ -40,7 +40,9 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import Form from './../../components/Form'
|
import Form from './../../components/Form'
|
||||||
import WebAuthn from './../../components/WebAuthn'
|
import WebauthnService from './../../webauthn/WebauthnService'
|
||||||
|
import { webauthnAbortService } from './../../webauthn/webauthnAbortService'
|
||||||
|
import { identifyRegistrationError } from './../../webauthn/identifyRegistrationError'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data(){
|
data(){
|
||||||
@ -56,8 +58,7 @@
|
|||||||
}),
|
}),
|
||||||
showWebauthnRegistration: false,
|
showWebauthnRegistration: false,
|
||||||
deviceRegistered: false,
|
deviceRegistered: false,
|
||||||
deviceId : null,
|
deviceId : null
|
||||||
webauthn: new WebAuthn()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -87,6 +88,8 @@
|
|||||||
* Register a new security device
|
* Register a new security device
|
||||||
*/
|
*/
|
||||||
async registerWebauthnDevice() {
|
async registerWebauthnDevice() {
|
||||||
|
let webauthnService = new WebauthnService()
|
||||||
|
|
||||||
// Check https context
|
// Check https context
|
||||||
if (!window.isSecureContext) {
|
if (!window.isSecureContext) {
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.https_required') })
|
this.$notify({ type: 'is-danger', text: this.$t('errors.https_required') })
|
||||||
@ -94,29 +97,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check browser support
|
// Check browser support
|
||||||
if (this.webauthn.doesntSupportWebAuthn) {
|
if (webauthnService.doesntSupportWebAuthn) {
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.browser_does_not_support_webauthn') })
|
this.$notify({ type: 'is-danger', text: this.$t('errors.browser_does_not_support_webauthn') })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerOptions = await this.axios.post('/webauthn/register/options').then(res => res.data)
|
const registerOptions = await this.axios.post('/webauthn/register/options').then(res => res.data)
|
||||||
const publicKey = this.webauthn.parseIncomingServerOptions(registerOptions)
|
const publicKey = webauthnService.parseIncomingServerOptions(registerOptions)
|
||||||
let bufferedCredentials
|
|
||||||
|
|
||||||
|
let options = { publicKey }
|
||||||
|
options.signal = webauthnAbortService.createNewAbortSignal()
|
||||||
|
|
||||||
|
let bufferedCredentials
|
||||||
try {
|
try {
|
||||||
bufferedCredentials = await navigator.credentials.create({ publicKey })
|
bufferedCredentials = await navigator.credentials.create(options)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (error.name == 'AbortError') {
|
const webauthnError = identifyRegistrationError(error, options)
|
||||||
this.$notify({ type: 'is-warning', text: this.$t('errors.aborted_by_user') })
|
this.$notify({ type: webauthnError.type, text: this.$t(webauthnError.phrase) })
|
||||||
}
|
|
||||||
else if (error.name == 'NotAllowedError' || 'InvalidStateError') {
|
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.security_device_unsupported') })
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyCredential = this.webauthn.parseOutgoingCredentials(bufferedCredentials);
|
const publicKeyCredential = webauthnService.parseOutgoingCredentials(bufferedCredentials);
|
||||||
|
|
||||||
this.axios.post('/webauthn/register', publicKeyCredential, {returnError: true})
|
this.axios.post('/webauthn/register', publicKeyCredential, {returnError: true})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
@ -67,7 +67,9 @@
|
|||||||
<script>
|
<script>
|
||||||
|
|
||||||
import Form from './../../components/Form'
|
import Form from './../../components/Form'
|
||||||
import WebAuthn from './../../components/WebAuthn'
|
import WebauthnService from './../../webauthn/WebauthnService'
|
||||||
|
import { webauthnAbortService } from './../../webauthn/webauthnAbortService'
|
||||||
|
import { identifyRegistrationError } from './../../webauthn/identifyRegistrationError'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data(){
|
data(){
|
||||||
@ -77,8 +79,7 @@
|
|||||||
}),
|
}),
|
||||||
credentials: [],
|
credentials: [],
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isRemoteUser: false,
|
isRemoteUser: false
|
||||||
webauthn: new WebAuthn()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -135,6 +136,7 @@
|
|||||||
* Register a new security device
|
* Register a new security device
|
||||||
*/
|
*/
|
||||||
async register() {
|
async register() {
|
||||||
|
let webauthnService = new WebauthnService()
|
||||||
|
|
||||||
if (this.isRemoteUser) {
|
if (this.isRemoteUser) {
|
||||||
this.$notify({ type: 'is-warning', text: this.$t('errors.unsupported_with_reverseproxy') })
|
this.$notify({ type: 'is-warning', text: this.$t('errors.unsupported_with_reverseproxy') })
|
||||||
@ -148,38 +150,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check browser support
|
// Check browser support
|
||||||
if (this.webauthn.doesntSupportWebAuthn) {
|
if (webauthnService.doesntSupportWebAuthn) {
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.browser_does_not_support_webauthn') })
|
this.$notify({ type: 'is-danger', text: this.$t('errors.browser_does_not_support_webauthn') })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const registerOptions = await this.axios.post('/webauthn/register/options').then(res => res.data)
|
const registerOptions = await this.axios.post('/webauthn/register/options').then(res => res.data)
|
||||||
const publicKey = this.webauthn.parseIncomingServerOptions(registerOptions)
|
const publicKey = webauthnService.parseIncomingServerOptions(registerOptions)
|
||||||
let bufferedCredentials
|
|
||||||
|
|
||||||
|
let options = { publicKey }
|
||||||
|
options.signal = webauthnAbortService.createNewAbortSignal()
|
||||||
|
|
||||||
|
let bufferedCredentials
|
||||||
try {
|
try {
|
||||||
bufferedCredentials = await navigator.credentials.create({ publicKey })
|
bufferedCredentials = await navigator.credentials.create(options)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
if (error.name == 'AbortError') {
|
const webauthnError = identifyRegistrationError(error, options)
|
||||||
this.$notify({ type: 'is-warning', text: this.$t('errors.aborted_by_user') })
|
this.$notify({ type: webauthnError.type, text: this.$t(webauthnError.phrase) })
|
||||||
}
|
|
||||||
else if (error.name == 'SecurityError') {
|
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.security_error_check_rpid') })
|
|
||||||
}
|
|
||||||
else if (error.name == 'InvalidStateError') {
|
|
||||||
this.$notify({ type: 'is-danger', text: this.$t('errors.security_device_unsupported') })
|
|
||||||
}
|
|
||||||
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.unsupported_operation') })
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKeyCredential = this.webauthn.parseOutgoingCredentials(bufferedCredentials);
|
const publicKeyCredential = webauthnService.parseOutgoingCredentials(bufferedCredentials);
|
||||||
|
|
||||||
this.axios.post('/webauthn/register', publicKeyCredential, {returnError: true})
|
this.axios.post('/webauthn/register', publicKeyCredential, {returnError: true})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
|
160
resources/js/webauthn/identifyRegistrationError.js
Normal file
160
resources/js/webauthn/identifyRegistrationError.js
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* 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.create()`
|
||||||
|
*/
|
||||||
|
export function identifyRegistrationError(error, options) {
|
||||||
|
const { publicKey } = options;
|
||||||
|
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
if (options.signal instanceof AbortSignal) {
|
||||||
|
// Registration 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 === 'ConstraintError') {
|
||||||
|
if (publicKey.authenticatorSelection?.requireResidentKey === true) {
|
||||||
|
// Discoverable credentials were required but no available authenticator supported it
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 4)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.authenticator_missing_discoverable_credential_support',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (publicKey.authenticatorSelection?.userVerification === 'required') {
|
||||||
|
// User verification was required but no available authenticator supported it
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 5)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.authenticator_missing_user_verification_support',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (error.name === 'InvalidStateError') {
|
||||||
|
// The authenticator was previously registered
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 20)
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 3)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.security_device_already_registered',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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 === 'NotSupportedError') {
|
||||||
|
|
||||||
|
const validPubKeyCredParams = publicKey.pubKeyCredParams.filter(
|
||||||
|
(param) => param.type === 'public-key',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (validPubKeyCredParams.length === 0) {
|
||||||
|
// No entry in pubKeyCredParams was of type "public-key"
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 10)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.no_entry_was_of_type_public_key',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No available authenticator supported any of the specified pubKeyCredParams algorithms
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.no_authenticator_support_specified_algorithms',
|
||||||
|
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-createCredential (Step 7)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.2fauth_has_not_a_valid_domain',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (publicKey.rp.id !== effectiveDomain) {
|
||||||
|
// The RP ID "${publicKey.rp.id}" is invalid for this domain
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 8)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.security_error_check_rpid',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (error.name === 'TypeError') {
|
||||||
|
if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) {
|
||||||
|
// User ID was not between 1 and 64 characters
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 5)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.user_id_not_between_1_64',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (error.name === 'UnknownError') {
|
||||||
|
// The authenticator was unable to process the specified options, or could not create a new credential
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 1)
|
||||||
|
// https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 8)
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.unknown_error',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
phrase: 'errors.unknown_error',
|
||||||
|
type: 'is-danger'
|
||||||
|
}
|
||||||
|
}
|
15
resources/js/webauthn/isValidDomain.js
Normal file
15
resources/js/webauthn/isValidDomain.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* A simple test to determine if a hostname is a properly-formatted domain name
|
||||||
|
*
|
||||||
|
* A "valid domain" is defined here: https://url.spec.whatwg.org/#valid-domain
|
||||||
|
*
|
||||||
|
* Regex sourced from here:
|
||||||
|
* https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s15.html
|
||||||
|
*/
|
||||||
|
export function isValidDomain(hostname) {
|
||||||
|
return (
|
||||||
|
// Consider localhost valid as well since it's okay wrt Secure Contexts
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname)
|
||||||
|
);
|
||||||
|
}
|
51
resources/js/webauthn/webauthnAbortService.js
Normal file
51
resources/js/webauthn/webauthnAbortService.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* A way to cancel an existing WebAuthn request, for example to cancel a
|
||||||
|
* WebAuthn autofill authentication request for a manual authentication attempt.
|
||||||
|
*/
|
||||||
|
class WebauthnAbortService {
|
||||||
|
controller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an abort signal that will help support multiple auth attempts without needing to
|
||||||
|
* reload the page
|
||||||
|
*/
|
||||||
|
createNewAbortSignal() {
|
||||||
|
// Abort any existing calls to navigator.credentials.create() or navigator.credentials.get()
|
||||||
|
if (this.controller) {
|
||||||
|
const abortError = new Error(
|
||||||
|
'Cancelling existing WebAuthn API call for new one',
|
||||||
|
);
|
||||||
|
abortError.name = 'AbortError';
|
||||||
|
this.controller.abort(abortError);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newController = new AbortController();
|
||||||
|
|
||||||
|
this.controller = newController;
|
||||||
|
return newController.signal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const webauthnAbortService = new WebauthnAbortService();
|
@ -23,10 +23,10 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class WebAuthn {
|
export default class WebauthnService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new WebAuthn instance.
|
* Create a new WebauthnService instance.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor () {
|
constructor () {
|
||||||
@ -40,12 +40,12 @@ export default class WebAuthn {
|
|||||||
* @returns {Object}
|
* @returns {Object}
|
||||||
*/
|
*/
|
||||||
parseIncomingServerOptions(publicKey) {
|
parseIncomingServerOptions(publicKey) {
|
||||||
publicKey.challenge = WebAuthn.uint8Array(publicKey.challenge);
|
publicKey.challenge = WebauthnService.uint8Array(publicKey.challenge);
|
||||||
|
|
||||||
if ('user' in publicKey) {
|
if ('user' in publicKey) {
|
||||||
publicKey.user = {
|
publicKey.user = {
|
||||||
...publicKey.user,
|
...publicKey.user,
|
||||||
id: WebAuthn.uint8Array(publicKey.user.id)
|
id: WebauthnService.uint8Array(publicKey.user.id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ export default class WebAuthn {
|
|||||||
.filter(key => key in publicKey)
|
.filter(key => key in publicKey)
|
||||||
.forEach(key => {
|
.forEach(key => {
|
||||||
publicKey[key] = publicKey[key].map(data => {
|
publicKey[key] = publicKey[key].map(data => {
|
||||||
return {...data, id: WebAuthn.uint8Array(data.id)};
|
return {...data, id: WebauthnService.uint8Array(data.id)};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ export default class WebAuthn {
|
|||||||
let parseCredentials = {
|
let parseCredentials = {
|
||||||
id: credentials.id,
|
id: credentials.id,
|
||||||
type: credentials.type,
|
type: credentials.type,
|
||||||
rawId: WebAuthn.arrayToBase64String(credentials.rawId),
|
rawId: WebauthnService.arrayToBase64String(credentials.rawId),
|
||||||
response: {}
|
response: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export default class WebAuthn {
|
|||||||
"userHandle"
|
"userHandle"
|
||||||
]
|
]
|
||||||
.filter(key => key in credentials.response)
|
.filter(key => key in credentials.response)
|
||||||
.forEach(key => parseCredentials.response[key] = WebAuthn.arrayToBase64String(credentials.response[key]));
|
.forEach(key => parseCredentials.response[key] = WebauthnService.arrayToBase64String(credentials.response[key]));
|
||||||
|
|
||||||
return parseCredentials;
|
return parseCredentials;
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ export default class WebAuthn {
|
|||||||
*/
|
*/
|
||||||
static uint8Array(input, useAtob = false) {
|
static uint8Array(input, useAtob = false) {
|
||||||
return Uint8Array.from(
|
return Uint8Array.from(
|
||||||
useAtob ? atob(input) : WebAuthn.base64UrlDecode(input), c => c.charCodeAt(0)
|
useAtob ? atob(input) : WebauthnService.base64UrlDecode(input), c => c.charCodeAt(0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ export default class WebAuthn {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
static supportsWebAuthn() {
|
static supportsWebAuthn() {
|
||||||
return typeof PublicKeyCredential != "undefined";
|
return (window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,11 +33,16 @@
|
|||||||
'https_required' => 'HTTPS context required',
|
'https_required' => 'HTTPS context required',
|
||||||
'browser_does_not_support_webauthn' => 'Your device does not support webauthn. Try again later using a more modern browser',
|
'browser_does_not_support_webauthn' => 'Your device does not support webauthn. Try again later using a more modern browser',
|
||||||
'aborted_by_user' => 'Aborted by user',
|
'aborted_by_user' => 'Aborted by user',
|
||||||
'security_device_unsupported' => 'Unsupported or in use device',
|
'security_device_already_registered' => 'Device already registered',
|
||||||
'not_allowed_operation' => 'Operation not allowed',
|
'not_allowed_operation' => 'Operation not allowed',
|
||||||
'unsupported_operation' => 'Unsupported operation',
|
'no_authenticator_support_specified_algorithms' => 'No authenticators support specified algorithms',
|
||||||
|
'authenticator_missing_discoverable_credential_support' => 'Authenticator missing discoverable credential support',
|
||||||
|
'authenticator_missing_user_verification_support' => 'Authenticator missing user verification support',
|
||||||
'unknown_error' => 'Unknown error',
|
'unknown_error' => 'Unknown error',
|
||||||
'security_error_check_rpid' => 'Security error<br/>Check your WEBAUTHN_ID env var',
|
'security_error_check_rpid' => 'Security error<br/>Check your WEBAUTHN_ID env var',
|
||||||
|
'2fauth_has_not_a_valid_domain' => '2FAuth\'s domain is not a valid domain',
|
||||||
|
'user_id_not_between_1_64' => 'User ID was not between 1 and 64 chars',
|
||||||
|
'no_entry_was_of_type_public_key' => 'No entry was of type "public-key"',
|
||||||
'unsupported_with_reverseproxy' => 'Not applicable when using an auth proxy',
|
'unsupported_with_reverseproxy' => 'Not applicable when using an auth proxy',
|
||||||
'user_deletion_failed' => 'User account deletion failed, no data have been deleted',
|
'user_deletion_failed' => 'User account deletion failed, no data have been deleted',
|
||||||
'auth_proxy_failed' => 'Proxy authentication failed',
|
'auth_proxy_failed' => 'Proxy authentication failed',
|
||||||
|
Loading…
Reference in New Issue
Block a user