Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117907647
D2674.1775392093.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
27 KB
Referenced Files
None
Subscribers
None
D2674.1775392093.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D2674: NGINX Controller, 2fa for client connections and companion app support
Attached
Detach File
Event Timeline