{{ 'New to x?'|trans }} {{ 'Sign up'|trans }}
+ {% endif %} {% endblock %} diff --git a/tests/Entity/User/UserTest.php b/tests/Entity/User/UserTest.php index 81302a4..ffe4116 100644 --- a/tests/Entity/User/UserTest.php +++ b/tests/Entity/User/UserTest.php @@ -84,4 +84,51 @@ public function testUserIsDisabledAfterDisable(): void $this->assertFalse($user->isEnabled()); } + + public function testNewUserIsNotAnAzureUser(): void + { + $user = new User('user@example.com', []); + + $this->assertFalse($user->isAzureUser()); + $this->assertNull($user->getAzureObjectId()); + } + + public function testLinkAzureAccount(): void + { + $user = new User('user@example.com', []); + $user->linkAzureAccount('azure-oid-123'); + + $this->assertTrue($user->isAzureUser()); + $this->assertSame('azure-oid-123', $user->getAzureObjectId()); + } + + public function testUnlinkAzureAccount(): void + { + $user = new User('user@example.com', []); + $user->linkAzureAccount('azure-oid-123'); + $user->unlinkAzureAccount(); + + $this->assertFalse($user->isAzureUser()); + $this->assertNull($user->getAzureObjectId()); + } + + public function testCreateFromAzureProfileIsEnabledAndConfirmed(): void + { + $user = User::createFromAzureProfile('azure@sumocoders.be', 'azure-oid-123'); + + $this->assertSame('azure@sumocoders.be', $user->getEmail()); + $this->assertSame('azure-oid-123', $user->getAzureObjectId()); + $this->assertTrue($user->isAzureUser()); + $this->assertTrue($user->isEnabled()); + $this->assertTrue($user->isConfirmed()); + $this->assertContains('ROLE_USER', $user->getRoles()); + } + + public function testCreateFromAzureProfileWithCustomRoles(): void + { + $user = User::createFromAzureProfile('azure@sumocoders.be', 'azure-oid-123', ['ROLE_ADMIN']); + + $this->assertContains('ROLE_ADMIN', $user->getRoles()); + $this->assertContains('ROLE_USER', $user->getRoles()); + } } diff --git a/tests/Security/OAuth/AzureUserProviderTest.php b/tests/Security/OAuth/AzureUserProviderTest.php new file mode 100644 index 0000000..6ca9330 --- /dev/null +++ b/tests/Security/OAuth/AzureUserProviderTest.php @@ -0,0 +1,249 @@ +userRepository = $this->createMock(UserRepository::class); + $this->eventDispatcher = $this->createMock(EventDispatcherInterface::class); + } + + private function makeProvider(string $allowedDomain = 'sumocoders.be'): AzureUserProvider + { + return new AzureUserProvider($this->userRepository, $this->eventDispatcher, $allowedDomain); + } + + /** + * @param string[] $roles + */ + private function makeResponse(string $oid, string $email, array $roles = []): UserResponseInterface&MockObject + { + $response = $this->createMock(UserResponseInterface::class); + $response->method('getData')->willReturn(['oid' => $oid, 'roles' => $roles]); + $response->method('getEmail')->willReturn($email); + + return $response; + } + + public function testExistingUserFoundByOidIsReturned(): void + { + $user = User::createFromAzureProfile('user@sumocoders.be', 'oid-abc'); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], $user], + ]); + + $response = $this->makeResponse('oid-abc', 'user@sumocoders.be'); + + $result = $this->makeProvider()->loadUserByOAuthUserResponse($response); + + $this->assertSame($user, $result); + } + + public function testExistingUserFoundByOidHasEmailUpdatedWhenChanged(): void + { + $user = User::createFromAzureProfile('old@sumocoders.be', 'oid-abc'); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], $user], + ]); + $this->userRepository->expects($this->once())->method('save'); + + $response = $this->makeResponse('oid-abc', 'new@sumocoders.be'); + + $result = $this->makeProvider()->loadUserByOAuthUserResponse($response); + + $this->assertSame('new@sumocoders.be', $result->getEmail()); + } + + public function testExistingLocalUserFoundByEmailGetsOidLinked(): void + { + $user = new User('user@sumocoders.be', []); + $user->confirm(); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], null], + [['email' => 'user@sumocoders.be'], $user], + ]); + $this->userRepository->expects($this->once())->method('save'); + + $response = $this->makeResponse('oid-abc', 'user@sumocoders.be'); + + $result = $this->makeProvider()->loadUserByOAuthUserResponse($response); + + $this->assertSame($user, $result); + $this->assertSame('oid-abc', $result->getAzureObjectId()); + } + + public function testNewUserIsAutoProvisionedWhenDomainMatches(): void + { + $this->userRepository + ->method('findOneBy') + ->willReturn(null); + $this->userRepository->expects($this->once())->method('add'); + + $response = $this->makeResponse('oid-new', 'newuser@sumocoders.be'); + + $result = $this->makeProvider('sumocoders.be')->loadUserByOAuthUserResponse($response); + + $this->assertSame('newuser@sumocoders.be', $result->getEmail()); + $this->assertSame('oid-new', $result->getAzureObjectId()); + $this->assertTrue($result->isEnabled()); + $this->assertTrue($result->isConfirmed()); + } + + public function testNewUserIsAutoProvisionedWhenNoDomainRestriction(): void + { + $this->userRepository + ->method('findOneBy') + ->willReturn(null); + $this->userRepository->expects($this->once())->method('add'); + + $response = $this->makeResponse('oid-new', 'anyone@otherdomain.com'); + + $result = $this->makeProvider('')->loadUserByOAuthUserResponse($response); + + $this->assertSame('anyone@otherdomain.com', $result->getEmail()); + } + + public function testNewUserWithWrongDomainIsRejected(): void + { + $this->userRepository + ->method('findOneBy') + ->willReturn(null); + + $response = $this->makeResponse('oid-new', 'attacker@evil.com'); + + $this->expectException(AccessDeniedException::class); + + $this->makeProvider('sumocoders.be')->loadUserByOAuthUserResponse($response); + } + + public function testDomainCheckIsCaseInsensitive(): void + { + $this->userRepository + ->method('findOneBy') + ->willReturn(null); + $this->userRepository->expects($this->once())->method('add'); + + $response = $this->makeResponse('oid-new', 'user@SUMOCODERS.BE'); + + $result = $this->makeProvider('sumocoders.be')->loadUserByOAuthUserResponse($response); + + $this->assertSame('user@SUMOCODERS.BE', $result->getEmail()); + } + + public function testAzureRolesAreSyncedOnEveryLoginByOid(): void + { + $user = User::createFromAzureProfile('user@sumocoders.be', 'oid-abc'); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], $user], + ]); + + $response = $this->makeResponse('oid-abc', 'user@sumocoders.be', ['ROLE_ADMIN']); + + $result = $this->makeProvider()->loadUserByOAuthUserResponse($response); + + $this->assertContains('ROLE_ADMIN', $result->getRoles()); + $this->assertContains('ROLE_USER', $result->getRoles()); + } + + public function testAzureRolesAreSyncedWhenLocalUserLinksAccount(): void + { + $user = new User('user@sumocoders.be', []); + $user->confirm(); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], null], + [['email' => 'user@sumocoders.be'], $user], + ]); + + $response = $this->makeResponse('oid-abc', 'user@sumocoders.be', ['ROLE_ADMIN']); + + $result = $this->makeProvider()->loadUserByOAuthUserResponse($response); + + $this->assertContains('ROLE_ADMIN', $result->getRoles()); + } + + public function testAzureRolesAreSyncedForNewUser(): void + { + $this->userRepository + ->method('findOneBy') + ->willReturn(null); + $this->userRepository->expects($this->once())->method('add'); + + $response = $this->makeResponse('oid-new', 'newuser@sumocoders.be', ['ROLE_ADMIN']); + + $result = $this->makeProvider('sumocoders.be')->loadUserByOAuthUserResponse($response); + + $this->assertContains('ROLE_ADMIN', $result->getRoles()); + } + + public function testLoginEventIsDispatchedOnEverySuccessfulLogin(): void + { + $user = User::createFromAzureProfile('user@sumocoders.be', 'oid-abc'); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], $user], + ]); + + $this->eventDispatcher + ->expects($this->once()) + ->method('dispatch') + ->with($this->isInstanceOf(AzureLoginEvent::class)); + + $response = $this->makeResponse('oid-abc', 'user@sumocoders.be'); + $this->makeProvider()->loadUserByOAuthUserResponse($response); + } + + public function testEmptyAzureRolesResultInOnlyRoleUser(): void + { + $user = User::createFromAzureProfile('user@sumocoders.be', 'oid-abc', ['ROLE_ADMIN']); + + $this->userRepository + ->method('findOneBy') + ->willReturnMap([ + [['azureObjectId' => 'oid-abc'], $user], + ]); + + // Azure sends no roles this time (e.g. role was revoked) + $response = $this->makeResponse('oid-abc', 'user@sumocoders.be', []); + + $result = $this->makeProvider()->loadUserByOAuthUserResponse($response); + + $this->assertNotContains('ROLE_ADMIN', $result->getRoles()); + $this->assertContains('ROLE_USER', $result->getRoles()); + } +} diff --git a/translations/messages.nl.yaml b/translations/messages.nl.yaml index 244a2c2..5ba83f6 100644 --- a/translations/messages.nl.yaml +++ b/translations/messages.nl.yaml @@ -106,3 +106,7 @@ user.actions.switchback: 'terug naar eigen account' 'Yes': 'Ja' 'No': 'Nee' 'Cancel': 'Annuleer' + +'Sign in with Microsoft': 'Aanmelden met Microsoft' +'Email domain is not allowed to log in via Azure.': 'Je e-maildomein heeft geen toegang via Azure.' +'Sign in with SumoCoders': 'Aanmelden met SumoCoders'