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);
+    }
 }