Page MenuHomePhorge

D4716.1775397085.diff
No OneTemporary

Authored By
Unknown
Size
7 KB
Referenced Files
None
Subscribers
None

D4716.1775397085.diff

diff --git a/src/app/Auth/Utils.php b/src/app/Auth/Utils.php
new file mode 100644
--- /dev/null
+++ b/src/app/Auth/Utils.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace App\Auth;
+
+use Carbon\Carbon;
+
+class Utils
+{
+ /**
+ * Create a simple authentication token
+ *
+ * @param string $userid User identifier
+ *
+ * @return string|null Encrypted token, Null on failure
+ */
+ public static function tokenCreate($userid): ?string
+ {
+ // Note: Laravel's Crypt::encryptString() creates output that is too long
+ // We need output string to be max. 127 characters. For that reason
+ // we use a custom implementation, and we use user ID instead of login.
+
+ $cipher = strtolower(config('app.cipher'));
+ $key = config('app.key');
+ $iv = random_bytes(openssl_cipher_iv_length($cipher));
+
+ $data = $userid . '!' . now()->addSeconds(10)->format('YmdHis');
+
+ $value = openssl_encrypt($data, $cipher, $key, 0, $iv, $tag);
+
+ if ($value === false) {
+ return null;
+ }
+
+ return trim(base64_encode($iv), '=')
+ . '!'
+ . trim(base64_encode($tag), '=')
+ . '!'
+ . trim(base64_encode($value), '=');
+ }
+
+ /**
+ * Vaidate a simple authentication token
+ *
+ * @param string $token Token
+ *
+ * @return string|null User identifier, Null on failure
+ */
+ public static function tokenValidate($token): ?string
+ {
+ if (!preg_match('|^[a-zA-Z0-9!+/]{50,}$|', $token)) {
+ // this isn't a token, probably a normal password
+ return null;
+ }
+
+ [$iv, $tag, $payload] = explode('!', $token);
+
+ $iv = base64_decode($iv);
+ $tag = base64_decode($tag);
+ $payload = base64_decode($payload);
+
+ $cipher = strtolower(config('app.cipher'));
+ $key = config('app.key');
+
+ $decrypted = openssl_decrypt($payload, $cipher, $key, 0, $iv, $tag);
+
+ if ($decrypted === false) {
+ return null;
+ }
+
+ $payload = explode('!', $decrypted);
+
+ if (count($payload) != 2
+ || !preg_match('|^[0-9]+$|', $payload[0])
+ || !preg_match('|^[0-9]{14}+$|', $payload[1])
+ ) {
+ // Invalid payload format
+ return null;
+ }
+
+ // Check expiration date
+ try {
+ $expiry = Carbon::create(
+ (int) substr($payload[1], 0, 4),
+ (int) substr($payload[1], 4, 2),
+ (int) substr($payload[1], 6, 2),
+ (int) substr($payload[1], 8, 2),
+ (int) substr($payload[1], 10, 2),
+ (int) substr($payload[1], 12, 2)
+ );
+
+ if (now() > $expiry) {
+ return null;
+ }
+ } catch (\Exception $e) {
+ return null;
+ }
+
+ return $payload[0];
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/NGINXController.php b/src/app/Http/Controllers/API/V4/NGINXController.php
--- a/src/app/Http/Controllers/API/V4/NGINXController.php
+++ b/src/app/Http/Controllers/API/V4/NGINXController.php
@@ -2,7 +2,9 @@
namespace App\Http\Controllers\API\V4;
+use App\Auth\Utils as AuthUtils;
use App\Http\Controllers\Controller;
+use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -28,7 +30,16 @@
throw new \Exception("Empty password");
}
- $user = \App\User::where('email', $login)->first();
+ if ($userid = AuthUtils::tokenValidate($password)) {
+ $user = User::find($userid);
+ if ($user && $user->email == $login) {
+ return $user;
+ }
+
+ throw new \Exception("Password mismatch");
+ }
+
+ $user = User::where('email', $login)->first();
if (!$user) {
throw new \Exception("User not found");
}
@@ -65,7 +76,16 @@
throw new \Exception("No client ip");
}
- $result = \App\User::findAndAuthenticate($login, $password, $clientIP);
+ if ($userid = AuthUtils::tokenValidate($password)) {
+ $user = User::find($userid);
+ if ($user && $user->email == $login) {
+ return $user;
+ }
+
+ throw new \Exception("Password mismatch");
+ }
+
+ $result = User::findAndAuthenticate($login, $password, $clientIP);
if (empty($result['user'])) {
throw new \Exception($result['errorMessage'] ?? "Unknown error");
diff --git a/src/tests/Feature/Controller/NGINXTest.php b/src/tests/Feature/Controller/NGINXTest.php
--- a/src/tests/Feature/Controller/NGINXTest.php
+++ b/src/tests/Feature/Controller/NGINXTest.php
@@ -2,6 +2,7 @@
namespace Tests\Feature\Controller;
+use App\Auth\Utils as AuthUtils;
use Tests\TestCase;
class NGINXTest extends TestCase
@@ -129,7 +130,6 @@
$response->assertStatus(200);
$response->assertHeader('auth-status', 'authentication failure');
-
// Guam
$john->setSettings(['guam_enabled' => 'true']);
@@ -139,7 +139,6 @@
$response->assertHeader('auth-server', gethostbyname(\config('imap.host')));
$response->assertHeader('auth-port', \config('imap.guam_port'));
-
$companionApp = $this->getTestCompanionApp(
'testdevice',
$john,
@@ -170,7 +169,6 @@
$response->assertStatus(200);
$response->assertHeader('auth-status', 'OK');
-
// Geo-lockin (failure)
$john->setSettings(['limit_geo' => '["PL","US"]']);
@@ -200,6 +198,19 @@
$response->assertHeader('auth-status', 'OK');
$this->assertCount(0, \App\AuthAttempt::where('user_id', $john->id)->get());
+
+ // Token auth (valid)
+ $modifiedHeaders['Auth-Pass'] = AuthUtils::tokenCreate($john->id);
+ $modifiedHeaders['Auth-Protocol'] = 'smtp';
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx");
+ $response->assertStatus(200);
+ $response->assertHeader('auth-status', 'OK');
+
+ // Token auth (invalid payload)
+ $modifiedHeaders['Auth-User'] = 'jack@kolab.org';
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx");
+ $response->assertStatus(200);
+ $response->assertHeader('auth-status', 'authentication failure');
}
/**
@@ -289,4 +300,20 @@
$response = $this->withHeaders($headers)->get("api/webhooks/nginx-httpauth");
$response->assertStatus(200);
}
+
+ /**
+ * Test the roundcube webhook
+ */
+ public function testRoundcubeHook(): void
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Test the cyrus-sasl webhook
+ */
+ public function testCyrusSaslHook(): void
+ {
+ $this->markTestIncomplete();
+ }
}
diff --git a/src/tests/Unit/Auth/UtilsTest.php b/src/tests/Unit/Auth/UtilsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Auth/UtilsTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Tests\Unit\Auth;
+
+use App\Auth\Utils;
+use Carbon\Carbon;
+use Tests\TestCase;
+
+class UtilsTest extends TestCase
+{
+ /**
+ * Test token creation and validation
+ */
+ public function testTokenCreateAndValidate(): void
+ {
+ $userid = '1234567890';
+ $token = Utils::tokenCreate($userid);
+
+ $this->assertTrue(strlen($token) > 50 && strlen($token) < 128);
+ $this->assertTrue(preg_match('|^[a-zA-Z0-9!+/]+$|', $token) === 1);
+
+ $this->assertSame($userid, Utils::tokenValidate($token));
+
+ // Expired token
+ Carbon::setTestNow(Carbon::now()->addSeconds(11));
+ $this->assertNull(Utils::tokenValidate($token));
+
+ // Invalid token
+ $this->assertNull(Utils::tokenValidate('sdfsdfsfsdfsdfs!asd!sdfsdfsdfrwet'));
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 1:51 PM (21 h, 57 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18833587
Default Alt Text
D4716.1775397085.diff (7 KB)

Event Timeline