Completed
Pull Request — master (#127)
by
unknown
09:10
created

SAMLController::acs()   D

Complexity

Conditions 16
Paths 29

Size

Total Lines 108

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 108
rs 4.4532
c 0
b 0
f 0
cc 16
nc 29
nop 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
 * Class SAMLController
4
 *
5
 * This controller handles serving metadata requests for the IdP, as well as handling
6
 * creating new users and logging them into SilverStripe after being authenticated at the IdP.
7
 */
8
class SAMLController extends Controller
9
{
10
    /**
11
     * @var array
12
     */
13
    private static $allowed_actions = [
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
14
        'index',
15
        'login',
16
        'logout',
17
        'acs',
18
        'sls',
19
        'metadata'
20
    ];
21
22
    /**
23
     * Assertion Consumer Service
24
     *
25
     * The user gets sent back here after authenticating with the IdP, off-site.
26
     * The earlier redirection to the IdP can be found in the SAMLAuthenticator::authenticate.
27
     *
28
     * After this handler completes, we end up with a rudimentary Member record (which will be created on-the-fly
29
     * if not existent), with the user already logged in. Login triggers memberLoggedIn hooks, which allows
30
     * LDAP side of this module to finish off loading Member data.
31
     *
32
     * @throws OneLogin_Saml2_Error
33
     */
34
    public function acs()
35
    {
36
        /** @var OneLogin_Saml2_Auth $auth */
37
        $auth = Injector::inst()->get('SAMLHelper')->getSAMLAuth();
38
        $auth->processResponse();
39
40
        $error = $auth->getLastErrorReason();
41
        if (!empty($error)) {
42
            SS_Log::log($error, SS_Log::ERR);
43
            Form::messageForForm("SAMLLoginForm_LoginForm", "Authentication error: '{$error}'", 'bad');
44
            Session::save();
45
46
            return $this->getRedirect();
47
        }
48
49
        if (!$auth->isAuthenticated()) {
50
            Form::messageForForm("SAMLLoginForm_LoginForm", _t('Member.ERRORWRONGCRED'), 'bad');
51
            Session::save();
52
53
            return $this->getRedirect();
54
        }
55
56
        $decodedNameId = base64_decode($auth->getNameId());
57
        // check that the NameID is a binary string (which signals that it is a guid
58
        if (ctype_print($decodedNameId)) {
59
            Form::messageForForm("SAMLLoginForm_LoginForm", "Name ID provided by IdP is not a binary GUID.", 'bad');
60
            Session::save();
61
62
            return $this->getRedirect();
63
        }
64
65
        /**
66
         * If processing reaches here, then the user is authenticated - the rest of this method is just processing their
67
         * legitimate information and configuring their account.
68
         */
69
70
        // If we expect the NameID to be a binary version of the GUID (ADFS), check that it actually is
71
        // If we are configured not to expect a binary NameID, then we assume it is a direct GUID (Azure AD)
72
        if (Config::inst()->get('SAMLConfiguration', 'expect_binary_nameid')) {
73
            // transform the binary NameId to guid
74
            $guid = LDAPUtil::bin_to_str_guid($decodedNameId);
75
        } else {
76
            $guid = $auth->getNameId();
77
        }
78
79
        if (!LDAPUtil::validGuid($guid)) {
80
            $errorMessage = "Not a valid GUID '{$guid}' recieved from server.";
81
            SS_Log::log($errorMessage, SS_Log::ERR);
82
            Form::messageForForm("SAMLLoginForm_LoginForm", $errorMessage, 'bad');
83
            Session::save();
84
            return $this->getRedirect();
85
        }
86
87
        $this->extend('updateGuid', $guid);
88
89
        $fieldToClaimMap = array_flip(Member::config()->claims_field_mappings);
90
        $attributes = $auth->getAttributes();
91
92
        // Write a rudimentary member with basic fields on every login, so that we at least have something
93
        // if there is no further sync (e.g. via LDAP)
94
        $member = Member::get()->filter('GUID', $guid)->limit(1)->first();
95
        if (!($member && $member->exists())
96
            &&  Config::inst()->get('SAMLConfiguration', 'allow_insecure_email_linking')
97
            && isset($fieldToClaimMap['Email'])
98
        ) {
99
            // If there is no member found via GUID and we allow linking via email, search by email
100
            $member = Member::get()->filter('Email', $attributes[$fieldToClaimMap['Email']])->limit(1)->first();
101
102
            if (!($member && $member->exists())) {
103
                $member = new Member();
104
            }
105
106
            $member->GUID = $guid;
107
        } elseif (!($member && $member->exists())) {
108
            // If the member doesn't exist and we don't allow linking via email, then create a new member
109
            $member = new Member();
110
            $member->GUID = $guid;
111
        }
112
113
        foreach ($member->config()->claims_field_mappings as $claim => $field) {
114
            if (!isset($attributes[$claim][0])) {
115
                SS_Log::log(
116
                    sprintf(
117
                        'Claim rule \'%s\' configured in LDAPMember.claims_field_mappings, but wasn\'t passed through. Please check IdP claim rules.',
118
                        $claim
119
                    ),
120
                    SS_Log::WARN
121
                );
122
123
                continue;
124
            }
125
126
            $member->$field = $attributes[$claim][0];
127
        }
128
129
        $member->SAMLSessionIndex = $auth->getSessionIndex();
130
131
        $member->write();
132
133
        // This will trigger LDAP update through LDAPMemberExtension::memberLoggedIn.
134
        // The LDAP update will also write the Member record. We shouldn't write before
135
        // calling this, as any onAfterWrite hooks that attempt to update LDAP won't
136
        // have the Username field available yet for new Member records, and fail.
137
        // Both SAML and LDAP identify Members by the GUID field.
138
        $member->logIn();
139
140
        return $this->getRedirect();
141
    }
142
143
    /**
144
     * Generate this SP's metadata. This is needed for intialising the SP-IdP relationship.
145
     * IdP is instructed to call us back here to establish the relationship. IdP may also be configured
146
     * to hit this endpoint periodically during normal operation, to check the SP availability.
147
     */
148
    public function metadata()
149
    {
150
        try {
151
            $auth = Injector::inst()->get('SAMLHelper')->getSAMLAuth();
152
            $settings = $auth->getSettings();
153
            $metadata = $settings->getSPMetadata();
154
            $errors = $settings->validateMetadata($metadata);
155
            if (empty($errors)) {
156
                header('Content-Type: text/xml');
157
                echo $metadata;
158
            } else {
159
                throw new \OneLogin_Saml2_Error(
160
                    'Invalid SP metadata: ' . implode(', ', $errors),
161
                    \OneLogin_Saml2_Error::METADATA_SP_INVALID
162
                );
163
            }
164
        } catch (Exception $e) {
165
            SS_Log::log($e->getMessage(), SS_Log::ERR);
166
            echo $e->getMessage();
167
        }
168
    }
169
170
    /**
171
     * @return SS_HTTPResponse
172
     */
173
    protected function getRedirect()
174
    {
175
        // Absolute redirection URLs may cause spoofing
176 View Code Duplication
        if (Session::get('BackURL') && Director::is_site_url(Session::get('BackURL'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
177
            return $this->redirect(Session::get('BackURL'));
178
        }
179
180
        // Spoofing attack, redirect to homepage instead of spoofing url
181 View Code Duplication
        if (Session::get('BackURL') && !Director::is_site_url(Session::get('BackURL'))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
            return $this->redirect(Director::absoluteBaseURL());
183
        }
184
185
        // If a default login dest has been set, redirect to that.
186
        if (Security::config()->default_login_dest) {
187
            return $this->redirect(Director::absoluteBaseURL() . Security::config()->default_login_dest);
188
        }
189
190
        // fallback to redirect back to home page
191
        return $this->redirect(Director::absoluteBaseURL());
192
    }
193
}
194