diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-04-16 12:57:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-16 12:57:34 +0200 |
commit | 7eec3b5a7250d111868a4121d96f33e6c0305e72 (patch) | |
tree | 23c76cd4b76f9e38199a5e4a45121e87da582f56 /core | |
parent | 90281375a3c9f991be79091450469cdfebc9277a (diff) | |
parent | a1a74ccc209954c4ce87277ebeb05520472b7c07 (diff) | |
download | nextcloud-server-7eec3b5a7250d111868a4121d96f33e6c0305e72.tar.gz nextcloud-server-7eec3b5a7250d111868a4121d96f33e6c0305e72.zip |
Merge pull request #44761 from nextcloud/fix/deps-webauthn-lib
fix(deps): Bump web-auth/webauthn-lib from 3.3.9 to 4.8.5
Diffstat (limited to 'core')
-rw-r--r-- | core/src/components/login/PasswordLessLoginForm.vue | 134 | ||||
-rw-r--r-- | core/src/services/WebAuthnAuthenticationService.js | 44 | ||||
-rw-r--r-- | core/src/services/WebAuthnAuthenticationService.ts | 59 | ||||
-rw-r--r-- | core/src/views/Login.vue | 2 |
4 files changed, 83 insertions, 156 deletions
diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue index 8a3886e52d0..128adddc303 100644 --- a/core/src/components/login/PasswordLessLoginForm.vue +++ b/core/src/components/login/PasswordLessLoginForm.vue @@ -1,5 +1,5 @@ <template> - <form v-if="(isHttps || isLocalhost) && hasPublicKeyCredential" + <form v-if="(isHttps || isLocalhost) && supportsWebauthn" ref="loginForm" method="post" name="login" @@ -20,7 +20,7 @@ @click="authenticate" /> </fieldset> </form> - <div v-else-if="!hasPublicKeyCredential" class="update"> + <div v-else-if="!supportsWebauthn" class="update"> <InformationIcon size="70" /> <h2>{{ t('core', 'Browser not supported') }}</h2> <p class="infogroup"> @@ -37,18 +37,16 @@ </template> <script> +import { browserSupportsWebAuthn } from '@simplewebauthn/browser' import { startAuthentication, finishAuthentication, -} from '../../services/WebAuthnAuthenticationService.js' +} from '../../services/WebAuthnAuthenticationService.ts' import LoginButton from './LoginButton.vue' import InformationIcon from 'vue-material-design-icons/Information.vue' import LockOpenIcon from 'vue-material-design-icons/LockOpen.vue' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' - -class NoValidCredentials extends Error { - -} +import logger from '../../logger' export default { name: 'PasswordLessLoginForm', @@ -79,11 +77,14 @@ export default { type: Boolean, default: false, }, - hasPublicKeyCredential: { - type: Boolean, - default: false, - }, }, + + setup() { + return { + supportsWebauthn: browserSupportsWebAuthn(), + } + }, + data() { return { user: this.username, @@ -92,7 +93,7 @@ export default { } }, methods: { - authenticate() { + async authenticate() { // check required fields if (!this.$refs.loginForm.checkValidity()) { return @@ -100,112 +101,25 @@ export default { console.debug('passwordless login initiated') - this.getAuthenticationData(this.user) - .then(publicKey => { - console.debug(publicKey) - return publicKey - }) - .then(this.sign) - .then(this.completeAuthentication) - .catch(error => { - if (error instanceof NoValidCredentials) { - this.validCredentials = false - return - } - console.debug(error) - }) + try { + const params = await startAuthentication(this.user) + await this.completeAuthentication(params) + } catch (error) { + if (error instanceof NoValidCredentials) { + this.validCredentials = false + return + } + logger.debug(error) + } }, changeUsername(username) { this.user = username this.$emit('update:username', this.user) }, - getAuthenticationData(uid) { - const base64urlDecode = function(input) { - // Replace non-url compatible chars with base64 standard chars - input = input - .replace(/-/g, '+') - .replace(/_/g, '/') - - // Pad out with standard base64 required padding characters - const pad = input.length % 4 - if (pad) { - if (pad === 1) { - throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding') - } - input += new Array(5 - pad).join('=') - } - - return window.atob(input) - } - - return startAuthentication(uid) - .then(publicKey => { - console.debug('Obtained PublicKeyCredentialRequestOptions') - console.debug(publicKey) - - if (!Object.prototype.hasOwnProperty.call(publicKey, 'allowCredentials')) { - console.debug('No credentials found.') - throw new NoValidCredentials() - } - - publicKey.challenge = Uint8Array.from(base64urlDecode(publicKey.challenge), c => c.charCodeAt(0)) - publicKey.allowCredentials = publicKey.allowCredentials.map(function(data) { - return { - ...data, - id: Uint8Array.from(base64urlDecode(data.id), c => c.charCodeAt(0)), - } - }) - - console.debug('Converted PublicKeyCredentialRequestOptions') - console.debug(publicKey) - return publicKey - }) - .catch(error => { - console.debug('Error while obtaining data') - throw error - }) - }, - sign(publicKey) { - const arrayToBase64String = function(a) { - return window.btoa(String.fromCharCode(...a)) - } - - const arrayToString = function(a) { - return String.fromCharCode(...a) - } - - return navigator.credentials.get({ publicKey }) - .then(data => { - console.debug(data) - console.debug(new Uint8Array(data.rawId)) - console.debug(arrayToBase64String(new Uint8Array(data.rawId))) - return { - id: data.id, - type: data.type, - rawId: arrayToBase64String(new Uint8Array(data.rawId)), - response: { - authenticatorData: arrayToBase64String(new Uint8Array(data.response.authenticatorData)), - clientDataJSON: arrayToBase64String(new Uint8Array(data.response.clientDataJSON)), - signature: arrayToBase64String(new Uint8Array(data.response.signature)), - userHandle: data.response.userHandle ? arrayToString(new Uint8Array(data.response.userHandle)) : null, - }, - } - }) - .then(challenge => { - console.debug(challenge) - return challenge - }) - .catch(error => { - console.debug('GOT AN ERROR!') - console.debug(error) // Example: timeout, interaction refused... - }) - }, completeAuthentication(challenge) { - console.debug('TIME TO COMPLETE') - const redirectUrl = this.redirectUrl - return finishAuthentication(JSON.stringify(challenge)) + return finishAuthentication(challenge) .then(({ defaultRedirectUrl }) => { console.debug('Logged in redirecting') // Redirect url might be false so || should be used instead of ??. diff --git a/core/src/services/WebAuthnAuthenticationService.js b/core/src/services/WebAuthnAuthenticationService.js deleted file mode 100644 index 3eabceef5e4..00000000000 --- a/core/src/services/WebAuthnAuthenticationService.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0-or-later - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ - -import Axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' - -/** - * @param {any} loginName - - */ -export function startAuthentication(loginName) { - const url = generateUrl('/login/webauthn/start') - - return Axios.post(url, { loginName }) - .then(resp => resp.data) -} - -/** - * @param {any} data - - */ -export function finishAuthentication(data) { - const url = generateUrl('/login/webauthn/finish') - - return Axios.post(url, { data }) - .then(resp => resp.data) -} diff --git a/core/src/services/WebAuthnAuthenticationService.ts b/core/src/services/WebAuthnAuthenticationService.ts new file mode 100644 index 00000000000..69b18551f62 --- /dev/null +++ b/core/src/services/WebAuthnAuthenticationService.ts @@ -0,0 +1,59 @@ +/** + * @copyright 2020, Roeland Jago Douma <roeland@famdouma.nl> + * + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types' + +import { startAuthentication as startWebauthnAuthentication } from '@simplewebauthn/browser' +import { generateUrl } from '@nextcloud/router' + +import Axios from '@nextcloud/axios' +import logger from '../logger' + +export class NoValidCredentials extends Error {} + +/** + * Start webautn authentication + * This loads the challenge, connects to the authenticator and returns the repose that needs to be sent to the server. + * + * @param loginName Name to login + */ +export async function startAuthentication(loginName: string) { + const url = generateUrl('/login/webauthn/start') + + const { data } = await Axios.post<PublicKeyCredentialRequestOptionsJSON>(url, { loginName }) + if (!data.allowCredentials || data.allowCredentials.length === 0) { + logger.error('No valid credentials returned for webauthn') + throw new NoValidCredentials() + } + return await startWebauthnAuthentication(data) +} + +/** + * Verify webauthn authentication + * @param authData The authentication data to sent to the server + */ +export async function finishAuthentication(authData: AuthenticationResponseJSON) { + const url = generateUrl('/login/webauthn/finish') + + const { data } = await Axios.post(url, { data: JSON.stringify(authData) }) + return data +} diff --git a/core/src/views/Login.vue b/core/src/views/Login.vue index 57634bcb8f8..f6b0d2ec64c 100644 --- a/core/src/views/Login.vue +++ b/core/src/views/Login.vue @@ -73,7 +73,6 @@ :auto-complete-allowed="autoCompleteAllowed" :is-https="isHttps" :is-localhost="isLocalhost" - :has-public-key-credential="hasPublicKeyCredential" @submit="loading = true" /> <NcButton type="tertiary" :aria-label="t('core', 'Back to login form')" @@ -178,7 +177,6 @@ export default { alternativeLogins: loadState('core', 'alternativeLogins', []), isHttps: window.location.protocol === 'https:', isLocalhost: window.location.hostname === 'localhost', - hasPublicKeyCredential: typeof (window.PublicKeyCredential) !== 'undefined', hideLoginForm: loadState('core', 'hideLoginForm', false), emailStates: loadState('core', 'emailStates', []), } |