Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichaIng <micha@dietpi.com>2021-06-20 13:20:10 +0300
committerGitHub <noreply@github.com>2021-06-20 13:20:10 +0300
commit4f47bec782c90d89a501e7ed5ed43c2816a8ce81 (patch)
treead5bac61b7a005a92f234a44531354ff6ab24032 /core/src/components/login
parent4d51ed3918032c44df612fad6b2c12b0e9eff693 (diff)
parent61a31dcdd73aae9a728551421116c5947e5b3089 (diff)
Merge branch 'master' into improve-contributing-docsimprove-contributing-docs
Diffstat (limited to 'core/src/components/login')
-rw-r--r--core/src/components/login/LoginButton.vue56
-rw-r--r--core/src/components/login/LoginForm.vue43
-rw-r--r--core/src/components/login/PasswordLessLoginForm.vue216
-rw-r--r--core/src/components/login/ResetPassword.vue101
-rw-r--r--core/src/components/login/UpdatePassword.vue2
5 files changed, 349 insertions, 69 deletions
diff --git a/core/src/components/login/LoginButton.vue b/core/src/components/login/LoginButton.vue
new file mode 100644
index 00000000000..f7d426e6c63
--- /dev/null
+++ b/core/src/components/login/LoginButton.vue
@@ -0,0 +1,56 @@
+<!--
+ - @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ -
+ - @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - 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/>.
+ -->
+
+<template>
+ <div id="submit-wrapper" @click="$emit('click')">
+ <input id="submit-form"
+ type="submit"
+ class="login primary"
+ title=""
+ :value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')">
+ <div class="submit-icon"
+ :class="{
+ 'icon-confirm-white': !loading,
+ 'icon-loading-small': loading && invertedColors,
+ 'icon-loading-small-dark': loading && !invertedColors,
+ }" />
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'LoginButton',
+ props: {
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ invertedColors: {
+ type: Boolean,
+ default: false,
+ },
+ },
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/core/src/components/login/LoginForm.vue b/core/src/components/login/LoginForm.vue
index 687896ceb54..c75b6f2c7ea 100644
--- a/core/src/components/login/LoginForm.vue
+++ b/core/src/components/login/LoginForm.vue
@@ -20,9 +20,10 @@
-->
<template>
- <form method="post"
+ <form ref="loginForm"
+ method="post"
name="login"
- :action="OC.generateUrl('login')"
+ :action="loginActionUrl"
@submit="submit">
<fieldset>
<div v-if="apacheAuthFailed"
@@ -46,7 +47,7 @@
class="hidden">
<img class="float-spinner"
alt=""
- :src="OC.imagePath('core', 'loading-dark.gif')">
+ :src="loadingIcon">
<span id="messageText" />
<!-- the following div ensures that the spinner is always inside the #message div -->
<div style="clear: both;" />
@@ -58,12 +59,13 @@
v-model="user"
type="text"
name="user"
+ autocapitalize="off"
:autocomplete="autoCompleteAllowed ? 'on' : 'off'"
:placeholder="t('core', 'Username or email')"
:aria-label="t('core', 'Username or email')"
required
@change="updateUsername">
- <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
+ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
</p>
<p class="groupbottom"
@@ -80,23 +82,11 @@
<label for="password"
class="infield">{{ t('Password') }}</label>
<a href="#" class="toggle-password" @click.stop.prevent="togglePassword">
- <img :src="OC.imagePath('core', 'actions/toggle.svg')">
+ <img :src="toggleIcon" :alt="t('core', 'Toggle password visibility')">
</a>
</p>
- <div id="submit-wrapper">
- <input id="submit-form"
- type="submit"
- class="login primary"
- title=""
- :value="!loading ? t('core', 'Log in') : t('core', 'Logging in …')">
- <div class="submit-icon"
- :class="{
- 'icon-confirm-white': !loading,
- 'icon-loading-small': loading && invertedColors,
- 'icon-loading-small-dark': loading && !invertedColors,
- }" />
- </div>
+ <LoginButton :loading="loading" :inverted-colors="invertedColors" />
<p v-if="invalidPassword"
class="warning wrongPasswordMsg">
@@ -104,7 +94,7 @@
</p>
<p v-else-if="userDisabled"
class="warning userDisabledMsg">
- {{ t('lib', 'User disabled') }}
+ {{ t('core', 'User disabled') }}
</p>
<p v-if="throttleDelay && throttleDelay > 5000"
@@ -135,9 +125,15 @@
<script>
import jstz from 'jstimezonedetect'
+import LoginButton from './LoginButton'
+import {
+ generateUrl,
+ imagePath,
+} from '@nextcloud/router'
export default {
name: 'LoginForm',
+ components: { LoginButton },
props: {
username: {
type: String,
@@ -193,6 +189,15 @@ export default {
userDisabled() {
return this.errors.indexOf('userdisabled') !== -1
},
+ toggleIcon() {
+ return imagePath('core', 'actions/toggle.svg')
+ },
+ loadingIcon() {
+ return imagePath('core', 'loading-dark.gif')
+ },
+ loginActionUrl() {
+ return generateUrl('login')
+ },
},
mounted() {
if (this.username === '') {
diff --git a/core/src/components/login/PasswordLessLoginForm.vue b/core/src/components/login/PasswordLessLoginForm.vue
new file mode 100644
index 00000000000..0203bd74c48
--- /dev/null
+++ b/core/src/components/login/PasswordLessLoginForm.vue
@@ -0,0 +1,216 @@
+<template>
+ <form v-if="(isHttps || isLocalhost) && hasPublicKeyCredential"
+ ref="loginForm"
+ method="post"
+ name="login"
+ @submit.prevent="submit">
+ <fieldset>
+ <p class="grouptop groupbottom">
+ <input id="user"
+ ref="user"
+ v-model="user"
+ type="text"
+ name="user"
+ :autocomplete="autoCompleteAllowed ? 'on' : 'off'"
+ :placeholder="t('core', 'Username or email')"
+ :aria-label="t('core', 'Username or email')"
+ required
+ @change="$emit('update:username', user)">
+ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
+ </p>
+
+ <div v-if="!validCredentials">
+ {{ t('core', 'Your account is not setup for passwordless login.') }}
+ </div>
+
+ <LoginButton v-if="validCredentials"
+ :loading="loading"
+ :inverted-colors="invertedColors"
+ @click="authenticate" />
+ </fieldset>
+ </form>
+ <div v-else-if="!hasPublicKeyCredential">
+ {{ t('core', 'Passwordless authentication is not supported in your browser.') }}
+ </div>
+ <div v-else-if="!isHttps && !isLocalhost">
+ {{ t('core', 'Passwordless authentication is only available over a secure connection.') }}
+ </div>
+</template>
+
+<script>
+import {
+ startAuthentication,
+ finishAuthentication,
+} from '../../services/WebAuthnAuthenticationService'
+import LoginButton from './LoginButton'
+
+class NoValidCredentials extends Error {
+
+}
+
+export default {
+ name: 'PasswordLessLoginForm',
+ components: {
+ LoginButton,
+ },
+ props: {
+ username: {
+ type: String,
+ default: '',
+ },
+ redirectUrl: {
+ type: String,
+ },
+ invertedColors: {
+ type: Boolean,
+ default: false,
+ },
+ autoCompleteAllowed: {
+ type: Boolean,
+ default: true,
+ },
+ isHttps: {
+ type: Boolean,
+ default: false,
+ },
+ isLocalhost: {
+ type: Boolean,
+ default: false,
+ },
+ hasPublicKeyCredential: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ user: this.username,
+ loading: false,
+ validCredentials: true,
+ }
+ },
+ methods: {
+ authenticate() {
+ 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)
+ })
+ },
+ 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 location = this.redirectUrl
+
+ return finishAuthentication(JSON.stringify(challenge))
+ .then(data => {
+ console.debug('Logged in redirecting')
+ window.location.href = location
+ })
+ .catch(error => {
+ console.debug('GOT AN ERROR WHILE SUBMITTING CHALLENGE!')
+ console.debug(error) // Example: timeout, interaction refused...
+ })
+ },
+ submit() {
+ // noop
+ },
+ },
+}
+</script>
+
+<style scoped>
+
+</style>
diff --git a/core/src/components/login/ResetPassword.vue b/core/src/components/login/ResetPassword.vue
index 2d045692285..79589a96aca 100644
--- a/core/src/components/login/ResetPassword.vue
+++ b/core/src/components/login/ResetPassword.vue
@@ -21,61 +21,64 @@
<template>
<form @submit.prevent="submit">
- <p>
- <input id="user"
- v-model="user"
- type="text"
- name="user"
- :placeholder="t('core', 'Username or email')"
- :aria-label="t('core', 'Username or email')"
- required
- @change="updateUsername">
- <!--<?php p($_['user_autofocus'] ? 'autofocus' : ''); ?>
- autocomplete="<?php p($_['login_form_autocomplete']); ?>" autocapitalize="none" autocorrect="off"-->
- <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
- </p>
- <div id="reset-password-wrapper">
- <input id="reset-password-submit"
- type="submit"
- class="login primary"
- title=""
- :value="t('core', 'Reset password')">
- <div class="submit-icon"
- :class="{
- 'icon-confirm-white': !loading,
- 'icon-loading-small': loading && invertedColors,
- 'icon-loading-small-dark': loading && !invertedColors,
- }" />
- </div>
- <p v-if="message === 'send-success'"
- class="update">
- {{ t('core', 'A password reset message has been sent to the e-mail address of this account. If you do not receive it, check your spam/junk folders or ask your local administrator for help.') }}
- <br>
- {{ t('core', 'If it is not there ask your local administrator.') }}
- </p>
- <p v-else-if="message === 'send-error'"
- class="update warning">
- {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
- </p>
- <p v-else-if="message === 'reset-error'"
- class="update warning">
- {{ t('core', 'Password can not be changed. Please contact your administrator.') }}
- </p>
- <p v-else-if="message"
- class="update"
- :class="{warning: error}" />
+ <fieldset>
+ <p>
+ <input id="user"
+ v-model="user"
+ type="text"
+ name="user"
+ autocapitalize="off"
+ :placeholder="t('core', 'Username or email')"
+ :aria-label="t('core', 'Username or email')"
+ required
+ @change="updateUsername">
+ <!--<?php p($_['user_autofocus'] ? 'autofocus' : ''); ?>
+ autocomplete="<?php p($_['login_form_autocomplete']); ?>" autocapitalize="none" autocorrect="off"-->
+ <label for="user" class="infield">{{ t('core', 'Username or email') }}</label>
+ </p>
+ <div id="reset-password-wrapper">
+ <input id="reset-password-submit"
+ type="submit"
+ class="login primary"
+ title=""
+ :value="t('core', 'Reset password')">
+ <div class="submit-icon"
+ :class="{
+ 'icon-confirm-white': !loading,
+ 'icon-loading-small': loading && invertedColors,
+ 'icon-loading-small-dark': loading && !invertedColors,
+ }" />
+ </div>
+ <p v-if="message === 'send-success'"
+ class="update">
+ {{ t('core', 'A password reset message has been sent to the email address of this account. If you do not receive it, check your spam/junk folders or ask your local administrator for help.') }}
+ <br>
+ {{ t('core', 'If it is not there ask your local administrator.') }}
+ </p>
+ <p v-else-if="message === 'send-error'"
+ class="update warning">
+ {{ t('core', 'Couldn\'t send reset email. Please contact your administrator.') }}
+ </p>
+ <p v-else-if="message === 'reset-error'"
+ class="update warning">
+ {{ t('core', 'Password cannot be changed. Please contact your administrator.') }}
+ </p>
+ <p v-else-if="message"
+ class="update"
+ :class="{warning: error}" />
- <a href="#"
- @click.prevent="$emit('abort')">
- {{ t('core', 'Back to login') }}
- </a>
+ <a href="#"
+ @click.prevent="$emit('abort')">
+ {{ t('core', 'Back to login') }}
+ </a>
+ </fieldset>
</form>
</template>
<script>
import axios from '@nextcloud/axios'
-import { generateUrl } from '../../OC/routing'
+import { generateUrl } from '@nextcloud/router'
export default {
name: 'ResetPassword',
@@ -130,7 +133,7 @@ export default {
this.message = 'send-success'
})
.catch(e => {
- console.error('could not send reset e-mail request', e)
+ console.error('could not send reset email request', e)
this.error = true
this.message = 'send-error'
diff --git a/core/src/components/login/UpdatePassword.vue b/core/src/components/login/UpdatePassword.vue
index 3fa3c60773c..cb615a846d2 100644
--- a/core/src/components/login/UpdatePassword.vue
+++ b/core/src/components/login/UpdatePassword.vue
@@ -125,7 +125,7 @@ export default {
}
} catch (e) {
this.error = true
- this.message = e.message ? e.message : t('core', 'Password can not be changed. Please contact your administrator.')
+ this.message = e.message ? e.message : t('core', 'Password cannot be changed. Please contact your administrator.')
} finally {
this.loading = false
}