Skip to content

Commit

Permalink
Support multi-factor authentication via 'onBeforeSuccess' callback
Browse files Browse the repository at this point in the history
  • Loading branch information
ocram committed Jul 2, 2017
1 parent 6aa3f58 commit 0909291
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 19 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Migrating from an earlier version of this project? See our [upgrade guide](Migra
* [Creating a random string](#creating-a-random-string)
* [Creating a UUID v4 as per RFC 4122](#creating-a-uuid-v4-as-per-rfc-4122)
* [Reading and writing session data](#reading-and-writing-session-data)
* [Implementing multi-factor authentication](#implementing-multi-factor-authentication)

### Creating a new instance

Expand Down Expand Up @@ -490,6 +491,24 @@ $uuid = \Delight\Auth\Auth::createUuid();

For detailed information on how to read and write session data conveniently, please refer to [the documentation of the session library](https://github.com/delight-im/PHP-Cookie#reading-and-writing-session-data), which is included by default.

### Implementing multi-factor authentication

You can pass a callback, e.g. an anonymous function, to the two methods `login` or `loginWithUsername` as their fourth parameter. Such a callback could look like this:

```php
function ($userId) {
// ...

return false;
// or
// return true;
}
```

The callback will be executed if (and only if) authentication is successful, but it will run *right before* completing authentication. This lets you hook into the login flow.

In that callback, you receive the authenticating user's ID as the only parameter. Return `true` from the callback to let authentication proceed, or return `false` to cancel the attempt. Be ready to catch the `AttemptCancelledException` in the latter case.

## Frequently asked questions

### What about password hashing?
Expand Down
45 changes: 28 additions & 17 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,15 @@ public function registerWithUniqueUsername($email, $password, $username = null,
* @param string $email the user's email address
* @param string $password the user's password
* @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
* @param callable|null $onBeforeSuccess (optional) a function that receives the user's ID as its single parameter and is executed before successful authentication; must return `true` to proceed or `false` to cancel
* @throws InvalidEmailException if the email address was invalid or could not be found
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function login($email, $password, $rememberDuration = null) {
$this->authenticateUserInternal($password, $email, null, $rememberDuration);
public function login($email, $password, $rememberDuration = null, callable $onBeforeSuccess = null) {
$this->authenticateUserInternal($password, $email, null, $rememberDuration, $onBeforeSuccess);
}

/**
Expand All @@ -216,14 +218,16 @@ public function login($email, $password, $rememberDuration = null) {
* @param string $username the user's username
* @param string $password the user's password
* @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
* @param callable|null $onBeforeSuccess (optional) a function that receives the user's ID as its single parameter and is executed before successful authentication; must return `true` to proceed or `false` to cancel
* @throws UnknownUsernameException if the specified username does not exist
* @throws AmbiguousUsernameException if the specified username is ambiguous, i.e. there are multiple users with that name
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
public function loginWithUsername($username, $password, $rememberDuration = null) {
$this->authenticateUserInternal($password, null, $username, $rememberDuration);
public function loginWithUsername($username, $password, $rememberDuration = null, callable $onBeforeSuccess = null) {
$this->authenticateUserInternal($password, null, $username, $rememberDuration, $onBeforeSuccess);
}

/**
Expand Down Expand Up @@ -605,14 +609,16 @@ public function forgotPassword($email, callable $callback, $requestExpiresAfter
* @param string|null $email (optional) the user's email address
* @param string|null $username (optional) the user's username
* @param int|null $rememberDuration (optional) the duration in seconds to keep the user logged in ("remember me"), e.g. `60 * 60 * 24 * 365.25` for one year
* @param callable|null $onBeforeSuccess (optional) a function that receives the user's ID as its single parameter and is executed before successful authentication; must return `true` to proceed or `false` to cancel
* @throws InvalidEmailException if the email address was invalid or could not be found
* @throws UnknownUsernameException if an attempt has been made to authenticate with a non-existing username
* @throws AmbiguousUsernameException if an attempt has been made to authenticate with an ambiguous username
* @throws InvalidPasswordException if the password was invalid
* @throws EmailNotVerifiedException if the email address has not been verified yet via confirmation email
* @throws AttemptCancelledException if the attempt has been cancelled by the supplied callback that is executed before success
* @throws AuthError if an internal problem occurred (do *not* catch)
*/
private function authenticateUserInternal($password, $email = null, $username = null, $rememberDuration = null) {
private function authenticateUserInternal($password, $email = null, $username = null, $rememberDuration = null, callable $onBeforeSuccess = null) {
$columnsToFetch = [ 'id', 'email', 'password', 'verified', 'username', 'status' ];

if ($email !== null) {
Expand Down Expand Up @@ -680,21 +686,26 @@ private function authenticateUserInternal($password, $email = null, $username =
}

if ((int) $userData['verified'] === 1) {
$this->onLoginSuccessful($userData['id'], $userData['email'], $userData['username'], $userData['status'], false);
if (!isset($onBeforeSuccess) || (\is_callable($onBeforeSuccess) && $onBeforeSuccess($userData['id']) === true)) {
$this->onLoginSuccessful($userData['id'], $userData['email'], $userData['username'], $userData['status'], false);

// continue to support the old parameter format
if ($rememberDuration === true) {
$rememberDuration = 60 * 60 * 24 * 28;
}
elseif ($rememberDuration === false) {
$rememberDuration = null;
}
// continue to support the old parameter format
if ($rememberDuration === true) {
$rememberDuration = 60 * 60 * 24 * 28;
}
elseif ($rememberDuration === false) {
$rememberDuration = null;
}

if ($rememberDuration !== null) {
$this->createRememberDirective($userData['id'], $rememberDuration);
}
if ($rememberDuration !== null) {
$this->createRememberDirective($userData['id'], $rememberDuration);
}

return;
return;
}
else {
throw new AttemptCancelledException();
}
}
else {
throw new EmailNotVerifiedException();
Expand Down
11 changes: 9 additions & 2 deletions tests/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,16 @@ function processRequestData(\Delight\Auth\Auth $auth) {
$rememberDuration = null;
}

$onBeforeSuccess = function ($userId) {
return \mt_rand(1, 100) <= 50;
};

try {
if (isset($_POST['email'])) {
$auth->login($_POST['email'], $_POST['password'], $rememberDuration);
$auth->login($_POST['email'], $_POST['password'], $rememberDuration, $onBeforeSuccess);
}
elseif (isset($_POST['username'])) {
$auth->loginWithUsername($_POST['username'], $_POST['password'], $rememberDuration);
$auth->loginWithUsername($_POST['username'], $_POST['password'], $rememberDuration, $onBeforeSuccess);
}
else {
return 'either email address or username required';
Expand All @@ -77,6 +81,9 @@ function processRequestData(\Delight\Auth\Auth $auth) {
catch (\Delight\Auth\EmailNotVerifiedException $e) {
return 'email not verified';
}
catch (\Delight\Auth\AttemptCancelledException $e) {
return 'attempt cancelled';
}
catch (\Delight\Auth\TooManyRequestsException $e) {
return 'too many requests';
}
Expand Down

0 comments on commit 0909291

Please sign in to comment.