1<?php if (!defined('PmWiki')) exit();
2/*  Copyright 2005-2021 Patrick R. Michaud (pmichaud@pobox.com)
3    This file is part of PmWiki; you can redistribute it and/or modify
4    it under the terms of the GNU General Public License as published
5    by the Free Software Foundation; either version 2 of the License, or
6    (at your option) any later version.  See pmwiki.php for full details.
7
8    The APR compatible MD5 encryption algorithm in _crypt() below is
9    based on code Copyright 2005 by D. Faure and the File::Passwd
10    PEAR library module by Mike Wallner <mike@php.net>.
11
12    This script enables simple authentication based on username and
13    password combinations.  At present this script can authenticate
14    from passwords held in arrays or in .htpasswd-formatted files,
15    but eventually it will support authentication via sources such
16    as LDAP and Active Directory.
17
18    To configure a .htpasswd-formatted file for authentication, do
19        $AuthUser['htpasswd'] = '/path/to/.htpasswd';
20    prior to including this script.
21
22    Individual username/password combinations can also be placed
23    directly in the $AuthUser array, such as:
24        $AuthUser['pmichaud'] = pmcrypt('secret');
25
26    To authenticate against an LDAP server, put the url for
27    the server in $AuthUser['ldap'], as in:
28        $AuthUser['ldap'] = 'ldap://ldap.example.com/ou=People,o=example?uid';
29
30    Script maintained by Petko YOTOV www.pmwiki.org/petko
31*/
32
33# let Site.AuthForm know that we're doing user-based authorization
34$EnableAuthUser = 1;
35
36if (@$_POST['authid'])
37  AuthUserId($pagename, stripmagic(@$_POST['authid']),
38             stripmagic(@$_POST['authpw']));
39else SessionAuth($pagename);
40
41function AuthUserId($pagename, $id, $pw=NULL) {
42  global $AuthUser, $AuthUserPageFmt, $AuthUserFunctions,
43    $AuthId, $MessagesFmt, $AuthUserPat, $MultiFactorAuthFunction;
44
45  $auth = array();
46  foreach((array)$AuthUser as $k=>$v) $auth[$k] = (array)$v;
47  $authid = '';
48
49  # load information from SiteAdmin.AuthUser (or page in $AuthUserPageFmt)
50  SDV($AuthUserPageFmt, '$SiteAdminGroup.AuthUser');
51  SDVA($AuthUserFunctions, array(
52    'htpasswd' => 'AuthUserHtPasswd',
53    'ldap' => 'AuthUserLDAP',
54#    'mysql' => 'AuthUserMySQL',
55    $id => 'AuthUserConfig'));
56
57  SDV($AuthUserPat, "/^\\s*([@\\w][^\\s:]*):(.*)/m");
58  foreach ( (array)$AuthUserPageFmt as $aupn) {
59    $pn = FmtPageName($aupn, $pagename);
60    $apage = ReadPage($pn, READPAGE_CURRENT);
61    if ($apage && preg_match_all($AuthUserPat,
62                                $apage['text'], $matches, PREG_SET_ORDER)) {
63      foreach($matches as $m) {
64        if (!preg_match_all('/\\bldaps?:\\S+|[^\\s,]+/', $m[2], $v))
65          continue;
66        if ($m[1][0] == '@')
67          foreach($v[0] as $g) $auth[$g][] = $m[1];
68        else $auth[$m[1]] = array_merge((array)@$auth[$m[1]], $v[0]);
69      }
70    }
71  }
72
73  $authlist = array();
74  if (func_num_args()==2) $authid = $id;
75  else
76    foreach($AuthUserFunctions as $k => $fn)
77      if (@$auth[$k] && $fn($pagename, $id, $pw, $auth[$k], $authlist))
78        { $authid = $id; break; }
79
80  if (!$authid) { $GLOBALS['InvalidLogin'] = 1; return; }
81  if (IsEnabled($MultiFactorAuthFunction, 0) && function_exists($MultiFactorAuthFunction)) {
82    if (! $MultiFactorAuthFunction($id, $pw) ) return;
83  }
84
85
86  if (!isset($AuthId)) $AuthId = $authid;
87  $authlist["id:$authid"] = 1;
88  $authlist["id:-$authid"] = -1;
89  foreach(preg_grep('/^@/', (array)@$auth[$authid]) as $g)
90    $authlist[$g] = 1;
91  foreach(preg_grep('/^@/', (array)@$auth['*']) as $g)
92    $authlist[$g] = 1;
93  foreach(preg_grep('/^@/', array_keys($auth)) as $g) # useless? PITS:01201
94    if (in_array($authid, $auth[$g])) $authlist[$g] = 1;
95  if (@$auth['htgroup']) {
96    foreach(AuthUserHtGroup($pagename, $id, $pw, $auth['htgroup']) as $g)
97      $authlist["@$g"] = 1;
98  }
99  foreach(preg_grep('/^@/', (array)@$auth["-$authid"]) as $g)
100    unset($authlist[$g]);
101  SessionAuth($pagename, array('authid' => $authid, 'authlist' => $authlist));
102}
103
104
105function AuthUserConfig($pagename, $id, $pw, $pwlist) {
106  foreach ((array)$pwlist as $chal)
107    if (_crypt($pw, $chal) == $chal) return true;
108  return false;
109}
110
111
112function AuthUserHtPasswd($pagename, $id, $pw, $pwlist) {
113  foreach ((array)$pwlist as $f) {
114    $fp = fopen($f, "r"); if (!$fp) continue;
115    while ($x = fgets($fp, 1024)) {
116      $x = rtrim($x);
117      @list($i, $c, $r) = explode(':', $x, 3);
118      if ($i == $id && _crypt($pw, $c) == $c) { fclose($fp); return true; }
119    }
120    fclose($fp);
121  }
122  return false;
123}
124
125
126function AuthUserHtGroup($pagename, $id, $pw, $pwlist) {
127  $groups = array();
128  foreach ((array)$pwlist as $f) {
129    $fp = fopen($f, 'r'); if (!$fp) continue;
130    while ($x = fgets($fp, 4096)) {
131      if (preg_match('/^(\\w[^\\s:]+)\\s*:(.*)$/', trim($x), $match)) {
132        $glist = preg_split('/[\\s,]+/', $match[2], -1, PREG_SPLIT_NO_EMPTY);
133        if (in_array($id, $glist)) $groups[$match[1]] = 1;
134      }
135    }
136    fclose($fp);
137  }
138  return array_keys($groups);
139}
140
141
142function AuthUserLDAP($pagename, $id, $pw, $pwlist) {
143  global $AuthLDAPBindDN, $AuthLDAPBindPassword, $AuthLDAPReferrals;
144  if (!$pw) return false;
145  if (!function_exists('ldap_connect'))
146    Abort('authuser: LDAP authentication requires PHP ldap functions','ldapfn');
147  foreach ((array)$pwlist as $ldap) {
148    if (!preg_match('!(ldaps?://[^/]+)/(.*)$!', $ldap, $match))
149      continue;
150    ##  connect to the LDAP server
151    list($z, $url, $path) = $match;
152    $ds = ldap_connect($url);
153    ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3);
154    if(isset($AuthLDAPReferrals)) # *NOT* IsEnabled
155      ldap_set_option($ds, LDAP_OPT_REFERRALS, $AuthLDAPReferrals);
156    ##  For Active Directory, don't specify a path and we simply
157    ##  attempt to bind with the username and password directly
158    if (!$path && @ldap_bind($ds, $id, $pw)) { ldap_close($ds); return true; }
159    ##  Otherwise, we use Apache-style urls for LDAP authentication
160    ##  Split the path into its search components
161    list($basedn, $attr, $sub, $filter) = explode('?', $path);
162    if (!$attr) $attr = 'uid';
163    if (!$sub) $sub = 'one';
164    if (!$filter) $filter = '(objectClass=*)';
165    $binddn = @$AuthLDAPBindDN;
166    $bindpw = @$AuthLDAPBindPassword;
167    if (ldap_bind($ds, $binddn, $bindpw)) {
168      ##  Search for the appropriate uid
169      $fn = ($sub == 'sub') ? 'ldap_search' : 'ldap_list';
170      $sr = $fn($ds, $basedn, "(&$filter($attr=$id))", array($attr));
171      $x = ldap_get_entries($ds, $sr);
172      ##  If we find a unique id, bind to it for success
173      if ($x['count'] == 1) {
174        $dn = $x[0]['dn'];
175        if (@ldap_bind($ds, $dn, $pw)) { ldap_close($ds); return true; }
176      }
177    }
178    ldap_close($ds);
179  }
180  return false;
181}
182
183
184#  The _crypt function provides support for SHA1 encrypted passwords
185#  (keyed by '{SHA}') and Apache MD5 encrypted passwords (keyed by
186#  '$apr1$'); otherwise it just calls PHP's crypt() for the rest.
187#  The APR MD5 encryption code was contributed by D. Faure.
188
189function _crypt($plain, $salt=null) {
190  if (strncmp($salt, '{SHA}', 5) == 0)
191    return '{SHA}'.base64_encode(pack('H*', sha1($plain)));
192  if (strncmp($salt, '$apr1$', 6) == 0) {
193    preg_match('/^\\$apr1\\$([^$]+)/', $salt, $match);
194    $salt = $match[1];
195    $length = strlen($plain);
196    $context = $plain . '$apr1$' . $salt;
197    $binary = pack('H32', md5($plain . $salt . $plain));
198    for($i = $length; $i > 0; $i -= 16)
199      $context .= substr($binary, 0, min(16, $i));
200    for($i = $length; $i > 0; $i >>= 1)
201      $context .= ($i & 1) ? chr(0) : $plain[0];
202    $binary = pack('H32', md5($context));
203    for($i = 0; $i < 1000; $i++) {
204      $new = ($i & 1) ? $plain : $binary;
205      if ($i % 3) $new .= $salt;
206      if ($i % 7) $new .= $plain;
207      $new .= ($i & 1) ? $binary : $plain;
208      $binary = pack('H32', md5($new));
209    }
210    $q = '';
211    for ($i = 0; $i < 5; $i++) {
212      $k = $i + 6;
213      $j = $i + 12;
214      if ($j == 16) $j = 5;
215      $q = $binary[$i].$binary[$k].$binary[$j] . $q;
216    }
217    $q = chr(0).chr(0).$binary[11] . $q;
218    $q = strtr(strrev(substr(base64_encode($q), 2)),
219           'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
220           './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
221    return "\$apr1\$$salt\$$q";
222  }
223  if (md5($plain) == $salt) return $salt;
224  return pmcrypt($plain, $salt);
225}
226