Completed
Push — master ( 3d6026...8dfc9c )
by Guy
12s queued 10s
created

RegisterHandlerTest::testEnforcesSudoMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 13
rs 10
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;
0 ignored issues
show
Bug introduced by
The type SilverStripe\SecurityExt...udoModeServiceInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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