Completed
Pull Request — master (#135)
by Stig
01:15
created

LDAPAuthenticator::authenticate()   B

Complexity

Conditions 8
Paths 11

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 7.8464
c 0
b 0
f 0
cc 8
nc 11
nop 2
1
<?php
2
/**
3
 * Class LDAPAuthenticator.
4
 *
5
 * Authenticate a user against LDAP, without the single sign-on component.
6
 *
7
 * See SAMLAuthenticator for further information.
8
 */
9
class LDAPAuthenticator extends Authenticator
10
{
11
    /**
12
     * @var string
13
     */
14
    private $name = 'LDAP';
15
16
    /**
17
     * Set to 'yes' to indicate if this module should look up usernames in LDAP by matching the email addresses.
18
     *
19
     * CAVEAT #1: only set to 'yes' for systems that enforce email uniqueness.
20
     * Otherwise only the first LDAP user with matching email will be accessible.
21
     *
22
     * CAVEAT #2: this is untested for systems that use LDAP with principal style usernames (i.e. [email protected]).
23
     * The system will misunderstand emails for usernames with uncertain outcome.
24
     *
25
     * @var string 'no' or 'yes'
26
     */
27
    private static $allow_email_login = 'no';
28
29
    /**
30
     * Set to 'yes' to fallback login attempts to {@link $fallback_authenticator}.
31
     * This will occur if LDAP fails to authenticate the user.
32
     *
33
     * @var string 'no' or 'yes'
34
     */
35
    private static $fallback_authenticator = 'no';
36
37
    /**
38
     * The class of {@link Authenticator} to use as the fallback authenticator.
39
     *
40
     * @var string
41
     */
42
    private static $fallback_authenticator_class = 'MemberAuthenticator';
43
44
    /**
45
     * @return string
46
     */
47
    public static function get_name()
48
    {
49
        return Config::inst()->get('LDAPAuthenticator', 'name');
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Config::inst()->...uthenticator', 'name'); (array|integer|double|string|boolean) is incompatible with the return type of the parent method Authenticator::get_name of type string|null.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
50
    }
51
52
    /**
53
     * @param Controller $controller
54
     *
55
     * @return LDAPLoginForm
56
     */
57
    public static function get_login_form(Controller $controller)
58
    {
59
        return new LDAPLoginForm($controller, 'LoginForm');
60
    }
61
62
    /**
63
     * Performs the login, but will also create and sync the Member record on-the-fly, if not found.
64
     *
65
     * @param array $data
66
     * @param Form  $form
67
     *
68
     * @throws SS_HTTPResponse_Exception
69
     *
70
     * @return bool|Member|void
71
     */
72
    public static function authenticate($data, Form $form = null)
73
    {
74
        /** @var LDAPService $service */
75
        $service = Injector::inst()->get('LDAPService');
76
77
        $login = trim($data['Login']);
78
        $username = $login;
79
        if (Email::is_valid_address($login)) {
80
            if (!self::allow_email_logins()) {
81
                self::form_error_msg($form, _t('LDAPAuthenticator.PLEASEUSEUSERNAME', 'Please enter your username instead of your email to log in.'));
82
83
                return;
84
            }
85
            $username = $service->getUsernameByEmail($login);
86
87
            if (!$username) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $username of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
88
                $fallbackMember = self::fallback_authenticate($data, $form);
89
                if ($fallbackMember) {
90
                    return $fallbackMember;
91
                }
92
93
                self::form_error_msg($form, _t('LDAPAuthenticator.INVALIDCREDENTIALS', 'Invalid credentials'));
94
95
                return;
96
            }
97
        }
98
99
        $result = $service->authenticate($username, $data['Password']);
100
        if (true !== $result['success']) {
101
            $fallbackMember = self::fallback_authenticate($data, $form);
102
            if ($fallbackMember) {
103
                return $fallbackMember;
104
            }
105
106
            self::form_error_msg($form, $result['message']);
107
108
            return;
109
        }
110
111
        $identity = $result['identity'];
112
113
        $member = self::get_member($identity);
114
        if (!$member) {
115
            self::form_error_msg($form, _t('LDAPAuthenticator.PROBLEMFINDINGDATA', 'There was a problem retrieving your user data'));
116
        }
117
118
        Session::clear('BackURL');
119
120
        return $member;
121
    }
122
123
    /**
124
     * Try to authenticate using the fallback authenticator if enabled via config fallback_authenticator.
125
     *
126
     * @param array     $data
127
     * @param Form|null $form
128
     *
129
     * @return Member|null
130
     */
131
    protected static function fallback_authenticate($data, Form $form = null)
132
    {
133
        if ('yes' !== Config::inst()->get('LDAPAuthenticator', 'fallback_authenticator')) {
134
            return null;
135
        }
136
137
        $authClass = Config::inst()->get('LDAPAuthenticator', 'fallback_authenticator_class');
138
139
        SS_Log::log(sprintf('Using fallback authenticator "%s"', $authClass), SS_Log::DEBUG);
140
141
        return call_user_func(
142
            [$authClass, 'authenticate'],
143
            array_merge($data, ['Email' => $data['Login']]),
144
            $form
145
        );
146
    }
147
148
    private static function form_error_msg($form, $message)
149
    {
150
        if (!$form) {
151
            return;
152
        }
153
154
        $form->sessionMessage($message, 'bad');
155
    }
156
157
    /**
158
     * @return bool
159
     */
160
    private static function allow_email_logins()
161
    {
162
        return 'yes' === Config::inst()->get('LDAPAuthenticator', 'allow_email_login');
163
    }
164
165
    /**
166
     * @param string $identity
167
     * @return Member|null
168
     */
169
    private static function get_member($identity)
170
    {
171
        /** @var LDAPService $service */
172
        $service = Injector::inst()->get('LDAPService');
173
174
        $data = $service->getUserByUsername($identity);
175
        if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
176
            return null;
177
        }
178
179
        // LDAPMemberExtension::memberLoggedIn() will update any other AD attributes mapped to Member fields
180
        /** @var Member $member */
181
        $member = Member::get()->filter('GUID', $data['objectguid'])->limit(1)->first();
182 View Code Duplication
        if (!($member && $member->exists())) {
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...
183
            $member = new Member();
184
            $member->GUID = $data['objectguid'];
185
        }
186
187
        // Update the users from LDAP so we are sure that the email is correct.
188
        // This will also write the Member record.
189
        $service->updateMemberFromLDAP($member);
190
191
        return $member;
192
    }
193
}
194