Completed
Push — master ( c14fb2...0f9e03 )
by Robbie
22s queued 11s
created

testFinishRegistrationValidatesCSRF()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 23
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace SilverStripe\MFA\Tests\Authenticator;
4
5
use PHPUnit_Framework_MockObject_MockObject;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Core\Injector\Injector;
8
use SilverStripe\Dev\FunctionalTest;
9
use SilverStripe\MFA\Authenticator\MemberAuthenticator;
10
use SilverStripe\MFA\Extension\MemberExtension;
11
use SilverStripe\MFA\Method\Handler\RegisterHandlerInterface;
12
use SilverStripe\MFA\Method\MethodInterface;
13
use SilverStripe\MFA\Service\MethodRegistry;
14
use SilverStripe\MFA\State\Result;
15
use SilverStripe\MFA\Store\SessionStore;
16
use SilverStripe\MFA\Tests\Stub\BasicMath\Method;
17
use SilverStripe\Security\Member;
18
use SilverStripe\Security\Security;
19
use SilverStripe\SecurityExtensions\Service\SudoModeServiceInterface;
20
use SilverStripe\Security\SecurityToken;
21
22
/**
23
 * Class RegisterHandlerTest
24
 *
25
 * @package SilverStripe\MFA\Tests\Authenticator
26
 */
27
class RegisterHandlerTest extends FunctionalTest
28
{
29
    const URL = 'Security/login/default/mfa/register/basic-math/';
30
31
    protected static $fixture_file = 'RegisterHandlerTest.yml';
32
33
    protected function setUp()
34
    {
35
        parent::setUp();
36
        Config::modify()->set(MethodRegistry::class, 'methods', [Method::class]);
37
38
        Injector::inst()->load([
39
            Security::class => [
40
                'properties' => [
41
                    'authenticators' => [
42
                        'default' => '%$' . MemberAuthenticator::class,
43
                    ]
44
                ]
45
            ]
46
        ]);
47
48
        /** @var SudoModeServiceInterface&PHPUnit_Framework_MockObject_MockObject $sudoModeService */
49
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
50
        $sudoModeService->expects($this->any())->method('check')->willReturn(true);
51
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
52
    }
53
54
    /**
55
     * Tests that the registration flow can't be started without being logged in (or past basic auth)
56
     */
57
    public function testRegisterRouteIsPrivateWithGETMethod()
58
    {
59
        $response = $this->get(self::URL);
60
        $this->assertEquals(403, $response->getStatusCode());
61
    }
62
63
    /**
64
     * Tests that the registration flow can't be finished without being logged in (or past basic auth)
65
     */
66
    public function testRegisterRouteIsPrivateWithPOSTMethod()
67
    {
68
        // See https://github.com/silverstripe/silverstripe-framework/pull/8987 for why we have to provide $data.
69
        $response = $this->post(self::URL, ['dummy' => 'data']);
70
        $this->assertEquals(403, $response->getStatusCode());
71
    }
72
73
    /**
74
     * Tests that a member can't register a new method during login if they've already registered one before
75
     */
76
    public function testStartRegistrationFailsWhenInvalidMethodIsPassed()
77
    {
78
        /** @var Member $freshMember */
79
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
80
81
        $this->scaffoldPartialLogin($freshMember);
82
83
        $response = $this->get('Security/login/default/mfa/register/inert/');
84
        $this->assertEquals(400, $response->getStatusCode());
85
        $this->assertContains('No such method is available', $response->getBody());
86
    }
87
88
    /**
89
     * Tests that a member can't register a new method during login if they've already registered one before
90
     */
91
    public function testStartRegistrationFailsWhenRegisteredMethodExists()
92
    {
93
        /** @var Member $staleMember */
94
        $staleMember = $this->objFromFixture(Member::class, 'stale-member');
95
96
        $this->scaffoldPartialLogin($staleMember);
97
98
        $response = $this->get(self::URL);
99
        $this->assertEquals(400, $response->getStatusCode());
100
        $this->assertContains('This member already has an MFA method', $response->getBody());
101
    }
102
103
    /**
104
     * Tests that a member can't register the same method twice
105
     */
106
    public function testStartRegistrationFailsWhenMethodIsAlreadyRegistered()
107
    {
108
        $this->logInAs('stale-member');
109
110
        $response = $this->get(self::URL);
111
        $this->assertEquals(400, $response->getStatusCode());
112
        $this->assertContains('That method has already been registered against this Member', $response->getBody());
113
    }
114
115
    /**
116
     * Assuming the member passed the above checks, tests that the member can get context for registering a method
117
     */
118
    public function testStartRegistrationSucceeds()
119
    {
120
        /** @var Member $freshMember */
121
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
122
123
        $this->scaffoldPartialLogin($freshMember);
124
125
        $response = $this->get(self::URL);
126
        $this->assertEquals(200, $response->getStatusCode(), sprintf('Body: %s', $response->getBody()));
127
    }
128
129
    public function testStartRegistrationProvidesACSRFToken()
130
    {
131
        SecurityToken::enable();
132
133
        /** @var Member $freshMember */
134
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
135
136
        $this->scaffoldPartialLogin($freshMember);
137
138
        $response = $this->get(self::URL);
139
        $this->assertEquals(200, $response->getStatusCode(), sprintf('Body: %s', $response->getBody()));
140
        $this->assertSame(SecurityToken::inst()->getValue(), json_decode($response->getBody())->SecurityID);
141
    }
142
143
    /**
144
     * Tests that the start registration step must be called before the completion step
145
     */
146
    public function testFinishRegistrationFailsWhenCalledDirectly()
147
    {
148
        /** @var Member $freshMember */
149
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
150
151
        $this->scaffoldPartialLogin($freshMember);
152
153
        $response = $this->post(self::URL, ['dummy' => 'data'], null, $this->session(), json_encode(['number' => 7]));
154
        $this->assertEquals(400, $response->getStatusCode());
155
        $this->assertContains('No registration in progress', $response->getBody());
156
    }
157
158
    /**
159
     * Tests that a nefarious user can't change the method they're registering halfway through
160
     */
161
    public function testFinishRegistrationFailsWhenMethodIsMismatched()
162
    {
163
        /** @var Member $freshMember */
164
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
165
166
        $this->scaffoldPartialLogin($freshMember, self::class); // Purposefully set to the wrong class
167
168
        $response = $this->post(self::URL, ['dummy' => 'data'], null, $this->session(), json_encode(['number' => 7]));
169
        $this->assertEquals(400, $response->getStatusCode());
170
        $this->assertContains('Method does not match registration in progress', $response->getBody());
171
    }
172
173
    public function testFinishRegistrationFailsWhenMethodCannotBeRegistered()
174
    {
175
        $registerHandlerMock = $this->createMock(RegisterHandlerInterface::class);
176
        $registerHandlerMock
177
            ->expects($this->once())
178
            ->method('register')
179
            ->willReturn(Result::create(false, 'No. Bad user'));
180
181
        $methodMock = $this->createMock(MethodInterface::class);
182
        $methodMock
183
            ->expects($this->once())
184
            ->method('getRegisterHandler')
185
            ->willReturn($registerHandlerMock);
186
        $methodMock
187
            ->expects($this->once())
188
            ->method('getURLSegment')
189
            ->willReturn('mock-method');
190
191
        $methodRegistryMock = $this->createMock(MethodRegistry::class);
192
        $methodRegistryMock
193
            ->expects($this->once())
194
            ->method('getMethodByURLSegment')
195
            ->willReturn($methodMock);
196
197
        Injector::inst()->registerService($methodRegistryMock, MethodRegistry::class);
198
199
        /** @var Member $freshMember */
200
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
201
202
        $this->scaffoldPartialLogin($freshMember, 'mock-method');
203
204
        $response = $this->post(self::URL, ['dummy' => 'data'], null, $this->session(), json_encode(['number' => 7]));
205
        $this->assertEquals(400, $response->getStatusCode());
206
        $this->assertContains('No. Bad user', $response->getBody());
207
    }
208
209
    /**
210
     * Assuming the member passed the above checks, tests that the member can complete a registration attempt
211
     */
212
    public function testFinishRegistrationSucceeds()
213
    {
214
        /** @var Member&MemberExtension $freshMember */
215
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
216
217
        $this->scaffoldPartialLogin($freshMember, 'basic-math');
218
219
        $response = $this->post(self::URL, ['dummy' => 'data'], null, $this->session(), json_encode(['number' => 7]));
220
        $this->assertEquals(201, $response->getStatusCode());
221
222
        // Make sure the registration made it into the database
223
        $registeredMethod = $freshMember->RegisteredMFAMethods()->first();
224
        $this->assertNotNull($registeredMethod);
225
        $this->assertEquals('{"number":7}', $registeredMethod->Data);
226
    }
227
228
    public function testFinishRegistrationValidatesCSRF()
229
    {
230
        SecurityToken::enable();
231
232
        /** @var Member $freshMember */
233
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
234
235
        $this->scaffoldPartialLogin($freshMember);
236
237
        $response = $this->post(self::URL, ['dummy' => 'data'], null, $this->session(), json_encode(['number' => 7]));
238
        $this->assertEquals(403, $response->getStatusCode());
239
        $this->assertContains('Your request timed out', $response->getBody());
240
241
        $this->scaffoldPartialLogin($freshMember, 'basic-math');
242
243
        $response = $this->post(
244
            self::URL,
245
            [SecurityToken::inst()->getName() => SecurityToken::inst()->getValue()],
246
            null,
247
            $this->session(),
248
            json_encode(['number' => 7])
249
        );
250
        $this->assertEquals(201, $response->getStatusCode(), sprintf('Body: %s', $response->getBody()));
251
    }
252
253
    public function testEnforcesSudoMode()
254
    {
255
        $sudoModeService = $this->createMock(SudoModeServiceInterface::class);
256
        $sudoModeService->expects($this->any())->method('check')->willReturn(false);
257
        Injector::inst()->registerService($sudoModeService, SudoModeServiceInterface::class);
258
259
        /** @var Member&MemberExtension $freshMember */
260
        $freshMember = $this->objFromFixture(Member::class, 'fresh-member');
261
        $this->scaffoldPartialLogin($freshMember, 'basic-math');
262
263
        $response = $this->post(self::URL, ['dummy' => 'data'], null, $this->session(), json_encode(['number' => 7]));
264
        $this->assertSame(403, $response->getStatusCode());
265
        $this->assertContains('You must be logged or logging in', (string) $response->getBody());
266
    }
267
268
    /**
269
     * Mark the given user as partially logged in - ie. they've entered their email/password and are currently going
270
     * through the MFA process
271
     *
272
     * @param Member $member
273
     * @param string $method
274
     */
275
    protected function scaffoldPartialLogin(Member $member, $method = null)
276
    {
277
        $this->logOut();
278
279
        $store = new SessionStore($member);
280
        if ($method) {
281
            $store->setMethod($method);
282
        }
283
284
        $this->session()->set(SessionStore::SESSION_KEY, $store);
285
    }
286
}
287