Page MenuHomePhorge

D2984.1775157900.diff
No OneTemporary

Authored By
Unknown
Size
12 KB
Referenced Files
None
Subscribers
None

D2984.1775157900.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
@@ -109,8 +109,9 @@
proxy_cache_bypass 1;
}
+ # FIXME do we need to whitelist certain requests that are unauthenticated?
location /Microsoft-Server-ActiveSync {
- #auth_request /auth;
+ auth_request /auth;
#auth_request_set $auth_status $upstream_status;
proxy_pass http://127.0.0.1:9080;
@@ -124,9 +125,6 @@
}
location ~* ^/\\.well-known/(caldav|carddav) {
- #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;
@@ -134,7 +132,7 @@
}
location /iRony {
- #auth_request /auth;
+ auth_request /auth;
#auth_request_set $auth_status $upstream_status;
proxy_pass http://127.0.0.1:9080;
@@ -145,7 +143,7 @@
location = /auth {
internal;
- proxy_pass http://127.0.0.1:8000/api/webhooks/nginx;
+ 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 "";
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,29 @@
class NGINXController extends Controller
{
/**
- * 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?
+ * Authorize with the provided credentials.
*
- * @param \Illuminate\Http\Request $request The API request.
+ * @throws \Exception $e If the authorization fails.
*
- * @return \Illuminate\Http\Response The response
+ * @return \App\User The user
*/
- 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 +46,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 +63,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 +74,104 @@
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;
+ }
+
+ /**
+ * 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");
+ \Log::debug($request->headers);
+
+ try {
+ $this->authorizeRequest(
+ $request->headers->get('Php-Auth-User', null),
+ $request->headers->get('Php-Auth-Pw', null),
+ $request->headers->get('X-Real-Ip', null),
+ );
+ } catch (\Exception $e) {
+ \Log::debug("Authentication attempt failed: {$e->getMessage()}");
+ return $this->errorResponse(403);
+ }
+
+ \Log::debug("Authentication attempt succeeded");
+ $response = response("")->withHeaders(
+ [
+ "Auth-Status" => "OK"
+ ]
+ );
+ 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");
+ \Log::debug($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
@@ -146,6 +146,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,78 @@
$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);
+
+ // 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
Thu, Apr 2, 7:25 PM (1 d, 17 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820487
Default Alt Text
D2984.1775157900.diff (12 KB)

Event Timeline