Passed
Push — master ( 879ca4...d0a2dc )
by Robbie
03:13
created

SAMLController::acs()   C

Complexity

Conditions 9
Paths 10

Size

Total Lines 75
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 75
rs 5.875
c 0
b 0
f 0
cc 9
eloc 42
nc 10
nop 0

How to fix   Long Method   

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_Error;
7
use Psr\Log\LoggerInterface;
8
use SilverStripe\SAML\Authenticators\SAMLLoginForm;
9
use SilverStripe\SAML\Helpers\SAMLHelper;
10
use SilverStripe\Control\Controller;
11
use SilverStripe\Control\Director;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\Security\Member;
15
use SilverStripe\Security\Security;
16
17
/**
18
 * Class SAMLController
19
 *
20
 * This controller handles serving metadata requests for the IdP, as well as handling
21
 * creating new users and logging them into SilverStripe after being authenticated at the IdP.
22
 *
23
 * @package activedirectory
24
 */
25
class SAMLController extends Controller
26
{
27
    /**
28
     * @var array
29
     */
30
    private static $allowed_actions = [
0 ignored issues
show
Unused Code introduced by
The property $allowed_actions is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
31
        'index',
32
        'login',
33
        'logout',
34
        'acs',
35
        'sls',
36
        'metadata'
37
    ];
38
39
    /**
40
     * Assertion Consumer Service
41
     *
42
     * The user gets sent back here after authenticating with the IdP, off-site.
43
     * The earlier redirection to the IdP can be found in the SAMLAuthenticator::authenticate.
44
     *
45
     * After this handler completes, we end up with a rudimentary Member record (which will be created on-the-fly
46
     * if not existent), with the user already logged in. Login triggers memberLoggedIn hooks, which allows
47
     * LDAP side of this module to finish off loading Member data.
48
     *
49
     * @throws OneLogin_Saml2_Error
50
     */
51
    public function acs()
52
    {
53
        $auth = Injector::inst()->get(SAMLHelper::class)->getSAMLAuth();
54
        $auth->processResponse();
55
56
        $error = $auth->getLastErrorReason();
57
        if (!empty($error)) {
58
            $this->getLogger()->error($error);
59
60
            $this->getForm()->sessionMessage("Authentication error: '{$error}'", 'bad');
61
            $this->getRequest()->getSession()->save($this->getRequest());
62
            return $this->getRedirect();
63
        }
64
65
        if (!$auth->isAuthenticated()) {
66
            $this->getForm()->sessionMessage(_t('SilverStripe\\Security\\Member.ERRORWRONGCRED'), 'bad');
67
            $this->getRequest()->getSession()->save($this->getRequest());
68
            return $this->getRedirect();
69
        }
70
71
        $decodedNameId = base64_decode($auth->getNameId());
72
        // check that the NameID is a binary string (which signals that it is a guid
73
        if (ctype_print($decodedNameId)) {
74
            $this->getForm()->sessionMessage('Name ID provided by IdP is not a binary GUID.', 'bad');
75
            $this->getRequest()->getSession()->save($this->getRequest());
76
            return $this->getRedirect();
77
        }
78
79
        // transform the NameId to guid
80
        $helper = SAMLHelper::singleton();
81
        $guid = $helper->binToStrGuid($decodedNameId);
82
        if (!$helper->validGuid($guid)) {
83
            $errorMessage = "Not a valid GUID '{$guid}' recieved from server.";
84
            $this->getLogger()->error($errorMessage);
85
            $this->getForm()->sessionMessage($errorMessage, 'bad');
86
            $this->getRequest()->getSession()->save($this->getRequest());
87
            return $this->getRedirect();
88
        }
89
90
        // Write a rudimentary member with basic fields on every login, so that we at least have something
91
        // if LDAP synchronisation fails.
92
        $member = Member::get()->filter('GUID', $guid)->limit(1)->first();
93
        if (!($member && $member->exists())) {
94
            $member = new Member();
95
            $member->GUID = $guid;
96
        }
97
98
        $attributes = $auth->getAttributes();
99
100
        foreach ($member->config()->claims_field_mappings as $claim => $field) {
101
            if (!isset($attributes[$claim][0])) {
102
                $this->getLogger()->warning(
103
                    sprintf(
104
                        'Claim rule \'%s\' configured in LDAPMember.claims_field_mappings, ' .
105
                                'but wasn\'t passed through. Please check IdP claim rules.',
106
                        $claim
107
                    )
108
                );
109
110
                continue;
111
            }
112
113
            $member->$field = $attributes[$claim][0];
114
        }
115
116
        $member->SAMLSessionIndex = $auth->getSessionIndex();
117
118
        // This will trigger LDAP update through LDAPMemberExtension::memberLoggedIn.
119
        // The LDAP update will also write the Member record. We shouldn't write before
120
        // calling this, as any onAfterWrite hooks that attempt to update LDAP won't
121
        // have the Username field available yet for new Member records, and fail.
122
        // Both SAML and LDAP identify Members by the GUID field.
123
        Security::setCurrentUser($member);
124
125
        return $this->getRedirect();
126
    }
127
128
    /**
129
     * Generate this SP's metadata. This is needed for intialising the SP-IdP relationship.
130
     * IdP is instructed to call us back here to establish the relationship. IdP may also be configured
131
     * to hit this endpoint periodically during normal operation, to check the SP availability.
132
     */
133
    public function metadata()
134
    {
135
        try {
136
            $auth = Injector::inst()->get('SilverStripe\\SAML\\Helpers\\SAMLHelper')->getSAMLAuth();
137
            $settings = $auth->getSettings();
138
            $metadata = $settings->getSPMetadata();
139
            $errors = $settings->validateMetadata($metadata);
140
            if (empty($errors)) {
141
                header('Content-Type: text/xml');
142
                echo $metadata;
143
            } else {
144
                throw new \OneLogin_Saml2_Error(
145
                    'Invalid SP metadata: ' . implode(', ', $errors),
146
                    \OneLogin_Saml2_Error::METADATA_SP_INVALID
147
                );
148
            }
149
        } catch (Exception $e) {
150
            $this->getLogger()->error($e->getMessage());
151
            echo $e->getMessage();
152
        }
153
    }
154
155
    /**
156
     * @return HTTPResponse
157
     */
158
    protected function getRedirect()
159
    {
160
        // Absolute redirection URLs may cause spoofing
161
        if ($this->getRequest()->getSession()->get('BackURL')
162
            && Director::is_site_url($this->getRequest()->getSession()->get('BackURL'))) {
0 ignored issues
show
Bug introduced by
It seems like $this->getRequest()->getSession()->get('BackURL') can also be of type array; however, parameter $url of SilverStripe\Control\Director::is_site_url() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
            && Director::is_site_url(/** @scrutinizer ignore-type */ $this->getRequest()->getSession()->get('BackURL'))) {
Loading history...
163
            return $this->redirect($this->getRequest()->getSession()->get('BackURL'));
0 ignored issues
show
Bug introduced by
It seems like $this->getRequest()->getSession()->get('BackURL') can also be of type array; however, parameter $url of SilverStripe\Control\Controller::redirect() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
            return $this->redirect(/** @scrutinizer ignore-type */ $this->getRequest()->getSession()->get('BackURL'));
Loading history...
164
        }
165
166
        // Spoofing attack, redirect to homepage instead of spoofing url
167
        if ($this->getRequest()->getSession()->get('BackURL')
168
            && !Director::is_site_url($this->getRequest()->getSession()->get('BackURL'))) {
169
            return $this->redirect(Director::absoluteBaseURL());
170
        }
171
172
        // If a default login dest has been set, redirect to that.
173
        if (Security::config()->default_login_dest) {
174
            return $this->redirect(Director::absoluteBaseURL() . Security::config()->default_login_dest);
175
        }
176
177
        // fallback to redirect back to home page
178
        return $this->redirect(Director::absoluteBaseURL());
179
    }
180
181
    /**
182
     * Get a logger
183
     *
184
     * @return LoggerInterface
185
     */
186
    public function getLogger()
187
    {
188
        return Injector::inst()->get(LoggerInterface::class);
189
    }
190
191
    /**
192
     * Gets the login form
193
     *
194
     * @return SAMLLoginForm
195
     */
196
    public function getForm()
197
    {
198
        return Injector::inst()->get(SAMLLoginForm::class);
199
    }
200
}
201