Changeset View
Changeset View
Standalone View
Standalone View
src/app/User.php
Show First 20 Lines • Show All 681 Lines • ▼ Show 20 Lines | public function validateLocation($ip): bool | ||||
if (empty($countryCodes)) { | if (empty($countryCodes)) { | ||||
return true; | return true; | ||||
} | } | ||||
return in_array(\App\Utils::countryForIP($ip), $countryCodes); | return in_array(\App\Utils::countryForIP($ip), $countryCodes); | ||||
} | } | ||||
/** | /** | ||||
* Check if multi factor verification is enabled | |||||
* | |||||
* @return bool | |||||
*/ | |||||
public function mfaEnabled(): bool | |||||
{ | |||||
return \App\CompanionApp::where('user_id', $this->id) | |||||
->where('mfa_enabled', true) | |||||
->exists(); | |||||
} | |||||
/** | |||||
* Retrieve and authenticate a user | * Retrieve and authenticate a user | ||||
* | * | ||||
* @param string $username The username | * @param string $username The username | ||||
* @param string $password The password in plain text | * @param string $password The password in plain text | ||||
* @param ?string $clientIP The IP address of the client | * @param ?string $clientIP The IP address of the client | ||||
* | * | ||||
* @return array ['user', 'reason', 'errorMessage'] | * @return array ['user', 'reason', 'errorMessage'] | ||||
*/ | */ | ||||
public static function findAndAuthenticate($username, $password, $clientIP = null): array | public static function findAndAuthenticate($username, $password, $clientIP = null, $verifyMFA = true): array | ||||
{ | { | ||||
$error = null; | $error = null; | ||||
if (!$clientIP) { | if (!$clientIP) { | ||||
$clientIP = request()->ip(); | $clientIP = request()->ip(); | ||||
} | } | ||||
$user = User::where('email', $username)->first(); | $user = User::where('email', $username)->first(); | ||||
if (!$user) { | if (!$user) { | ||||
$error = AuthAttempt::REASON_NOTFOUND; | $error = AuthAttempt::REASON_NOTFOUND; | ||||
} | } | ||||
// Check user password | // Check user password | ||||
if (!$error && !$user->validateCredentials($username, $password)) { | if (!$error && !$user->validateCredentials($username, $password)) { | ||||
$error = AuthAttempt::REASON_PASSWORD; | $error = AuthAttempt::REASON_PASSWORD; | ||||
} | } | ||||
if ($verifyMFA) { | |||||
// Check user (request) location | // Check user (request) location | ||||
if (!$error && !$user->validateLocation($clientIP)) { | if (!$error && !$user->validateLocation($clientIP)) { | ||||
$error = AuthAttempt::REASON_GEOLOCATION; | $error = AuthAttempt::REASON_GEOLOCATION; | ||||
} | } | ||||
// Check 2FA | // Check 2FA | ||||
if (!$error) { | if (!$error) { | ||||
try { | try { | ||||
(new \App\Auth\SecondFactor($user))->validate(request()->secondfactor); | (new \App\Auth\SecondFactor($user))->validate(request()->secondfactor); | ||||
} catch (\Exception $e) { | } catch (\Exception $e) { | ||||
$error = AuthAttempt::REASON_2FA_GENERIC; | $error = AuthAttempt::REASON_2FA_GENERIC; | ||||
$message = $e->getMessage(); | $message = $e->getMessage(); | ||||
} | } | ||||
} | } | ||||
// Check 2FA - Companion App | // Check 2FA - Companion App | ||||
if (!$error && \App\CompanionApp::where('user_id', $user->id)->exists()) { | if (!$error && $user->mfaEnabled()) { | ||||
$attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP); | $attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP); | ||||
if (!$attempt->waitFor2FA()) { | if (!$attempt->waitFor2FA()) { | ||||
$error = AuthAttempt::REASON_2FA; | $error = AuthAttempt::REASON_2FA; | ||||
} | } | ||||
} | } | ||||
} | |||||
if ($error) { | if ($error) { | ||||
if ($user && empty($attempt)) { | if ($user && empty($attempt)) { | ||||
$attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP); | $attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP); | ||||
if (!$attempt->isAccepted()) { | if (!$attempt->isAccepted()) { | ||||
$attempt->deny($error); | $attempt->deny($error); | ||||
$attempt->save(); | $attempt->save(); | ||||
$attempt->notify(); | $attempt->notify(); | ||||
Show All 16 Lines | class User extends Authenticatable | ||||
* Hook for passport | * Hook for passport | ||||
* | * | ||||
* @throws \Throwable | * @throws \Throwable | ||||
* | * | ||||
* @return \App\User User model object if found | * @return \App\User User model object if found | ||||
*/ | */ | ||||
public static function findAndValidateForPassport($username, $password): User | public static function findAndValidateForPassport($username, $password): User | ||||
{ | { | ||||
$result = self::findAndAuthenticate($username, $password); | $verifyMFA = true; | ||||
if (request()->scope == "mfa") { | |||||
\Log::info("Not validating MFA because this is a request for an mfa scope."); | |||||
// Don't verify MFA if this is only an mfa token. | |||||
// If we didn't do this, we couldn't pair backup devices. | |||||
$verifyMFA = false; | |||||
} | |||||
$result = self::findAndAuthenticate($username, $password, null, $verifyMFA); | |||||
if (isset($result['reason'])) { | if (isset($result['reason'])) { | ||||
if ($result['reason'] == AuthAttempt::REASON_2FA_GENERIC) { | if ($result['reason'] == AuthAttempt::REASON_2FA_GENERIC) { | ||||
// This results in a json response of {'error': 'secondfactor', 'error_description': '$errorMessage'} | // This results in a json response of {'error': 'secondfactor', 'error_description': '$errorMessage'} | ||||
throw new OAuthServerException($result['errorMessage'], 6, 'secondfactor', 401); | throw new OAuthServerException($result['errorMessage'], 6, 'secondfactor', 401); | ||||
} | } | ||||
// TODO: Display specific error message if 2FA via Companion App was expected? | // TODO: Display specific error message if 2FA via Companion App was expected? | ||||
throw OAuthServerException::invalidCredentials(); | throw OAuthServerException::invalidCredentials(); | ||||
} | } | ||||
return $result['user']; | return $result['user']; | ||||
} | } | ||||
} | } |