Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117826553
D2984.1775299580.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
14 KB
Referenced Files
None
Subscribers
None
D2984.1775299580.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 10:46 AM (6 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18829078
Default Alt Text
D2984.1775299580.diff (14 KB)
Attached To
Mode
D2984: Proxy authorization for irony/syncroton
Attached
Detach File
Event Timeline