1<?php
2
3/**
4 * Pluggable Authentication Module
5 */
6class ElggPAM {
7
8	/**
9	 * @var array
10	 * @internal
11	 * @todo move state into a PAM service
12	 */
13	public static $_handlers = [];
14
15	/**
16	 * @var string PAM policy type: user, api or plugin-defined policies
17	 */
18	protected $policy;
19
20	/**
21	 * @var array Failure mesages
22	 */
23	protected $messages;
24
25	/**
26	 * \ElggPAM constructor
27	 *
28	 * @param string $policy PAM policy type: user, api, or plugin-defined policies
29	 */
30	public function __construct($policy) {
31		$this->policy = $policy;
32		$this->messages = ['sufficient' => [], 'required' => []];
33	}
34
35	/**
36	 * Authenticate a set of credentials against a policy
37	 * This function will process all registered PAM handlers or stop when the first
38	 * handler fails. A handler fails by either returning false or throwing an
39	 * exception. The advantage of throwing an exception is that it returns a message
40	 * that can be passed to the user. The processing order of the handlers is
41	 * determined by the order that they were registered.
42	 *
43	 * If $credentials are provided, the PAM handler should authenticate using the
44	 * provided credentials. If not, then credentials should be prompted for or
45	 * otherwise retrieved (eg from the HTTP header or $_SESSION).
46	 *
47	 * @param array $credentials Credentials array dependant on policy type
48	 * @return bool
49	 */
50	public function authenticate($credentials = []) {
51		if (!isset(self::$_handlers[$this->policy]) ||
52			!is_array(self::$_handlers[$this->policy])) {
53			return false;
54		}
55
56		$authenticated = false;
57
58		foreach (self::$_handlers[$this->policy] as $v) {
59			$handler = $v->handler;
60			if (!is_callable($handler)) {
61				continue;
62			}
63			/* @var callable $handler */
64
65			$importance = $v->importance;
66
67			try {
68				// Execute the handler
69				// @todo don't assume $handler is a global function
70				$result = call_user_func($handler, $credentials);
71				if ($result) {
72					$authenticated = true;
73				} elseif ($result === false) {
74					if ($importance == 'required') {
75						$this->messages['required'][] = "$handler:failed";
76						return false;
77					} else {
78						$this->messages['sufficient'][] = "$handler:failed";
79					}
80				}
81			} catch (Exception $e) {
82				if ($importance == 'required') {
83					$this->messages['required'][] = $e->getMessage();
84					return false;
85				} else {
86					$this->messages['sufficient'][] = $e->getMessage();
87				}
88			}
89		}
90
91		return $authenticated;
92	}
93
94	/**
95	 * Get a failure message to display to user
96	 *
97	 * @return string
98	 */
99	public function getFailureMessage() {
100		$message = _elgg_services()->translator->translate('auth:nopams');
101		if (!empty($this->messages['required'])) {
102			$message = $this->messages['required'][0];
103		} elseif (!empty($this->messages['sufficient'])) {
104			$message = $this->messages['sufficient'][0];
105		}
106
107		return _elgg_services()->hooks->trigger('fail', 'auth', $this->messages, $message);
108	}
109}
110