SessionStore::getMethod()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SilverStripe\MFA\Store;
6
7
use RuntimeException;
8
use Serializable;
9
use SilverStripe\Control\HTTPRequest;
10
use SilverStripe\MFA\Exception\InvalidMethodException;
11
use SilverStripe\MFA\Extension\MemberExtension;
12
use SilverStripe\ORM\DataObject;
13
use SilverStripe\Security\Member;
14
15
/**
16
 * This class provides an interface to store data in session during an MFA process. This is implemented as a measure to
17
 * prevent bleeding state between individual MFA auth types
18
 *
19
 * @package SilverStripe\MFA
20
 */
21
class SessionStore implements StoreInterface, Serializable
22
{
23
    public const SESSION_KEY = 'MFASessionStore';
24
25
    /**
26
     * The member that is currently going through the MFA process
27
     *
28
     * @var Member
29
     */
30
    protected $member;
31
32
    /**
33
     * MemberID is only used on unserialising from session as we can defer the DB call for the member
34
     *
35
     * @var int
36
     */
37
    protected $memberID;
38
39
    /**
40
     * A string representing the current authentication method that is underway
41
     *
42
     * @var string
43
     */
44
    protected $method;
45
46
    /**
47
     * Any state that the current authentication method needs to retain while it is underway
48
     *
49
     * @var array
50
     */
51
    protected $state = [];
52
53
    /**
54
     * The URL segment identifiers of methods that have been verified in this session
55
     *
56
     * @var string[]
57
     */
58
    protected $verifiedMethods = [];
59
60
    /**
61
     * Attempt to create a store from the given request getting any existing state from the session of the request
62
     *
63
     * {@inheritdoc}
64
     */
65
    public function __construct(Member $member)
66
    {
67
        $this->setMember($member);
68
    }
69
70
    /**
71
     * @return Member&MemberExtension|null
72
     */
73
    public function getMember(): ?Member
74
    {
75
        if (!$this->member && $this->memberID) {
76
            $this->member = DataObject::get_by_id(Member::class, $this->memberID);
77
        }
78
79
        return $this->member;
80
    }
81
82
    /**
83
     * @param Member $member
84
     * @return $this
85
     */
86
    public function setMember(Member $member): StoreInterface
87
    {
88
        // Early return if there's no change
89
        if ($this->member && $this->member->ID === $member->ID) {
90
            return $this;
91
        }
92
93
        // If the member has changed we should null out the method that's underway and the state of it
94
        $this->resetMethod();
95
96
        $this->member = $member;
97
        $this->memberID = $member->ID;
98
99
        // When the member changes the list of verified methods should reset
100
        $this->verifiedMethods = [];
101
102
        return $this;
103
    }
104
105
    /**
106
     * @return string|null
107
     */
108
    public function getMethod(): ?string
109
    {
110
        return $this->method;
111
    }
112
113
    /**
114
     * @param string|null $method
115
     * @return $this
116
     */
117
    public function setMethod(?string $method): StoreInterface
118
    {
119
        if (in_array($method, $this->getVerifiedMethods())) {
120
            throw new InvalidMethodException('You cannot verify with a method you have already verified');
121
        }
122
123
        $this->method = $method;
124
125
        return $this;
126
    }
127
128
    public function getState(): array
129
    {
130
        return $this->state;
131
    }
132
133
    public function setState(array $state): StoreInterface
134
    {
135
        $this->state = $state;
136
        return $this;
137
    }
138
139
    public function addState(array $state): StoreInterface
140
    {
141
        $this->state = array_merge($this->state, $state);
142
        return $this;
143
    }
144
145
    public function addVerifiedMethod(string $method): StoreInterface
146
    {
147
        if (!in_array($method, $this->verifiedMethods)) {
148
            $this->verifiedMethods[] = $method;
149
        }
150
151
        return $this;
152
    }
153
154
    public function getVerifiedMethods(): array
155
    {
156
        return $this->verifiedMethods;
157
    }
158
159
    /**
160
     * Save this store into the session of the given request
161
     *
162
     * {@inheritdoc}
163
     */
164
    public function save(HTTPRequest $request): StoreInterface
165
    {
166
        $request->getSession()->set(static::SESSION_KEY, $this);
167
168
        return $this;
169
    }
170
171
    /**
172
     * Load a StoreInterface from the given request and return it if it exists
173
     *
174
     * @param HTTPRequest $request
175
     * @return StoreInterface|null
176
     */
177
    public static function load(HTTPRequest $request): ?StoreInterface
178
    {
179
        $store = $request->getSession()->get(static::SESSION_KEY);
180
        return $store instanceof self ? $store : null;
181
    }
182
183
    /**
184
     * Clear any stored values for the given request
185
     *
186
     * {@inheritdoc}
187
     */
188
    public static function clear(HTTPRequest $request): void
189
    {
190
        $request->getSession()->clear(static::SESSION_KEY);
191
    }
192
193
    /**
194
     * "Reset" the method currently in progress by clearing the identifier and state
195
     *
196
     * @return StoreInterface
197
     */
198
    protected function resetMethod(): StoreInterface
199
    {
200
        $this->setMethod(null)->setState([]);
201
202
        return $this;
203
    }
204
205
    public function serialize(): string
206
    {
207
        // Use the stored member ID by default. We should do this because we can avoid ever fetching the member object
208
        // from the database if the member was never accessed during this request.
209
        $memberID = $this->memberID;
210
211
        if (!$memberID && ($member = $this->getMember())) {
0 ignored issues
show
Unused Code introduced by
The assignment to $member is dead and can be removed.
Loading history...
212
            $memberID = $this->getMember()->ID;
213
        }
214
215
        $stuff = json_encode([
216
            'member' => $memberID,
217
            'method' => $this->getMethod(),
218
            'state' => $this->getState(),
219
            'verifiedMethods' => $this->getVerifiedMethods(),
220
        ]);
221
222
        if (!$stuff) {
223
            throw new RuntimeException(json_last_error_msg());
224
        }
225
226
        return $stuff;
227
    }
228
229
    public function unserialize($serialized): void
230
    {
231
        $state = json_decode($serialized, true);
232
233
        if (is_array($state) && $state['member']) {
234
            $this->memberID = $state['member'];
235
            $this->setMethod($state['method']);
236
            $this->setState($state['state']);
237
238
            foreach ($state['verifiedMethods'] as $method) {
239
                $this->addVerifiedMethod($method);
240
            }
241
        }
242
    }
243
}
244