Skip to content

Commit

Permalink
Added functional authentication system
Browse files Browse the repository at this point in the history
Build a small project to demonstrate CSRF, authentication and storage of
user credentials, ACL (access control levels), OTP code expiry on
invalid attempts, OTP code expiry on valid use, scratch codes (secure
random numbers), attaching/removing authenticator from account in a
secure manner,  and page transition/posting using CSRF tokens.
  • Loading branch information
dkrusky committed Apr 14, 2016
1 parent 02575df commit 099e14c
Show file tree
Hide file tree
Showing 18 changed files with 797 additions and 5 deletions.
4 changes: 4 additions & 0 deletions .htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Files "config.inc.php">
Order Allow,Deny
Deny from all
</Files>
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ One time password generator, validator, and qrcode generator that has no web dep

OTP-Thing is a drop in class that makes it easy to implement 2FA with all the bells and whistles. The current implementation supports QR code generation from versions 1 to 40, as well as the various parity levels and quiet zone adjustments. This class is self-contained with the exception that it does require the `qrdata.db` file in order to successfuly generate QR codes. This file contains a compressed and encoded version of all the various QR versions and specs required.

Additionally, it now includes a full authentication system implementing most of the suggestions stated in the 'Warnings' section of this document.

Due to the nature of how this class works, it *can* be used in an evironment with *no internet* access.

## Requirements
Expand All @@ -13,6 +15,13 @@ This was tested on PHP 5.6, while every effort was made to ensure backwards comp

* SQLite3
* GD/GD2
* openssl

For the full project (not just otp.class.php), you also need to have

* MySQLi (nd)
* Crypt


## Warnings

Expand Down Expand Up @@ -54,3 +63,17 @@ GenerateQRCode() - returns `array` [ *image* `string` { base64 encoded image str


*Everything except GetTime() and ValidateCode() is inside the demo.php for a demonstration on how to use*

## The dmeo project

`otp_standalone_demo.php`

This file is a demonstration on how to generate all the required parameters for creating an OTP qr-code, and displays all the values.


`index.php`

A full demonstration on how user authentication can be done with or without an OTP code using CSRF tokens and tracking of OTP code usage, while restricting attempts to a maximum amount and returning a generic error when login validation fails.

This uses `password_hash()` for password storage in the database, and `password_verify()` to validate the password. To create the authentication database, you should run `install.php` after setting up the appropriate database credentials in the `config.inc.php` file.

20 changes: 20 additions & 0 deletions config.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
// prevent this file from being called directly
if(!defined('LIVE')) { exit(); };

// define the public application root
define('APP_ROOT', '/app/');

// OTP Configuration
define('OTP_COMPANY', 'ACME'); // company name
define('OTP_MAX_TRIES', 3); // maximum attempts before preventing use of the current timeblock
define('OTP_LENGTH', 6); // length of digits a code should be
define('OTP_ALGORITHM', 'sha256'); // the hashing algorithm used


// Database Credentials used for user authentication
define('SQL_SERVER', 'localhost');
define('SQL_USERNAME', 'root');
define('SQL_PASSWORD', '');
define('SQL_DATA', 'test');
define('SQL_PREFIX', 'auth_');
223 changes: 223 additions & 0 deletions index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<?php
define('LIVE', true);

include('config.inc.php');
require('lib/session.class.php');
require('lib/otp.class.php');
require('lib/users.class.php');

try {

if(isset($_REQUEST['logout'])) {
// if '?logout=true' supplied, execute logout and redirect to root of application
session::destroy();
header('location: ' . APP_ROOT);
exit(0);
}

// if session is still valid (user is still logged in)
if( session::verify() === true ) {
// regenerate session
session::regenerate();

$page = '';
if(isset($_REQUEST['page'])) {
$page = trim(filter_input(INPUT_GET, 'page', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
}

// you should verify the token on each request through post or get.
// if posting as part of a form
$token = "";
if(isset($_POST['csrf'])) { $token = trim(filter_input(INPUT_POST, 'csrf', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH))); }
elseif( isset($_REQUEST['token']) ) { $token = trim(filter_input(INPUT_GET, 'token', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH))); }
if($token != $_SESSION['csrf']) {
session::destroy();
throw new Exception("CSRF Attack Detected");
}
$newtoken = session::csrf(true);

// get information about logged in user
$userinfo = users::info($_SESSION['user']);

// write your application logic and handler stuff here
// case select for which page you are on (simple test for multiple pages)
switch($page) {
case 'otp':
if(trim($userinfo['otp'] . '') == '') {
// OTP Stuff
otp::$algorithm = OTP_ALGORITHM;
otp::$digits = OTP_LENGTH;
otp::$company = OTP_COMPANY;
$otp = isset($_SESSION['otp']) ? $_SESSION['otp'] : otp::GenerateSecret( $_SESSION['user'] );
$_SESSION['otp'] = $otp;
otp::SetSecret($_SESSION['user'], $otp);

// regenerate and save session data
session::regenerate();
if(isset($_SESSION['otp'])) {
$scratch_codes = false;
if(!empty($_POST)) {
$password = trim(filter_input(INPUT_POST, 'password', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
$otpcode = trim(filter_input(INPUT_POST, 'otp', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
$scratch_codes = users::addotp( $_SESSION['user'], $password, $otp, $otpcode );
}
if($scratch_codes) {
// display scratch codes
var_export($scratch_codes);
echo str_replace(
Array(
'URLHOME',
'CSRFVALUE',
'CODE1',
'CODE2',
'CODE3'
),
Array(
APP_ROOT,
session::csrf(),
$scratch_codes['code1'],
$scratch_codes['code2'],
$scratch_codes['code3']
),
file_get_contents('view/otp_scratch_codes.html')
);
} else {
// display add qr form
$img = otp::GenerateQRCode();
echo str_replace(
Array(
'URLHOME',
'CSRFVALUE',
'QRCODE'
),
Array(
APP_ROOT,
session::csrf(),
$img['image']
),
file_get_contents('view/otp_verify.html')
);
}
}
} else {
$removed = false;
otp::$algorithm = OTP_ALGORITHM;
otp::$digits = OTP_LENGTH;
otp::$company = OTP_COMPANY;
otp::SetSecret($userinfo['username'], $userinfo['otp']);

if(!empty($_POST)) {
$password = trim(filter_input(INPUT_POST, 'password', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
$otpcode = trim(filter_input(INPUT_POST, 'otp', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
$code = otp::GetCode();
if($code->code == $otpcode) {
if(users::remove_otp($userinfo['username'], $password)) {
echo str_replace(
Array(
'URLHOME',
'CSRFVALUE'
),
Array(
APP_ROOT,
session::csrf()
),
file_get_contents('view/otp_removed.html')
);
exit(0);
}
} else {
echo 'Invalid Credentials';
}
}
echo str_replace(
Array(
'URLHOME',
'CSRFVALUE'
),
Array(
APP_ROOT,
session::csrf(),
),
file_get_contents('view/otp_remove.html')
);
}
break;

default:
// write your application logic and handler stuff here
echo str_replace(
Array(
'URLHOME',
'CSRFVALUE'
),
Array(
APP_ROOT,
session::csrf(),
),
file_get_contents('view/index.html')
);
}

} else {
if ( !empty($_POST) ) {
if ( isset($_POST['csrf']) && isset($_POST['username']) && isset($_POST['password']) ) {
// check CSRF token and if match ti token stored in session
$csrf = trim(filter_input(INPUT_POST, 'csrf', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
if($csrf != $_SESSION['csrf']) { session::destroy(); throw new Exception("CSRF Attack Detected"); }

// sanitize username and password
$username = trim(filter_input(INPUT_POST, 'username', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));
$password = trim(filter_input(INPUT_POST, 'password', FILTER_SANITIZE_SPECIAL_CHARS, array('flags' => FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH)));

$otp = -1;
if( isset($_POST['otp']) ) {
// sanitize otp code
$otp = intval(filter_input(INPUT_POST, 'otp', FILTER_SANITIZE_NUMBER_INT, array('default' => -1)) );
}

if($username == '' || $password == '') {
// username or password can not be empty
throw new Exception("Invalid Credentials");
} else {
// verify user. if account has OTP enabled, and
// $otp has been filled out in the form, it will validate
// using the OTP settings as defined in config.inc.php
// additionally, it will automatically lock out used timeblocks
// as well as limit attempts (returning false if past) to the
// value set for OTP_MAX_TRIES
$auth = users::validate($username, $password, $otp);
if($auth) {
// regenerate csrf token
$token = session::csrf(true);
session::create( $username );

// redirect back to application root
header('location: ' . APP_ROOT . '?token=' . $token);
exit();
}
}

// user was not authenticated, regenerate csrf token to prevent form spam
session::csrf(true);
echo '<h3>Invalid credentials</h3>';

} else {
// CSRF token did not match stored token in session
throw new Exception('CSRF Attack Detected');
}
}

// display login form
echo str_replace('CSRFVALUE', session::csrf(), file_get_contents('view/login.html') );

}

} catch ( Exception $e ) {
echo $e->getMessage();
}

function exception_handler($e) {
echo $e->getMessage();
exit(0);
}
?>
32 changes: 32 additions & 0 deletions install.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
define('LIVE', true);
include('config.inc.php');

$admin_user = 'admin'; // the default username for admin
$admin_pass = 'admin'; // the default password for admin

// create the database if it doesn't exist
$sql = "CREATE TABLE IF NOT EXIST `" . SQL_PREFIX . "users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, `enabled` int(1) NOT NULL DEFAULT '0' COMMENT '0 = disabled, 1 = enabled', `otp_key` varchar(32) DEFAULT '', `otp_last_time` int(11) NOT NULL DEFAULT '0', `otp_last_time_count` int(1) NOT NULL DEFAULT '0', `acl` int(11) NOT NULL DEFAULT '0', `otp_scratch_codes` text, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;";
$result = false;
$db = new mysqli(SQL_SERVER, SQL_USERNAME, SQL_PASSWORD, SQL_DATA);
if($db->connect_errno > 0){ throw new Exception("Connection Failed: " . $db->connect_error); }
if($db->query($sql) === true) {
$result = true;
}

// add admin user to table if it doesn't exist
$sql = "INSERT IGNORE INTO `" . SQL_PREFIX . "users` VALUES ('1', ?, ?, '1', null, '0', '0', '9999', null);";
$db_stmt = $db->prepare($sql);
$db_stmt->bind_params('ss',
$admin_user,
password_hash($admin_pass, PASSWORD_DEFAULT)
);
$db_stmt->execute();
if($db_stmt->errno) {
$result = false;
throw new Exception('Database Error: ' . $db_stmt->error);
}
$db_stmt->close();

$db->close();

1 change: 1 addition & 0 deletions lib/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deny from all
8 changes: 4 additions & 4 deletions otp.class.php → lib/otp.class.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
if(!defined('LIVE')) { exit(); };
/*
#
# QRcode/OTP class library for PHP5
Expand All @@ -19,7 +20,7 @@ class otp {
private static $secret;
private static $user;

public static $qrcode_database = "qrdata.db";
public static $qrcode_database = "lib/qrdata.db";

static $qrcode_version = 6;
static $qrcode_errorcorrect = "L";
Expand Down Expand Up @@ -81,7 +82,7 @@ public static function GetCode() {
((ord($hash[$offset+2]) & 0xff) << 8 ) |
(ord($hash[$offset+3]) & 0xff)
) % pow(10, self::$digits);

return (object)Array(
'timeblock' => $time,
'code' => str_pad($OTP, self::$digits, '0', STR_PAD_LEFT)
Expand All @@ -93,11 +94,10 @@ public static function ValidateCode($code, $hashtype=null) {
// get valid code for current timeblock
$vcode = self::GetCode();
if($hashtype != null) {
// TODO : add check for supported hash methods.
$hashtype = strtolower($hashtype);
$vcode->code = hash($hashtype, $vcode->code);
}

if($code === $vcode->code) {
return true;
// recommended action on true is to use mark the current time
Expand Down
File renamed without changes.
Loading

0 comments on commit 099e14c

Please sign in to comment.