1<?php
2/**
3 * ADFS SAML authentication plugin
4 *
5 * @author     Andreas Gohr <gohr@cosmocode.de>
6 */
7class auth_plugin_adfs extends auth_plugin_authplain
8{
9    /** @var OneLogin_Saml2_Auth the SAML authentication library */
10    protected $saml;
11
12    /** @inheritdoc */
13    public function __construct()
14    {
15        parent::__construct();
16
17        $this->cando['external'] = true;
18        $this->cando['logoff'] = true;
19        /* We only want auth_plain for e-mail tracking and group storage */
20        $this->cando['addUser'] = false;
21        $this->cando['modLogin'] = false;
22        $this->cando['modPass'] = false;
23        $this->cando['modName'] = false;
24        $this->cando['modMail'] = false;
25        $this->cando['modGroups'] = false;
26
27        /** @var helper_plugin_adfs $hlp */
28        $hlp = plugin_load('helper', 'adfs');
29        $this->saml = $hlp->getSamlLib();
30    }
31
32    /**
33     * Checks the session to see if the user is already logged in
34     *
35     * If not logged in, redirects to SAML provider
36     */
37    public function trustExternal($user, $pass, $sticky = false)
38    {
39        global $USERINFO;
40        global $ID;
41        global $ACT;
42        if (empty($ID)) $ID = getID();
43
44        // trust session info, no need to recheck
45        if (isset($_SESSION[DOKU_COOKIE]['auth']) &&
46            $_SESSION[DOKU_COOKIE]['auth']['buid'] == auth_browseruid() &&
47            isset($_SESSION[DOKU_COOKIE]['auth']['user'])
48        ) {
49
50            $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user'];
51            $USERINFO = $_SESSION[DOKU_COOKIE]['auth']['info'];
52
53            return true;
54        }
55
56        if (!isset($_POST['SAMLResponse']) && ($ACT == 'login' || get_doku_pref('adfs_autologin', 0))) {
57            // Initiate SAML auth request
58            $url = $this->saml->login(
59                null, // returnTo: is configured in our settings
60                [], // parameter: we do not send any additional paramters to ADFS
61                false, // forceAuthn: would skip any available SSO data, not what we want
62                false, // isPassive: would avoid all user interaction, not what we want
63                true, // stay: do not redirect, we do that ourselves
64                false // setNamedIdPolicy: we need to disable this or ADFS complains about our request
65            );
66            $_SESSION['adfs_redirect'] = wl($ID, '', true, '&'); // remember current page
67            send_redirect($url);
68        } elseif (isset($_POST['SAMLResponse'])) {
69            // consume SAML response
70            try {
71                $this->saml->processResponse();
72                if ($this->saml->isAuthenticated()) {
73                    // Always read the userid from the saml response
74                    $USERINFO = $this->getUserDataFromResponse();
75                    $_SERVER['REMOTE_USER'] = $USERINFO['user'];
76
77                    if ($this->getConf('autoprovisioning')) {
78                        // In case of auto-provisionning we override the local DB info with those retrieve during the SAML negociation
79                        if (
80                            $this->triggerUserMod('modify', array(
81                                $USERINFO['user'],
82                                $USERINFO
83                            )) === false
84                        ) {
85                            $this->triggerUserMod('create', array(
86                                $USERINFO['user'],
87                                "\0\0nil\0\0",
88                                $USERINFO['name'],
89                                $USERINFO['mail'],
90                                $USERINFO['grps']
91                            ));
92                        }
93                    } else {
94                        // In case the autoprovisionning is disabled we rely on the local DB for the info such as the group and the fullname.
95                        // It also means that the user should exists already in the DB
96                        $dbUserInfo = $this->getUserData($USERINFO['user']);
97                        if($dbUserInfo === false) throw new \Exception('This user is not in the local user database and may not login');
98                        $USERINFO['name'] = $dbUserInfo["name"];
99                        $USERINFO['mail'] = $dbUserInfo["mail"];
100                        $USERINFO['grps'] = $dbUserInfo["grps"];
101                    }
102
103                    // Store that in the cookie
104                    $_SESSION[DOKU_COOKIE]['auth']['user'] = $_SERVER['REMOTE_USER'];
105                    $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
106                    $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid(); // cache login
107
108                    // successful login
109                    if (isset($_SESSION['adfs_redirect'])) {
110                        $go = $_SESSION['adfs_redirect'];
111                        unset($_SESSION['adfs_redirect']);
112                    } else {
113                        $go = wl($ID, '', true, '&');
114                    }
115                    set_doku_pref('adfs_autologin', 1);
116                    send_redirect($go); // decouple the history from POST
117                    return true;
118                } else {
119                    $this->logOff();
120
121                    msg('ADFS: '.hsc($this->saml->getLastErrorReason()), -1);
122                    return false;
123                }
124            } catch (Exception $e) {
125                $this->logOff();
126                msg('Invalid SAML response: ' . hsc($e->getMessage()), -1);
127                return false;
128            }
129        }
130        // no login happened
131        return false;
132    }
133
134
135    /** @inheritdoc */
136    public function logOff()
137    {
138        set_doku_pref('adfs_autologin', 0);
139    }
140
141    /** @inheritdoc */
142    public function cleanUser($user)
143    {
144        // strip disallowed characters
145        $user = strtr(
146            $user, array(
147                ',' => '',
148                '/' => '',
149                '#' => '',
150                ';' => '',
151                ':' => ''
152            )
153        );
154        if ($this->getConf('lowercase')) {
155            return utf8_strtolower($user);
156        } else {
157            return $user;
158        }
159    }
160
161    /** @inheritdoc */
162    public function cleanGroup($group)
163    {
164        return $this->cleanUser($group);
165    }
166
167
168    /**
169     * Build user data from the response
170     *
171     * @return array the user data
172     * @throws Exception when attributes are missing
173     */
174    protected function getUserDataFromResponse()
175    {
176        global $conf;
177
178        // which attributes should be in the response?
179        $attributes = [
180            'user' => $this->getConf('userid_attr_name')
181        ];
182        if ($this->getConf('autoprovisioning')) {
183            $attributes['name'] = $this->getConf('fullname_attr_name');
184            if (empty($attributes['name'])) $attributes['name'] = $attributes['user']; // fall back to login
185            $attributes['mail'] = $this->getConf('email_attr_name');
186            $attributes['grps'] = $this->getConf('groups_attr_name');
187            if (empty($attributes['grps'])) unset($attributes['grps']); // groups are optional
188        }
189
190        // get attributes from response
191        $userdata = ['user' => '', 'mail' => '', 'name' => '', 'grps' => []];
192        foreach ($attributes as $key => $attr) {
193            $data = $this->saml->getAttribute($attr);
194            if ($data === null) throw new \Exception('SAML Response is missing attribute ' . $attr);
195            $userdata[$key] = $data;
196        }
197
198        // clean up data
199        $userdata['user'] = $this->cleanUser($userdata['user'][0]);
200        $userdata['name'] = $userdata['name'][0];
201        $userdata['mail'] = $userdata['mail'][0];
202        $userdata['grps'] = (array)$userdata['grps'];
203        $userdata['grps'][] = $conf['defaultgroup'];
204        $userdata['grps'] = array_map([$this, 'cleanGroup'], $userdata['grps']);
205
206        return $userdata;
207    }
208}
209