1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2026
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Auth;
24:
25: use \Bcrypt;
26: use Psr\Log\LogLevel;
27: use SimpleID\Auth\AuthManager;
28: use SimpleID\Auth\LoginFormBuildEvent;
29: use SimpleID\Store\StoreManager;
30: use SimpleID\Util\Events\BaseDataCollectionEvent;
31: use SimpleID\Util\Forms\FormSubmitEvent;
32: use SimpleID\Util\UI\Template;
33:
34: /**
35: * Password-based authentication scheme.
36: *
37: * This authentication scheme uses a user name and a password supplied
38: * by the user. A hash is generated from the password, which is compared
39: * against the hash stored in the user store.
40: *
41: * Currently only bcrypt and pbkdf2 password hashing algorithms are
42: * supported.
43: */
44: class PasswordAuthSchemeModule extends AuthSchemeModule {
45: /**
46: * Displays the login form, with input fields for the user name
47: * and password
48: *
49: * @param LoginFormBuildEvent $event
50: * @return void
51: */
52: public function onLoginFormBuild(LoginFormBuildEvent $event) {
53: $form_state = $event->getFormState();
54: $additional = [ 'title' => $this->f3->get('intl.core.auth_password.login_block_title') ];
55:
56: if ($form_state['mode'] == AuthManager::MODE_IDENTIFY_USER) {
57: $event->showUIDBlock();
58: $additional['region'] = LoginFormBuildEvent::PASSWORD_REGION;
59: }
60:
61: if (in_array($form_state['mode'], [ AuthManager::MODE_IDENTIFY_USER, AuthManager::MODE_CREDENTIALS, AuthManager::MODE_REENTER_CREDENTIALS ])) {
62: $tpl = Template::instance();
63:
64: $this->f3->set('login_form_module', 'password');
65:
66: $event->addBlock('auth_password', $tpl->render('auth_password.html', false), 0, $additional);
67: }
68: }
69:
70: /**
71: * Validates the login form.
72: *
73: * @param FormSubmitEvent $event
74: * @return void
75: */
76: public function onLoginFormValidate(FormSubmitEvent $event) {
77: $form_state = $event->getFormState();
78:
79: if (($form_state['mode'] == AuthManager::MODE_CREDENTIALS || $form_state['mode'] == AuthManager::MODE_REENTER_CREDENTIALS) && $this->isBlockActive('auth_password')) {
80: $uid = ($form_state['mode'] == AuthManager::MODE_CREDENTIALS) ? $this->f3->get('POST.uid') : $form_state['uid'];
81: if (($uid === false) || ($uid === null)) $uid = '';
82:
83: if (($uid == '') || ($this->f3->exists('POST.password.password') === false)) {
84: if ($this->f3->exists('PARAMS.continue')) {
85: // User came from a log in form.
86: $event->addMessage($this->f3->get('intl.core.auth_password.missing_password'));
87: }
88: $event->setInvalid();
89: }
90: }
91: }
92:
93: /**
94: * Processes the login form by verifying password credentials supplied
95: * by the user.
96: *
97: * @param LoginFormSubmitEvent $event
98: * @return void
99: */
100: public function onLoginFormSubmit(LoginFormSubmitEvent $event) {
101: $store = StoreManager::instance();
102: $form_state = $event->getFormState();
103:
104: if (($form_state['mode'] == AuthManager::MODE_CREDENTIALS || $form_state['mode'] == AuthManager::MODE_REENTER_CREDENTIALS) && $this->isBlockActive('auth_password')) {
105: $uid = ($form_state['mode'] == AuthManager::MODE_CREDENTIALS) ? $this->f3->get('POST.uid') : $form_state['uid'];
106:
107: if ($this->verifyCredentials($uid, $this->f3->get('POST')) === false) {
108: $event->addMessage($this->f3->get('intl.core.auth_password.invalid_password'));
109: $event->setInvalid();
110: return;
111: }
112:
113: $test_user = $store->loadUser($uid);
114:
115: $event->addAuthModuleName(self::class);
116: $event->setUser($test_user);
117: $event->setAuthLevel(($form_state['mode'] == AuthManager::MODE_CREDENTIALS) ? AuthManager::AUTH_LEVEL_CREDENTIALS : AuthManager::AUTH_LEVEL_REENTER_CREDENTIALS);
118: }
119: }
120:
121: /**
122: * @see SimpleID\Auth\LoginEvent
123: * @return void
124: */
125: public function onLoginEvent(LoginEvent $event) {
126: $user = $event->getUser();
127: $level = $event->getAuthLevel();
128: $store = StoreManager::instance();
129:
130: if ($level >= AuthManager::AUTH_LEVEL_CREDENTIALS) {
131: $user->set('auth_login_last_active_block.' . AuthManager::MODE_CREDENTIALS, 'auth_password');
132: $store->saveUser($user);
133: }
134: }
135:
136: /**
137: * Verifies a set of credentials using the default user name-password authentication
138: * method.
139: *
140: * @param string $uid the name of the user to verify
141: * @param array<string, mixed> $credentials the credentials supplied by the browser
142: * @return bool whether the credentials supplied matches those for the specified
143: * user
144: */
145: protected function verifyCredentials($uid, $credentials) {
146: $store = StoreManager::instance();
147:
148: $test_user = $store->loadUser($uid);
149: if ($test_user == NULL) return false;
150:
151: list($dummy, $prefix, $content) = explode('$', $test_user['password']['password'], 3);
152: if ($prefix == null) return false;
153:
154: switch ($prefix) {
155: case '2y':
156: return password_verify($credentials['password']['password'], $test_user['password']['password']);
157: case 'pbkdf2':
158: $params = [];
159: list($param_string, $hash, $salt) = explode('$', $content, 3);
160: parse_str($param_string, $params);
161: if (!isset($params['f']) || is_array($params['f'])) $params['f'] = 'sha256';
162: if (!isset($params['dk'])) $params['dk'] = 0;
163: // @phpstan-ignore argument.type, argument.type, argument.type
164: return $this->secureCompare(hash_pbkdf2(strval($params['f']), $credentials['password']['password'], base64_decode($salt), intval($params['c']), intval($params['dk']), true),
165: base64_decode($hash));
166: default:
167: $this->logger->log(LogLevel::WARNING, 'Unknown password prefix: ' . $prefix);
168: return false;
169: }
170: }
171:
172: /**
173: * @return void
174: */
175: public function onUserSecretDataPaths(BaseDataCollectionEvent $event) {
176: $event->addResult('password.password');
177: }
178: }
179: ?>