1 | <?php |
||
2 | |||
3 | namespace SilverStripe\MFA\RequestHandler; |
||
4 | |||
5 | use Exception; |
||
6 | use SilverStripe\Control\HTTPRequest; |
||
7 | use SilverStripe\Control\HTTPResponse; |
||
8 | use SilverStripe\Core\Injector\Injector; |
||
9 | use SilverStripe\MFA\Method\MethodInterface; |
||
10 | use SilverStripe\MFA\Service\RegisteredMethodManager; |
||
11 | use SilverStripe\MFA\State\RegisteredMethodDetailsInterface; |
||
12 | use SilverStripe\MFA\State\Result; |
||
13 | use SilverStripe\MFA\Store\StoreInterface; |
||
14 | use SilverStripe\ORM\ValidationResult; |
||
15 | use SilverStripe\Security\SecurityToken; |
||
16 | |||
17 | /** |
||
18 | * This trait encapsulates logic that can be added to a `RequestHandler` to work with registering MFA authenticators |
||
19 | * using the MFA front-end app. It provides two main methods; @see createStartRegistrationResponse - creates a response |
||
20 | * that can be easily consumed by the MFA app to start the registration process for a method, and |
||
21 | * @see completeRegistrationRequest - used to complete the registration flow for a method using details sent back by the |
||
22 | * MFA app. |
||
23 | */ |
||
24 | trait RegistrationHandlerTrait |
||
25 | { |
||
26 | /** |
||
27 | * Create a response that can be consumed by a front-end for starting a registration |
||
28 | * |
||
29 | * @param StoreInterface $store |
||
30 | * @param MethodInterface $method |
||
31 | * @param bool $allowReregistration By default this method will return an error response when registering methods |
||
32 | * that already have a registration. |
||
33 | * @return HTTPResponse |
||
34 | */ |
||
35 | public function createStartRegistrationResponse( |
||
36 | StoreInterface $store, |
||
37 | MethodInterface $method, |
||
38 | bool $allowReregistration = false |
||
39 | ): HTTPResponse { |
||
40 | $member = $store->getMember(); |
||
41 | |||
42 | // Sanity check that the method hasn't already been registered |
||
43 | $existingRegisteredMethod = RegisteredMethodManager::singleton()->getFromMember($member, $method); |
||
44 | |||
45 | $response = HTTPResponse::create() |
||
46 | ->addHeader('Content-Type', 'application/json'); |
||
47 | |||
48 | if (!$allowReregistration && $existingRegisteredMethod) { |
||
49 | return $response->setBody(json_encode(['errors' => [_t( |
||
50 | __CLASS__ . '.METHOD_ALREADY_REGISTERED', |
||
51 | 'That method has already been registered against this Member' |
||
52 | )]]))->setStatusCode(400); |
||
53 | } |
||
54 | |||
55 | // Mark the given method as started within the session |
||
56 | $store->setMethod($method->getURLSegment()); |
||
57 | // Allow the registration handler to begin the process and generate some data to pass through to the front-end |
||
58 | $data = $method->getRegisterHandler()->start($store); |
||
59 | |||
60 | // Add a CSRF token |
||
61 | $token = SecurityToken::inst(); |
||
62 | $data[$token->getName()] = $token->getValue(); |
||
63 | |||
64 | return $response->setBody(json_encode($data)); |
||
65 | } |
||
66 | |||
67 | /** |
||
68 | * Complete a registration request, returning a result object with a message and context for the result of the |
||
69 | * registration attempt. |
||
70 | * |
||
71 | * @param StoreInterface $store |
||
72 | * @param MethodInterface $method |
||
73 | * @param HTTPRequest $request |
||
74 | * @return Result |
||
75 | * @throws \SilverStripe\ORM\ValidationException |
||
76 | */ |
||
77 | public function completeRegistrationRequest( |
||
78 | StoreInterface $store, |
||
79 | MethodInterface $method, |
||
80 | HTTPRequest $request |
||
81 | ): Result { |
||
82 | if (!SecurityToken::inst()->checkRequest($request)) { |
||
83 | return Result::create(false, _t( |
||
84 | __CLASS__ . '.CSRF_FAILURE', |
||
85 | 'Your request timed out. Please refresh and try again' |
||
86 | ), ['code' => 403]); |
||
87 | } |
||
88 | |||
89 | $storedMethodName = $store->getMethod(); |
||
90 | |||
91 | // If a registration process hasn't been initiated in a previous request, calling this method is invalid |
||
92 | if (!$storedMethodName) { |
||
93 | return Result::create(false, _t(__CLASS__ . '.NO_REGISTRATION_IN_PROGRESS', 'No registration in progress')); |
||
94 | } |
||
95 | |||
96 | // Assert the method in progress matches the request for completion |
||
97 | if ($storedMethodName !== $method->getURLSegment()) { |
||
98 | return Result::create( |
||
99 | false, |
||
100 | _t(__CLASS__ . '.METHOD_MISMATCH', 'Method does not match registration in progress') |
||
101 | ); |
||
102 | } |
||
103 | |||
104 | $registrationHandler = $method->getRegisterHandler(); |
||
105 | $result = $registrationHandler->register($request, $store); |
||
106 | |||
107 | $member = $store->getMember(); |
||
108 | if ($result->isSuccessful()) { |
||
109 | RegisteredMethodManager::singleton() |
||
110 | ->registerForMember($member, $method, $result->getContext()); |
||
111 | } else { |
||
112 | $this->extend('onRegisterMethodFailure', $member, $method); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
113 | } |
||
114 | |||
115 | // Replace the context with detail of the registered method |
||
116 | return $result->setContext([ |
||
117 | 'registeredMethod' => Injector::inst()->create(RegisteredMethodDetailsInterface::class, $method) |
||
118 | ]); |
||
119 | } |
||
120 | } |
||
121 |