Page MenuHomePhorge

D2984.1775372420.diff
No OneTemporary

Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None

D2984.1775372420.diff

diff --git a/docker/proxy/rootfs/etc/nginx/nginx.conf b/docker/proxy/rootfs/etc/nginx/nginx.conf
--- a/docker/proxy/rootfs/etc/nginx/nginx.conf
+++ b/docker/proxy/rootfs/etc/nginx/nginx.conf
@@ -73,6 +73,74 @@
proxy_set_header Host $host;
}
+ location /roundcubemail {
+ proxy_pass http://127.0.0.1:9080;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_no_cache 1;
+ proxy_cache_bypass 1;
+ }
+
+ location /kolab-webadmin {
+ proxy_pass http://127.0.0.1:9080;
+ proxy_redirect off;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_no_cache 1;
+ proxy_cache_bypass 1;
+ }
+
+ location /Microsoft-Server-ActiveSync {
+ auth_request /auth;
+ #auth_request_set $auth_status $upstream_status;
+
+ proxy_pass http://127.0.0.1:9080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_send_timeout 910s;
+ proxy_read_timeout 910s;
+ fastcgi_send_timeout 910s;
+ fastcgi_read_timeout 910s;
+ }
+
+ location ~* ^/\\.well-known/(caldav|carddav) {
+ proxy_pass http://127.0.0.1:9080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location /iRony {
+ auth_request /auth;
+ #auth_request_set $auth_status $upstream_status;
+
+ proxy_pass http://127.0.0.1:9080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location = /auth {
+ internal;
+ proxy_pass http://127.0.0.1:8000/api/webhooks/nginx-httpauth;
+ proxy_pass_request_body off;
+ proxy_set_header Host services.APP_WEBSITE_DOMAIN;
+ proxy_set_header Content-Length "";
+ proxy_set_header X-Original-URI $request_uri;
+
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
error_page 404 /404.html;
location = /40x.html {
}
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
@@ -10,62 +10,33 @@
class NGINXController extends Controller
{
/**
- * Authentication request.
+ * Authorize with the provided credentials.
*
- * @todo: Separate IMAP(+STARTTLS) from IMAPS, same for SMTP/submission. =>
- * I suppose that's not necessary given that we have the information avialable in the headers?
+ * @param string $login The login name
+ * @param string $password The password
+ * @param string $clientIP The client ip
*
- * @param \Illuminate\Http\Request $request The API request.
+ * @return \App\User The user
*
- * @return \Illuminate\Http\Response The response
+ * @throws \Exception If the authorization fails.
*/
- public function authenticate(Request $request)
+ private function authorizeRequest($login, $password, $clientIP)
{
- /**
- * 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::debug("Authentication attempt");
- \Log::debug($request->headers);
-
- $login = $request->headers->get('Auth-User', null);
-
if (empty($login)) {
- return $this->byebye($request, "Empty login");
+ throw new \Exception("Empty login");
}
- // validate password, otherwise bye bye
- $password = $request->headers->get('Auth-Pass', null);
-
if (empty($password)) {
- return $this->byebye($request, "Empty password");
+ throw new \Exception("Empty password");
}
- $clientIP = $request->headers->get('Client-Ip', null);
-
if (empty($clientIP)) {
- return $this->byebye($request, "No client ip");
+ throw new \Exception("No client ip");
}
- // validate user exists, otherwise bye bye
$user = \App\User::where('email', $login)->first();
-
if (!$user) {
- return $this->byebye($request, "User not found");
+ throw new \Exception("User not found");
}
// TODO: validate the user's domain is A-OK (active, confirmed, not suspended, ldapready)
@@ -79,8 +50,7 @@
$attempt->save();
$attempt->notify();
}
- \Log::info("Failed authentication attempt due to password mismatch for user: {$login}");
- return $this->byebye($request, "Password mismatch");
+ throw new \Exception("Password mismatch");
}
// validate country of origin against restrictions, otherwise bye bye
@@ -97,7 +67,7 @@
$attempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP);
$attempt->deny(\App\AuthAttempt::REASON_GEOLOCATION);
$attempt->notify();
- return $this->byebye($request, "Country code mismatch");
+ throw new \Exception("Country code mismatch");
}
}
@@ -108,9 +78,126 @@
if ($user->getSetting('2fa_enabled', false)) {
$authAttempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP);
if (!$authAttempt->waitFor2FA()) {
- return $this->byebye($request, "2fa failed");
+ throw new \Exception("2fa failed");
+ }
+ }
+ return $user;
+ }
+
+
+ /**
+ * Convert domain.tld\username into username@domain for activesync
+ *
+ * @param string $username The original username.
+ *
+ * @return string The username in canonical form
+ */
+ private function normalizeUsername($username)
+ {
+ $usernameParts = explode("\\", $username);
+ if (count($usernameParts) == 2) {
+ $username = $usernameParts[1];
+ if (!strpos($username, '@') && !empty($usernameParts[0])) {
+ $username .= '@' . $usernameParts[0];
}
}
+ return $username;
+ }
+
+
+ /**
+ * Authentication request from the ngx_http_auth_request_module
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\Response The response
+ */
+ public function httpauth(Request $request)
+ {
+ /**
+ Php-Auth-Pw: simple123
+ Php-Auth-User: john@kolab.org
+ Sec-Fetch-Dest: document
+ Sec-Fetch-Mode: navigate
+ Sec-Fetch-Site: cross-site
+ Sec-Gpc: 1
+ Upgrade-Insecure-Requests: 1
+ User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:93.0) Gecko/20100101 Firefox/93.0
+ X-Forwarded-For: 31.10.153.58
+ X-Forwarded-Proto: https
+ X-Original-Uri: /iRony/
+ X-Real-Ip: 31.10.153.58
+ */
+
+ \Log::debug("Authentication attempt\n{$request->headers}");
+
+ $username = $this->normalizeUsername($request->headers->get('Php-Auth-User', ""));
+ $password = $request->headers->get('Php-Auth-Pw', null);
+
+ if (empty($password)) {
+ \Log::debug("Authentication attempt failed: Empty password provided.");
+ return response("", 401);
+ }
+
+ try {
+ $this->authorizeRequest(
+ $username,
+ $password,
+ $request->headers->get('X-Real-Ip', null),
+ );
+ } catch (\Exception $e) {
+ \Log::debug("Authentication attempt failed: {$e->getMessage()}");
+ return response("", 403);
+ }
+
+ \Log::debug("Authentication attempt succeeded");
+ return response("");
+ }
+
+
+ /**
+ * Authentication request.
+ *
+ * @todo: Separate IMAP(+STARTTLS) from IMAPS, same for SMTP/submission. =>
+ * I suppose that's not necessary given that we have the information avialable in the headers?
+ *
+ * @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::debug("Authentication attempt\n{$request->headers}");
+
+ $password = $request->headers->get('Auth-Pass', null);
+
+ try {
+ $user = $this->authorizeRequest(
+ $request->headers->get('Auth-User', null),
+ $password,
+ $request->headers->get('Client-Ip', null),
+ );
+ } catch (\Exception $e) {
+ return $this->byebye($request, $e->getMessage());
+ }
// All checks passed
switch ($request->headers->get('Auth-Protocol')) {
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -169,6 +169,7 @@
],
function () {
Route::get('nginx', 'API\V4\NGINXController@authenticate');
+ Route::get('nginx-httpauth', 'API\V4\NGINXController@httpauth');
Route::post('policy/greylist', 'API\V4\PolicyController@greylist');
Route::post('policy/ratelimit', 'API\V4\PolicyController@ratelimit');
Route::post('policy/spf', 'API\V4\PolicyController@senderPolicyFramework');
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
@@ -163,4 +163,84 @@
$response->assertStatus(200);
$response->assertHeader('auth-status', 'OK');
}
+
+ /**
+ * Test the httpauth webhook
+ */
+ public function testNGINXHttpAuthHook(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ $response = $this->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+ $pass = \App\Utils::generatePassphrase();
+ $headers = [
+ 'Php-Auth-Pw' => $pass,
+ 'Php-Auth-User' => 'john@kolab.org',
+ 'X-Forwarded-For' => '127.0.0.1',
+ 'X-Forwarded-Proto' => 'https',
+ 'X-Original-Uri' => '/iRony/',
+ 'X-Real-Ip' => '127.0.0.1',
+ ];
+
+ // Pass
+ $response = $this->withHeaders($headers)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(200);
+
+ // domain.tld\username
+ $modifiedHeaders = $headers;
+ $modifiedHeaders['Php-Auth-User'] = "kolab.org\\john";
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(200);
+
+ // Invalid Password
+ $modifiedHeaders = $headers;
+ $modifiedHeaders['Php-Auth-Pw'] = "Invalid";
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+ // Empty Password
+ $modifiedHeaders = $headers;
+ $modifiedHeaders['Php-Auth-Pw'] = "";
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+ // Empty User
+ $modifiedHeaders = $headers;
+ $modifiedHeaders['Php-Auth-User'] = "";
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+ // Invalid User
+ $modifiedHeaders = $headers;
+ $modifiedHeaders['Php-Auth-User'] = "foo@kolab.org";
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+ // Empty Ip
+ $modifiedHeaders = $headers;
+ $modifiedHeaders['X-Real-Ip'] = "";
+ $response = $this->withHeaders($modifiedHeaders)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+
+ // 2-FA without device
+ $john->setSettings(
+ [
+ '2fa_enabled' => true,
+ ]
+ );
+ \App\CompanionApp::where('user_id', $john->id)->delete();
+
+ $response = $this->withHeaders($headers)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(403);
+
+ // 2-FA with accepted auth attempt
+ $authAttempt = \App\AuthAttempt::recordAuthAttempt($john, "127.0.0.1");
+ $authAttempt->accept();
+
+ $response = $this->withHeaders($headers)->get("api/webhooks/nginx-httpauth");
+ $response->assertStatus(200);
+ }
}

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 7:00 AM (8 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18829078
Default Alt Text
D2984.1775372420.diff (14 KB)

Event Timeline