diff --git a/src/app/Auth/LDAPUserProvider.php b/src/app/Auth/LDAPUserProvider.php index bbbee25c..6c9a21c2 100644 --- a/src/app/Auth/LDAPUserProvider.php +++ b/src/app/Auth/LDAPUserProvider.php @@ -1,101 +1,101 @@ get(); + $entries = User::where('email', \strtolower($credentials['email']))->get(); $count = $entries->count(); if ($count == 1) { return $entries->first(); } if ($count > 1) { \Log::warning("Multiple entries for {$credentials['email']}"); } else { \Log::warning("No entries for {$credentials['email']}"); } return null; } /** * Validate the credentials for a user. * * @param Authenticatable $user The user. * @param array $credentials The credentials. * * @return bool */ public function validateCredentials(Authenticatable $user, array $credentials): bool { $authenticated = false; - if ($user->email == $credentials['email']) { + if ($user->email === \strtolower($credentials['email'])) { if (!empty($user->password)) { if (Hash::check($credentials['password'], $user->password)) { $authenticated = true; } } elseif (!empty($user->password_ldap)) { if (substr($user->password_ldap, 0, 6) == "{SSHA}") { $salt = substr(base64_decode(substr($user->password_ldap, 6)), 20); $hash = '{SSHA}' . base64_encode( sha1($credentials['password'] . $salt, true) . $salt ); if ($hash == $user->password_ldap) { $authenticated = true; } } elseif (substr($user->password_ldap, 0, 9) == "{SSHA512}") { $salt = substr(base64_decode(substr($user->password_ldap, 9)), 64); $hash = '{SSHA512}' . base64_encode( pack('H*', hash('sha512', $credentials['password'] . $salt)) . $salt ); if ($hash == $user->password_ldap) { $authenticated = true; } } } else { \Log::error("Incomplete credentials for {$user->email}"); } } if ($authenticated) { \Log::info("Successful authentication for {$user->email}"); // TODO: update last login time if (empty($user->password) || empty($user->password_ldap)) { $user->password = $credentials['password']; $user->save(); } } else { // TODO: Try actual LDAP? \Log::info("Authentication failed for {$user->email}"); } return $authenticated; } } diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php index 7dd7ce31..71711486 100644 --- a/src/tests/Feature/Controller/AuthTest.php +++ b/src/tests/Feature/Controller/AuthTest.php @@ -1,185 +1,195 @@ deleteTestUser('UsersControllerTest1@userscontroller.com'); $this->deleteTestDomain('userscontroller.com'); } /** * {@inheritDoc} */ public function tearDown(): void { $this->deleteTestUser('UsersControllerTest1@userscontroller.com'); $this->deleteTestDomain('userscontroller.com'); parent::tearDown(); } /** * Test fetching current user info (/api/auth/info) */ public function testInfo(): void { $user = $this->getTestUser('UsersControllerTest1@userscontroller.com'); $domain = $this->getTestDomain('userscontroller.com', [ 'status' => Domain::STATUS_NEW, 'type' => Domain::TYPE_PUBLIC, ]); $response = $this->actingAs($user)->get("api/auth/info"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals($user->id, $json['id']); $this->assertEquals($user->email, $json['email']); $this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']); $this->assertTrue(is_array($json['statusInfo'])); $this->assertTrue(is_array($json['settings'])); $this->assertTrue(is_array($json['aliases'])); $this->assertTrue(!isset($json['access_token'])); // Note: Details of the content are tested in testUserResponse() // Test token refresh via the info request // First we log in as we need the token (actingAs() will not work) $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; $response = $this->post("api/auth/login", $post); $json = $response->json(); $response = $this->withHeaders(['Authorization' => 'Bearer ' . $json['access_token']]) ->get("api/auth/info?refresh_token=1"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('john@kolab.org', $json['email']); $this->assertTrue(is_array($json['statusInfo'])); $this->assertTrue(is_array($json['settings'])); $this->assertTrue(is_array($json['aliases'])); $this->assertTrue(!empty($json['access_token'])); $this->assertTrue(!empty($json['expires_in'])); } /** * Test /api/auth/login */ public function testLogin(): string { // Request with no data $response = $this->post("api/auth/login", []); $response->assertStatus(422); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertCount(2, $json['errors']); $this->assertArrayHasKey('email', $json['errors']); $this->assertArrayHasKey('password', $json['errors']); // Request with invalid password $post = ['email' => 'john@kolab.org', 'password' => 'wrong']; $response = $this->post("api/auth/login", $post); $response->assertStatus(401); $json = $response->json(); $this->assertSame('error', $json['status']); $this->assertSame('Invalid username or password.', $json['message']); // Valid user+password $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; $response = $this->post("api/auth/login", $post); $json = $response->json(); $response->assertStatus(200); $this->assertTrue(!empty($json['access_token'])); $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); $this->assertEquals('bearer', $json['token_type']); + // Valid user+password (upper-case) + $post = ['email' => 'John@Kolab.org', 'password' => 'simple123']; + $response = $this->post("api/auth/login", $post); + $json = $response->json(); + + $response->assertStatus(200); + $this->assertTrue(!empty($json['access_token'])); + $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); + $this->assertEquals('bearer', $json['token_type']); + // TODO: We have browser tests for 2FA but we should probably also test it here return $json['access_token']; } /** * Test /api/auth/logout * * @depends testLogin */ public function testLogout($token): void { // Request with no token, testing that it requires auth $response = $this->post("api/auth/logout"); $response->assertStatus(401); // Test the same using JSON mode $response = $this->json('POST', "api/auth/logout", []); $response->assertStatus(401); // Request with valid token $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/logout"); $response->assertStatus(200); $json = $response->json(); $this->assertEquals('success', $json['status']); $this->assertEquals('Successfully logged out.', $json['message']); // Check if it really destroyed the token? $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->get("api/auth/info"); $response->assertStatus(401); } /** * Test /api/auth/refresh */ public function testRefresh(): void { // Request with no token, testing that it requires auth $response = $this->post("api/auth/refresh"); $response->assertStatus(401); // Test the same using JSON mode $response = $this->json('POST', "api/auth/refresh", []); $response->assertStatus(401); // Login the user to get a valid token $post = ['email' => 'john@kolab.org', 'password' => 'simple123']; $response = $this->post("api/auth/login", $post); $response->assertStatus(200); $json = $response->json(); $token = $json['access_token']; // Request with a valid token $response = $this->withHeaders(['Authorization' => 'Bearer ' . $token])->post("api/auth/refresh"); $response->assertStatus(200); $json = $response->json(); $this->assertTrue(!empty($json['access_token'])); $this->assertTrue($json['access_token'] != $token); $this->assertEquals(\config('jwt.ttl') * 60, $json['expires_in']); $this->assertEquals('bearer', $json['token_type']); $new_token = $json['access_token']; // TODO: Shall we invalidate the old token? // And if the new token is working $response = $this->withHeaders(['Authorization' => 'Bearer ' . $new_token])->get("api/auth/info"); $response->assertStatus(200); } }