1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-2025
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\Store\StoreManager;
29: use SimpleID\Util\Events\BaseDataCollectionEvent;
30: use SimpleID\Util\Forms\FormBuildEvent;
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 FormBuildEvent $event
50: * @return void
51: */
52: public function onLoginFormBuild(FormBuildEvent $event) {
53: $form_state = $event->getFormState();
54:
55: if ($form_state['mode'] == AuthManager::MODE_CREDENTIALS || $form_state['mode'] == AuthManager::MODE_REENTER_CREDENTIALS) {
56: $tpl = Template::instance();
57:
58: $this->f3->set('login_form_module', 'password');
59:
60: $event->addBlock('auth_password', $tpl->render('auth_password.html', false), 0);
61: }
62: }
63:
64: /**
65: * Validates the login form.
66: *
67: * @param FormSubmitEvent $event
68: * @return void
69: */
70: public function onLoginFormValidate(FormSubmitEvent $event) {
71: $form_state = $event->getFormState();
72:
73: if ($form_state['mode'] == AuthManager::MODE_CREDENTIALS || $form_state['mode'] == AuthManager::MODE_REENTER_CREDENTIALS) {
74: $uid = ($form_state['mode'] == AuthManager::MODE_CREDENTIALS) ? $this->f3->get('POST.uid') : $form_state['uid'];
75: if (($uid === false) || ($uid === null)) $uid = '';
76:
77: if (($uid == '') || ($this->f3->exists('POST.password.password') === false)) {
78: if ($this->f3->exists('PARAMS.continue')) {
79: // User came from a log in form.
80: $event->addMessage($this->f3->get('intl.core.auth_password.missing_password'));
81: }
82: $event->setInvalid();
83: }
84: }
85: }
86:
87: /**
88: * Processes the login form by verifying password credentials supplied
89: * by the user.
90: *
91: * @param LoginFormSubmitEvent $event
92: * @return void
93: */
94: public function onLoginFormSubmit(LoginFormSubmitEvent $event) {
95: $store = StoreManager::instance();
96: $form_state = $event->getFormState();
97:
98: if ($form_state['mode'] == AuthManager::MODE_CREDENTIALS || $form_state['mode'] == AuthManager::MODE_REENTER_CREDENTIALS) {
99: $uid = ($form_state['mode'] == AuthManager::MODE_CREDENTIALS) ? $this->f3->get('POST.uid') : $form_state['uid'];
100:
101: if ($this->verifyCredentials($uid, $this->f3->get('POST')) === false) {
102: $event->addMessage($this->f3->get('intl.core.auth_password.invalid_password'));
103: $event->setInvalid();
104: return;
105: }
106:
107: $test_user = $store->loadUser($uid);
108:
109: $event->addAuthModuleName(self::class);
110: $event->setUser($test_user);
111: $event->setAuthLevel(($form_state['mode'] == AuthManager::MODE_CREDENTIALS) ? AuthManager::AUTH_LEVEL_CREDENTIALS : AuthManager::AUTH_LEVEL_REENTER_CREDENTIALS);
112: }
113: }
114:
115: /**
116: * Verifies a set of credentials using the default user name-password authentication
117: * method.
118: *
119: * @param string $uid the name of the user to verify
120: * @param array<string, mixed> $credentials the credentials supplied by the browser
121: * @return bool whether the credentials supplied matches those for the specified
122: * user
123: */
124: protected function verifyCredentials($uid, $credentials) {
125: $store = StoreManager::instance();
126:
127: $test_user = $store->loadUser($uid);
128: if ($test_user == NULL) return false;
129:
130: list($dummy, $prefix, $content) = explode('$', $test_user['password']['password'], 3);
131: if ($prefix == null) return false;
132:
133: switch ($prefix) {
134: case '2y':
135: return password_verify($credentials['password']['password'], $test_user['password']['password']);
136: case 'pbkdf2':
137: $params = [];
138: list($param_string, $hash, $salt) = explode('$', $content, 3);
139: parse_str($param_string, $params);
140: if (!isset($params['f']) || is_array($params['f'])) $params['f'] = 'sha256';
141: if (!isset($params['dk'])) $params['dk'] = 0;
142: // @phpstan-ignore argument.type, argument.type, argument.type
143: return $this->secureCompare(hash_pbkdf2(strval($params['f']), $credentials['password']['password'], base64_decode($salt), intval($params['c']), intval($params['dk']), true),
144: base64_decode($hash));
145: default:
146: $this->logger->log(LogLevel::WARNING, 'Unknown password prefix: ' . $prefix);
147: return false;
148: }
149: }
150:
151: /**
152: * @return void
153: */
154: public function onUserSecretDataPaths(BaseDataCollectionEvent $event) {
155: $event->addResult('password.password');
156: }
157: }
158: ?>