Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace factor_token;
18
 
19
use stdClass;
20
use tool_mfa\local\factor\object_factor_base;
21
use tool_mfa\local\secret_manager;
22
 
23
/**
24
 * Token factor class.
25
 *
26
 * @package     factor_token
27
 * @author      Peter Burnett <peterburnett@catalyst-au.net>
28
 * @copyright   Catalyst IT
29
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30
 */
31
class factor extends object_factor_base {
32
 
33
    /**
34
     * Token implementation.
35
     *
36
     * {@inheritDoc}
37
     */
38
    public function has_input(): bool {
39
        return false;
40
    }
41
 
42
    /**
43
     * Token implementation.
44
     * This factor is a singleton, return single instance.
45
     *
46
     * @param stdClass $user the user to check against.
47
     * @return array
48
     */
49
    public function get_all_user_factors(stdClass $user): array {
50
        global $DB;
51
        $records = $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
52
 
53
        if (!empty($records)) {
54
            return $records;
55
        }
56
 
57
        // Null records returned, build new record.
58
        $record = [
59
            'userid' => $user->id,
60
            'factor' => $this->name,
61
            'timecreated' => time(),
62
            'createdfromip' => $user->lastip,
63
            'timemodified' => time(),
64
            'revoked' => 0,
65
        ];
66
        $record['id'] = $DB->insert_record('tool_mfa', $record, true);
67
        return [(object) $record];
68
    }
69
 
70
    /**
71
     * Token implementation.
72
     * Checks whether the user has selected roles in any context.
73
     *
74
     * {@inheritDoc}
75
     */
76
    public function get_state(): string {
77
        global $USER;
78
 
79
        // Check if there was a previous locked status to return.
80
        $state = parent::get_state();
81
        if ($state === \tool_mfa\plugininfo\factor::STATE_LOCKED) {
82
            return \tool_mfa\plugininfo\factor::STATE_LOCKED;
83
        }
84
 
85
        // Check cookie Exists.
86
        $cookie = 'MFA_TOKEN_' . $USER->id;
87
        if (NO_MOODLE_COOKIES || empty($_COOKIE[$cookie])) {
88
            return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
89
        }
90
        $token = $_COOKIE[$cookie];
91
 
92
        $secretmanager = new secret_manager($this->name);
93
        $verified = $secretmanager->validate_secret($token, true);
94
 
95
        // If we got a bad cookie value, someone is likely being dodgy.
96
        // In this instance we should just lock and make the user re-MFA.
97
        if ($verified === secret_manager::NONVALID) {
98
            $this->set_state(\tool_mfa\plugininfo\factor::STATE_LOCKED);
99
            return \tool_mfa\plugininfo\factor::STATE_LOCKED;
100
        } else if ($verified === secret_manager::VALID) {
101
            return \tool_mfa\plugininfo\factor::STATE_PASS;
102
        }
103
 
104
        // We should never get here. Factor cannot be revoked.
105
        return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
106
    }
107
 
108
    /**
109
     * Token Implementation.
110
     * We can't get_state like the parent here or it will recurse forever.
111
     *
112
     * @param string $state the state constant to set
113
     * @return bool
114
     */
115
    public function set_state($state): bool {
116
        global $SESSION;
117
        $property = 'factor_' . $this->name;
118
        $SESSION->$property = $state;
119
        return true;
120
    }
121
 
122
    /**
123
     * Token implementation.
124
     *
125
     * @param stdClass $user
126
     * @return array
127
     */
128
    public function possible_states(stdClass $user): array {
129
        return [
130
            \tool_mfa\plugininfo\factor::STATE_PASS,
131
            \tool_mfa\plugininfo\factor::STATE_NEUTRAL,
132
            \tool_mfa\plugininfo\factor::STATE_LOCKED,
133
        ];
134
    }
135
 
136
    /**
137
     * Token implementation.
138
     * Inject a checkbox into every auth form if needed.
139
     *
140
     * @param \MoodleQuickForm $mform Form to inject global elements into.
141
     * @return void
142
     */
143
    public function global_definition_after_data($mform): void {
144
        global $SESSION;
145
 
146
        // First thing, we need to decide on whether we should show the checkbox.
147
        $noproperty = !property_exists($SESSION, 'tool_mfa_factor_token');
148
        $nostate = $this->get_state() !== \tool_mfa\plugininfo\factor::STATE_PASS;
149
 
150
        if ($noproperty && $nostate) {
151
            $expiry = get_config('factor_token', 'expiry');
152
            $expirystring = format_time($expiry);
153
            $mform->addElement('advcheckbox', 'factor_token_trust', '', get_string('form:trust', 'factor_token', $expirystring));
154
            $mform->setType('factor_token_trust', PARAM_BOOL);
155
            $mform->setDefault('factor_token_trust', true);
156
        }
157
    }
158
 
159
    /**
160
     * Token implementation.
161
     * Store information about the token status.
162
     *
163
     * @param object $data Data from the form.
164
     * @return void
165
     */
166
    public function global_submit($data): void {
167
        global $SESSION;
168
 
169
        // Store any kind of response here, we shouldnt show again.
170
        $trust = $data->factor_token_trust;
171
        $SESSION->tool_mfa_factor_token = $trust;
172
    }
173
 
174
    /**
175
     * Token implementation.
176
     * Pass hook to set the cookie for use in subsequent auths.
177
     *
178
     * {@inheritDoc}
179
     */
180
    public function post_pass_state(): void {
181
        global $CFG, $SESSION, $USER;
182
 
183
        if (!property_exists($SESSION, 'tool_mfa_factor_token')) {
184
            return;
185
        }
186
        $settoken = $SESSION->tool_mfa_factor_token;
187
        if (!$settoken) {
188
            return;
189
        }
190
        $cookie = 'MFA_TOKEN_' . $USER->id;
191
 
192
        list($expirytime, $expiry) = $this->calculate_expiry_time();
193
 
194
        // Store this secret in the database.
195
        $secretmanager = new secret_manager($this->name);
196
        $secret = base64_encode(random_bytes(256));
197
        $secretmanager->create_secret($expiry, false, $secret);
198
 
199
        // All the prep is now done, we can set this cookie.
200
        setcookie($cookie, $secret, $expirytime, $CFG->sessioncookiepath, $CFG->sessioncookiedomain, false, true);
201
 
202
        // Finally emit a log event for storing the cookie.
203
        $state = [
204
            'expiry' => $expirytime,
205
            'cookie' => $cookie,
206
        ];
207
        $event = \factor_token\event\token_created::token_created_event($USER, $state);
208
        $event->trigger();
209
    }
210
 
211
    /**
212
     * Calculate the expiry time of the token, based on configuration.
213
     *
214
     * @param integer|null $basetime time to use for calcalations.
215
     * @return array
216
     */
217
    public function calculate_expiry_time($basetime = null): array {
218
        if (empty($basetime)) {
219
            $basetime = time();
220
        }
221
 
222
        // Calculate the expiry time. This is provided by config,
223
        // But optionally might need to be rounded  to expire a few hours after 0000 server time.
224
        $expiry = get_config('factor_token', 'expiry');
225
        $expirytime = $basetime + $expiry;
226
 
227
        // If expiring overnight, it should expire at 2am the following morning, if required.
228
        $expireovernight = get_config('factor_token', 'expireovernight');
229
        if ($expireovernight) {
230
            // Find out what 2am the following morning time is.
231
            $datetime = new \DateTime();
232
            $timezone = \core_date::get_user_timezone_object();
233
 
234
            // Bit to ensure 'expireovernight' works when 'expire' is longer than one day.
235
            $difftime = 0;
236
            if ($expiry > DAYSECS) {
237
                // Ensures a safe amount of days is added before doing the 2am checks.
238
                $difftime = $expiry - DAYSECS;
239
            }
240
 
241
            // Calculte the overnight expiry time, ignoring 'expiry' duration period.
242
            $workingexpirytime = $basetime + $difftime;
243
            $datetime->setTimezone($timezone);
244
            $datetime->setTimestamp($workingexpirytime);
245
            $datetime->add(new \DateInterval('P1D'));
246
            $datetime->setTime(2, 0); // Set the hour to 2am.
247
 
248
            // Ensure whatever happens, ensure the expiry never goes over the default 'expiry' time.
249
            $overnightexpirytime = $datetime->getTimestamp();
250
            $expirytime = min($overnightexpirytime, $expirytime);
251
            $expiry = $expirytime - $basetime;
252
        }
253
 
254
        return [$expirytime, $expiry];
255
    }
256
}