Completed
Pull Request — master (#11)
by
unknown
04:27
created

SAMLController::acs()   C

Complexity

Conditions 13
Paths 36

Size

Total Lines 118
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 62
nc 36
nop 0
dl 0
loc 118
rs 6.1224
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\SAML\Control;
4
5
use Exception;
6
use OneLogin_Saml2_Auth;
7
use OneLogin_Saml2_Error;
8
use OneLogin_Saml2_Utils;
9
use Psr\Log\LoggerInterface;
10
use SilverStripe\ORM\ValidationResult;
11
use SilverStripe\SAML\Authenticators\SAMLAuthenticator;
12
use SilverStripe\SAML\Authenticators\SAMLLoginForm;
13
use SilverStripe\SAML\Helpers\SAMLHelper;
14
use SilverStripe\Control\Controller;
15
use SilverStripe\Control\Director;
16
use SilverStripe\Control\HTTPResponse;
17
use SilverStripe\Core\Injector\Injector;
18
use SilverStripe\Security\IdentityStore;
19
use SilverStripe\Security\Member;
20
use SilverStripe\Security\Security;
21
use function uniqid;
22
23
/**
24
 * Class SAMLController
25
 *
26
 * This controller handles serving metadata requests for the identity provider (IdP), as well as handling the creation
27
 * of new users and logging them into SilverStripe after being authenticated at the IdP.
28
 */
29
class SAMLController extends Controller
30
{
31
    /**
32
     * @var array
33
     */
34
    private static $allowed_actions = [
35
        'index',
36
        'acs',
37
        'metadata'
38
    ];
39
40
    public function index()
41
    {
42
        return $this->redirect('/');
43
    }
44
45
    /**
46
     * Assertion Consumer Service
47
     *
48
     * The user gets sent back here after authenticating with the IdP, off-site.
49
     * The earlier redirection to the IdP can be found in the SAMLAuthenticator::authenticate.
50
     *
51
     * After this handler completes, we end up with a rudimentary Member record (which will be created on-the-fly
52
     * if not existent), with the user already logged in. Login triggers memberLoggedIn hooks, which allows
53
     * LDAP side of this module to finish off loading Member data.
54
     *
55
     * @throws OneLogin_Saml2_Error
56
     * @throws \Psr\Container\NotFoundExceptionInterface
57
     */
58
    public function acs()
59
    {
60
        /** @var \OneLogin_Saml2_Auth $auth */
61
        $auth = Injector::inst()->get(SAMLHelper::class)->getSAMLAuth();
62
        $caughtException = null;
63
64
        // Force php-saml module to use the current absolute base URL (e.g. https://www.example.com/saml). This avoids
65
        // errors that we otherwise get when having a multi-directory ACS URL like /saml/acs).
66
        // See https://github.com/onelogin/php-saml/issues/249
67
        OneLogin_Saml2_Utils::setBaseURL(Controller::join_links($auth->getSettings()->getSPData()['entityId'], 'saml'));
68
69
        // Attempt to process the SAML response. If there are errors during this, log them and redirect to the generic
70
        // error page. Note: This does not necessarily include all SAML errors (e.g. we still need to confirm if the
71
        // user is authenticated after this block
72
        try {
73
            $auth->processResponse();
74
            $error = $auth->getLastErrorReason();
75
        } catch (Exception $e) {
76
            $caughtException = $e;
77
        }
78
79
        // If there was an issue with the SAML response, if it was missing or if the SAML response indicates that they
80
        // aren't authorised, then log the issue and provide a traceable error back to the user via the LoginForm
81
        if ($caughtException || !empty($error) || !$auth->isAuthenticated()) {
82
            // Log both errors (reported by php-saml and thrown as exception) with a common ID for later tracking
83
            $id = uniqid('SAML-');
84
85
            if ($caughtException instanceof Exception) {
86
                $this->getLogger()->error(sprintf(
87
                    '[%s] [code: %s] %s (%s:%s)',
88
                    $id,
89
                    $e->getCode(),
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $e does not seem to be defined for all execution paths leading up to this point.
Loading history...
90
                    $e->getMessage(),
91
                    $e->getFile(),
92
                    $e->getLine()
93
                ));
94
            }
95
96
            if (!empty($error)) {
97
                $this->getLogger()->error(sprintf('[%s] %s', $id, $error));
98
            }
99
100
            $this->getForm()->sessionMessage(
101
                _t(
102
                    'SilverStripe\\SAML\\Control\\SAMLController.ERR_SAML_ACS_FAILURE',
103
                    'Unfortunately we couldn\'t log you in. If this continues, please contact your I.T. department'
104
                    . ' with the following reference: {ref}',
105
                    ['ref' => $id]
106
                ),
107
                ValidationResult::TYPE_ERROR
108
            );
109
110
            // Redirect the user back to the login form to display the generic error message and reference
111
            $this->getRequest()->getSession()->save($this->getRequest());
112
            return $this->redirect('Security/login');
113
        }
114
115
        // If processing reaches here, then the user is authenticated - the rest of this method is just processing their
116
        // legitimate information and configuring their account.
117
118
        // Check that the NameID is a binary string (which signals that it is a guid
119
        $decodedNameId = base64_decode($auth->getNameId());
120
        if (ctype_print($decodedNameId)) {
121
            $this->getForm()->sessionMessage('NameID from IdP is not a binary GUID.', ValidationResult::TYPE_ERROR);
122
            $this->getRequest()->getSession()->save($this->getRequest());
123
            return $this->getRedirect();
124
        }
125
126
        // transform the NameId to guid
127
        $helper = SAMLHelper::singleton();
128
        $guid = $helper->binToStrGuid($decodedNameId);
129
        if (!$helper->validGuid($guid)) {
130
            $errorMessage = "Not a valid GUID '{$guid}' recieved from server.";
131
            $this->getLogger()->error($errorMessage);
132
            $this->getForm()->sessionMessage($errorMessage, ValidationResult::TYPE_ERROR);
133
            $this->getRequest()->getSession()->save($this->getRequest());
134
            return $this->getRedirect();
135
        }
136
137
        // Write a rudimentary member with basic fields on every login, so that we at least have something
138
        // if LDAP synchronisation fails.
139
        $member = Member::get()->filter('GUID', $guid)->limit(1)->first();
140
        if (!($member && $member->exists())) {
141
            $member = new Member();
142
            $member->GUID = $guid;
143
        }
144
145
        $attributes = $auth->getAttributes();
146
147
        foreach ($member->config()->claims_field_mappings as $claim => $field) {
148
            if (!isset($attributes[$claim][0])) {
149
                $this->getLogger()->warning(
150
                    sprintf(
151
                        'Claim rule \'%s\' configured in LDAPMember.claims_field_mappings, ' .
152
                                'but wasn\'t passed through. Please check IdP claim rules.',
153
                        $claim
154
                    )
155
                );
156
157
                continue;
158
            }
159
160
            $member->$field = $attributes[$claim][0];
161
        }
162
163
        $member->SAMLSessionIndex = $auth->getSessionIndex();
164
165
        // This will trigger LDAP update through LDAPMemberExtension::memberLoggedIn. The LDAP update will also write
166
        // the Member record a second time, but the member must be written before IdentityStore->logIn() is called.
167
        // Both SAML and LDAP identify Members by the GUID field.
168
        $member->write();
169
170
        /** @var IdentityStore $identityStore */
171
        $identityStore = Injector::inst()->get(IdentityStore::class);
172
        $persistent = Security::config()->get('autologin_enabled');
173
        $identityStore->logIn($member, $persistent, $this->getRequest());
174
175
        return $this->getRedirect();
176
    }
177
178
    /**
179
     * Generate this SP's metadata. This is needed for intialising the SP-IdP relationship.
180
     * IdP is instructed to call us back here to establish the relationship. IdP may also be configured
181
     * to hit this endpoint periodically during normal operation, to check the SP availability.
182
     */
183
    public function metadata()
184
    {
185
        try {
186
            /** @var OneLogin_Saml2_Auth $auth */
187
            $auth = Injector::inst()->get(SAMLHelper::class)->getSAMLAuth();
188
            $settings = $auth->getSettings();
189
            $metadata = $settings->getSPMetadata();
190
            $errors = $settings->validateMetadata($metadata);
191
            if (empty($errors)) {
192
                header('Content-Type: text/xml');
193
                echo $metadata;
194
            } else {
195
                throw new \OneLogin_Saml2_Error(
196
                    'Invalid SP metadata: ' . implode(', ', $errors),
197
                    \OneLogin_Saml2_Error::METADATA_SP_INVALID
198
                );
199
            }
200
        } catch (Exception $e) {
201
            $this->getLogger()->error($e->getMessage());
202
            echo $e->getMessage();
203
        }
204
    }
205
206
    /**
207
     * @return HTTPResponse
208
     */
209
    protected function getRedirect()
210
    {
211
        // Absolute redirection URLs may cause spoofing
212
        if ($this->getRequest()->getSession()->get('BackURL')
213
            && Director::is_site_url($this->getRequest()->getSession()->get('BackURL'))) {
214
            return $this->redirect($this->getRequest()->getSession()->get('BackURL'));
215
        }
216
217
        // Spoofing attack, redirect to homepage instead of spoofing url
218
        if ($this->getRequest()->getSession()->get('BackURL')
219
            && !Director::is_site_url($this->getRequest()->getSession()->get('BackURL'))) {
220
            return $this->redirect(Director::absoluteBaseURL());
221
        }
222
223
        // If a default login dest has been set, redirect to that.
224
        if (Security::config()->default_login_dest) {
225
            return $this->redirect(Director::absoluteBaseURL() . Security::config()->default_login_dest);
226
        }
227
228
        // fallback to redirect back to home page
229
        return $this->redirect(Director::absoluteBaseURL());
230
    }
231
232
    /**
233
     * Get a logger
234
     *
235
     * @return LoggerInterface
236
     */
237
    public function getLogger()
238
    {
239
        return Injector::inst()->get(LoggerInterface::class);
240
    }
241
242
    /**
243
     * Gets the login form
244
     *
245
     * @return SAMLLoginForm
246
     */
247
    public function getForm()
248
    {
249
        return Injector::inst()->get(SAMLLoginForm::class, true, [$this, SAMLAuthenticator::class, 'LoginForm']);
250
    }
251
}
252