diff --git a/src/app/Http/Controllers/API/NGINXController.php b/src/app/Http/Controllers/API/NGINXController.php index d54d0ebb..869d167f 100644 --- a/src/app/Http/Controllers/API/NGINXController.php +++ b/src/app/Http/Controllers/API/NGINXController.php @@ -1,242 +1,268 @@ headers); // TODO: Apply some sort of limit for Auth-Login-Attempt -- docs say it is the number of // attempts over the same connection. switch ($request->headers->get('Auth-Protocol')) { case "imap": return $this->authenticateIMAP($request); case "smtp": return $this->authenticateSMTP($request); default: return $this->byebye($request); } } private function authenticateIMAP(Request $request) { $login = $request->headers->get('Auth-User', null); if (empty($login)) { return $this->byebye($request, __LINE__); } // validate user exists, otherwise bye bye $user = \App\User::where('email', $login)->first(); if (!$user) { return $this->byebye($request, __LINE__); } // validate password, otherwise bye bye $password = $request->headers->get('Auth-Pass', null); if (empty($password)) { return $this->byebye($request, __LINE__); } $result = Hash::check($password, $user->password); if (!$result) { + // TODO: Log, notify user. return $this->byebye($request, __LINE__); } // validate country of origin against restrictions, otherwise bye bye $clientIP = $request->headers->get('Client-Ip', null); $countryCodes = json_decode($user->getSetting('limit_geo', "[]")); \Log::debug("Countries for {$user->email}: " . var_export($countryCodes, true)); + // TODO: Consider "new geographical area notification". + if (!empty($countryCodes)) { // fake the country is NL, and the limitation is CH if ($clientIP == '127.0.0.1' && $login == "piet@kolab.org") { $country = "NL"; } else { // TODO: GeoIP reliance $country = "CH"; } if (!in_array($country, $countryCodes)) { + // TODO: Log, notify user. return $this->byebye($request, __LINE__); } } // determine 2fa preference $pref2fa = $user->getSetting('2fa_plz', false); if ($pref2fa) { $result = $this->waitFor2fa($request); if (!$result) { return $this->byebye($request, __LINE__); } } $prefGuam = $user->getSetting('guam_plz', false); if ($prefGuam) { $port = 9143; } else { $port = 10143; } $response = response("")->withHeaders( [ "Auth-Status" => "OK", "Auth-Server" => "127.0.0.1", "Auth-Port" => $port, "Auth-Pass" => $password ] ); \Log::debug("Response with headers:\n{$response->headers}"); return $response; + } - // limit the rate at which new second factor authentication is required; - // - change of client ip address justifies another OTP, - // - a $x hour interval justifies another OTP + private function authenticateSMTP(Request $request) + { + $login = $request->headers->get('Auth-User', null); - /** - * ports: - * - * 143 nginx - * 465 nginx - * 587 nginx - * 993 nginx - * - * 9143 guam starttls (thus also plain) - * 9993 guam ssl - * 10143 cyrus-imapd allows plaintext - * 10465 postfix ssl - * 10587 postfix starttls - * 11143 cyrus-imapd starttls required - * 11993 cyrus-imapd ssl - */ - switch ($request->headers->get("Auth-Protocol")) { - case "imap": - // without guam - $response = response("")->withHeaders( - [ - "Auth-Status" => 'OK', - "Auth-Server" => '127.0.0.1', - "Auth-Port" => '12143', - "Auth-Pass" => $request->headers->get('Auth-Pass') - ] - ); - - // with guam - $response = response("")->withHeaders( - [ - "Auth-Status" => 'OK', - "Auth-Server" => '127.0.0.1', - "Auth-Port" => '9143', - "Auth-Pass" => $request->headers->get('Auth-Pass') - ] - ); + if (empty($login)) { + return $this->byebye($request, __LINE__); + } - break; + // validate user exists, otherwise bye bye + $user = \App\User::where('email', $login)->first(); - case "smtp": - $response = response("")->withHeaders( - [ - "Auth-Status" => "OK", - "Auth-Server" => '127.0.0.1', - "Auth-Port" => '10465', - "Auth-Pass" => $request->headers->get('Auth-Pass') - ] - ); + if (!$user) { + return $this->byebye($request, __LINE__); + } - break; + // validate password, otherwise bye bye + $password = $request->headers->get('Auth-Pass', null); + + if (empty($password)) { + return $this->byebye($request, __LINE__); + } + + $result = Hash::check($password, $user->password); + + if (!$result) { + // TODO: Log, notify user. + return $this->byebye($request, __LINE__); + } + + // validate country of origin against restrictions, otherwise bye bye + $clientIP = $request->headers->get('Client-Ip', null); + + $countryCodes = json_decode($user->getSetting('limit_geo', "[]")); + + \Log::debug("Countries for {$user->email}: " . var_export($countryCodes, true)); + + // TODO: Consider "new geographical area notification". + + if (!empty($countryCodes)) { + // fake the country is NL, and the limitation is CH + if ($clientIP == '127.0.0.1' && $login == "piet@kolab.org") { + $country = "NL"; + } else { + // TODO: GeoIP reliance + $country = "CH"; + } + + if (!in_array($country, $countryCodes)) { + // TODO: Log, notify user. + return $this->byebye($request, __LINE__); + } } - \Log::debug($response->headers); + // determine 2fa preference + $pref2fa = $user->getSetting('2fa_plz', false); + + if ($pref2fa) { + $result = $this->waitFor2fa($request); + + if (!$result) { + return $this->byebye($request, __LINE__); + } + } + + $response = response("")->withHeaders( + [ + "Auth-Status" => "OK", + "Auth-Server" => "127.0.0.1", + "Auth-Port" => 10465, + "Auth-Pass" => $password + ] + ); + + \Log::debug("Response with headers:\n{$response->headers}"); return $response; } private function byebye(Request $request, $code = null) { $response = response("")->withHeaders( [ // TODO code only for development "Auth-Status" => "NO {$code}", "Auth-Wait" => 3 ] ); \Log::debug("Response with headers:\n{$response->headers}"); return $response; } private function waitFor2fa(Request $request) { + // TODO: Don't require a confirmation for every single hit. + // + // This likely means storing (a hash of) the client IP, a timeout, and a user ID. $code = \App\SignupCode::create( [ 'data' => [ 'email' => $request->headers->get('Auth-User') ], 'expires_at' => Carbon::now()->addMinutes(2) ] ); \Log::debug("visit http://127.0.0.1:8000/api/confirm/{$code->short_code}"); $confirmed = false; $maxTries = 300; do { $confirmCode = \App\SignupCode::find($code->code); if (!$confirmCode) { $confirmed = true; break; } sleep(1); $maxTries--; } while (!$confirmed && $maxTries > 0); return $confirmed; } }