• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / security / 30133545030

24 Jul 2026 11:19PM UTC coverage: 92.845% (+0.4%) from 92.487%
30133545030

push

github

dg
SimpleAuthenticator: added support for hashed passwords

Stored passwords may now be crypt-format hashes and can be freely
mixed with plain-text ones. Hashes are detected by the '$ident$'
prefix followed by at least 20 characters, the same way
password_verify() identifies them, so besides
bcrypt and argon2 the legacy crypt() formats work too, and a hash
with an algorithm unknown to the PHP build fails closed instead of
being compared as plain text. Plain-text comparison is timing-safe
via hash_equals().

Hashes work in the 'users' configuration section without any extra
option; argon2 hashes must be quoted in NEON because of '=' and ','.

4 of 4 new or added lines in 2 files covered. (100.0%)

19 existing lines in 4 files now uncovered.

558 of 601 relevant lines covered (92.85%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.06
/src/Bridges/SecurityDI/SecurityExtension.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Bridges\SecurityDI;
9

10
use Nette;
11
use Nette\Schema\Expect;
12
use Tracy;
13
use function array_pad, is_array;
14

15

16
/**
17
 * Security extension for Nette DI.
18
 *
19
 * @property object{
20
 *     debugger: bool|null,
21
 *     users: array<string, string|array{password: string, roles?: string|list<string>, data?: array<string, mixed>}>,
22
 *     roles: array<string, string|list<string>|null>,
23
 *     resources: array<string, string|null>,
24
 *     rules: object{allow: list<string|list<string|list<string>|null>>, deny: list<string|list<string|list<string>|null>>},
25
 *     authentication: object{
26
 *         storage: 'session'|'cookie',
27
 *         expiration: string|null,
28
 *         persistIdentity: bool,
29
 *         cookieName: string|null,
30
 *         cookieDomain: string|null,
31
 *         cookieSamesite: 'Lax'|'Strict'|'None'|null,
32
 *     },
33
 * } $config
34
 */
35
class SecurityExtension extends Nette\DI\CompilerExtension
36
{
37
        public function __construct(
1✔
38
                private readonly bool $debugMode = false,
39
        ) {
40
        }
1✔
41

42

43
        public function getConfigSchema(): Nette\Schema\Schema
44
        {
45
                $rule = Expect::anyOf(Expect::string(), Expect::listOf('string|array|null')->max(3));
1✔
46
                return Expect::structure([
1✔
47
                        'debugger' => Expect::bool(),
1✔
48
                        'users' => Expect::arrayOf(
1✔
49
                                Expect::anyOf(
1✔
50
                                        Expect::string()->dynamic(), // user => password (plain or hash)
1✔
51
                                        Expect::structure([ // user => password + roles + data
1✔
52
                                                'password' => Expect::string()->dynamic(),
1✔
53
                                                'roles' => Expect::anyOf(Expect::string(), Expect::listOf('string')),
1✔
54
                                                'data' => Expect::array(),
1✔
55
                                        ])->castTo('array'),
1✔
56
                                ),
57
                        ),
58
                        'roles' => Expect::arrayOf('string|array|null'), // role => parent(s)
1✔
59
                        'resources' => Expect::arrayOf('string|null'), // resource => parent
1✔
60
                        'rules' => Expect::structure([ // [role(s), resource(s), privilege(s)] or just role(s)
1✔
61
                                'allow' => Expect::listOf($rule),
1✔
62
                                'deny' => Expect::listOf($rule),
1✔
63
                        ]),
64
                        'authentication' => Expect::structure([
1✔
65
                                'storage' => Expect::anyOf('session', 'cookie')->default('session'),
1✔
66
                                'expiration' => Expect::string()->dynamic(),
1✔
67
                                'persistIdentity' => Expect::bool(true),
1✔
68
                                'cookieName' => Expect::string(),
1✔
69
                                'cookieDomain' => Expect::string(),
1✔
70
                                'cookieSamesite' => Expect::anyOf('Lax', 'Strict', 'None'),
1✔
71
                        ]),
72
                ]);
73
        }
74

75

76
        public function loadConfiguration(): void
77
        {
78
                $config = $this->config;
1✔
79
                $builder = $this->getContainerBuilder();
1✔
80

81
                $builder->addDefinition($this->prefix('passwords'))
1✔
82
                        ->setFactory(Nette\Security\Passwords::class);
1✔
83

84
                $auth = $config->authentication;
1✔
85
                $storage = $builder->addDefinition($this->prefix('userStorage'))
1✔
86
                        ->setType(Nette\Security\UserStorage::class)
1✔
87
                        ->setFactory([
1✔
88
                                'session' => Nette\Bridges\SecurityHttp\SessionStorage::class,
89
                                'cookie' => Nette\Bridges\SecurityHttp\CookieStorage::class,
90
                        ][$auth->storage]);
1✔
91

92
                if ($auth->storage === 'cookie') {
1✔
93
                        $cookieDomain = $auth->cookieDomain === 'domain'
1✔
94
                                ? $builder::literal('$this->getByType(Nette\Http\IRequest::class)->getUrl()->getDomain(2)')
1✔
UNCOV
95
                                : $auth->cookieDomain;
×
96

97
                        $storage->addSetup('setCookieParameters', [$auth->cookieName, $cookieDomain, $auth->cookieSamesite]);
1✔
98
                }
99

100
                $user = $builder->addDefinition($this->prefix('user'))
1✔
101
                        ->setFactory(Nette\Security\User::class);
1✔
102

103
                if ($auth->expiration) {
1✔
104
                        $user->addSetup('setExpiration', [$auth->expiration]);
1✔
105
                }
106

107
                if (!$auth->persistIdentity) {
1✔
108
                        $user->addSetup('$persistIdentity', [false]);
1✔
109
                }
110

111
                if ($config->users) {
1✔
112
                        $usersList = $usersRoles = $usersData = [];
1✔
113
                        foreach ($config->users as $username => $data) {
1✔
114
                                $data = is_array($data) ? $data : ['password' => $data];
1✔
115
                                $usersList[$username] = $data['password'];
1✔
116
                                $usersRoles[$username] = $data['roles'] ?? null;
1✔
117
                                $usersData[$username] = $data['data'] ?? [];
1✔
118
                        }
119

120
                        $builder->addDefinition($this->prefix('authenticator'))
1✔
121
                                ->setType(Nette\Security\IAuthenticator::class)
1✔
122
                                ->setFactory(Nette\Security\SimpleAuthenticator::class, [$usersList, $usersRoles, $usersData]);
1✔
123

124
                        if ($this->name === 'security') {
1✔
125
                                $builder->addAlias('nette.authenticator', $this->prefix('authenticator'));
1✔
126
                        }
127
                }
128

129
                if ($config->roles || $config->resources || $config->rules->allow || $config->rules->deny) {
1✔
130
                        $authorizator = $builder->addDefinition($this->prefix('authorizator'))
1✔
131
                                ->setType(Nette\Security\Authorizator::class)
1✔
132
                                ->setFactory(Nette\Security\Permission::class);
1✔
133

134
                        foreach ($config->roles as $role => $parents) {
1✔
135
                                $authorizator->addSetup('addRole', [$role, $parents]);
1✔
136
                        }
137

138
                        foreach ($config->resources as $resource => $parents) {
1✔
139
                                $authorizator->addSetup('addResource', [$resource, $parents]);
1✔
140
                        }
141

142
                        foreach (['allow', 'deny'] as $type) {
1✔
143
                                foreach ($config->rules->$type as $rule) {
1✔
144
                                        $authorizator->addSetup($type, array_pad(is_array($rule) ? $rule : [$rule], 3, null));
1✔
145
                                }
146
                        }
147

148
                        if ($this->name === 'security') {
1✔
149
                                $builder->addAlias('nette.authorizator', $this->prefix('authorizator'));
1✔
150
                        }
151
                }
152

153
                if ($this->name === 'security') {
1✔
154
                        $builder->addAlias('user', $this->prefix('user'));
1✔
155
                        $builder->addAlias('nette.userStorage', $this->prefix('userStorage'));
1✔
156
                }
157
        }
1✔
158

159

160
        public function beforeCompile(): void
161
        {
162
                $builder = $this->getContainerBuilder();
1✔
163

164
                if (
165
                        $this->debugMode &&
1✔
166
                        ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class))
1✔
167
                ) {
UNCOV
168
                        $definition = $builder->getDefinition($this->prefix('user'));
×
169
                        assert($definition instanceof Nette\DI\Definitions\ServiceDefinition);
UNCOV
170
                        $definition->addSetup('@Tracy\Bar::addPanel', [
×
UNCOV
171
                                new Nette\DI\Definitions\Statement(Nette\Bridges\SecurityTracy\UserPanel::class),
×
172
                        ]);
173
                }
174
        }
1✔
175
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc