1<?php
2
3namespace Sabre\DAV\Auth;
4
5use Sabre\DAV\Exception\NotAuthenticated;
6use Sabre\DAV\Server;
7use Sabre\DAV\ServerPlugin;
8use Sabre\HTTP\RequestInterface;
9use Sabre\HTTP\ResponseInterface;
10
11/**
12 * This plugin provides Authentication for a WebDAV server.
13 *
14 * It works by providing a Auth\Backend class. Several examples of these
15 * classes can be found in the Backend directory.
16 *
17 * It's possible to provide more than one backend to this plugin. If more than
18 * one backend was provided, each backend will attempt to authenticate. Only if
19 * all backends fail, we throw a 401.
20 *
21 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
22 * @author Evert Pot (http://evertpot.com/)
23 * @license http://sabre.io/license/ Modified BSD License
24 */
25class Plugin extends ServerPlugin {
26
27    /**
28     * By default this plugin will require that the user is authenticated,
29     * and refuse any access if the user is not authenticated.
30     *
31     * If this setting is set to false, we let the user through, whether they
32     * are authenticated or not.
33     *
34     * This is useful if you want to allow both authenticated and
35     * unauthenticated access to your server.
36     *
37     * @param bool
38     */
39    public $autoRequireLogin = true;
40
41    /**
42     * authentication backends
43     */
44    protected $backends;
45
46    /**
47     * The currently logged in principal. Will be `null` if nobody is currently
48     * logged in.
49     *
50     * @var string|null
51     */
52    protected $currentPrincipal;
53
54    /**
55     * Creates the authentication plugin
56     *
57     * @param Backend\BackendInterface $authBackend
58     */
59    function __construct(Backend\BackendInterface $authBackend = null) {
60
61        if (!is_null($authBackend)) {
62            $this->addBackend($authBackend);
63        }
64
65    }
66
67    /**
68     * Adds an authentication backend to the plugin.
69     *
70     * @param Backend\BackendInterface $authBackend
71     * @return void
72     */
73    function addBackend(Backend\BackendInterface $authBackend) {
74
75        $this->backends[] = $authBackend;
76
77    }
78
79    /**
80     * Initializes the plugin. This function is automatically called by the server
81     *
82     * @param Server $server
83     * @return void
84     */
85    function initialize(Server $server) {
86
87        $server->on('beforeMethod', [$this, 'beforeMethod'], 10);
88
89    }
90
91    /**
92     * Returns a plugin name.
93     *
94     * Using this name other plugins will be able to access other plugins
95     * using DAV\Server::getPlugin
96     *
97     * @return string
98     */
99    function getPluginName() {
100
101        return 'auth';
102
103    }
104
105    /**
106     * Returns the currently logged-in principal.
107     *
108     * This will return a string such as:
109     *
110     * principals/username
111     * principals/users/username
112     *
113     * This method will return null if nobody is logged in.
114     *
115     * @return string|null
116     */
117    function getCurrentPrincipal() {
118
119        return $this->currentPrincipal;
120
121    }
122
123    /**
124     * This method is called before any HTTP method and forces users to be authenticated
125     *
126     * @param RequestInterface $request
127     * @param ResponseInterface $response
128     * @return bool
129     */
130    function beforeMethod(RequestInterface $request, ResponseInterface $response) {
131
132        if ($this->currentPrincipal) {
133
134            // We already have authentication information. This means that the
135            // event has already fired earlier, and is now likely fired for a
136            // sub-request.
137            //
138            // We don't want to authenticate users twice, so we simply don't do
139            // anything here. See Issue #700 for additional reasoning.
140            //
141            // This is not a perfect solution, but will be fixed once the
142            // "currently authenticated principal" is information that's not
143            // not associated with the plugin, but rather per-request.
144            //
145            // See issue #580 for more information about that.
146            return;
147
148        }
149
150        $authResult = $this->check($request, $response);
151
152        if ($authResult[0]) {
153            // Auth was successful
154            $this->currentPrincipal = $authResult[1];
155            $this->loginFailedReasons = null;
156            return;
157        }
158
159
160
161        // If we got here, it means that no authentication backend was
162        // successful in authenticating the user.
163        $this->currentPrincipal = null;
164        $this->loginFailedReasons = $authResult[1];
165
166        if ($this->autoRequireLogin) {
167            $this->challenge($request, $response);
168            throw new NotAuthenticated(implode(', ', $authResult[1]));
169        }
170
171    }
172
173    /**
174     * Checks authentication credentials, and logs the user in if possible.
175     *
176     * This method returns an array. The first item in the array is a boolean
177     * indicating if login was successful.
178     *
179     * If login was successful, the second item in the array will contain the
180     * current principal url/path of the logged in user.
181     *
182     * If login was not successful, the second item in the array will contain a
183     * an array with strings. The strings are a list of reasons why login was
184     * unsuccessful. For every auth backend there will be one reason, so usually
185     * there's just one.
186     *
187     * @param RequestInterface $request
188     * @param ResponseInterface $response
189     * @return array
190     */
191    function check(RequestInterface $request, ResponseInterface $response) {
192
193        if (!$this->backends) {
194            throw new \Sabre\DAV\Exception('No authentication backends were configured on this server.');
195        }
196        $reasons = [];
197        foreach ($this->backends as $backend) {
198
199            $result = $backend->check(
200                $request,
201                $response
202            );
203
204            if (!is_array($result) || count($result) !== 2 || !is_bool($result[0]) || !is_string($result[1])) {
205                throw new \Sabre\DAV\Exception('The authentication backend did not return a correct value from the check() method.');
206            }
207
208            if ($result[0]) {
209                $this->currentPrincipal = $result[1];
210                // Exit early
211                return [true, $result[1]];
212            }
213            $reasons[] = $result[1];
214
215        }
216
217        return [false, $reasons];
218
219    }
220
221    /**
222     * This method sends authentication challenges to the user.
223     *
224     * This method will for example cause a HTTP Basic backend to set a
225     * WWW-Authorization header, indicating to the client that it should
226     * authenticate.
227     *
228     * @param RequestInterface $request
229     * @param ResponseInterface $response
230     * @return array
231     */
232    function challenge(RequestInterface $request, ResponseInterface $response) {
233
234        foreach ($this->backends as $backend) {
235            $backend->challenge($request, $response);
236        }
237
238    }
239
240    /**
241     * List of reasons why login failed for the last login operation.
242     *
243     * @var string[]|null
244     */
245    protected $loginFailedReasons;
246
247    /**
248     * Returns a list of reasons why login was unsuccessful.
249     *
250     * This method will return the login failed reasons for the last login
251     * operation. One for each auth backend.
252     *
253     * This method returns null if the last authentication attempt was
254     * successful, or if there was no authentication attempt yet.
255     *
256     * @return string[]|null
257     */
258    function getLoginFailedReasons() {
259
260        return $this->loginFailedReasons;
261
262    }
263
264    /**
265     * Returns a bunch of meta-data about the plugin.
266     *
267     * Providing this information is optional, and is mainly displayed by the
268     * Browser plugin.
269     *
270     * The description key in the returned array may contain html and will not
271     * be sanitized.
272     *
273     * @return array
274     */
275    function getPluginInfo() {
276
277        return [
278            'name'        => $this->getPluginName(),
279            'description' => 'Generic authentication plugin',
280            'link'        => 'http://sabre.io/dav/authentication/',
281        ];
282
283    }
284
285}
286