Changeset View
Standalone View
src/app/AuthAttempt.php
- This file was added.
<?php | |||||
namespace App; | |||||
use Illuminate\Database\Eloquent\Model; | |||||
use Carbon\Carbon; | |||||
/** | |||||
* The eloquent definition of an AuthAttempt. | |||||
* | |||||
* A AuthAttempt is any a authAttempt from any application/client. | |||||
*/ | |||||
class AuthAttempt extends Model | |||||
machniak: Something's wrong with this sentence ;) | |||||
{ | |||||
protected $fillable = [ | |||||
'ip', | |||||
'user_id', | |||||
'status', | |||||
'expires_at', | |||||
'last_seen', | |||||
]; | |||||
protected $casts = [ | |||||
'expires_at' => 'datetime', | |||||
'last_seen' => 'datetime' | |||||
]; | |||||
/** | |||||
* Prepare a date for array / JSON serialization. | |||||
* | |||||
* Required to not omit timezone and match the format of update_at/created_at timestamps. | |||||
* | |||||
* @param \DateTimeInterface $date | |||||
* @return string | |||||
*/ | |||||
protected function serializeDate(\DateTimeInterface $date) | |||||
{ | |||||
return $date->toIso8601ZuluString('microseconds'); | |||||
} | |||||
public function isAccepted() | |||||
{ | |||||
if ($this->status == 'ACCEPTED' && Carbon::now() < $this->expires_at) { | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
public function isDenied() | |||||
{ | |||||
return ($this->status == 'DENIED'); | |||||
} | |||||
public function accept() | |||||
{ | |||||
$this->expires_at = Carbon::now()->addHours(8); | |||||
$this->status = "ACCEPTED"; | |||||
} | |||||
public function deny() | |||||
Done Inline ActionsThese status labels could be class constants. Maybe it would be better to have isExpired() method and not include that check here. machniak: These status labels could be class constants. Maybe it would be better to have isExpired()… | |||||
{ | |||||
$this->status = "DENIED"; | |||||
} | |||||
Done Inline ActionsWouldn't be better to have/use isAccepted() and isExpired() methods instead? machniak: Wouldn't be better to have/use isAccepted() and isExpired() methods instead? | |||||
Done Inline ActionsI don't think so. We're not currently using isExpired() (but we could of course), and the idea is that that the entry isAccepted until it expires (at which point it is no longer accepted. mollekopf: I don't think so. We're not currently using isExpired() (but we could of course), and the idea… | |||||
private static function pushFirebaseNotification($deviceIds, $data) | |||||
{ | |||||
$url = \config('firebase.api_url'); | |||||
$apiKey = \config('firebase.api_key'); | |||||
$fields = [ | |||||
'registration_ids' => $deviceIds, | |||||
'data' => $data | |||||
]; | |||||
$headers = array( | |||||
'Content-Type:application/json', | |||||
"Authorization:key={$apiKey}" | |||||
); | |||||
$ch = curl_init(); | |||||
curl_setopt($ch, CURLOPT_URL, $url); | |||||
curl_setopt($ch, CURLOPT_POST, true); | |||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); | |||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); | |||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); | |||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); | |||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields)); | |||||
$result = curl_exec($ch); | |||||
if ($result === false) { | |||||
throw new \Exception('FCM Send Error: ' . curl_error($ch)); | |||||
} | |||||
curl_close($ch); | |||||
return $result; | |||||
} | |||||
private static function notifyAndWait($notificationTokens, $authAttempt) | |||||
{ | |||||
//FIXME if the webclient can confirm too we don't need to abort here. | |||||
if (empty($notificationTokens)) { | |||||
\Log::warning("There is no 2fa device to notify."); | |||||
return false; | |||||
} | |||||
\Log::debug("Authentication attempt: {$authAttempt->id}"); | |||||
\Log::debug("sending notification to " . var_export($notificationTokens, true)); | |||||
self::pushFirebaseNotification($notificationTokens, [ | |||||
'token' => $authAttempt->id, | |||||
]); | |||||
$confirmationTimeout = 120; | |||||
$timeout = Carbon::now()->addSeconds($confirmationTimeout); | |||||
do { | |||||
// TODO: move the notification logic to the companion app in the first place? | |||||
if ($authAttempt->isDenied()) { | |||||
\Log::debug("The authentication attempt was denied {$authAttempt->id}"); | |||||
return false; | |||||
} | |||||
if ($authAttempt->isAccepted()) { | |||||
\Log::debug("The authentication attempt was accepted {$authAttempt->id}"); | |||||
return true; | |||||
} | |||||
if ($timeout < Carbon::now()) { | |||||
\Log::debug("The authentication attempt timed-out: {$authAttempt->id}"); | |||||
return false; | |||||
} | |||||
sleep(2); | |||||
$authAttempt = $authAttempt->fresh(); | |||||
} while (true); | |||||
} | |||||
public static function recordAuthAttempt(\App\User $user, $clientIP) | |||||
Done Inline ActionsI think you can just do $this->refresh() and do not need the extra variable. And updating the $this state here might even be better. machniak: I think you can just do `$this->refresh()` and do not need the extra variable. And updating the… | |||||
{ | |||||
$authAttempt = \App\AuthAttempt::where('ip', $clientIP)->where('user_id', $user->id)->first(); | |||||
if (!$authAttempt) { | |||||
$authAttempt = new \App\AuthAttempt(); | |||||
Done Inline Actions"... or update existing one". Documentation for arguments, please. machniak: "... or update existing one". Documentation for arguments, please. | |||||
$authAttempt->ip = $clientIP; | |||||
$authAttempt->user_id = $user->id; | |||||
} | |||||
$authAttempt->last_seen = Carbon::now(); | |||||
$authAttempt->save(); | |||||
return $authAttempt; | |||||
} | |||||
Done Inline ActionsFound by phpstan, should be $clientIP machniak: Found by phpstan, should be `$clientIP` | |||||
public static function waitFor2FA(\App\AuthAttempt $authAttempt) | |||||
{ | |||||
if ($authAttempt->isAccepted()) { | |||||
return true; | |||||
} | |||||
if ($authAttempt->isDenied()) { | |||||
return false; | |||||
} | |||||
$notificationTokens = \App\CompanionApp::where('user_id', $authAttempt->user_id) | |||||
->where('mfa_enabled', true) | |||||
->get() | |||||
->map(function ($app) { | |||||
return $app->notification_token; | |||||
}) | |||||
->all(); | |||||
if (!self::notifyAndWait($notificationTokens, $authAttempt)) { | |||||
return false; | |||||
} | |||||
// Ensure the authAttempt is now accepted | |||||
$authAttempt = $authAttempt->fresh(); | |||||
return $authAttempt->isAccepted(); | |||||
Done Inline ActionsReturn value is not self explanatory, please add some description. machniak: Return value is not self explanatory, please add some description. | |||||
} | |||||
} | |||||
Done Inline ActionsAgain, just $this->refresh(). machniak: Again, just `$this->refresh()`. | |||||
Done Inline ActionsActually I don't see why you would need a refresh here. machniak: Actually I don't see why you would need a refresh here. | |||||
Done Inline ActionsYou're right, we no longer need it because we now refresh $this. mollekopf: You're right, we no longer need it because we now refresh $this. |
Something's wrong with this sentence ;)