Overview
SHA1:633bea8e4ac1a43a4c919ca678446e72ceb07e28
Date: 2017-01-26 00:19:53
User: bohwaz
Comment:Ajout authentification à double facteur (OTP)
Timelines: family | ancestors | descendants | both | dev
Downloads: Tarball | ZIP archive
Other Links: files | file ages | folders | manifest
Tags And Properties
Context
2017-01-26
23:50
[243ffc229a] Connexion double facteur avec OTP (user: bohwaz, tags: dev)
00:19
[633bea8e4a] Ajout authentification à double facteur (OTP) (user: bohwaz, tags: dev)
2017-01-23
05:39
[55df9fab5d] Merge avec trunk (user: bohwaz, tags: dev)
Changes

Modified src/VERSION from [62a409c97d] to [51107ddecc].

1
0.7.6
|
1
0.8.0

Added src/include/data/0.8.0.sql version [737893df79].





>
>
1
2
-- Ajouter champ pour OTP
ALTER TABLE membres ADD COLUMN secret_otp TEXT NULL;

Modified src/include/data/schema.sql from [5ab5416f01] to [6b49dd6beb].

24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    cacher INT DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id)
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir class.champs_membres.php

CREATE TABLE cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY,
    id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta








|







24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
    cacher INT DEFAULT 0,

    id_cotisation_obligatoire INTEGER NULL REFERENCES cotisations (id)
);

-- Membres de l'asso
-- Table dynamique générée par l'application
-- voir Garradin\Membres\Champs.php

CREATE TABLE cotisations
-- Types de cotisations et activités
(
    id INTEGER PRIMARY KEY,
    id_categorie_compta INTEGER NULL, -- NULL si le type n'est pas associé automatiquement à la compta

Modified src/include/lib/Garradin/Membres.php from [aa1ce7b0d7] to [207dab663f].

50
51
52
53
54
55
56
57
58
59

60
61

62

63

64
65
66
67

68
69

70



71
72
73
74

















































75
76
77
78
79
80
81
...
189
190
191
192
193
194
195





196
197
198







199
200
201
202
203
204
205
...
810
811
812
813
814
815
816
817
818
    }

    public function login($id, $passe)
    {
        $db = DB::getInstance();
        $champ_id = Config::getInstance()->get('champ_identifiant');

        $r = $db->simpleQuerySingle('SELECT id, passe, id_categorie FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));

        if (empty($r))

            return false;


        if (!$this->_checkPassword(trim($passe), $r['passe']))

            return false;


        $droits = $this->getDroits($r['id_categorie']);

        if ($droits['connexion'] == self::DROIT_AUCUN)

            return false;


        $this->_sessionStart(true);



        $db->simpleExec('UPDATE membres SET date_connexion = datetime(\'now\') WHERE id = ?;', $r['id']);

        return $this->updateSessionData($r['id'], $droits);
    }


















































    public function recoverPasswordCheck($id)
    {
        $db = DB::getInstance();
        $config = Config::getInstance();

        $champ_id = $config->get('champ_identifiant');
................................................................................
            if (defined('Garradin\LOCAL_LOGIN'))
            {
                return $this->localLogin();
            }

            return false;
        }






        return true;
    }








    public function getLoggedUser()
    {
        if (!$this->isLogged())
            return false;

        return $_SESSION['logged_user'];
................................................................................
            'membres'   =>  $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'),
            'categories'=>  $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'),
        ];

        return $tables;
    }
}

?>







|


>

|
>

>

>




>

|
>

>
>
>




>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>
>



>
>
>
>
>
>
>







 







<
<
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
...
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
...
880
881
882
883
884
885
886


    }

    public function login($id, $passe)
    {
        $db = DB::getInstance();
        $champ_id = Config::getInstance()->get('champ_identifiant');

        $r = $db->simpleQuerySingle('SELECT id, passe, id_categorie, secret_otp FROM membres WHERE '.$champ_id.' = ? LIMIT 1;', true, trim($id));

        if (empty($r))
        {
            return false;
        }

        if (!$this->_checkPassword(trim($passe), $r['passe']))
        {
            return false;
        }

        $droits = $this->getDroits($r['id_categorie']);

        if ($droits['connexion'] == self::DROIT_AUCUN)
        {
            return false;
        }

        $this->_sessionStart(true);

        $_SESSION['otp_required'] = !empty($r['secret_otp']) ? true : false;

        $db->simpleExec('UPDATE membres SET date_connexion = datetime(\'now\') WHERE id = ?;', $r['id']);

        return $this->updateSessionData($r['id'], $droits);
    }

    public function loginOTP($code)
    {
        $this->_sessionStart(true);

        if (empty($_SESSION['logged_user']))
        {
            return false;
        }

        $membre = $_SESSION['logged_user'];

        if (!\KD2\Security_OTP::TOTP($membre['secret_otp'], $code))
        {
            return false;
        }

        $_SESSION['otp_required'] = false;

        return true;
    }

    public function setOTP()
    {
        $membre = $this->getLoggedUser();

        $secret = \KD2\Security_OTP::getRandomSecret();

        DB::getInstance()->simpleExec('UPDATE membres SET secret_otp = ? WHERE id = ?;', $secret, $membre['id']);

        $membre['secret_otp'] = $secret;

        $this->updateSessionData($membre);

        return $secret;
    }

    public function disableOTP()
    {
        $membre = $this->getLoggedUser();

        DB::getInstance()->simpleExec('UPDATE membres SET secret_otp = NULL WHERE id = ?;', $membre['id']);

        $membre['secret_otp'] = null;

        $this->updateSessionData($membre);

        return true;
    }

    public function recoverPasswordCheck($id)
    {
        $db = DB::getInstance();
        $config = Config::getInstance();

        $champ_id = $config->get('champ_identifiant');
................................................................................
            if (defined('Garradin\LOCAL_LOGIN'))
            {
                return $this->localLogin();
            }

            return false;
        }

        if (!empty($_SESSION['otp_required']))
        {
            return false;
        }

        return true;
    }

    public function isOTPRequired()
    {
        $this->_sessionStart();

        return empty($_SESSION['otp_required']) ? false : true;
    }

    public function getLoggedUser()
    {
        if (!$this->isLogged())
            return false;

        return $_SESSION['logged_user'];
................................................................................
            'membres'   =>  $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres\';'),
            'categories'=>  $db->querySingle('SELECT sql FROM sqlite_master WHERE type = \'table\' AND name = \'membres_categories\';'),
        ];

        return $tables;
    }
}


Modified src/include/lib/Garradin/Membres/Champs.php from [da0a542d80] to [d222f084dc].

386
387
388
389
390
391
392

393
394
395
396
397
398
399
400
401
402
403
404

405
406
407
408
409
410
411

    	// Champs à créer
    	$create = [
    		'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement',
    		'id_categorie INTEGER NOT NULL, -- Numéro de catégorie',
            'date_connexion TEXT NULL, -- Date de dernière connexion',
            'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription',

    	];

        $create_keys = [
            'FOREIGN KEY (id_categorie) REFERENCES membres_categories (id)'
        ];

    	// Champs à recopier
    	$copy = [
    		'id',
    		'id_categorie',
            'date_connexion',
            'date_inscription',

    	];

        $anciens_champs = $config->get('champs_membres');
    	$anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll();

    	foreach ($this->champs as $key=>$cfg)
    	{







>












>







386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413

    	// Champs à créer
    	$create = [
    		'id INTEGER PRIMARY KEY, -- Numéro attribué automatiquement',
    		'id_categorie INTEGER NOT NULL, -- Numéro de catégorie',
            'date_connexion TEXT NULL, -- Date de dernière connexion',
            'date_inscription TEXT NOT NULL DEFAULT CURRENT_DATE, -- Date d\'inscription',
            'secret_otp TEXT NULL, -- Code secret pour TOTP'
    	];

        $create_keys = [
            'FOREIGN KEY (id_categorie) REFERENCES membres_categories (id)'
        ];

    	// Champs à recopier
    	$copy = [
    		'id',
    		'id_categorie',
            'date_connexion',
            'date_inscription',
            'secret_otp',
    	];

        $anciens_champs = $config->get('champs_membres');
    	$anciens_champs = is_null($anciens_champs) ? $this->champs : $anciens_champs->getAll();

    	foreach ($this->champs as $key=>$cfg)
    	{

Modified src/templates/admin/mes_infos.tpl from [3af422ca58] to [105fa33fc8].

1
2
3
4
5
6














7
8
9
10
11
12
13
..
39
40
41
42
43
44
45


















46
47
48
49
50
51
52
{include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1}

{if $error}
    <p class="error">
        {$error}
    </p>














{/if}

<form method="post" action="{$self_url}">


    <fieldset>
        <legend>Informations personnelles</legend>
................................................................................
                <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern=".{ldelim}5,{rdelim}" /></dd>
                <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt>
                <dd><input type="password" name="repasse" id="f_repasse" value="{form_field name=repasse}" pattern=".{ldelim}5,{rdelim}" /></dd>
            </dl>
        {/if}
    </fieldset>



















    <p class="submit">
        {csrf_field key="edit_me"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>







>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
..
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{include file="admin/_head.tpl" title="Mes informations personnelles" current="mes_infos" js=1}

{if $error}
    <p class="error">
        {$error}
    </p>
{elseif $otp_status == 'off'}
    <p class="confirm">
        L'authentification à double facteur a été désactivée.
    </p>
{elseif $otp_status}
    <div class="alert">
        <img class="qrcode" src="{$otp_qrcode}" alt="" />
        <p class="confirm">L'authentification à double facteur a été activée.</p>
        <p class="help">
            Votre clé secrète est&nbsp;:<br />
            <code>{$otp_status}</code><br />
            Recopiez-la ou scannez le QR code pour configurer votre application.
        </p>
    </div>
{/if}

<form method="post" action="{$self_url}">


    <fieldset>
        <legend>Informations personnelles</legend>
................................................................................
                <dd><input type="password" name="passe" id="f_passe" value="{form_field name=passe}" pattern=".{ldelim}5,{rdelim}" /></dd>
                <dt><label for="f_repasse">Encore le mot de passe</label> (vérification)</dt>
                <dd><input type="password" name="repasse" id="f_repasse" value="{form_field name=repasse}" pattern=".{ldelim}5,{rdelim}" /></dd>
            </dl>
        {/if}
    </fieldset>

    <fieldset>
        <legend>Authentification à double facteur (2FA)</legend>
        <p class="help">Pour renforcer la sécurité de votre connexion en cas de vol de votre mot de passe, vous pouvez activer
            l'authentification à double facteur. Cela nécessite d'installer une application comme <a href="https://freeotp.github.io/">FreeOTP</a>
            sur votre téléphone.</p>
        <dl>
            <dt>Authentification à double facteur (TOTP)</dt>
        {if $user.secret_otp}
            <dd><label><input type="radio" name="otp" value="" checked="checked" /> <strong>Activée</strong></label></dd>
            <dd><label><input type="radio" name="otp" value="generate" /> Régénérer une nouvelle clé secrète</label></dd>
            <dd><label><input type="radio" name="otp" value="disable" /> Désactiver l'authentification à double facteur</label></dd>
        {else}
            <dd><em>Désactivée</em></dd>
            <dd><label><input type="checkbox" name="otp" value="generate" /> Activer</label></dd>
        {/if}
        </dl>
    </fieldset>

    <p class="submit">
        {csrf_field key="edit_me"}
        <input type="submit" name="save" value="Enregistrer &rarr;" />
    </p>

</form>

Modified src/www/admin/_inc.php from [0483c8df72] to [846ebd1438].

16
17
18
19
20
21
22






23

24
25
26
27
28
29
30

$membres = new Membres;

if (!defined('Garradin\LOGIN_PROCESS'))
{
    if (!$membres->isLogged())
    {






        Utils::redirect('/admin/login.php');

    }

    $tpl->assign('config', Config::getInstance()->getConfig());
    $tpl->assign('is_logged', true);
    $tpl->assign('user', $membres->getLoggedUser());
    $user = $membres->getLoggedUser();








>
>
>
>
>
>
|
>







16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

$membres = new Membres;

if (!defined('Garradin\LOGIN_PROCESS'))
{
    if (!$membres->isLogged())
    {
        if ($membres->isOTPRequired())
        {
            Utils::redirect('/admin/login_otp.php');
        }
        else
        {
            Utils::redirect('/admin/login.php');
        }
    }

    $tpl->assign('config', Config::getInstance()->getConfig());
    $tpl->assign('is_logged', true);
    $tpl->assign('user', $membres->getLoggedUser());
    $user = $membres->getLoggedUser();

Modified src/www/admin/mes_infos.php from [a68d27a87a] to [15a9109208].

34
35
36
37
38
39
40












41

42
43
44
45
46
47
48
49
50












51
52
53
54
55
56
57
58
                    $data[$key] = Utils::post($key);
                }
            }

            $membres->edit($membre['id'], $data, false);
            $membres->updateSessionData();













            Utils::redirect('/admin/');

        }
        catch (UserException $e)
        {
            $error = $e->getMessage();
        }
    }
}

$tpl->assign('error', $error);












$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $config->get('champs_membres')->getAll());

$tpl->assign('membre', $membre);

$tpl->display('admin/mes_infos.tpl');

?>







>
>
>
>
>
>
>
>
>
>
>
>
|
>









>
>
>
>
>
>
>
>
>
>
>
>






<
<
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81


                    $data[$key] = Utils::post($key);
                }
            }

            $membres->edit($membre['id'], $data, false);
            $membres->updateSessionData();

            if (Utils::post('otp') == 'generate')
            {
                $secret = $membres->setOTP();
                Utils::redirect('/admin/mes_infos.php?otp=' . rawurlencode($secret));
            }
            elseif (Utils::post('otp') == 'disable')
            {
                $secret = $membres->disableOTP();
                Utils::redirect('/admin/mes_infos.php?otp=off');
            }
            else
            {
                Utils::redirect('/admin/');
            }
        }
        catch (UserException $e)
        {
            $error = $e->getMessage();
        }
    }
}

$tpl->assign('error', $error);
$tpl->assign('otp_status', Utils::get('otp'));

if (Utils::get('otp') && Utils::get('otp') != 'off')
{
    $url = \KD2\Security_OTP::getOTPAuthURL($config->get('nom_asso'), Utils::get('otp'));
    $qrcode = new \KD2\QRCode($url);
    $qrcode = 'data:image/svg+xml;base64,' . base64_encode($qrcode->toSVG());
    $tpl->assign('otp_qrcode', $qrcode);

    $tpl->assign('otp_status', implode(' ', str_split(Utils::get('otp'), 4)));
}

$tpl->assign('passphrase', Utils::suggestPassword());
$tpl->assign('champs', $config->get('champs_membres')->getAll());

$tpl->assign('membre', $membre);

$tpl->display('admin/mes_infos.tpl');


Modified src/www/admin/static/admin.css from [da88202611] to [f57930187e].

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165

166
167
168
169
170
171
172
....
1278
1279
1280
1281
1282
1283
1284







    color: #090;
}

span.alert, b.alert {
    color: #990;
}

p.error {
    border: 1px solid #c00;
    background: #fcc;
    padding: 0.5em;
    margin-bottom: 1em;
}

p.confirm {
    border: 1px solid #0c0;
    background: #cfc;
    padding: 0.5em;
    margin-bottom: 1em;
}

p.alert {
    border: 1px solid #cc0;
    background: #ffc;
    padding: 0.5em;
    margin-bottom: 1em;

}

p.help {
    margin: 1em;
    color: #666;
}

................................................................................
}

form#insertImage .align input:hover, form#insertImage .cancel input:hover  {
    cursor: pointer;
    background-color: #eee;
    color: darkred;
}














|






|






|




>







 







>
>
>
>
>
>
>
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
....
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
    color: #090;
}

span.alert, b.alert {
    color: #990;
}

p.error, div.error {
    border: 1px solid #c00;
    background: #fcc;
    padding: 0.5em;
    margin-bottom: 1em;
}

p.confirm, div.confirm {
    border: 1px solid #0c0;
    background: #cfc;
    padding: 0.5em;
    margin-bottom: 1em;
}

p.alert, div.alert {
    border: 1px solid #cc0;
    background: #ffc;
    padding: 0.5em;
    margin-bottom: 1em;
    overflow: auto;
}

p.help {
    margin: 1em;
    color: #666;
}

................................................................................
}

form#insertImage .align input:hover, form#insertImage .cancel input:hover  {
    cursor: pointer;
    background-color: #eee;
    color: darkred;
}

img.qrcode {
    float: right;
    padding: .5em;
    border: .5em solid #000;
    background: #fff;
}

Modified src/www/admin/upgrade.php from [e7450be78d] to [80018cd596].

240
241
242
243
244
245
246
247
248
249
250
251
252
253
254


255
256
257
258
259
260
261
262
263
264
265
266
...
275
276
277
278
279
280
281
282
283

    // Mise à jour base de données
    $db->exec(file_get_contents(ROOT . '/include/data/0.7.2.sql'));

    $db->exec('END;');
}

if (version_compare($v, '0.7.3', '<'))
{
    // Bug étrange dans la 0.7.2 où la base de données n'est pas mise à jour,
    // donc on vérifie et refait la màj ici
    try {
        $db->exec('SELECT id_auteur FROM compta_rapprochement;');
    }
    catch (\Exception $e)


    {
        $db->exec('PRAGMA foreign_keys = OFF; BEGIN;');
        $db->exec(file_get_contents(ROOT . '/include/data/0.7.2.sql'));
        $db->exec('END;');
    }
}

Utils::clearCaches();

$config->setVersion(garradin_version());

echo '<h2>Mise à jour terminée.</h2>
................................................................................
        stopAnimatedLoader();
    }, 1000);
    </script>';
}

echo '
</body>';

?>







|

|
<
<
<
|
<
>
>
|
<
<
|
<







 







<
<
240
241
242
243
244
245
246
247
248
249



250

251
252
253


254

255
256
257
258
259
260
261
...
270
271
272
273
274
275
276



    // Mise à jour base de données
    $db->exec(file_get_contents(ROOT . '/include/data/0.7.2.sql'));

    $db->exec('END;');
}

if (version_compare($v, '0.8.0', '<'))
{
    $db->exec('PRAGMA foreign_keys = OFF; BEGIN;');





    // Mise à jour base de données
    $db->exec(file_get_contents(ROOT . '/include/data/0.8.0.sql'));



    $db->exec('END;');

}

Utils::clearCaches();

$config->setVersion(garradin_version());

echo '<h2>Mise à jour terminée.</h2>
................................................................................
        stopAnimatedLoader();
    }, 1000);
    </script>';
}

echo '
</body>';