Page MenuHomePhorge

D2674.1775392093.diff
No OneTemporary

Authored By
Unknown
Size
27 KB
Referenced Files
None
Subscribers
None

D2674.1775392093.diff

diff --git a/src/app/AuthAttempt.php b/src/app/AuthAttempt.php
new file mode 100644
--- /dev/null
+++ b/src/app/AuthAttempt.php
@@ -0,0 +1,180 @@
+<?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
+{
+ 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()
+ {
+ $this->status = "DENIED";
+ }
+
+
+
+ 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)
+ {
+ $authAttempt = \App\AuthAttempt::where('ip', $clientIP)->where('user_id', $user->id)->first();
+
+ if (!$authAttempt) {
+ $authAttempt = new \App\AuthAttempt();
+ $authAttempt->ip = $clientIP;
+ $authAttempt->user_id = $user->id;
+ }
+
+ $authAttempt->last_seen = Carbon::now();
+ $authAttempt->save();
+
+ return $authAttempt;
+ }
+
+ 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();
+ }
+}
diff --git a/src/app/CompanionApp.php b/src/app/CompanionApp.php
new file mode 100644
--- /dev/null
+++ b/src/app/CompanionApp.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a CompanionApp.
+ *
+ * A CompanionApp is an kolab companion app that the user registered
+ */
+class CompanionApp extends Model
+{
+ protected $fillable = [
+ 'name',
+ 'user_id',
+ 'device_id',
+ 'notification_token',
+ 'mfa_enabled',
+ ];
+}
diff --git a/src/app/Console/Commands/AuthAttemptDelete.php b/src/app/Console/Commands/AuthAttemptDelete.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/AuthAttemptDelete.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Console\Command;
+use App\AuthAttempt;
+
+class AuthAttemptDelete extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'authattempt:delete {id}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Delete an AuthAttempt';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $authAttempt = AuthAttempt::findOrFail($this->argument('id'));
+
+ if (!$authAttempt) {
+ return 1;
+ }
+
+ $authAttempt->delete();
+ }
+}
diff --git a/src/app/Console/Commands/AuthAttemptList.php b/src/app/Console/Commands/AuthAttemptList.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/AuthAttemptList.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Console\Command;
+use App\AuthAttempt;
+
+class AuthAttemptList extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'authattempt:list {--deleted}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'List auth attempts';
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ if ($this->option('deleted')) {
+ $authAttempts = AuthAttempt::withTrashed()->orderBy('last_seen');
+ } else {
+ $authAttempts = AuthAttempt::orderBy('last_seen');
+ }
+
+ $authAttempts->each(
+ function ($authAttempt) {
+ $msg = var_export($authAttempt->toArray(), true);
+ $this->info($msg);
+ }
+ );
+ }
+}
diff --git a/src/app/Http/Controllers/API/NGINXController.php b/src/app/Http/Controllers/API/NGINXController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/NGINXController.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace App\Http\Controllers\API;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Hash;
+use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Str;
+
+class NGINXController extends Controller
+{
+ /**
+ * Authentication request.
+ *
+ * @todo: Separate IMAP(+STARTTLS) from IMAPS, same for SMTP/submission.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\Response The response
+ */
+ public function authenticate(Request $request)
+ {
+ /**
+ * Auth-Login-Attempt: 1
+ * Auth-Method: plain
+ * Auth-Pass: simple123
+ * Auth-Protocol: imap
+ * Auth-Ssl: on
+ * Auth-User: john@kolab.org
+ * Client-Ip: 127.0.0.1
+ * Host: 127.0.0.1
+ *
+ * Auth-SSL: on
+ * Auth-SSL-Verify: SUCCESS
+ * Auth-SSL-Subject: /CN=example.com
+ * Auth-SSL-Issuer: /CN=example.com
+ * Auth-SSL-Serial: C07AD56B846B5BFF
+ * Auth-SSL-Fingerprint: 29d6a80a123d13355ed16b4b04605e29cb55a5ad
+ */
+
+ \Log::info("Authentication attempt");
+ \Log::debug($request->headers);
+
+ $login = $request->headers->get('Auth-User', null);
+
+ if (empty($login)) {
+ return $this->byebye($request, __LINE__);
+ }
+
+ // validate user exists, otherwise bye bye
+ $user = \App\User::where('email', $login)->first();
+
+ if (!$user) {
+ return $this->byebye($request, __LINE__);
+ }
+
+ // TODO: validate the user's domain is A-OK (active, confirmed, not suspended, ldapready)
+ // TODO: validate the user is A-OK (active, not suspended, ldapready, imapready)
+
+ // validate password, otherwise bye bye
+ $password = $request->headers->get('Auth-Pass', null);
+
+ if (empty($password)) {
+ return $this->byebye($request, __LINE__);
+ }
+
+ $result = Hash::check($password, $user->password);
+
+ if (!$result) {
+ // TODO: Log, notify user.
+ return $this->byebye($request, __LINE__);
+ }
+
+ $clientIP = $request->headers->get('Client-Ip', null);
+
+
+ // validate country of origin against restrictions, otherwise bye bye
+ $countryCodes = json_decode($user->getSetting('limit_geo', "[]"));
+
+ \Log::debug("Countries for {$user->email}: " . var_export($countryCodes, true));
+
+ // TODO: Consider "new geographical area notification".
+
+ if (!empty($countryCodes)) {
+ // fake the country is NL, and the limitation is CH
+ if ($clientIP == '127.0.0.1' && $login == "piet@kolab.org") {
+ $country = "NL";
+ } else {
+ // TODO: GeoIP reliance
+ $country = "CH";
+ }
+
+ if (!in_array($country, $countryCodes)) {
+ // TODO: Log, notify user.
+ return $this->byebye($request, __LINE__);
+ }
+ }
+
+ // TODO: Apply some sort of limit for Auth-Login-Attempt -- docs say it is the number of
+ // attempts over the same authAttempt.
+
+ // Check 2fa
+ if ($user->getSetting('2fa_plz', false)) {
+ $authAttempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP);
+ if (!\App\AuthAttempt::waitFor2FA($authAttempt)) {
+ return $this->byebye($request, __LINE__);
+ }
+ }
+
+ // All checks passed
+ switch ($request->headers->get('Auth-Protocol')) {
+ case "imap":
+ return $this->authenticateIMAP($request, $user->getSetting('guam_plz', false), $password);
+ case "smtp":
+ return $this->authenticateSMTP($request, $password);
+ default:
+ return $this->byebye($request);
+ }
+ }
+
+ private function authenticateIMAP(Request $request, $prefGuam, $password)
+ {
+ if ($prefGuam) {
+ if ($request->headers->get('Auth-Ssl') == 'on') {
+ $port = 9993;
+ } else {
+ $port = 9143;
+ }
+ } else {
+ if ($request->headers->get('Auth-Ssl') == 'on') {
+ $port = 11993;
+ } else {
+ $port = 12143;
+ }
+ }
+
+ $response = response("")->withHeaders(
+ [
+ "Auth-Status" => "OK",
+ "Auth-Server" => "127.0.0.1",
+ "Auth-Port" => $port,
+ "Auth-Pass" => $password
+ ]
+ );
+
+ \Log::debug("Response with headers:\n{$response->headers}");
+
+ return $response;
+ }
+
+ private function authenticateSMTP(Request $request, $password)
+ {
+ $response = response("")->withHeaders(
+ [
+ "Auth-Status" => "OK",
+ "Auth-Server" => "127.0.0.1",
+ "Auth-Port" => 10465,
+ "Auth-Pass" => $password
+ ]
+ );
+
+ \Log::debug("Response with headers:\n{$response->headers}");
+
+ return $response;
+ }
+
+ private function byebye(Request $request, $code = null)
+ {
+ $response = response("")->withHeaders(
+ [
+ // TODO code only for development
+ "Auth-Status" => "NO {$code}",
+ "Auth-Wait" => 3
+ ]
+ );
+
+ \Log::debug("Response with headers:\n{$response->headers}");
+
+ return $response;
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/AuthAttemptsController.php b/src/app/Http/Controllers/API/V4/AuthAttemptsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/AuthAttemptsController.php
@@ -0,0 +1,97 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\AuthAttempt;
+use App\Http\Controllers\Controller;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class AuthAttemptsController extends Controller
+{
+
+ public function confirm($id)
+ {
+ $authAttempt = AuthAttempt::findOrFail($id);
+
+ $user = Auth::guard()->user();
+ if ($user->id != $authAttempt->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ \Log::debug("Confirm on {$authAttempt->id}");
+ $authAttempt->accept();
+ $authAttempt->save();
+ return response("", 200);
+ }
+
+ public function deny($id)
+ {
+ $authAttempt = AuthAttempt::findOrFail($id);
+
+ $user = Auth::guard()->user();
+ if ($user->id != $authAttempt->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ \Log::debug("Deny on {$authAttempt->id}");
+ $authAttempt->deny();
+ $authAttempt->save();
+ return response("", 200);
+ }
+
+ public function details($id)
+ {
+ $authAttempt = AuthAttempt::findOrFail($id);
+ $user = Auth::guard()->user();
+
+ \Log::debug("Getting details {$authAttempt->user_id} {$user->id}");
+ if ($user->id != $authAttempt->user_id) {
+ return $this->errorResponse(403);
+ }
+
+ \Log::debug("Details on {$authAttempt->id}");
+ return response()->json([
+ 'status' => 'success',
+ 'username' => $user->email,
+ 'ip' => $authAttempt->ip,
+ 'timestamp' => $authAttempt->updated_at,
+ 'country' => \App\Utils::countryForIP($authAttempt->ip),
+ 'entry' => $authAttempt->toArray()
+ ]);
+ }
+
+ /**
+ * Listing of client authAttempts.
+ *
+ * All authAttempt attempts from the current user
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index(Request $request)
+ {
+ $user = Auth::guard()->user();
+
+ $pageSize = 10;
+ $page = intval($request->input('page')) ?: 1;
+ $hasMore = false;
+
+ $result = \App\AuthAttempt::where('user_id', $user->id)
+ ->orderBy('updated_at', 'desc')
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+
+ $result = $result->map(function ($authAttempt) {
+ return $authAttempt->toArray();
+ });
+
+ return response()->json($result);
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/CompanionAppsController.php b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/CompanionAppsController.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\Controller;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
+
+class CompanionAppsController extends Controller
+{
+
+ public function register(Request $request)
+ {
+ $user = Auth::guard()->user();
+ if (!$user) {
+ throw new \Exception("Authentication required.");
+ }
+ $notificationToken = $request->notificationToken;
+ $deviceId = $request->deviceId;
+
+ \Log::debug("Registering app. Notification token: {$notificationToken} Device id: {$deviceId}");
+
+ $app = \App\CompanionApp::where('device_id', $deviceId)->first();
+ if (!$app) {
+ $app = new \App\CompanionApp();
+ $app->user_id = $user->id;
+ $app->device_id = $deviceId;
+ $app->mfa_enabled = true;
+ }
+
+ $app->notification_token = $notificationToken;
+ $app->save();
+
+ $result['status'] = 'success';
+ return response()->json($result);
+ }
+
+ /* //Require TOTP? */
+ /* //We could also use a 2fa auth capability, that we could then use to guard the clientscontroller */
+ /* public function enable2FA($token) */
+ /* { */
+ /* \Log::debug("Confirm on {$token}"); */
+ /* /1* if ($connectionId = Cache::get("confirm-{$token}")) { *1/ */
+ /* /1* if ($connection = \App\AuthAttempt::where('id', $connectionId)->first()) { *1/ */
+ /* /1* $connection->accept(); *1/ */
+ /* /1* $connection->save(); *1/ */
+ /* /1* } else { *1/ */
+ /* /1* \Log::warning("Couldn't find the connection: {$connectionId}"); *1/ */
+ /* /1* } *1/ */
+ /* /1* Cache::forget("confirm-{$token}"); *1/ */
+ /* /1* } else { *1/ */
+ /* /1* \Log::warning("Couldn't find the token: {$token}"); *1/ */
+ /* /1* } *1/ */
+
+ /* return response("", 200); */
+ /* } */
+}
diff --git a/src/config/firebase.php b/src/config/firebase.php
new file mode 100644
--- /dev/null
+++ b/src/config/firebase.php
@@ -0,0 +1,6 @@
+<?php
+ return [
+ /* api_key available in: Firebase Console -> Project Settings -> CLOUD MESSAGING -> Server key*/
+ 'api_key' => env('FIREBASE_API_KEY'),
+ 'api_url' => env('FIREBASE_API_URL', 'https://fcm.googleapis.com/fcm/send'),
+ ];
diff --git a/src/database/migrations/2021_03_25_144555_create_auth_attempts_table.php b/src/database/migrations/2021_03_25_144555_create_auth_attempts_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_03_25_144555_create_auth_attempts_table.php
@@ -0,0 +1,44 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateAuthAttemptsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('auth_attempts', function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('user_id')->index();
+ $table->string('ip', 36);
+ $table->string('status', 36)->default('NEW');
+ $table->datetime('expires_at')->nullable();
+ $table->datetime('last_seen')->nullable();
+ $table->timestamps();
+
+ $table->index(['user_id', 'ip']);
+
+ $table->foreign('user_id')
+ ->references('id')->on('users')
+ ->onDelete('cascade')
+ ->onUpdate('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('auth_attempts');
+ }
+}
diff --git a/src/database/migrations/2021_05_05_134357_create_companion_apps_table.php b/src/database/migrations/2021_05_05_134357_create_companion_apps_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2021_05_05_134357_create_companion_apps_table.php
@@ -0,0 +1,42 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+// phpcs:ignore
+class CreateCompanionAppsTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create('companion_apps', function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->bigInteger('user_id')->index();
+ $table->string('notification_token')->nullable();
+ $table->string('device_id', 100);
+ $table->string('name')->nullable();
+ $table->boolean('mfa_enabled');
+ $table->timestamps();
+
+ $table->foreign('user_id')
+ ->references('id')->on('users')
+ ->onDelete('cascade')
+ ->onUpdate('cascade');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('companion_apps');
+ }
+}
diff --git a/src/database/seeds/local/UserSeeder.php b/src/database/seeds/local/UserSeeder.php
--- a/src/database/seeds/local/UserSeeder.php
+++ b/src/database/seeds/local/UserSeeder.php
@@ -48,6 +48,9 @@
'external_email' => 'john.doe.external@gmail.com',
'organization' => 'Kolab Developers',
'phone' => '+1 509-248-1111',
+ // 'limit_geo' => json_encode(["CH"]),
+ 'guam_plz' => false,
+ '2fa_plz' => true
]
);
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -54,6 +54,8 @@
}
);
+
+
Route::group(
[
'domain' => \config('app.domain'),
@@ -61,6 +63,13 @@
'prefix' => $prefix . 'api/v4'
],
function () {
+ Route::post('companion/register', 'API\V4\CompanionAppsController@register');
+
+ Route::post('auth-attempts/{id}/confirm', 'API\V4\AuthAttemptsController@confirm');
+ Route::post('auth-attempts/{id}/deny', 'API\V4\AuthAttemptsController@deny');
+ Route::get('auth-attempts/{id}/details', 'API\V4\AuthAttemptsController@details');
+ Route::get('auth-attempts', 'API\V4\AuthAttemptsController@index');
+
Route::apiResource('domains', API\V4\DomainsController::class);
Route::get('domains/{id}/confirm', 'API\V4\DomainsController@confirm');
Route::get('domains/{id}/status', 'API\V4\DomainsController@status');
@@ -138,6 +147,7 @@
function () {
Route::post('payment/{provider}', 'API\V4\PaymentsController@webhook');
Route::post('meet/openvidu', 'API\V4\OpenViduController@webhook');
+ Route::get('nginx', 'API\NGINXController@authenticate');
}
);
diff --git a/src/tests/Feature/Controller/AuthAttemptsTest.php b/src/tests/Feature/Controller/AuthAttemptsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/AuthAttemptsTest.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\User;
+use App\AuthAttempt;
+use Tests\TestCase;
+
+class AuthAttemptsTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('UsersControllerTest1@userscontroller.com');
+ $this->deleteTestDomain('userscontroller.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('UsersControllerTest1@userscontroller.com');
+ $this->deleteTestDomain('userscontroller.com');
+
+ parent::tearDown();
+ }
+
+ public function testRecord(): void
+ {
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $authAttempt = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.1");
+ $this->assertEquals($authAttempt->user_id, $user->id);
+ $this->assertEquals($authAttempt->ip, "10.0.0.1");
+ $authAttempt->refresh();
+ $this->assertEquals($authAttempt->status, "NEW");
+
+ $authAttempt2 = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.1");
+ $this->assertEquals($authAttempt->id, $authAttempt2->id);
+
+ $authAttempt3 = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.2");
+ $this->assertNotEquals($authAttempt->id, $authAttempt3->id);
+ }
+
+
+ public function testAcceptDeny(): void
+ {
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $authAttempt = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.1");
+
+ $response = $this->actingAs($user)->post("api/v4/auth-attempts/{$authAttempt->id}/confirm");
+ $response->assertStatus(200);
+ $authAttempt->refresh();
+ $this->assertTrue($authAttempt->isAccepted());
+
+ $response = $this->actingAs($user)->post("api/v4/auth-attempts/{$authAttempt->id}/deny");
+ $response->assertStatus(200);
+ $authAttempt->refresh();
+ $this->assertTrue($authAttempt->isDenied());
+ }
+
+
+ public function testDetails(): void
+ {
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $authAttempt = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.1");
+
+ $response = $this->actingAs($user)->get("api/v4/auth-attempts/{$authAttempt->id}/details");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $authAttempt->refresh();
+
+ $this->assertEquals($user->email, $json['username']);
+ $this->assertEquals($authAttempt->ip, $json['ip']);
+ $this->assertEquals(json_encode($authAttempt->updated_at), "\"" . $json['timestamp'] . "\"");
+ $this->assertEquals("CH", $json['country']);
+ }
+
+
+ public function testList(): void
+ {
+ $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $authAttempt = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.1");
+ $authAttempt2 = \App\AuthAttempt::recordAuthAttempt($user, "10.0.0.2");
+
+ $response = $this->actingAs($user)->get("api/v4/auth-attempts");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ /* var_export($json); */
+
+ $this->assertEquals(count($json), 2);
+ $this->assertEquals($json[0]['id'], $authAttempt->id);
+ $this->assertEquals($json[1]['id'], $authAttempt2->id);
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 12:28 PM (20 h, 59 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833442
Default Alt Text
D2674.1775392093.diff (27 KB)

Event Timeline