refactor(core): migrate login flow ui from jQuery to Vue

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2026-01-13 18:39:21 +01:00
parent 038d940df1
commit d26ec02aa1
No known key found for this signature in database
GPG key ID: 7E849AE05218500F
19 changed files with 463 additions and 383 deletions

View file

@ -16,6 +16,7 @@ module.exports = {
files_fileinfo: path.join(__dirname, 'core/src', 'files/fileinfo.js'),
install: path.join(__dirname, 'core/src', 'install.ts'),
login: path.join(__dirname, 'core/src', 'login.js'),
login_flow: path.join(__dirname, 'core/src', 'login-flow.ts'),
main: path.join(__dirname, 'core/src', 'main.js'),
maintenance: path.join(__dirname, 'core/src', 'maintenance.js'),
'public-page-menu': path.resolve(__dirname, 'core/src', 'public-page-menu.ts'),

View file

@ -25,6 +25,7 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Authentication\Token\IToken;
@ -35,11 +36,11 @@ use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Security\ICrypto;
use OCP\Security\ISecureRandom;
use OCP\Session\Exceptions\SessionNotAvailableException;
use OCP\Util;
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class ClientFlowLoginController extends Controller {
@ -61,6 +62,7 @@ class ClientFlowLoginController extends Controller {
private IEventDispatcher $eventDispatcher,
private ITimeFactory $timeFactory,
private IConfig $config,
private IInitialState $initialState,
) {
parent::__construct($appName, $request);
}
@ -135,24 +137,36 @@ class ClientFlowLoginController extends Controller {
$csp->addAllowedFormActionDomain('nc://*');
}
$this->initialState->provideInitialState('loginFlowState', 'auth');
$this->initialState->provideInitialState('loginFlowAuth', [
'client' => $clientName,
'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'stateToken' => $stateToken,
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => (bool)$direct,
'providedRedirectUri' => $providedRedirectUri,
'loginRedirectUrl' => $this->urlGenerator->linkToRoute(
'core.ClientFlowLogin.grantPage',
[
'stateToken' => $stateToken,
'clientIdentifier' => $clientIdentifier,
'oauthState' => $this->session->get('oauth.state'),
'user' => $user,
'direct' => $direct,
'providedRedirectUri' => $providedRedirectUri,
]),
'appTokenUrl' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.apptokenRedirect'),
]);
Util::addScript('core', 'login_flow');
$response = new StandaloneTemplateResponse(
$this->appName,
'loginflow/authpicker',
[
'client' => $clientName,
'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'user' => $user,
'direct' => $direct,
'providedRedirectUri' => $providedRedirectUri,
],
'guest'
'loginflow',
renderAs: 'guest'
);
$response->setContentSecurityPolicy($csp);
return $response;
}
@ -188,26 +202,31 @@ class ClientFlowLoginController extends Controller {
$csp->addAllowedFormActionDomain('nc://*');
}
/** @var IUser $user */
$user = $this->userSession->getUser();
\assert($user !== null);
$this->initialState->provideInitialState('loginFlowState', 'grant');
$this->initialState->provideInitialState('loginFlowGrant', [
'actionUrl' => $this->urlGenerator->linkToRouteAbsolute(
'core.ClientFlowLogin.generateAppPassword',
),
'client' => $clientName,
'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'stateToken' => $stateToken,
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => $direct,
'providedRedirectUri' => $providedRedirectUri,
'userDisplayName' => $user->getDisplayName(),
'userId' => $user->getUID(),
]);
Util::addScript('core', 'login_flow');
$response = new StandaloneTemplateResponse(
$this->appName,
'loginflow/grant',
[
'userId' => $user->getUID(),
'userDisplayName' => $user->getDisplayName(),
'client' => $clientName,
'clientIdentifier' => $clientIdentifier,
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'serverHost' => $this->getServerPath(),
'oauthState' => $this->session->get('oauth.state'),
'direct' => $direct,
'providedRedirectUri' => $providedRedirectUri,
],
'guest'
'loginflow',
renderAs: 'guest'
);
$response->setContentSecurityPolicy($csp);

View file

@ -26,16 +26,17 @@ use OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\Defaults;
use OCP\IL10N;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Security\ISecureRandom;
use OCP\Server;
use OCP\Util;
/**
* @psalm-import-type CoreLoginFlowV2Credentials from ResponseDefinitions
@ -58,6 +59,7 @@ class ClientFlowLoginV2Controller extends Controller {
private Defaults $defaults,
private ?string $userId,
private IL10N $l10n,
private IInitialState $initialState,
) {
parent::__construct($appName, $request);
}
@ -122,18 +124,21 @@ class ClientFlowLoginV2Controller extends Controller {
);
$this->session->set(self::STATE_NAME, $stateToken);
$this->initialState->provideInitialState('loginFlowState', 'auth');
$this->initialState->provideInitialState('loginFlowAuth', [
'client' => $flow->getClientName(),
'instanceName' => $this->defaults->getName(),
'stateToken' => $stateToken,
'loginRedirectUrl' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.grantPage', ['stateToken' => $stateToken, 'user' => $user, 'direct' => $direct]),
'appTokenUrl' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.apptokenRedirect'),
]);
Util::addScript('core', 'login_flow');
return new StandaloneTemplateResponse(
$this->appName,
'loginflowv2/authpicker',
[
'client' => $flow->getClientName(),
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'user' => $user,
'direct' => $direct,
],
'guest'
'loginflow',
renderAs: 'guest'
);
}
@ -161,22 +166,26 @@ class ClientFlowLoginV2Controller extends Controller {
return $this->loginTokenForbiddenClientResponse();
}
/** @var IUser $user */
$user = $this->userSession->getUser();
\assert($user !== null);
$this->initialState->provideInitialState('loginFlowState', 'grant');
$this->initialState->provideInitialState('loginFlowGrant', [
'actionUrl' => $this->urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.generateAppPassword'),
'userId' => $user->getUID(),
'userDisplayName' => $user->getDisplayName(),
'client' => $flow->getClientName(),
'instanceName' => $this->defaults->getName(),
'stateToken' => $stateToken,
'direct' => $direct === 1,
]);
Util::addScript('core', 'login_flow');
return new StandaloneTemplateResponse(
$this->appName,
'loginflowv2/grant',
[
'userId' => $user->getUID(),
'userDisplayName' => $user->getDisplayName(),
'client' => $flow->getClientName(),
'instanceName' => $this->defaults->getName(),
'urlGenerator' => $this->urlGenerator,
'stateToken' => $stateToken,
'direct' => $direct,
],
'guest'
'loginflow',
renderAs: 'guest'
);
}
@ -260,11 +269,12 @@ class ClientFlowLoginV2Controller extends Controller {
private function handleFlowDone(bool $result): StandaloneTemplateResponse {
if ($result) {
Util::addScript('core', 'login_flow');
$this->initialState->provideInitialState('loginFlowState', 'done');
return new StandaloneTemplateResponse(
$this->appName,
'loginflowv2/done',
[],
'guest'
'loginflow',
renderAs: 'guest'
);
}

View file

@ -1,19 +0,0 @@
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
jQuery(document).ready(function() {
$('#app-token-login').click(function(e) {
e.preventDefault()
$(this).addClass('hidden')
$('#redirect-link').addClass('hidden')
$('#app-token-login-field').removeClass('hidden')
})
document.getElementById('login-form').addEventListener('submit', function(e) {
e.preventDefault()
document.location.href = e.target.attributes.action.value
})
$('#login-form input').removeAttr('disabled')
})

View file

@ -1,32 +0,0 @@
/**
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
const form = document.querySelector('form')
form.addEventListener('submit', function(event) {
const wrapper = document.getElementById('submit-wrapper')
if (wrapper === null) {
return
}
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
// stop the event
event.preventDefault()
event.stopPropagation()
// handle password confirmation
OC.PasswordConfirmation.requirePasswordConfirmation(function() {
// when password is confirmed we submit the form
form.submit()
})
return false
}
Array.from(wrapper.getElementsByClassName('icon-confirm-white')).forEach(function(el) {
el.classList.remove('icon-confirm-white')
el.classList.add(OCA.Theming && OCA.Theming.inverted ? 'icon-loading-small' : 'icon-loading-small-dark')
el.disabled = true
})
})

View file

@ -0,0 +1,49 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { getRequestToken } from '@nextcloud/auth'
import { t } from '@nextcloud/l10n'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcFormBox from '@nextcloud/vue/components/NcFormBox'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcTextField from '@nextcloud/vue/components/NcTextField'
defineProps<{
appTokenUrl: string
direct: boolean
stateToken: string
}>()
const requestToken = getRequestToken()
</script>
<template>
<form :action="appTokenUrl" :class="$style.loginFlowAuthAppToken" method="post">
<NcFormBox>
<NcTextField name="user" :label="t('core', 'Login')" />
<NcPasswordField name="password" :label="t('core', 'App password')" />
</NcFormBox>
<input type="hidden" name="stateToken" :value="stateToken">
<input type="hidden" name="requesttoken" :value="requestToken">
<input
v-if="direct"
type="hidden"
name="direct"
value="1">
<NcButton type="submit" variant="primary" wide>
{{ t('core', 'Grant access') }}
</NcButton>
</form>
</template>
<style module>
.loginFlowAuthAppToken {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
}
</style>

View file

@ -0,0 +1,26 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import NcGuestContent from '@nextcloud/vue/components/NcGuestContent'
defineProps<{
heading: string
}>()
</script>
<template>
<NcGuestContent class="picker-window" :class="$style.loginFlowContainer">
<h2>{{ heading }}</h2>
<slot />
</NcGuestContent>
</template>
<style module>
.loginFlowContainer {
display: flex;
flex-direction: column;
}
</style>

22
core/src/login-flow.ts Normal file
View file

@ -0,0 +1,22 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { getCSPNonce } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import Vue, { defineAsyncComponent } from 'vue'
__webpack_nonce__ = getCSPNonce()
const LoginFlowAuth = defineAsyncComponent(() => import('./views/LoginFlowAuth.vue'))
const LoginFlowGrant = defineAsyncComponent(() => import('./views/LoginFlowGrant.vue'))
const LoginFlowDone = defineAsyncComponent(() => import('./views/LoginFlowDone.vue'))
const state = loadState<'auth' | 'grant' | 'done'>('core', 'loginFlowState')
const app = new Vue({
render: (h) => h(state === 'auth'
? LoginFlowAuth
: (state === 'grant' ? LoginFlowGrant : LoginFlowDone)),
})
app.$mount('#core-loginflow')

View file

@ -0,0 +1,73 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { ref } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import LoginFlowAuthAppToken from '../components/LoginFlow/LoginFlowAuthAppToken.vue'
import LoginFlowContainer from '../components/LoginFlow/LoginFlowContainer.vue'
const {
client,
direct,
instanceName,
loginRedirectUrl,
appTokenUrl,
stateToken,
} = loadState<{
client: string
direct?: boolean
instanceName: string
loginRedirectUrl: string
appTokenUrl: string
stateToken: string
}>('core', 'loginFlowAuth')
const useAppTokenLogin = ref(false)
</script>
<template>
<LoginFlowContainer :heading="t('core', 'Connect to your account')">
<NcNoteCard type="info">
{{ t('core', 'Please log in before granting "{client}" access to your {instanceName} account.', { client, instanceName }) }}
</NcNoteCard>
<NcNoteCard type="warning" :heading="t('core', 'Security warning')">
{{ t('core', 'If you are not trying to set up a new device or app, someone is trying to trick you into granting them access to your data. In this case do not proceed and instead contact your system administrator.') }}
</NcNoteCard>
<NcButton
v-if="!useAppTokenLogin"
:class="$style.loginFlowAuth__button"
:href="loginRedirectUrl"
variant="primary">
{{ t('core', 'Log in') }}
</NcButton>
<LoginFlowAuthAppToken
v-else
:app-token-url="appTokenUrl"
:direct="direct ?? false"
:state-token="stateToken" />
<NcButton
:class="$style.loginFlowAuth__button"
variant="tertiary"
@click="useAppTokenLogin = !useAppTokenLogin">
{{ useAppTokenLogin ? t('core', 'Log in using password') : t('core', 'Alternative log in using app password') }}
</NcButton>
</LoginFlowContainer>
</template>
<style module>
.loginFlowAuth__button {
margin-top: 0.5rem;
margin-inline: auto;
min-width: 50% !important;
}
</style>

View file

@ -0,0 +1,20 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import LoginFlowContainer from '../components/LoginFlow/LoginFlowContainer.vue'
</script>
<template>
<LoginFlowContainer :heading="t('core', 'Account connected')">
<NcNoteCard type="info">
{{ t('core', 'Your client should now be connected!') }}
<br>
{{ t('core', 'You can close this window.') }}
</NcNoteCard>
</LoginFlowContainer>
</template>

View file

@ -0,0 +1,106 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import { getRequestToken } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { confirmPassword, isPasswordConfirmationRequired, PwdConfirmationMode } from '@nextcloud/password-confirmation'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import LoginFlowContainer from '../components/LoginFlow/LoginFlowContainer.vue'
const {
clientIdentifier,
oauthState,
providedRedirectUri,
actionUrl,
client,
direct,
instanceName,
stateToken,
userDisplayName,
userId,
} = loadState<{
clientIdentifier?: string
oauthState?: string
providedRedirectUri?: string
actionUrl: string
client: string
direct: boolean
instanceName: string
stateToken: string
userId: string
userDisplayName: string
}>('core', 'loginFlowGrant')
const requestToken = getRequestToken()
/**
* Handle submit event to confirm password if required
*
* @param event - The submit event
*/
async function onSubmit(event: SubmitEvent) {
if (isPasswordConfirmationRequired(PwdConfirmationMode.Lax)) {
event.preventDefault()
event.stopPropagation()
await confirmPassword()
;(event.target as HTMLFormElement).submit()
return false
}
}
</script>
<template>
<LoginFlowContainer :heading="t('core', 'Account access')">
<NcNoteCard type="info">
{{ t('core', 'Currently logged in as {userDisplayName} ({userId}).', { userDisplayName, userId }) }}
<br>
{{ t('core', 'You are about to grant "{client}" access to your {instanceName} account.', { client, instanceName }) }}
</NcNoteCard>
<form method="POST" :action="actionUrl" @submit="onSubmit">
<input type="hidden" name="requesttoken" :value="requestToken">
<input type="hidden" name="stateToken" :value="stateToken">
<input
v-if="direct"
type="hidden"
name="direct"
value="1">
<input
v-if="clientIdentifier !== undefined"
type="hidden"
name="clientIdentifier"
:value="clientIdentifier">
<input
v-if="oauthState !== undefined"
type="hidden"
name="oauthState"
:value="oauthState">
<input
v-if="providedRedirectUri !== undefined"
type="hidden"
name="providedRedirectUri"
:value="providedRedirectUri">
<NcButton :class="$style.loginFlowGrant__button" type="submit" variant="primary">
{{ t('core', 'Grant access') }}
</NcButton>
</form>
</LoginFlowContainer>
</template>
<style module>
.loginFlowGrant__button {
margin-top: 0.5rem;
margin-inline: auto;
min-width: 50% !important;
}
</style>

View file

@ -0,0 +1,8 @@
<?php
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
?>
<div id="core-loginflow"></div>

View file

@ -1,59 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
\OCP\Util::addScript('core', 'login/authpicker', 'core');
style('core', 'login/authpicker');
/** @var array $_ */
/** @var \OCP\IURLGenerator $urlGenerator */
$urlGenerator = $_['urlGenerator'];
?>
<div class="picker-window">
<h2><?php p($l->t('Connect to your account')) ?></h2>
<p class="info">
<?php print_unescaped($l->t('Please log in before granting %1$s access to your %2$s account.', [
'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
\OCP\Util::sanitizeHTML($_['instanceName'])
])) ?>
</p>
<div class="notecard warning">
<h3><?php p($l->t('Security warning')) ?></h3>
<p>
<?php p($l->t('If you are not trying to set up a new device or app, someone is trying to trick you into granting them access to your data. In this case do not proceed and instead contact your system administrator.')) ?>
</p>
</div>
<br/>
<p id="redirect-link">
<form id="login-form" action="<?php p($urlGenerator->linkToRoute('core.ClientFlowLogin.grantPage', ['stateToken' => $_['stateToken'], 'clientIdentifier' => $_['clientIdentifier'], 'oauthState' => $_['oauthState'], 'user' => $_['user'], 'direct' => $_['direct'], 'providedRedirectUri' => $_['providedRedirectUri']])) ?>" method="get">
<input type="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Log in')) ?>" disabled>
</form>
</p>
<form action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.apptokenRedirect')); ?>" method="post" id="app-token-login-field" class="hidden">
<p class="grouptop">
<input type="text" name="user" id="user" placeholder="<?php p($l->t('Login')) ?>">
<label for="user" class="infield"><?php p($l->t('Login')) ?></label>
</p>
<p class="groupbottom">
<input type="password" name="password" id="password" placeholder="<?php p($l->t('App password')) ?>">
<label for="password" class="infield"><?php p($l->t('Password')) ?></label>
</p>
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>">
<?php if ($_['direct'] !== 0) { ?>
<input type="hidden" name="direct" value="<?php p($_['direct']) ?>">
<?php } ?>
<input id="submit-app-token-login" type="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Grant access')) ?>">
</form>
<?php if (empty($_['oauthState'])): ?>
<a id="app-token-login" class="apptoken-link" href="#"><?php p($l->t('Alternative log in using app password')) ?></a>
<?php endif; ?>
</div>

View file

@ -1,47 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
\OCP\Util::addScript('core', 'login/grant', 'core');
style('core', 'login/authpicker');
/** @var array $_ */
/** @var \OCP\IURLGenerator $urlGenerator */
$urlGenerator = $_['urlGenerator'];
?>
<div class="picker-window small">
<h2><?php p($l->t('Account access')) ?></h2>
<p class="info">
<?php p($l->t('Currently logged in as %1$s (%2$s).', [
$_['userDisplayName'],
$_['userId'],
])) ?>
</p>
<p class="info">
<?php print_unescaped($l->t('You are about to grant %1$s access to your %2$s account.', [
'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
\OCP\Util::sanitizeHTML($_['instanceName'])
])) ?>
</p>
<br/>
<p id="redirect-link">
<form method="POST" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLogin.generateAppPassword')) ?>">
<input type="hidden" name="clientIdentifier" value="<?php p($_['clientIdentifier']) ?>" />
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
<input type="hidden" name="oauthState" value="<?php p($_['oauthState']) ?>" />
<input type="hidden" name="providedRedirectUri" value="<?php p($_['providedRedirectUri']) ?>">
<?php if ($_['direct']) { ?>
<input type="hidden" name="direct" value="1" />
<?php } ?>
<div id="submit-wrapper">
<input type="submit" class="login primary icon-confirm-white" title="" value="<?php p($l->t('Grant access')); ?>" />
</div>
</form>
</p>
</div>

View file

@ -1,56 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
style('core', 'login/authpicker');
\OCP\Util::addScript('core', 'login/authpicker', 'core');
/** @var array $_ */
/** @var \OCP\IURLGenerator $urlGenerator */
$urlGenerator = $_['urlGenerator'];
?>
<div class="picker-window">
<h2><?php p($l->t('Connect to your account')) ?></h2>
<p class="info">
<?php print_unescaped($l->t('Please log in before granting %1$s access to your %2$s account.', [
'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
\OCP\Util::sanitizeHTML($_['instanceName'])
])) ?>
</p>
<div class="notecard warning">
<h3><?php p($l->t('Security warning')) ?></h3>
<p>
<?php p($l->t('If you are not trying to set up a new device or app, someone is trying to trick you into granting them access to your data. In this case do not proceed and instead contact your system administrator.')) ?>
</p>
</div>
<br/>
<p id="redirect-link">
<form id="login-form" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.grantPage', ['stateToken' => $_['stateToken'], 'user' => $_['user'], 'direct' => $_['direct'] ?? 0])) ?>" method="get">
<input type="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Log in')) ?>" disabled>
</form>
</p>
<form action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.apptokenRedirect')); ?>" method="post" id="app-token-login-field" class="hidden">
<p class="grouptop">
<input type="text" name="user" id="user" placeholder="<?php p($l->t('Login')) ?>">
<label for="user" class="infield"><?php p($l->t('Login')) ?></label>
</p>
<p class="groupbottom">
<input type="password" name="password" id="password" placeholder="<?php p($l->t('App password')) ?>">
<label for="password" class="infield"><?php p($l->t('Password')) ?></label>
</p>
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>">
<input id="submit-app-token-login" type="submit" class="login primary icon-confirm-white" value="<?php p($l->t('Grant access')) ?>">
</form>
<?php if (empty($_['oauthState'])): ?>
<a id="app-token-login" class="apptoken-link" href="#"><?php p($l->t('Alternative log in using app password')) ?></a>
<?php endif; ?>
</div>

View file

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
style('core', 'login/authpicker');
/** @var array $_ */
?>
<div class="picker-window">
<h2><?php p($l->t('Account connected')) ?></h2>
<p class="info">
<?php p($l->t('Your client should now be connected!')) ?><br/>
<?php p($l->t('You can close this window.')) ?>
</p>
<br/>
</div>

View file

@ -1,44 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
\OCP\Util::addScript('core', 'login/grant', 'core');
style('core', 'login/authpicker');
/** @var array $_ */
/** @var \OCP\IURLGenerator $urlGenerator */
$urlGenerator = $_['urlGenerator'];
?>
<div class="picker-window small">
<h2><?php p($l->t('Account access')) ?></h2>
<p class="info">
<?php p($l->t('Currently logged in as %1$s (%2$s).', [
$_['userDisplayName'],
$_['userId'],
])) ?>
</p>
<p class="info">
<?php print_unescaped($l->t('You are about to grant %1$s access to your %2$s account.', [
'<strong>' . \OCP\Util::sanitizeHTML($_['client']) . '</strong>',
\OCP\Util::sanitizeHTML($_['instanceName'])
])) ?>
</p>
<br/>
<p id="redirect-link">
<form method="POST" action="<?php p($urlGenerator->linkToRouteAbsolute('core.ClientFlowLoginV2.generateAppPassword')) ?>">
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']) ?>" />
<input type="hidden" name="stateToken" value="<?php p($_['stateToken']) ?>" />
<?php if ($_['direct']) { ?>
<input type="hidden" name="direct" value="1" />
<?php } ?>
<div id="submit-wrapper">
<input type="submit" class="login primary icon-confirm-white" title="" value="<?php p($l->t('Grant access')); ?>" />
</div>
</form>
</p>
</div>

View file

@ -22,6 +22,7 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\EventDispatcher\IEventDispatcher;
@ -53,6 +54,7 @@ class ClientFlowLoginControllerTest extends TestCase {
private IEventDispatcher&MockObject $eventDispatcher;
private ITimeFactory&MockObject $timeFactory;
private IConfig&MockObject $config;
private IInitialState&MockObject $initialState;
private ClientFlowLoginController $clientFlowLoginController;
@ -79,6 +81,7 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->config = $this->createMock(IConfig::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->clientFlowLoginController = new ClientFlowLoginController(
'core',
@ -96,6 +99,7 @@ class ClientFlowLoginControllerTest extends TestCase {
$this->eventDispatcher,
$this->timeFactory,
$this->config,
$this->initialState,
);
}
@ -138,7 +142,7 @@ class ClientFlowLoginControllerTest extends TestCase {
->method('set')
->with('client.flow.state.token', 'StateToken');
$this->session
->expects($this->once())
->expects($this->atLeastOnce())
->method('get')
->with('oauth.state')
->willReturn('OauthStateToken');
@ -154,27 +158,39 @@ class ClientFlowLoginControllerTest extends TestCase {
->method('getServerProtocol')
->willReturn('https');
$initialState = [];
$this->initialState->expects($this->exactly(2))
->method('provideInitialState')
->willReturnCallback(function () use (&$initialState) {
$initialState[] = func_get_args();
});
$expected = new StandaloneTemplateResponse(
'core',
'loginflow/authpicker',
[
'client' => 'Mac OS X Sync Client',
'clientIdentifier' => '',
'instanceName' => 'ExampleCloud',
'urlGenerator' => $this->urlGenerator,
'stateToken' => 'StateToken',
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'user' => '',
'direct' => 0,
'providedRedirectUri' => '',
],
'guest'
'loginflow',
renderAs: 'guest'
);
$csp = new ContentSecurityPolicy();
$csp->addAllowedFormActionDomain('nc://*');
$expected->setContentSecurityPolicy($csp);
$this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage());
self::assertEquals([
['loginFlowState', 'auth'],
[
'loginFlowAuth', [
'client' => 'Mac OS X Sync Client',
'clientIdentifier' => '',
'instanceName' => 'ExampleCloud',
'stateToken' => 'StateToken',
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'direct' => false,
'providedRedirectUri' => '',
'appTokenUrl' => '',
'loginRedirectUrl' => '',
],
],
], $initialState);
}
public function testShowAuthPickerPageWithOauth(): void {
@ -205,7 +221,7 @@ class ClientFlowLoginControllerTest extends TestCase {
->method('set')
->with('client.flow.state.token', 'StateToken');
$this->session
->expects($this->once())
->expects($this->atLeastOnce())
->method('get')
->with('oauth.state')
->willReturn('OauthStateToken');
@ -221,27 +237,39 @@ class ClientFlowLoginControllerTest extends TestCase {
->method('getServerProtocol')
->willReturn('https');
$initialState = [];
$this->initialState->expects($this->exactly(2))
->method('provideInitialState')
->willReturnCallback(function () use (&$initialState) {
$initialState[] = func_get_args();
});
$expected = new StandaloneTemplateResponse(
'core',
'loginflow/authpicker',
[
'client' => 'My external service',
'clientIdentifier' => 'MyClientIdentifier',
'instanceName' => 'ExampleCloud',
'urlGenerator' => $this->urlGenerator,
'stateToken' => 'StateToken',
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'user' => '',
'direct' => 0,
'providedRedirectUri' => '',
],
'guest'
'loginflow',
renderAs: 'guest'
);
$csp = new ContentSecurityPolicy();
$csp->addAllowedFormActionDomain('https://example.com/redirect.php');
$expected->setContentSecurityPolicy($csp);
$this->assertEquals($expected, $this->clientFlowLoginController->showAuthPickerPage('MyClientIdentifier'));
self::assertEquals([
['loginFlowState', 'auth'],
[
'loginFlowAuth', [
'client' => 'My external service',
'clientIdentifier' => 'MyClientIdentifier',
'instanceName' => 'ExampleCloud',
'stateToken' => 'StateToken',
'serverHost' => 'https://example.com',
'oauthState' => 'OauthStateToken',
'direct' => false,
'providedRedirectUri' => '',
'appTokenUrl' => '',
'loginRedirectUrl' => '',
],
],
], $initialState);
}
public function testGenerateAppPasswordWithInvalidToken(): void {

View file

@ -17,6 +17,7 @@ use OC\Core\Service\LoginFlowV2Service;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\StandaloneTemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Defaults;
use OCP\IL10N;
use OCP\IRequest;
@ -29,22 +30,15 @@ use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;
class ClientFlowLoginV2ControllerTest extends TestCase {
/** @var IRequest|MockObject */
private $request;
/** @var LoginFlowV2Service|MockObject */
private $loginFlowV2Service;
/** @var IURLGenerator|MockObject */
private $urlGenerator;
/** @var ISession|MockObject */
private $session;
/** @var IUserSession|MockObject */
private $userSession;
/** @var ISecureRandom|MockObject */
private $random;
/** @var Defaults|MockObject */
private $defaults;
/** @var IL10N|MockObject */
private $l;
private IRequest&MockObject $request;
private LoginFlowV2Service&MockObject $loginFlowV2Service;
private IURLGenerator&MockObject $urlGenerator;
private ISession&MockObject $session;
private IUserSession&MockObject $userSession;
private ISecureRandom&MockObject $random;
private Defaults&MockObject $defaults;
private IInitialState&MockObject $initialState;
private IL10N&MockObject $l;
/** @var ClientFlowLoginV2Controller */
private $controller;
@ -58,6 +52,7 @@ class ClientFlowLoginV2ControllerTest extends TestCase {
$this->userSession = $this->createMock(IUserSession::class);
$this->random = $this->createMock(ISecureRandom::class);
$this->defaults = $this->createMock(Defaults::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->l = $this->createMock(IL10N::class);
$this->l
->expects($this->any())
@ -75,7 +70,8 @@ class ClientFlowLoginV2ControllerTest extends TestCase {
$this->random,
$this->defaults,
'user',
$this->l
$this->l,
$this->initialState,
);
}