1 | <?php |
---|
2 | /** |
---|
3 | * A Class for handling HTTP Authentication |
---|
4 | * |
---|
5 | * @package davical |
---|
6 | * @subpackage HTTPAuthSession |
---|
7 | * @author Andrew McMillan <andrew@catalyst.net.nz> |
---|
8 | * @copyright Catalyst .Net Ltd |
---|
9 | * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 |
---|
10 | */ |
---|
11 | |
---|
12 | /** |
---|
13 | * A Class for handling a session using HTTP Basic Authentication |
---|
14 | * |
---|
15 | * @package davical |
---|
16 | */ |
---|
17 | class HTTPAuthSession { |
---|
18 | /**#@+ |
---|
19 | * @access private |
---|
20 | */ |
---|
21 | |
---|
22 | /** |
---|
23 | * User ID number |
---|
24 | * @var user_no int |
---|
25 | */ |
---|
26 | public $user_no; |
---|
27 | |
---|
28 | /** |
---|
29 | * User e-mail |
---|
30 | * @var email string |
---|
31 | */ |
---|
32 | public $email; |
---|
33 | |
---|
34 | /** |
---|
35 | * User full name |
---|
36 | * @var fullname string |
---|
37 | */ |
---|
38 | public $fullname; |
---|
39 | |
---|
40 | /** |
---|
41 | * Group rights |
---|
42 | * @var groups array |
---|
43 | */ |
---|
44 | public $groups; |
---|
45 | /**#@-*/ |
---|
46 | |
---|
47 | /** |
---|
48 | * The constructor, which just calls the actual type configured |
---|
49 | */ |
---|
50 | function HTTPAuthSession() { |
---|
51 | global $c; |
---|
52 | |
---|
53 | if ( isset($c->http_auth_mode) && $c->http_auth_mode == "Digest" ) { |
---|
54 | $this->DigestAuthSession(); |
---|
55 | } |
---|
56 | else { |
---|
57 | $this->BasicAuthSession(); |
---|
58 | } |
---|
59 | } |
---|
60 | |
---|
61 | /** |
---|
62 | * Authorisation failed, so we send some headers to say so. |
---|
63 | * |
---|
64 | * @param string $auth_header The WWW-Authenticate header details. |
---|
65 | */ |
---|
66 | function AuthFailedResponse( $auth_header = "" ) { |
---|
67 | global $c; |
---|
68 | if ( $auth_header == "" ) { |
---|
69 | $auth_header = sprintf( 'WWW-Authenticate: Basic realm="%s"', $c->system_name); |
---|
70 | } |
---|
71 | |
---|
72 | header('HTTP/1.1 401 Unauthorized', true, 401 ); |
---|
73 | header('Content-type: text/plain; ; charset="utf-8"' ); |
---|
74 | header( $auth_header ); |
---|
75 | echo 'Please log in for access to this system.'; |
---|
76 | dbg_error_log( "HTTPAuth", ":Session: User is not authorised" ); |
---|
77 | exit; |
---|
78 | } |
---|
79 | |
---|
80 | |
---|
81 | /** |
---|
82 | * Handle Basic HTTP Authentication (not secure unless https) |
---|
83 | */ |
---|
84 | function BasicAuthSession() { |
---|
85 | global $c; |
---|
86 | |
---|
87 | /** |
---|
88 | * Get HTTP Auth to work with PHP+FastCGI |
---|
89 | */ |
---|
90 | if (isset($_SERVER["AUTHORIZATION"]) && !empty($_SERVER["AUTHORIZATION"])) { |
---|
91 | list ($type, $cred) = split (" ", $_SERVER['AUTHORIZATION']); |
---|
92 | if ($type == 'Basic') { |
---|
93 | list ($user, $pass) = explode (":", base64_decode($cred)); |
---|
94 | $_SERVER['PHP_AUTH_USER'] = $user; |
---|
95 | $_SERVER['PHP_AUTH_PW'] = $pass; |
---|
96 | } |
---|
97 | } |
---|
98 | else if ( isset($c->authenticate_hook['server_auth_type']) && $c->authenticate_hook['server_auth_type'] == $_SERVER['AUTH_TYPE'] |
---|
99 | && isset($_SERVER["REMOTE_USER"]) && !empty($_SERVER["REMOTE_USER"])) { |
---|
100 | /** |
---|
101 | * The authentication has happened in the server, and we should accept it. |
---|
102 | * Perhaps this 'split' is not a good idea though. People may want to use the |
---|
103 | * full ID as the username. A further option may be desirable. |
---|
104 | * |
---|
105 | */ |
---|
106 | $_SERVER['PHP_AUTH_USER'] = $_SERVER['REMOTE_USER']; |
---|
107 | $_SERVER['PHP_AUTH_PW'] = 'Externally Authenticated'; |
---|
108 | if ( ! isset($c->authenticate_hook['call']) ) { |
---|
109 | /** |
---|
110 | * Since we still need to get the user's details from somewhere. We change the default |
---|
111 | * authentication hook to auth_external which simply retrieves a user row from the DB |
---|
112 | * and does no password checking. |
---|
113 | */ |
---|
114 | $c->authenticate_hook['call'] = 'auth_external'; |
---|
115 | } |
---|
116 | } |
---|
117 | |
---|
118 | |
---|
119 | /** |
---|
120 | * Fall through to the normal PHP authentication variables. |
---|
121 | */ |
---|
122 | if ( isset($_SERVER['PHP_AUTH_USER']) ) { |
---|
123 | if ( $u = $this->CheckPassword( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) { |
---|
124 | $this->AssignSessionDetails($u); |
---|
125 | return; |
---|
126 | } |
---|
127 | } |
---|
128 | |
---|
129 | if ( isset($c->allow_unauthenticated) && $c->allow_unauthenticated ) { |
---|
130 | $this->user_no = -1; |
---|
131 | $this->username = 'guest'; |
---|
132 | $this->fullname = 'Unauthenticated User'; |
---|
133 | $this->email = 'invalid'; |
---|
134 | return; |
---|
135 | } |
---|
136 | |
---|
137 | $this->AuthFailedResponse(); |
---|
138 | // Does not return |
---|
139 | } |
---|
140 | |
---|
141 | |
---|
142 | /** |
---|
143 | * Handle Digest HTTP Authentication (no passwords were harmed in this transaction!) |
---|
144 | * |
---|
145 | * Note that this will not actually work, unless we can either: |
---|
146 | * (A) store the password plain text in the database |
---|
147 | * (B) store an md5( username || realm || password ) in the database |
---|
148 | * |
---|
149 | * The problem is that potentially means that the administrator can collect the sorts |
---|
150 | * of things people use as passwords. I believe this is quite a bad idea. In scenario (B) |
---|
151 | * while they cannot see the password itself, they can see a hash which only varies when |
---|
152 | * the password varies, so can see when two users have the same password, or can use |
---|
153 | * some of the reverse lookup sites to attempt to reverse the hash. I think this is a |
---|
154 | * less bad idea, but not ideal. Probably better than running Basic auth of HTTP though! |
---|
155 | */ |
---|
156 | function DigestAuthSession() { |
---|
157 | global $c; |
---|
158 | |
---|
159 | if ( ! empty($_SERVER['PHP_AUTH_DIGEST'])) { |
---|
160 | // analyze the PHP_AUTH_DIGEST variable |
---|
161 | if ( $data = $this->ParseDigestHeader($_SERVER['PHP_AUTH_DIGEST']) ) { |
---|
162 | // generate the valid response |
---|
163 | $user_password = "Don't be silly! Why would a user have a password like this!!?"; |
---|
164 | /** |
---|
165 | * @todo At this point we need to query the database for something fitting |
---|
166 | * either strategy (A) or (B) above, in order to set $user_password to |
---|
167 | * something useful! |
---|
168 | */ |
---|
169 | $A1 = md5($data['username'] . ':' . $c->system_name . ':' . $user_password); |
---|
170 | $A2 = md5($_SERVER['REQUEST_METHOD'].':'.$data['uri']); |
---|
171 | $valid_response = md5($A1.':'.$data['nonce'].':'.$data['nc'].':'.$data['cnonce'].':'.$data['qop'].':'.$A2); |
---|
172 | |
---|
173 | if ( $data['response'] == $valid_response ) { |
---|
174 | $this->AssignSessionDetails($u); |
---|
175 | return; |
---|
176 | } |
---|
177 | } |
---|
178 | } |
---|
179 | |
---|
180 | $nonce = uniqid(); |
---|
181 | $opaque = md5($c->system_name); |
---|
182 | $this->AuthFailedResponse( sprintf('WWW-Authenticate: Digest realm="%s", qop="auth,auth-int", nonce="%s", opaque="%s"', $c->system_name, $nonce, $opaque ) ); |
---|
183 | } |
---|
184 | |
---|
185 | |
---|
186 | /** |
---|
187 | * Parse the HTTP Digest Auth Header |
---|
188 | * - largely sourced from the PHP documentation |
---|
189 | */ |
---|
190 | function ParseDigestHeader($auth_header) { |
---|
191 | // protect against missing data |
---|
192 | $needed_parts = array('nonce'=>1, 'nc'=>1, 'cnonce'=>1, 'qop'=>1, 'username'=>1, 'uri'=>1, 'response'=>1); |
---|
193 | $data = array(); |
---|
194 | |
---|
195 | preg_match_all('@(\w+)=(?:([\'"])([^\2]+)\2|([^\s,]+))@', $auth_header, $matches, PREG_SET_ORDER); |
---|
196 | |
---|
197 | foreach ($matches as $m) { |
---|
198 | $data[$m[1]] = $m[3] ? $m[3] : $m[4]; |
---|
199 | unset($needed_parts[$m[1]]); |
---|
200 | } |
---|
201 | |
---|
202 | return $needed_parts ? false : $data; |
---|
203 | } |
---|
204 | |
---|
205 | /** |
---|
206 | * CheckPassword does all of the password checking and |
---|
207 | * returns a user record object, or false if it all ends in tears. |
---|
208 | */ |
---|
209 | function CheckPassword( $username, $password ) { |
---|
210 | global $c; |
---|
211 | |
---|
212 | if ( isset($c->authenticate_hook) && isset($c->authenticate_hook['call']) && function_exists($c->authenticate_hook['call']) ) { |
---|
213 | /** |
---|
214 | * The authenticate hook needs to: |
---|
215 | * - Accept a username / password |
---|
216 | * - Confirm the username / password are correct |
---|
217 | * - Create (or update) a 'usr' record in our database |
---|
218 | * - Return the 'usr' record as an object |
---|
219 | * - Return === false when authentication fails |
---|
220 | * |
---|
221 | * It can expect that: |
---|
222 | * - Configuration data will be in $c->authenticate_hook['config'], which might be an array, or whatever is needed. |
---|
223 | */ |
---|
224 | $hook_response = call_user_func( $c->authenticate_hook['call'], $username, $password ); |
---|
225 | /** |
---|
226 | * make the authentication hook optional: if the flag is set, ignore a return value of 'false' |
---|
227 | */ |
---|
228 | if (isset($c->authenticate_hook['optional']) && $c->authenticate_hook['optional']) { |
---|
229 | if ($hook_response !== false) { return $hook_response; } |
---|
230 | } else { |
---|
231 | return $hook_response; |
---|
232 | } |
---|
233 | } |
---|
234 | |
---|
235 | if ( $usr = getUserByName($username) ) { |
---|
236 | dbg_error_log( "BasicAuth", ":CheckPassword: Name:%s, Pass:%s, File:%s, Active:%s", $username, $password, $usr->password, ($usr->active?'Yes':'No') ); |
---|
237 | if ( $usr->active && session_validate_password( $password, $usr->password ) ) { |
---|
238 | return $usr; |
---|
239 | } |
---|
240 | } |
---|
241 | return false; |
---|
242 | } |
---|
243 | |
---|
244 | /** |
---|
245 | * Checks whether a user is allowed to do something. |
---|
246 | * |
---|
247 | * The check is performed to see if the user has that role. |
---|
248 | * |
---|
249 | * @param string $whatever The role we want to know if the user has. |
---|
250 | * @return boolean Whether or not the user has the specified role. |
---|
251 | */ |
---|
252 | function AllowedTo ( $whatever ) { |
---|
253 | return ( isset($this->logged_in) && $this->logged_in && isset($this->roles[$whatever]) && $this->roles[$whatever] ); |
---|
254 | } |
---|
255 | |
---|
256 | |
---|
257 | /** |
---|
258 | * Internal function used to get the user's roles from the database. |
---|
259 | */ |
---|
260 | function GetRoles () { |
---|
261 | $this->roles = array(); |
---|
262 | $qry = new AwlQuery( 'SELECT role_name FROM role_member m join roles r ON r.role_no = m.role_no WHERE user_no = :user_no ', |
---|
263 | array( ':user_no' => $this->user_no) ); |
---|
264 | if ( $qry->Exec('BasicAuth') && $qry->rows() > 0 ) { |
---|
265 | while( $role = $qry->Fetch() ) { |
---|
266 | $this->roles[$role->role_name] = true; |
---|
267 | } |
---|
268 | } |
---|
269 | } |
---|
270 | |
---|
271 | |
---|
272 | /** |
---|
273 | * Internal function used to assign the session details to a user's new session. |
---|
274 | * @param object $u The user+session object we (probably) read from the database. |
---|
275 | */ |
---|
276 | function AssignSessionDetails( $u ) { |
---|
277 | if ( !isset($u->principal_id) ) { |
---|
278 | // If they don't have a principal_id set then we should re-read from our local database |
---|
279 | $qry = new AwlQuery('SELECT * FROM dav_principal WHERE username = :username', array(':username' => $u->username) ); |
---|
280 | if ( $qry->Exec() && $qry->rows() == 1 ) { |
---|
281 | $u = $qry->Fetch(); |
---|
282 | } |
---|
283 | } |
---|
284 | |
---|
285 | // Assign each field in the selected record to the object |
---|
286 | foreach( $u AS $k => $v ) { |
---|
287 | $this->{$k} = $v; |
---|
288 | } |
---|
289 | |
---|
290 | $this->GetRoles(); |
---|
291 | $this->logged_in = true; |
---|
292 | if ( function_exists("awl_set_locale") && isset($this->locale) && $this->locale != "" ) { |
---|
293 | awl_set_locale($this->locale); |
---|
294 | } |
---|
295 | } |
---|
296 | |
---|
297 | |
---|
298 | } |
---|
299 | |
---|