Overview
Comment:Ajout authentification à double facteur (OTP)
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA1: 633bea8e4ac1a43a4c919ca678446e72ceb07e28
User & Date: bohwaz on 2017-01-26 00:19:53
Other Links: branch diff | manifest | tags
Context
2017-01-26
23:50
Connexion double facteur avec OTP check-in: 243ffc229a user: bohwaz tags: dev
00:19
Ajout authentification à double facteur (OTP) check-in: 633bea8e4a user: bohwaz tags: dev
2017-01-23
05:39
Merge avec trunk check-in: 55df9fab5d 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
    }

    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');







|


>

|
>

>

>




>

|
>

>
>
>




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







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
    }

    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');
189
190
191
192
193
194
195





196
197
198







199
200
201
202
203
204
205
            if (defined('Garradin\LOCAL_LOGIN'))
            {
                return $this->localLogin();
            }

            return false;
        }






        return true;
    }








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

        return $_SESSION['logged_user'];







>
>
>
>
>



>
>
>
>
>
>
>







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
            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'];
810
811
812
813
814
815
816
817
818
            '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;
    }
}

?>







<
<
880
881
882
883
884
885
886


            '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
{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>






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







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
{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>
39
40
41
42
43
44
45


















46
47
48
49
50
51
52
                <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>








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







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
                <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
    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;
}








|






|






|




>







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
    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;
}

1278
1279
1280
1281
1282
1283
1284







}

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














>
>
>
>
>
>
>
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
}

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

    // 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>







|

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







240
241
242
243
244
245
246
247
248



249
250

251
252
253


254

255
256
257
258
259
260
261

    // 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>
275
276
277
278
279
280
281
282
283
        stopAnimatedLoader();
    }, 1000);
    </script>';
}

echo '
</body>';

?>







<
<
270
271
272
273
274
275
276


        stopAnimatedLoader();
    }, 1000);
    </script>';
}

echo '
</body>';