Overview
Comment:Improve login flow from NextCloud/ownCloud apps
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: b2887ad79513aa9510d0cd63e4c492a99f165e53d324adb9ae6e8da27998b1e3
User & Date: bohwaz on 2022-11-13 21:39:47
Other Links: branch diff | manifest | tags
Context
2022-11-13
22:39
Add HTTP_LOG_FILE constant check-in: d798cee6b5 user: bohwaz tags: dev
21:39
Improve login flow from NextCloud/ownCloud apps check-in: b2887ad795 user: bohwaz tags: dev
17:58
Fix router for root files check-in: bc10ac1dcb user: bohwaz tags: dev
Changes

Modified src/include/lib/Garradin/Files/Files.php from [afb6bfd1f0] to [c3c27c45ee].

29
30
31
32
33
34
35























36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
		self::$quota = true;
	}

	static public function disableQuota(): void
	{
		self::$quota = false;
	}
























	/**
	 * Returns an array of all file permissions for a given user
	 */
	static public function buildUserPermissions(Session $s): array
	{
		$is_admin = $s->canAccess($s::SECTION_CONFIG, $s::ACCESS_ADMIN);
		$is_web_admin = $is_admin || $s->canAccess($s::SECTION_WEB, $s::ACCESS_ADMIN);

		$p = [];

		if ($s->isLogged() && $id = $s::getUserId()) {
			// The user can always access his own profile files
			$p[File::CONTEXT_USER . '/' . $s::getUserId()] = [
				'mkdir' => false,
				'move' => false,
				'create' => false,
				'read' => true,
				'write' => false,
				'delete' => false,
				'share' => false,







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>













|







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
		self::$quota = true;
	}

	static public function disableQuota(): void
	{
		self::$quota = false;
	}

	static public function listContextsPermissions(Session $s): array
	{
		$perm = self::buildUserPermissions($s);
		$contexts = [
			'Fichiers de votre fiche de membre personnelle' => File::CONTEXT_USER . '/' . $s::getUserId() . '/',
			'Documents de l\'association' => File::CONTEXT_DOCUMENTS,
			'Fichiers des membres' => File::CONTEXT_USER . '//',
			'Fichiers des écritures comptables' => File::CONTEXT_TRANSACTION . '//',
			'Fichiers du site web (contenu des pages, images, etc.)' => File::CONTEXT_WEB . '//',
			'Squelettes du site web' => File::CONTEXT_SKELETON . '/web/',
			'Fichiers de la configuration (logo, etc.)' => File::CONTEXT_CONFIG,
			'Squelettes des formulaires' => File::CONTEXT_SKELETON,
		];

		$out = [];

		foreach ($contexts as $name => $path) {
			$out[$name] = $perm[$path] ?? null;
		}

		return $out;
	}

	/**
	 * Returns an array of all file permissions for a given user
	 */
	static public function buildUserPermissions(Session $s): array
	{
		$is_admin = $s->canAccess($s::SECTION_CONFIG, $s::ACCESS_ADMIN);
		$is_web_admin = $is_admin || $s->canAccess($s::SECTION_WEB, $s::ACCESS_ADMIN);

		$p = [];

		if ($s->isLogged() && $id = $s::getUserId()) {
			// The user can always access his own profile files
			$p[File::CONTEXT_USER . '/' . $s::getUserId() . '/'] = [
				'mkdir' => false,
				'move' => false,
				'create' => false,
				'read' => true,
				'write' => false,
				'delete' => false,
				'share' => false,
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
			'read' => $s->isLogged(), // All config files can be accessed by all logged-in users
			'write' => $is_admin,
			'delete' => false,
			'share' => false,
		];

		// Web skeletons
		$p[File::CONTEXT_SKELETON . '/web'] = [
			'mkdir' => $is_web_admin,
			'move' => $is_web_admin,
			'create' => $is_web_admin,
			'read' => $s->isLogged(),
			'write' => $is_web_admin,
			'delete' => $is_web_admin,
			'share' => false,







|







109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
			'read' => $s->isLogged(), // All config files can be accessed by all logged-in users
			'write' => $is_admin,
			'delete' => false,
			'share' => false,
		];

		// Web skeletons
		$p[File::CONTEXT_SKELETON . '/web/'] = [
			'mkdir' => $is_web_admin,
			'move' => $is_web_admin,
			'create' => $is_web_admin,
			'read' => $s->isLogged(),
			'write' => $is_web_admin,
			'delete' => $is_web_admin,
			'share' => false,

Modified src/include/lib/Garradin/Files/WebDAV/NextCloud.php from [3312455c47] to [beaec70d4b].

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29




30
31
32
33
34
35
36

use const Garradin\{SECRET_KEY, ADMIN_URL, CACHE_ROOT, WWW_URL};

class NextCloud extends WebDAV_NextCloud
{
	protected string $temporary_chunks_path;

	public function __construct(WebDAV $server)
	{
		$this->setServer($server);
		$this->temporary_chunks_path =  CACHE_ROOT . '/webdav.chunks';
		$this->setRootURL(WWW_URL);
	}

	public function auth(?string $login, ?string $password): bool
	{
		$session = Session::getInstance();

		if ($session->isLogged()) {
			return true;
		}





		if ($session->checkAppCredentials($login, $password)) {
			return true;
		}

		if ($session->login($login, $password)) {
			return true;







|

<











>
>
>
>







9
10
11
12
13
14
15
16
17

18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

use const Garradin\{SECRET_KEY, ADMIN_URL, CACHE_ROOT, WWW_URL};

class NextCloud extends WebDAV_NextCloud
{
	protected string $temporary_chunks_path;

	public function __construct()
	{

		$this->temporary_chunks_path =  CACHE_ROOT . '/webdav.chunks';
		$this->setRootURL(WWW_URL);
	}

	public function auth(?string $login, ?string $password): bool
	{
		$session = Session::getInstance();

		if ($session->isLogged()) {
			return true;
		}

		if (!$login || !$password) {
			return false;
		}

		if ($session->checkAppCredentials($login, $password)) {
			return true;
		}

		if ($session->login($login, $password)) {
			return true;
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
	{
		return Session::getInstance()->verifyAppToken($_POST['token']);
	}

	public function getLoginURL(?string $token): string
	{
		if ($token) {
			return sprintf('%slogin.php?nc=%s', ADMIN_URL, $token);
		}
		else {
			return sprintf('%slogin.php?nc=redirect', ADMIN_URL);
		}
	}

	public function getDirectDownloadSecret(string $uri, string $login): string
	{
		return hash('sha256', $uri . SECRET_KEY);
	}

	protected function cleanChunks(): void
	{
		// 36 hours
		$expire = time() - 36*3600;








|


|





|







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
	{
		return Session::getInstance()->verifyAppToken($_POST['token']);
	}

	public function getLoginURL(?string $token): string
	{
		if ($token) {
			return sprintf('%slogin.php?app=%s', ADMIN_URL, $token);
		}
		else {
			return sprintf('%slogin.php?app=redirect', ADMIN_URL);
		}
	}

	public function getDirectDownloadSecret(string $uri, string $login): string
	{
		return hash_hmac('sha1', $uri, SECRET_KEY);
	}

	protected function cleanChunks(): void
	{
		// 36 hours
		$expire = time() - 36*3600;

Modified src/include/lib/Garradin/Files/WebDAV/Server.php from [c6e87defd1] to [07a8dc31c3].

11
12
13
14
15
16
17


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Server
{
	static public function route(?string $uri = null): bool
	{
		$uri = '/' . ltrim($uri, '/');

		$dav = new WebDAV;


		$dav->setStorage(new Storage);

		header('Access-Control-Allow-Origin: *', true);
		$method = $_SERVER['REQUEST_METHOD'] ?? null;

		// Always say YES to OPTIONS
		if ($method == 'OPTIONS') {
			$dav->http_options();
			return true;
		}


		if (WOPI_DISCOVERY_URL) {
			$wopi = new WOPI;
			$wopi->setServer($dav);

			if ($wopi->route($uri)) {
				return true;
			}
		}

		$nc = new NextCloud($dav);

		if ($r = $nc->route($uri)) {
			// NextCloud route already replied something, stop here
			return true;
		}

		// If NextCloud layer didn't return anything







>
>
|




















|







11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Server
{
	static public function route(?string $uri = null): bool
	{
		$uri = '/' . ltrim($uri, '/');

		$dav = new WebDAV;
		$nc = new NextCloud($dav);
		$storage = new Storage($nc);
		$dav->setStorage($storage);

		header('Access-Control-Allow-Origin: *', true);
		$method = $_SERVER['REQUEST_METHOD'] ?? null;

		// Always say YES to OPTIONS
		if ($method == 'OPTIONS') {
			$dav->http_options();
			return true;
		}


		if (WOPI_DISCOVERY_URL) {
			$wopi = new WOPI;
			$wopi->setServer($dav);

			if ($wopi->route($uri)) {
				return true;
			}
		}

		$nc->setServer($dav);

		if ($r = $nc->route($uri)) {
			// NextCloud route already replied something, stop here
			return true;
		}

		// If NextCloud layer didn't return anything

Modified src/include/lib/Garradin/Files/WebDAV/Storage.php from [bc5fae8168] to [7cbcbef5e2].

18
19
20
21
22
23
24
25
26
27


28
29

30
31
32
33
34
35
36
{
	/**
	 * These file names will be ignored when doing a PUT
	 * as they are garbage, coming from some OS
	 */
	const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';

	protected $cache = [];
	protected $root = [];



	public function __construct()
	{

		$access = Files::listReadAccessContexts(Session::getInstance());

		$this->cache[''] = (object) ['name' => '', 'type' => File::TYPE_DIRECTORY];

		foreach ($access as $context => $name) {
			$this->cache[$context] = (object) ['name' => $context, 'type' => File::TYPE_DIRECTORY];
			$this->root[] = $context;







|
|

>
>
|

>







18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
	/**
	 * These file names will be ignored when doing a PUT
	 * as they are garbage, coming from some OS
	 */
	const PUT_IGNORE_PATTERN = '!^~(?:lock\.|^\._)|^(?:\.DS_Store|Thumbs\.db|desktop\.ini)$!';

	protected array $cache = [];
	protected array $root = [];

	protected NextCloud $nextcloud;

	public function __construct(NextCloud $nextcloud)
	{
		$this->nextcloud = $nextcloud;
		$access = Files::listReadAccessContexts(Session::getInstance());

		$this->cache[''] = (object) ['name' => '', 'type' => File::TYPE_DIRECTORY];

		foreach ($access as $context => $name) {
			$this->cache[$context] = (object) ['name' => $context, 'type' => File::TYPE_DIRECTORY];
			$this->root[] = $context;
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
			// NextCloud stuff
			case NextCloud::PROP_NC_HAS_PREVIEW:
			case NextCloud::PROP_NC_IS_ENCRYPTED:
				return 'false';
			case NextCloud::PROP_OC_SHARETYPES:
				return WebDAV::EMPTY_PROP_VALUE;
			case NextCloud::PROP_OC_DOWNLOADURL:
				return NextCloud::getDirectURL($uri, $this->users->current()->login);
			case Nextcloud::PROP_NC_RICH_WORKSPACE:
				return '';
			case NextCloud::PROP_OC_ID:
				return NextCloud::getDirectID('', $uri);
			case NextCloud::PROP_OC_PERMISSIONS:
				$permissions = [
					NextCloud::PERM_READ => $file->canRead(),







|







160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
			// NextCloud stuff
			case NextCloud::PROP_NC_HAS_PREVIEW:
			case NextCloud::PROP_NC_IS_ENCRYPTED:
				return 'false';
			case NextCloud::PROP_OC_SHARETYPES:
				return WebDAV::EMPTY_PROP_VALUE;
			case NextCloud::PROP_OC_DOWNLOADURL:
				return $this->nextcloud->getDirectURL($uri, 'null');
			case Nextcloud::PROP_NC_RICH_WORKSPACE:
				return '';
			case NextCloud::PROP_OC_ID:
				return NextCloud::getDirectID('', $uri);
			case NextCloud::PROP_OC_PERMISSIONS:
				$permissions = [
					NextCloud::PERM_READ => $file->canRead(),

Modified src/include/lib/Garradin/Users/Session.php from [38f0d3ac47] to [fa0c926a33].

153
154
155
156
157
158
159





160
161
162
163
164
165
166
		return $this->db->delete('users_sessions', $this->db->where('selector', $selector));
	}

	protected function deleteAllRememberMeSelectors($user_id)
	{
		return $this->db->delete('users_sessions', $this->db->where('id_user', $user_id));
	}






	/**
	 * Create a temporary app token for an external service session (eg. NextCloud)
	 */
	public function generateAppToken(): string
	{
		$token = hash('sha256', random_bytes(16));







>
>
>
>
>







153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
		return $this->db->delete('users_sessions', $this->db->where('selector', $selector));
	}

	protected function deleteAllRememberMeSelectors($user_id)
	{
		return $this->db->delete('users_sessions', $this->db->where('id_user', $user_id));
	}

	public function getAppLoginToken(): ?string
	{
		return $_GET['app'] ?? null;
	}

	/**
	 * Create a temporary app token for an external service session (eg. NextCloud)
	 */
	public function generateAppToken(): string
	{
		$token = hash('sha256', random_bytes(16));
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
		return true;
	}

	/**
	 * Verify temporary app token and create a session,
	 * this is similar to "remember me" sessions but without cookies
	 */
	public function verifyAppToken(string $token): ?\stdClass
	{
		if (!ctype_alnum($token) || strlen($token) > 64) {
			return null;
		}

		$token = $this->getRememberMeSelector('tok_' . $token);








|







205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
		return true;
	}

	/**
	 * Verify temporary app token and create a session,
	 * this is similar to "remember me" sessions but without cookies
	 */
	public function verifyAppToken(string $token): ?array
	{
		if (!ctype_alnum($token) || strlen($token) > 64) {
			return null;
		}

		$token = $this->getRememberMeSelector('tok_' . $token);

226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
		// Create a real session, not too long
		$selector = $this->createSelectorValues($token->user_id, $token->user_password, '+1 month');
		$this->storeRememberMeSelector($selector->selector, $selector->hash, $selector->expiry, $token->user_id);

		$login = $selector->selector;
		$password = $selector->verifier;

		return (object) compact('login', 'password');
	}


	public function createAppCredentials(): \stdClass
	{
		if (!$this->isLogged()) {
			throw new \LogicException('User is not logged');







|







231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
		// Create a real session, not too long
		$selector = $this->createSelectorValues($token->user_id, $token->user_password, '+1 month');
		$this->storeRememberMeSelector($selector->selector, $selector->hash, $selector->expiry, $token->user_id);

		$login = $selector->selector;
		$password = $selector->verifier;

		return compact('login', 'password');
	}


	public function createAppCredentials(): \stdClass
	{
		if (!$this->isLogged()) {
			throw new \LogicException('User is not logged');
260
261
262
263
264
265
266
267
268






269
270
271
272
273
274
275
276
		}

		if (!$this->checkRememberMeSelector($selector, $password)) {
			$this->deleteRememberMeSelector($selector->selector);
			return null;
		}

		$this->user = $this->getUserDataForSession($selector->user_id);







		return $this->user;
	}

	public function isLogged(bool $disable_local_login = false)
	{
		$logged = parent::isLogged();

		// Ajout de la gestion de LOCAL_LOGIN







|

>
>
>
>
>
>
|







265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
		}

		if (!$this->checkRememberMeSelector($selector, $password)) {
			$this->deleteRememberMeSelector($selector->selector);
			return null;
		}

		$this->_user = Users::get($selector->user_id);

		if (!$this->_user) {
			return null;
		}

		$this->user = $selector->user_id;

		return $this->_user;
	}

	public function isLogged(bool $disable_local_login = false)
	{
		$logged = parent::isLogged();

		// Ajout de la gestion de LOCAL_LOGIN

Modified src/include/lib/Garradin/Web/Router.php from [599059a936] to [4d6c8743e9].

23
24
25
26
27
28
29

30
31
32
33
34
35
36
class Router
{
	const DAV_ROUTES = [
		'dav',
		'wopi',
		'remote.php',
		'index.php',

		'ocs',
	];

	static public function route(): void
	{
		$uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';








>







23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Router
{
	const DAV_ROUTES = [
		'dav',
		'wopi',
		'remote.php',
		'index.php',
		'status.php',
		'ocs',
	];

	static public function route(): void
	{
		$uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';

71
72
73
74
75
76
77

78
79
80
81
82
83
84
85
			return;
		}
		elseif ('form' == $first) {
			$uri = substr($uri, 5);
			UserForms::serve($uri);
			return;
		}

		elseif (in_array($first, self::DAV_ROUTES) && WebDAV_Server::route($uri)) {
			return;
		}
		elseif (Files::getContext($uri)
			&& (($file = Files::getFromURI($uri))
				|| ($file = Web::getAttachmentFromURI($uri)))) {
			$size = null;








>
|







72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
			return;
		}
		elseif ('form' == $first) {
			$uri = substr($uri, 5);
			UserForms::serve($uri);
			return;
		}
		elseif ((in_array($uri, self::DAV_ROUTES) || in_array($first, self::DAV_ROUTES))
			&& WebDAV_Server::route($uri)) {
			return;
		}
		elseif (Files::getContext($uri)
			&& (($file = Files::getFromURI($uri))
				|| ($file = Web::getAttachmentFromURI($uri)))) {
			$size = null;

Modified src/templates/login.tpl from [89a2db753b] to [f6f5859e6b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{if $api_login}<?php $layout = 'public'; ?>{/if}
{include file="_head.tpl" title="Connexion"}

{form_errors}

{if $api_login == 'ok'}
	<p class="block confirm">Vous avez bien été connecté.</p>
	<div class="progressing block"></div>
	<p class="help">Vous pourrez fermer cette fenêtre quand l'application aura terminé l'autorisation.</p>
{else}
	{if $changed}
		<p class="block confirm">
			Votre mot de passe a bien été modifié.<br />
			Vous pouvez maintenant l'utiliser pour vous reconnecter.
		</p>
	{elseif isset($_GET['logout'])}
		<p class="block confirm">
			Vous avez bien été déconnecté.
		</p>
	{/if}

	<p class="block error" style="display: none;" id="old_browser">
		Le navigateur que vous utilisez n'est pas supporté. Des fonctionnalités peuvent ne pas fonctionner.<br />
		Merci d'utiliser un navigateur web moderne comme <a href="https://www.getfirefox.com/" target="_blank">Firefox</a> ou <a href="https://vivaldi.com/fr/" target="_blank">Vivaldi</a>.
	</p>

	<form method="post" action="{$self_url}" data-focus="{if $_POST}2{else}1{/if}">
		{if $api_login && $api_login != 'ok'}
			<p class="alert block">Une application tiers demande à accéder aux données de l'association.</p>
			<p class="help">L'application aura accès à tous vos fichiers.</p>
		{/if}

		<fieldset>
			<legend>
				{if $ssl_enabled}
					<span class="confirm">{icon shape="lock"} Connexion sécurisée</span>
				{else}
					<span class="alert">{icon shape="unlock"} Connexion non-sécurisée</span>
				{/if}
			</legend>
			<dl>
				{input type=$id_field.type label=$id_field.label required=true name="id"}
				{input type="password" name="password" label="Mot de passe" required=true}
				{if !$api_login}
				{input type="checkbox" name="permanent" value="1" label="Rester connecté⋅e" help="recommandé seulement sur ordinateur personnel"}
				{/if}
			</dl>
		</fieldset>

		{if $captcha}
		<fieldset>
			<legend>Vérification de sécurité</legend>
			<input type="hidden" name="c_hash" value="{$captcha.hash}" />
			<dl>
				<dt><label for="f_c_answer">Merci de recopier en chiffres (par exemple <em>1234</em>) le nombre suivant :<b>(obligatoire)</b></label></dt>
				<dd><tt>{$captcha.spellout}</tt></dd>
				<dd>{input name="c_answer" type="text" maxlength=4 label=null required=true}</dd>
				<dd class="help">Cette vérification est demandée après plusieurs tentatives de connexion infructueuses.</dd>
			</dl>
		</fieldset>
		{/if}

		<p class="submit">
			{csrf_field key="login"}
			{button type="submit" name="login" label="Se connecter" shape="right" class="main"}
			{if $api_login}
				<input type="hidden" name="token" value="{$api_login}" />
			{else}
				{linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"}
				{linkbutton href="!password.php?new" label="Première connexion ?" shape="user"}
			{/if}
		</p>

	</form>

	{literal}
	<script type="text/javascript" async="async">
	if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) {
		document.getElementById('old_browser').style.display = 'block';
	}
	</script>
	{/literal}
{/if}

{include file="_foot.tpl"}
<




<
<
<
<
<
|
|
|
|
|
|
|
|
|
|

|
|
|
|

|
|
|
<
|

|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
|
|

|
|
|
|
<
<
|
|
|
|

|

|
|
|
|
|
|
|
<



1
2
3
4





5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59


60
61
62
63
64
65
66
67
68
69
70
71
72
73

74
75

{include file="_head.tpl" title="Connexion"}

{form_errors}






{if $changed}
	<p class="block confirm">
		Votre mot de passe a bien été modifié.<br />
		Vous pouvez maintenant l'utiliser pour vous reconnecter.
	</p>
{elseif isset($_GET['logout'])}
	<p class="block confirm">
		Vous avez bien été déconnecté.
	</p>
{/if}

<p class="block error" style="display: none;" id="old_browser">
	Le navigateur que vous utilisez n'est pas supporté. Des fonctionnalités peuvent ne pas fonctionner.<br />
	Merci d'utiliser un navigateur web moderne comme <a href="https://www.getfirefox.com/" target="_blank">Firefox</a> ou <a href="https://vivaldi.com/fr/" target="_blank">Vivaldi</a>.
</p>

<form method="post" action="{$self_url}" data-focus="{if $_POST}2{else}1{/if}">
	{if $app_token}
		<p class="alert block">Une application tiers demande à accéder aux fichiers de l'association.<br />Connectez-vous pour pouvoir confirmer l'accès.</p>

	{/if}

	<fieldset>
		<legend>
			{if $ssl_enabled}
				<span class="confirm">{icon shape="lock"} Connexion sécurisée</span>
			{else}
				<span class="alert">{icon shape="unlock"} Connexion non-sécurisée</span>
			{/if}
		</legend>
		<dl>
			{input type=$id_field.type label=$id_field.label required=true name="id"}
			{input type="password" name="password" label="Mot de passe" required=true}
			{if !$app_token}
			{input type="checkbox" name="permanent" value="1" label="Rester connecté⋅e" help="recommandé seulement sur ordinateur personnel"}
			{/if}
		</dl>
	</fieldset>

	{if $captcha}
	<fieldset>
		<legend>Vérification de sécurité</legend>
		<input type="hidden" name="c_hash" value="{$captcha.hash}" />
		<dl>
			<dt><label for="f_c_answer">Merci de recopier en chiffres (par exemple <em>1234</em>) le nombre suivant :<b>(obligatoire)</b></label></dt>
			<dd><tt>{$captcha.spellout}</tt></dd>
			<dd>{input name="c_answer" type="text" maxlength=4 label=null required=true}</dd>
			<dd class="help">Cette vérification est demandée après plusieurs tentatives de connexion infructueuses.</dd>
		</dl>
	</fieldset>
	{/if}

	<p class="submit">
		{csrf_field key="login"}
		{button type="submit" name="login" label="Se connecter" shape="right" class="main"}
		{if !$app_token}


			{linkbutton href="!password.php" label="Mot de passe perdu ?" shape="help"}
			{linkbutton href="!password.php?new" label="Première connexion ?" shape="user"}
		{/if}
	</p>

</form>

{literal}
<script type="text/javascript" async="async">
if (window.navigator.userAgent.match(/MSIE|Trident\/|Edge\//)) {
	document.getElementById('old_browser').style.display = 'block';
}
</script>
{/literal}


{include file="_foot.tpl"}

Added src/templates/login_app.tpl version [317583c35e].



























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{include file="_head.tpl" title="Accès par une application tiers" layout="public"}

{if $app_token == 'ok'}
	<p class="block confirm">L'application a bien été autorisée.</p>
	<div class="progressing block"></div>
	<p class="help">Vous pourrez fermer cette fenêtre quand l'application aura terminé l'autorisation.</p>
{else}
	<p class="alert block">Une application tiers demande à accéder aux fichiers de l'association.</p>
	<form method="post" action="{$self_url}">
		<fieldset>
			<legend>Confirmer l'accès</legend>
			<h3 class="warning">Autoriser l'application à accéder aux fichiers&nbsp;?</h3>
			<p class="help">L'application aura accès aux fichiers suivants&nbsp;:</p>
			<table class="list auto">
				<thead>
					<tr>
						<td>Section</td>
						<td>Lecture</td>
						<td>Modification</td>
						<td>Suppression</td>
					</tr>
				</thead>
				<tbody>
					{foreach from=$permissions key="name" item="access"}
					<tr>
						<td>{$name}</td>
						<td class="check">{if $access.read}{icon shape="check"}{/if}</td>
						<td class="check">{if $access.write}{icon shape="check"}{/if}</td>
						<td class="check">{if $access.delete}{icon shape="check"}{/if}</td>
					</tr>
					{/foreach}
				</tbody>
			</table>
			<p class="actions">
				{csrf_field key=$csrf_key}
				{button type="submit" label="Autoriser l'accès" shape="right" class="main" name="confirm"}
			</p>
			<p class="submit">
				{button type="submit" label="Annuler" shape="left" name="cancel"}
			</p>
		</fieldset>
	</form>
{/if}

{include file="_foot.tpl"}

Modified src/templates/login_otp.tpl from [fbb6ee7328] to [6f52c70b8a].

1
2
3
4
5
6
7
8
9
10
11
12
{include file="_head.tpl" title="Connexion — double facteur"}

{form_errors}

<form method="post" action="{$self_url}">

	<fieldset>
		<legend>Authentification à double facteur</legend>
		<dl>
			{input type="text" class="otp" minlength=6 maxlength=6 label="Code TOTP" name="code" help="Entrez ici le code donné par l'application d'authentification double facteur." required=true}
		</dl>
	</fieldset>




|







1
2
3
4
5
6
7
8
9
10
11
12
{include file="_head.tpl" title="Connexion — double facteur"}

{form_errors}

<form method="post" action="{$self_url}" data-focus="1">

	<fieldset>
		<legend>Authentification à double facteur</legend>
		<dl>
			{input type="text" class="otp" minlength=6 maxlength=6 label="Code TOTP" name="code" help="Entrez ici le code donné par l'application d'authentification double facteur." required=true}
		</dl>
	</fieldset>

Modified src/www/admin/login.php from [93267989eb] to [722866c99b].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32


33
34
35




36

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?php
namespace Garradin;

use KD2\HTTP;
use KD2\Security;

use Garradin\Users\DynamicFields;
use Garradin\Users\Session;

use Garradin\ValidationException;

const LOGIN_PROCESS = true;

require_once __DIR__ . '/_inc.php';

$session = Session::getInstance();

// Relance session_start et renvoie une image de 1px transparente
if (qg('keepSessionAlive') !== null)
{
    $session->keepAlive();

    header('Cache-Control: no-cache, must-revalidate');
    header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');

    header('Content-Type: image/gif');
    echo base64_decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");

    exit;
}

$api_login = qg('tok') ?? (f('token') ?? null);



// L'utilisateur est déjà connecté
if (!$api_login && $session->isLogged()) {




    Utils::redirect(ADMIN_URL . '');

}

$id_field = DynamicFields::get(DynamicFields::getLoginField());
$id_field_name = $id_field->label;
$lock = Log::isLocked();

$form->runIf('login', function () use ($id_field_name, $session, $lock) {
    if ($lock == 1) {
        throw new UserException(sprintf("Vous avez dépassé la limite de tentatives de connexion.\nMerci d'attendre %d minutes avant de ré-essayer de vous connecter.", Log::LOCKOUT_DELAY/60));
    }
    elseif ($lock == -1 && !Security::checkCaptcha(SECRET_KEY, f('c_hash'), f('c_answer'))) {
        throw new UserException('Le code de vérification entré n\'est pas correct.');
    }

    $_POST['c_answer'] = null;

    if (!trim((string) f('id'))) {
        throw new UserException(sprintf('L\'identifiant (%s) n\'a pas été renseigné.', $id_field_name));
    }

    if (!trim((string) f('password'))) {
        throw new UserException('Le mot de passe n\'a pas été renseigné.');
    }

    if (!$session->login(f('id'), f('password'), (bool) f('permanent'))) {
        throw new UserException(sprintf("Connexion impossible.\nVérifiez votre identifiant (%s) et votre mot de passe.", $id_field_name));
    }

    if (f('token')) {
        try {
            if (f('token') == 'flow') {
                $data = $session->createAppCredentials();

            }
            else {
                $session->validateAppToken(f('token'));
            }
        }
        finally {
            // We don't want to be logged-in really
            $session->logout();
        }

        if ($data->redirect ?? null) {
            http_response_code(303);
            header('Location: ' . $data->redirect);
            exit;
        }

        Utils::redirect('!login.php?tok=ok');
    }

}, 'login', ADMIN_URL);

$captcha = $lock == -1 ? Security::createCaptcha(SECRET_KEY, 'fr_FR') : null;

$ssl_enabled = HTTP::getScheme() == 'https';
$changed = qg('changed') !== null;

$tpl->assign(compact('id_field', 'ssl_enabled', 'changed', 'api_login', 'captcha'));

$tpl->display('login.tpl');









|










|

|
|

|
|

|


|
>
>


|
>
>
>
>
|
>






|
|
|
|
|
|
|

|

|
|
|

|
|
|

|
<
|
|
<
<
<
<
>
|
<
<
|
<
<
<
|
<
|
<
<
<
<
|
|
|
|
<







|


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

69
70




71
72


73



74

75




76
77
78
79

80
81
82
83
84
85
86
87
88
89
<?php
namespace Garradin;

use KD2\HTTP;
use KD2\Security;

use Garradin\Users\DynamicFields;
use Garradin\Users\Session;

use Garradin\UserException;

const LOGIN_PROCESS = true;

require_once __DIR__ . '/_inc.php';

$session = Session::getInstance();

// Relance session_start et renvoie une image de 1px transparente
if (qg('keepSessionAlive') !== null)
{
	$session->keepAlive();

	header('Cache-Control: no-cache, must-revalidate');
	header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');

	header('Content-Type: image/gif');
	echo base64_decode("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==");

	exit;
}

$app_token = $session->getAppLoginToken();
$args = $app_token ? '?app=' . rawurlencode($app_token) : '';
$layout = $app_token ? 'public' : null;

// L'utilisateur est déjà connecté
if ($session->isLogged()) {
	if ($app_token) {
		Utils::redirect('!login_app.php' . $args);
	}
	else {
		Utils::redirect(ADMIN_URL);
	}
}

$id_field = DynamicFields::get(DynamicFields::getLoginField());
$id_field_name = $id_field->label;
$lock = Log::isLocked();

$form->runIf('login', function () use ($id_field_name, $session, $lock, $args) {
	if ($lock == 1) {
		throw new UserException(sprintf("Vous avez dépassé la limite de tentatives de connexion.\nMerci d'attendre %d minutes avant de ré-essayer de vous connecter.", Log::LOCKOUT_DELAY/60));
	}
	elseif ($lock == -1 && !Security::checkCaptcha(SECRET_KEY, f('c_hash'), f('c_answer'))) {
		throw new UserException('Le code de vérification entré n\'est pas correct.');
	}

	$_POST['c_answer'] = null;

	if (!trim((string) f('id'))) {
		throw new UserException(sprintf('L\'identifiant (%s) n\'a pas été renseigné.', $id_field_name));
	}

	if (!trim((string) f('password'))) {
		throw new UserException('Le mot de passe n\'a pas été renseigné.');
	}

	$ok = $session->login(f('id'), f('password'), (bool) f('permanent'));


	if (!$ok) {




		throw new UserException(sprintf("Connexion impossible.\nVérifiez votre identifiant (%s) et votre mot de passe.", $id_field_name));
	}






	if ($session::REQUIRE_OTP == $ok) {

		Utils::redirect('!login_otp.php' . $args);




	}
	elseif ($args) {
		Utils::redirect('!login_app.php' . $args);
	}

}, 'login', ADMIN_URL);

$captcha = $lock == -1 ? Security::createCaptcha(SECRET_KEY, 'fr_FR') : null;

$ssl_enabled = HTTP::getScheme() == 'https';
$changed = qg('changed') !== null;

$tpl->assign(compact('id_field', 'ssl_enabled', 'changed', 'app_token', 'layout', 'captcha'));

$tpl->display('login.tpl');

Added src/www/admin/login_app.php version [aeca79011c].

























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
namespace Garradin;

use Garradin\Files\Files;
use Garradin\Users\Session;

require_once __DIR__ . '/_inc.php';

$session = Session::getInstance();

$app_token = $session->getAppLoginToken();

if (!$app_token) {
	die("No app token was supplied.");
}

$csrf_key = 'app_confirm_' . $app_token;

$form->runIf('cancel', function () {
	Utils::redirect('!logout.php');
});

$form->runIf('confirm', function () use ($app_token, $session) {
	if ($app_token == 'redirect') {
		$data = $session->createAppCredentials();
	}
	else {
		$session->validateAppToken($app_token);
	}

	if ($data->redirect ?? null) {
		http_response_code(303);
		header('Location: ' . $data->redirect);
		exit;
	}

	Utils::redirect('!login_app.php?app=ok');
}, $csrf_key);

$permissions = Files::listContextsPermissions($session);

$tpl->assign(compact('app_token', 'csrf_key', 'permissions'));

$tpl->display('login_app.tpl');

Modified src/www/admin/login_otp.php from [bf071aca86] to [54b9408ded].

13
14
15
16
17
18
19




20
21
22
23




24
25
26
27
28
if (!$session->isOTPRequired()) {
	Utils::redirect(ADMIN_URL);
}

$login = null;
$csrf_key = 'login_otp';





$form->runIf('login', function () use ($session) {
	if (!$session->loginOTP(f('code'))) {
		throw new UserException(sprintf('Code incorrect. Vérifiez que votre téléphone est à l\'heure (heure du serveur : %s).', date('d/m/Y H:i:s')));
	}




}, $csrf_key, '!');

$tpl->assign(compact('csrf_key'));

$tpl->display('login_otp.tpl');







>
>
>
>
|



>
>
>
>


|


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (!$session->isOTPRequired()) {
	Utils::redirect(ADMIN_URL);
}

$login = null;
$csrf_key = 'login_otp';

$app_token = $session->getAppLoginToken();
$args = $app_token ? '?app=' . rawurlencode($app_token) : '';
$layout = $app_token ? 'public' : null;

$form->runIf('login', function () use ($session, $args) {
	if (!$session->loginOTP(f('code'))) {
		throw new UserException(sprintf('Code incorrect. Vérifiez que votre téléphone est à l\'heure (heure du serveur : %s).', date('d/m/Y H:i:s')));
	}

	if ($args) {
		Utils::redirect('!login_app.php' . $args);
	}
}, $csrf_key, '!');

$tpl->assign(compact('csrf_key', 'layout'));

$tpl->display('login_otp.tpl');

Modified src/www/admin/static/styles/06-tables.css from [70535fc490] to [bb14c89e08].

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
    font-weight: bold;
}

table.list .confirm {
    color: darkgreen;
}

table.list .num {
    text-align: center;
}

table.list .check {
    width: 1%;
}








|







70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
    font-weight: bold;
}

table.list .confirm {
    color: darkgreen;
}

table.list .num, table.list .check {
    text-align: center;
}

table.list .check {
    width: 1%;
}