diff options
Diffstat (limited to 'apps/oauth2/tests')
-rw-r--r-- | apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php | 255 | ||||
-rw-r--r-- | apps/oauth2/tests/Controller/OauthApiControllerTest.php | 607 | ||||
-rw-r--r-- | apps/oauth2/tests/Controller/SettingsControllerTest.php | 188 | ||||
-rw-r--r-- | apps/oauth2/tests/Db/AccessTokenMapperTest.php | 59 | ||||
-rw-r--r-- | apps/oauth2/tests/Db/ClientMapperTest.php | 82 | ||||
-rw-r--r-- | apps/oauth2/tests/Settings/AdminTest.php | 60 |
6 files changed, 1251 insertions, 0 deletions
diff --git a/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php new file mode 100644 index 00000000000..04ac0bfbd28 --- /dev/null +++ b/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php @@ -0,0 +1,255 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\OAuth2\Tests\Controller; + +use OC\Core\Controller\ClientFlowLoginController; +use OCA\OAuth2\Controller\LoginRedirectorController; +use OCA\OAuth2\Db\Client; +use OCA\OAuth2\Db\ClientMapper; +use OCA\OAuth2\Exceptions\ClientNotFoundException; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IAppConfig; +use OCP\IConfig; +use OCP\IL10N; +use OCP\IRequest; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\Security\ISecureRandom; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +/** + * @group DB + */ +class LoginRedirectorControllerTest extends TestCase { + private IRequest&MockObject $request; + private IURLGenerator&MockObject $urlGenerator; + private ClientMapper&MockObject $clientMapper; + private ISession&MockObject $session; + private IL10N&MockObject $l; + private ISecureRandom&MockObject $random; + private IAppConfig&MockObject $appConfig; + private IConfig&MockObject $config; + + private LoginRedirectorController $loginRedirectorController; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->clientMapper = $this->createMock(ClientMapper::class); + $this->session = $this->createMock(ISession::class); + $this->l = $this->createMock(IL10N::class); + $this->random = $this->createMock(ISecureRandom::class); + $this->appConfig = $this->createMock(IAppConfig::class); + $this->config = $this->createMock(IConfig::class); + + $this->loginRedirectorController = new LoginRedirectorController( + 'oauth2', + $this->request, + $this->urlGenerator, + $this->clientMapper, + $this->session, + $this->l, + $this->random, + $this->appConfig, + $this->config, + ); + } + + public function testAuthorize(): void { + $client = new Client(); + $client->setClientIdentifier('MyClientIdentifier'); + $this->clientMapper + ->expects($this->once()) + ->method('getByIdentifier') + ->with('MyClientId') + ->willReturn($client); + $this->session + ->expects($this->once()) + ->method('set') + ->with('oauth.state', 'MyState'); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with( + 'core.ClientFlowLogin.showAuthPickerPage', + [ + 'clientIdentifier' => 'MyClientIdentifier', + 'providedRedirectUri' => '', + ] + ) + ->willReturn('https://example.com/?clientIdentifier=foo'); + $this->config + ->expects($this->once()) + ->method('getSystemValueBool') + ->with('oauth2.enable_oc_clients', false) + ->willReturn(false); + + $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo'); + $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'code')); + } + + public function testAuthorizeSkipPicker(): void { + $client = new Client(); + $client->setName('MyClientName'); + $client->setClientIdentifier('MyClientIdentifier'); + $this->clientMapper + ->expects($this->once()) + ->method('getByIdentifier') + ->with('MyClientId') + ->willReturn($client); + $this->session + ->expects(static::exactly(2)) + ->method('set') + ->willReturnCallback(function (string $key, string $value): void { + switch ([$key, $value]) { + case ['oauth.state', 'MyState']: + case [ClientFlowLoginController::STATE_NAME, 'MyStateToken']: + /* Expected */ + break; + default: + throw new LogicException(); + } + }); + $this->appConfig + ->expects(static::once()) + ->method('getValueArray') + ->with('oauth2', 'skipAuthPickerApplications', []) + ->willReturn(['MyClientName']); + $this->random + ->expects(static::once()) + ->method('generate') + ->willReturn('MyStateToken'); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with( + 'core.ClientFlowLogin.grantPage', + [ + 'stateToken' => 'MyStateToken', + 'clientIdentifier' => 'MyClientIdentifier', + 'providedRedirectUri' => '', + ] + ) + ->willReturn('https://example.com/?clientIdentifier=foo'); + $this->config + ->expects($this->once()) + ->method('getSystemValueBool') + ->with('oauth2.enable_oc_clients', false) + ->willReturn(false); + + $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo'); + $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'code')); + } + + public function testAuthorizeWrongResponseType(): void { + $client = new Client(); + $client->setClientIdentifier('MyClientIdentifier'); + $client->setRedirectUri('http://foo.bar'); + $this->clientMapper + ->expects($this->once()) + ->method('getByIdentifier') + ->with('MyClientId') + ->willReturn($client); + $this->session + ->expects($this->never()) + ->method('set'); + + + $expected = new RedirectResponse('http://foo.bar?error=unsupported_response_type&state=MyState'); + $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'wrongcode')); + } + + public function testAuthorizeWithLegacyOcClient(): void { + $client = new Client(); + $client->setClientIdentifier('MyClientIdentifier'); + $client->setRedirectUri('http://localhost:*'); + $this->clientMapper + ->expects($this->once()) + ->method('getByIdentifier') + ->with('MyClientId') + ->willReturn($client); + $this->session + ->expects($this->once()) + ->method('set') + ->with('oauth.state', 'MyState'); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with( + 'core.ClientFlowLogin.showAuthPickerPage', + [ + 'clientIdentifier' => 'MyClientIdentifier', + 'providedRedirectUri' => 'http://localhost:30000', + ] + ) + ->willReturn('https://example.com/?clientIdentifier=foo&providedRedirectUri=http://localhost:30000'); + $this->config + ->expects($this->once()) + ->method('getSystemValueBool') + ->with('oauth2.enable_oc_clients', false) + ->willReturn(true); + + $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo&providedRedirectUri=http://localhost:30000'); + $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'code', 'http://localhost:30000')); + } + + public function testAuthorizeNotForwardingUntrustedURIs(): void { + $client = new Client(); + $client->setClientIdentifier('MyClientIdentifier'); + $this->clientMapper + ->expects($this->once()) + ->method('getByIdentifier') + ->with('MyClientId') + ->willReturn($client); + $this->session + ->expects($this->once()) + ->method('set') + ->with('oauth.state', 'MyState'); + $this->urlGenerator + ->expects($this->once()) + ->method('linkToRouteAbsolute') + ->with( + 'core.ClientFlowLogin.showAuthPickerPage', + [ + 'clientIdentifier' => 'MyClientIdentifier', + 'providedRedirectUri' => '', + ] + ) + ->willReturn('https://example.com/?clientIdentifier=foo'); + $this->config + ->expects($this->once()) + ->method('getSystemValueBool') + ->with('oauth2.enable_oc_clients', false) + ->willReturn(false); + + $expected = new RedirectResponse('https://example.com/?clientIdentifier=foo'); + $this->assertEquals($expected, $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'code', 'http://untrusted-uri.com')); + } + + + public function testClientNotFound(): void { + $clientNotFound = new ClientNotFoundException('could not find client test123', 0); + $this->clientMapper + ->expects($this->once()) + ->method('getByIdentifier') + ->willThrowException($clientNotFound); + $this->session + ->expects($this->never()) + ->method('set'); + + $response = $this->loginRedirectorController->authorize('MyClientId', 'MyState', 'wrongcode'); + $this->assertInstanceOf(TemplateResponse::class, $response); + + /** @var TemplateResponse $response */ + $this->assertEquals('404', $response->getTemplateName()); + $this->assertEquals('guest', $response->getRenderAs()); + } +} diff --git a/apps/oauth2/tests/Controller/OauthApiControllerTest.php b/apps/oauth2/tests/Controller/OauthApiControllerTest.php new file mode 100644 index 00000000000..53dd8549196 --- /dev/null +++ b/apps/oauth2/tests/Controller/OauthApiControllerTest.php @@ -0,0 +1,607 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\OAuth2\Tests\Controller; + +use OC\Authentication\Exceptions\ExpiredTokenException; +use OC\Authentication\Exceptions\InvalidTokenException; +use OC\Authentication\Token\IProvider as TokenProvider; +use OC\Authentication\Token\PublicKeyToken; +use OCA\OAuth2\Controller\OauthApiController; +use OCA\OAuth2\Db\AccessToken; +use OCA\OAuth2\Db\AccessTokenMapper; +use OCA\OAuth2\Db\Client; +use OCA\OAuth2\Db\ClientMapper; +use OCA\OAuth2\Exceptions\AccessTokenNotFoundException; +use OCA\OAuth2\Exceptions\ClientNotFoundException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IRequest; +use OCP\Security\Bruteforce\IThrottler; +use OCP\Security\ICrypto; +use OCP\Security\ISecureRandom; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +/* We have to use this to add a property to the mocked request and avoid warnings about dynamic properties on PHP>=8.2 */ +abstract class RequestMock implements IRequest { + public array $server = []; +} + +class OauthApiControllerTest extends TestCase { + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var ICrypto|\PHPUnit\Framework\MockObject\MockObject */ + private $crypto; + /** @var AccessTokenMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $accessTokenMapper; + /** @var ClientMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $clientMapper; + /** @var TokenProvider|\PHPUnit\Framework\MockObject\MockObject */ + private $tokenProvider; + /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */ + private $secureRandom; + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $time; + /** @var IThrottler|\PHPUnit\Framework\MockObject\MockObject */ + private $throttler; + /** @var LoggerInterface|\PHPUnit\Framework\MockObject\MockObject */ + private $logger; + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + /** @var OauthApiController */ + private $oauthApiController; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(RequestMock::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class); + $this->clientMapper = $this->createMock(ClientMapper::class); + $this->tokenProvider = $this->createMock(TokenProvider::class); + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->time = $this->createMock(ITimeFactory::class); + $this->throttler = $this->createMock(IThrottler::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + + $this->oauthApiController = new OauthApiController( + 'oauth2', + $this->request, + $this->crypto, + $this->accessTokenMapper, + $this->clientMapper, + $this->tokenProvider, + $this->secureRandom, + $this->time, + $this->logger, + $this->throttler, + $this->timeFactory + ); + } + + public function testGetTokenInvalidGrantType(): void { + $expected = new JSONResponse([ + 'error' => 'invalid_grant', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_grant' => 'foo']); + + $this->assertEquals($expected, $this->oauthApiController->getToken('foo', null, null, null, null)); + } + + public function testGetTokenInvalidCode(): void { + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidcode']); + + $this->accessTokenMapper->method('getByCode') + ->with('invalidcode') + ->willThrowException(new AccessTokenNotFoundException()); + + $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'invalidcode', null, null, null)); + } + + public function testGetTokenExpiredCode(): void { + $codeCreatedAt = 100; + $expiredSince = 123; + + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'authorization_code_expired', 'expired_since' => $expiredSince]); + + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setCodeCreatedAt($codeCreatedAt); + + $this->accessTokenMapper->method('getByCode') + ->with('validcode') + ->willReturn($accessToken); + + $tsNow = $codeCreatedAt + OauthApiController::AUTHORIZATION_CODE_EXPIRES_AFTER + $expiredSince; + $dateNow = (new \DateTimeImmutable())->setTimestamp($tsNow); + $this->timeFactory->method('now') + ->willReturn($dateNow); + + $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'validcode', null, null, null)); + } + + public function testGetTokenWithCodeForActiveToken(): void { + // if a token has already delivered oauth tokens, + // it should not be possible to get a new oauth token from a valid authorization code + $codeCreatedAt = 100; + + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'authorization_code_received_for_active_token']); + + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setCodeCreatedAt($codeCreatedAt); + $accessToken->setTokenCount(1); + + $this->accessTokenMapper->method('getByCode') + ->with('validcode') + ->willReturn($accessToken); + + $tsNow = $codeCreatedAt + 1; + $dateNow = (new \DateTimeImmutable())->setTimestamp($tsNow); + $this->timeFactory->method('now') + ->willReturn($dateNow); + + $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'validcode', null, null, null)); + } + + public function testGetTokenClientDoesNotExist(): void { + // In this test, the token's authorization code is valid and has not expired + // and we check what happens when the associated Oauth client does not exist + $codeCreatedAt = 100; + + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'client not found', 'client_id' => 42]); + + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setCodeCreatedAt($codeCreatedAt); + + $this->accessTokenMapper->method('getByCode') + ->with('validcode') + ->willReturn($accessToken); + + // 'now' is before the token's authorization code expiration + $tsNow = $codeCreatedAt + OauthApiController::AUTHORIZATION_CODE_EXPIRES_AFTER - 1; + $dateNow = (new \DateTimeImmutable())->setTimestamp($tsNow); + $this->timeFactory->method('now') + ->willReturn($dateNow); + + $this->clientMapper->method('getByUid') + ->with(42) + ->willThrowException(new ClientNotFoundException()); + + $this->assertEquals($expected, $this->oauthApiController->getToken('authorization_code', 'validcode', null, null, null)); + } + + public function testRefreshTokenInvalidRefreshToken(): void { + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'token not found', 'code' => 'invalidrefresh']); + + $this->accessTokenMapper->method('getByCode') + ->with('invalidrefresh') + ->willThrowException(new AccessTokenNotFoundException()); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'invalidrefresh', null, null)); + } + + public function testRefreshTokenClientDoesNotExist(): void { + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'client not found', 'client_id' => 42]); + + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + + $this->accessTokenMapper->method('getByCode') + ->with('validrefresh') + ->willReturn($accessToken); + + $this->clientMapper->method('getByUid') + ->with(42) + ->willThrowException(new ClientNotFoundException()); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null)); + } + + public static function invalidClientProvider() { + return [ + ['invalidClientId', 'invalidClientSecret'], + ['clientId', 'invalidClientSecret'], + ['invalidClientId', 'clientSecret'], + ]; + } + + /** + * + * @param string $clientId + * @param string $clientSecret + */ + #[\PHPUnit\Framework\Attributes\DataProvider('invalidClientProvider')] + public function testRefreshTokenInvalidClient($clientId, $clientSecret): void { + $expected = new JSONResponse([ + 'error' => 'invalid_client', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_client' => 'client ID or secret does not match']); + + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + + $this->accessTokenMapper->method('getByCode') + ->with('validrefresh') + ->willReturn($accessToken); + + $this->crypto + ->method('calculateHMAC') + ->with($this->callback(function (string $text) { + return $text === 'clientSecret' || $text === 'invalidClientSecret'; + })) + ->willReturnCallback(function (string $text) { + return $text === 'clientSecret' + ? 'hashedClientSecret' + : 'hashedInvalidClientSecret'; + }); + + $client = new Client(); + $client->setClientIdentifier('clientId'); + $client->setSecret(bin2hex('hashedClientSecret')); + $this->clientMapper->method('getByUid') + ->with(42) + ->willReturn($client); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', $clientId, $clientSecret)); + } + + public function testRefreshTokenInvalidAppToken(): void { + $expected = new JSONResponse([ + 'error' => 'invalid_request', + ], Http::STATUS_BAD_REQUEST); + $expected->throttle(['invalid_request' => 'token is invalid']); + + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setTokenId(1337); + $accessToken->setEncryptedToken('encryptedToken'); + + $this->accessTokenMapper->method('getByCode') + ->with('validrefresh') + ->willReturn($accessToken); + + $client = new Client(); + $client->setClientIdentifier('clientId'); + $client->setSecret(bin2hex('hashedClientSecret')); + $this->clientMapper->method('getByUid') + ->with(42) + ->willReturn($client); + + $this->crypto + ->method('decrypt') + ->with('encryptedToken') + ->willReturn('decryptedToken'); + + $this->crypto + ->method('calculateHMAC') + ->with('clientSecret') + ->willReturn('hashedClientSecret'); + + $this->tokenProvider->method('getTokenById') + ->with(1337) + ->willThrowException(new InvalidTokenException()); + + $this->accessTokenMapper->expects($this->once()) + ->method('delete') + ->with($accessToken); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret')); + } + + public function testRefreshTokenValidAppToken(): void { + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setTokenId(1337); + $accessToken->setEncryptedToken('encryptedToken'); + + $this->accessTokenMapper->method('getByCode') + ->with('validrefresh') + ->willReturn($accessToken); + + $client = new Client(); + $client->setClientIdentifier('clientId'); + $client->setSecret(bin2hex('hashedClientSecret')); + $this->clientMapper->method('getByUid') + ->with(42) + ->willReturn($client); + + $this->crypto + ->method('decrypt') + ->with('encryptedToken') + ->willReturn('decryptedToken'); + + $this->crypto + ->method('calculateHMAC') + ->with('clientSecret') + ->willReturn('hashedClientSecret'); + + $appToken = new PublicKeyToken(); + $appToken->setUid('userId'); + $this->tokenProvider->method('getTokenById') + ->with(1337) + ->willThrowException(new ExpiredTokenException($appToken)); + + $this->accessTokenMapper->expects($this->never()) + ->method('delete') + ->with($accessToken); + + $this->secureRandom->method('generate') + ->willReturnCallback(function ($len) { + return 'random' . $len; + }); + + $this->tokenProvider->expects($this->once()) + ->method('rotate') + ->with( + $appToken, + 'decryptedToken', + 'random72' + )->willReturn($appToken); + + $this->time->method('getTime') + ->willReturn(1000); + + $this->tokenProvider->expects($this->once()) + ->method('updateToken') + ->with( + $this->callback(function (PublicKeyToken $token) { + return $token->getExpires() === 4600; + }) + ); + + $this->crypto->method('encrypt') + ->with('random72', 'random128') + ->willReturn('newEncryptedToken'); + + $this->accessTokenMapper->expects($this->once()) + ->method('update') + ->with( + $this->callback(function (AccessToken $token) { + return $token->getHashedCode() === hash('sha512', 'random128') + && $token->getEncryptedToken() === 'newEncryptedToken'; + }) + ); + + $expected = new JSONResponse([ + 'access_token' => 'random72', + 'token_type' => 'Bearer', + 'expires_in' => 3600, + 'refresh_token' => 'random128', + 'user_id' => 'userId', + ]); + + $this->request->method('getRemoteAddress') + ->willReturn('1.2.3.4'); + + $this->throttler->expects($this->once()) + ->method('resetDelay') + ->with( + '1.2.3.4', + 'login', + ['user' => 'userId'] + ); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret')); + } + + public function testRefreshTokenValidAppTokenBasicAuth(): void { + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setTokenId(1337); + $accessToken->setEncryptedToken('encryptedToken'); + + $this->accessTokenMapper->method('getByCode') + ->with('validrefresh') + ->willReturn($accessToken); + + $client = new Client(); + $client->setClientIdentifier('clientId'); + $client->setSecret(bin2hex('hashedClientSecret')); + $this->clientMapper->method('getByUid') + ->with(42) + ->willReturn($client); + + $this->crypto + ->method('decrypt') + ->with('encryptedToken') + ->willReturn('decryptedToken'); + + $this->crypto + ->method('calculateHMAC') + ->with('clientSecret') + ->willReturn('hashedClientSecret'); + + $appToken = new PublicKeyToken(); + $appToken->setUid('userId'); + $this->tokenProvider->method('getTokenById') + ->with(1337) + ->willThrowException(new ExpiredTokenException($appToken)); + + $this->accessTokenMapper->expects($this->never()) + ->method('delete') + ->with($accessToken); + + $this->secureRandom->method('generate') + ->willReturnCallback(function ($len) { + return 'random' . $len; + }); + + $this->tokenProvider->expects($this->once()) + ->method('rotate') + ->with( + $appToken, + 'decryptedToken', + 'random72' + )->willReturn($appToken); + + $this->time->method('getTime') + ->willReturn(1000); + + $this->tokenProvider->expects($this->once()) + ->method('updateToken') + ->with( + $this->callback(function (PublicKeyToken $token) { + return $token->getExpires() === 4600; + }) + ); + + $this->crypto->method('encrypt') + ->with('random72', 'random128') + ->willReturn('newEncryptedToken'); + + $this->accessTokenMapper->expects($this->once()) + ->method('update') + ->with( + $this->callback(function (AccessToken $token) { + return $token->getHashedCode() === hash('sha512', 'random128') + && $token->getEncryptedToken() === 'newEncryptedToken'; + }) + ); + + $expected = new JSONResponse([ + 'access_token' => 'random72', + 'token_type' => 'Bearer', + 'expires_in' => 3600, + 'refresh_token' => 'random128', + 'user_id' => 'userId', + ]); + + $this->request->server['PHP_AUTH_USER'] = 'clientId'; + $this->request->server['PHP_AUTH_PW'] = 'clientSecret'; + + $this->request->method('getRemoteAddress') + ->willReturn('1.2.3.4'); + + $this->throttler->expects($this->once()) + ->method('resetDelay') + ->with( + '1.2.3.4', + 'login', + ['user' => 'userId'] + ); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', null, null)); + } + + public function testRefreshTokenExpiredAppToken(): void { + $accessToken = new AccessToken(); + $accessToken->setClientId(42); + $accessToken->setTokenId(1337); + $accessToken->setEncryptedToken('encryptedToken'); + + $this->accessTokenMapper->method('getByCode') + ->with('validrefresh') + ->willReturn($accessToken); + + $client = new Client(); + $client->setClientIdentifier('clientId'); + $client->setSecret(bin2hex('hashedClientSecret')); + $this->clientMapper->method('getByUid') + ->with(42) + ->willReturn($client); + + $this->crypto + ->method('decrypt') + ->with('encryptedToken') + ->willReturn('decryptedToken'); + + $this->crypto + ->method('calculateHMAC') + ->with('clientSecret') + ->willReturn('hashedClientSecret'); + + $appToken = new PublicKeyToken(); + $appToken->setUid('userId'); + $this->tokenProvider->method('getTokenById') + ->with(1337) + ->willReturn($appToken); + + $this->accessTokenMapper->expects($this->never()) + ->method('delete') + ->with($accessToken); + + $this->secureRandom->method('generate') + ->willReturnCallback(function ($len) { + return 'random' . $len; + }); + + $this->tokenProvider->expects($this->once()) + ->method('rotate') + ->with( + $appToken, + 'decryptedToken', + 'random72' + )->willReturn($appToken); + + $this->time->method('getTime') + ->willReturn(1000); + + $this->tokenProvider->expects($this->once()) + ->method('updateToken') + ->with( + $this->callback(function (PublicKeyToken $token) { + return $token->getExpires() === 4600; + }) + ); + + $this->crypto->method('encrypt') + ->with('random72', 'random128') + ->willReturn('newEncryptedToken'); + + $this->accessTokenMapper->expects($this->once()) + ->method('update') + ->with( + $this->callback(function (AccessToken $token) { + return $token->getHashedCode() === hash('sha512', 'random128') + && $token->getEncryptedToken() === 'newEncryptedToken'; + }) + ); + + $expected = new JSONResponse([ + 'access_token' => 'random72', + 'token_type' => 'Bearer', + 'expires_in' => 3600, + 'refresh_token' => 'random128', + 'user_id' => 'userId', + ]); + + $this->request->method('getRemoteAddress') + ->willReturn('1.2.3.4'); + + $this->throttler->expects($this->once()) + ->method('resetDelay') + ->with( + '1.2.3.4', + 'login', + ['user' => 'userId'] + ); + + $this->assertEquals($expected, $this->oauthApiController->getToken('refresh_token', null, 'validrefresh', 'clientId', 'clientSecret')); + } +} diff --git a/apps/oauth2/tests/Controller/SettingsControllerTest.php b/apps/oauth2/tests/Controller/SettingsControllerTest.php new file mode 100644 index 00000000000..030a220e3d7 --- /dev/null +++ b/apps/oauth2/tests/Controller/SettingsControllerTest.php @@ -0,0 +1,188 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\OAuth2\Tests\Controller; + +use OCA\OAuth2\Controller\SettingsController; +use OCA\OAuth2\Db\AccessTokenMapper; +use OCA\OAuth2\Db\Client; +use OCA\OAuth2\Db\ClientMapper; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\Authentication\Token\IProvider as IAuthTokenProvider; +use OCP\IL10N; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Security\ICrypto; +use OCP\Security\ISecureRandom; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class SettingsControllerTest extends TestCase { + /** @var IRequest|\PHPUnit\Framework\MockObject\MockObject */ + private $request; + /** @var ClientMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $clientMapper; + /** @var ISecureRandom|\PHPUnit\Framework\MockObject\MockObject */ + private $secureRandom; + /** @var AccessTokenMapper|\PHPUnit\Framework\MockObject\MockObject */ + private $accessTokenMapper; + /** @var IAuthTokenProvider|\PHPUnit\Framework\MockObject\MockObject */ + private $authTokenProvider; + /** @var IUserManager|\PHPUnit\Framework\MockObject\MockObject */ + private $userManager; + /** @var SettingsController */ + private $settingsController; + /** @var IL10N|\PHPUnit\Framework\MockObject\MockObject */ + private $l; + /** @var ICrypto|\PHPUnit\Framework\MockObject\MockObject */ + private $crypto; + + protected function setUp(): void { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->clientMapper = $this->createMock(ClientMapper::class); + $this->secureRandom = $this->createMock(ISecureRandom::class); + $this->accessTokenMapper = $this->createMock(AccessTokenMapper::class); + $this->authTokenProvider = $this->createMock(IAuthTokenProvider::class); + $this->userManager = $this->createMock(IUserManager::class); + $this->crypto = $this->createMock(ICrypto::class); + $this->l = $this->createMock(IL10N::class); + $this->l->method('t') + ->willReturnArgument(0); + $this->settingsController = new SettingsController( + 'oauth2', + $this->request, + $this->clientMapper, + $this->secureRandom, + $this->accessTokenMapper, + $this->l, + $this->authTokenProvider, + $this->userManager, + $this->crypto + ); + + } + + public function testAddClient(): void { + $this->secureRandom + ->expects($this->exactly(2)) + ->method('generate') + ->with(64, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') + ->willReturnOnConsecutiveCalls( + 'MySecret', + 'MyClientIdentifier'); + + $this->crypto + ->expects($this->once()) + ->method('calculateHMAC') + ->willReturn('MyHashedSecret'); + + $client = new Client(); + $client->setName('My Client Name'); + $client->setRedirectUri('https://example.com/'); + $client->setSecret(bin2hex('MyHashedSecret')); + $client->setClientIdentifier('MyClientIdentifier'); + + $this->clientMapper + ->expects($this->once()) + ->method('insert') + ->with($this->callback(function (Client $c) { + return $c->getName() === 'My Client Name' + && $c->getRedirectUri() === 'https://example.com/' + && $c->getSecret() === bin2hex('MyHashedSecret') + && $c->getClientIdentifier() === 'MyClientIdentifier'; + }))->willReturnCallback(function (Client $c) { + $c->setId(42); + return $c; + }); + + $result = $this->settingsController->addClient('My Client Name', 'https://example.com/'); + $this->assertInstanceOf(JSONResponse::class, $result); + + $data = $result->getData(); + + $this->assertEquals([ + 'id' => 42, + 'name' => 'My Client Name', + 'redirectUri' => 'https://example.com/', + 'clientId' => 'MyClientIdentifier', + 'clientSecret' => 'MySecret', + ], $data); + } + + public function testDeleteClient(): void { + + $userManager = Server::get(IUserManager::class); + // count other users in the db before adding our own + $count = 0; + $function = function (IUser $user) use (&$count): void { + if ($user->getLastLogin() > 0) { + $count++; + } + }; + $userManager->callForAllUsers($function); + $user1 = $userManager->createUser('test101', 'test101'); + $user1->updateLastLoginTimestamp(); + $tokenProviderMock = $this->getMockBuilder(IAuthTokenProvider::class)->getMock(); + + // expect one call per user and ensure the correct client name + $tokenProviderMock + ->expects($this->exactly($count + 1)) + ->method('invalidateTokensOfUser') + ->with($this->isType('string'), 'My Client Name'); + + $client = new Client(); + $client->setId(123); + $client->setName('My Client Name'); + $client->setRedirectUri('https://example.com/'); + $client->setSecret(bin2hex('MyHashedSecret')); + $client->setClientIdentifier('MyClientIdentifier'); + + $this->clientMapper + ->method('getByUid') + ->with(123) + ->willReturn($client); + $this->accessTokenMapper + ->expects($this->once()) + ->method('deleteByClientId') + ->with(123); + $this->clientMapper + ->expects($this->once()) + ->method('delete') + ->with($client); + + $settingsController = new SettingsController( + 'oauth2', + $this->request, + $this->clientMapper, + $this->secureRandom, + $this->accessTokenMapper, + $this->l, + $tokenProviderMock, + $userManager, + $this->crypto + ); + + $result = $settingsController->deleteClient(123); + $this->assertInstanceOf(JSONResponse::class, $result); + $this->assertEquals([], $result->getData()); + + $user1->delete(); + } + + public function testInvalidRedirectUri(): void { + $result = $this->settingsController->addClient('test', 'invalidurl'); + + $this->assertEquals(Http::STATUS_BAD_REQUEST, $result->getStatus()); + $this->assertSame(['message' => 'Your redirect URL needs to be a full URL for example: https://yourdomain.com/path'], $result->getData()); + } +} diff --git a/apps/oauth2/tests/Db/AccessTokenMapperTest.php b/apps/oauth2/tests/Db/AccessTokenMapperTest.php new file mode 100644 index 00000000000..41a79fe725b --- /dev/null +++ b/apps/oauth2/tests/Db/AccessTokenMapperTest.php @@ -0,0 +1,59 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\OAuth2\Tests\Db; + +use OCA\OAuth2\Db\AccessToken; +use OCA\OAuth2\Db\AccessTokenMapper; +use OCA\OAuth2\Exceptions\AccessTokenNotFoundException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IDBConnection; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class AccessTokenMapperTest extends TestCase { + /** @var AccessTokenMapper */ + private $accessTokenMapper; + + protected function setUp(): void { + parent::setUp(); + $this->accessTokenMapper = new AccessTokenMapper(Server::get(IDBConnection::class), Server::get(ITimeFactory::class)); + } + + public function testGetByCode(): void { + $this->accessTokenMapper->deleteByClientId(1234); + $token = new AccessToken(); + $token->setClientId(1234); + $token->setTokenId((string)time()); + $token->setEncryptedToken('MyEncryptedToken'); + $token->setHashedCode(hash('sha512', 'MyAwesomeToken')); + $this->accessTokenMapper->insert($token); + $token->resetUpdatedFields(); + + $result = $this->accessTokenMapper->getByCode('MyAwesomeToken'); + $this->assertEquals($token, $result); + $this->accessTokenMapper->delete($token); + } + + + public function testDeleteByClientId(): void { + $this->expectException(AccessTokenNotFoundException::class); + + $this->accessTokenMapper->deleteByClientId(1234); + $token = new AccessToken(); + $token->setClientId(1234); + $token->setTokenId((string)time()); + $token->setEncryptedToken('MyEncryptedToken'); + $token->setHashedCode(hash('sha512', 'MyAwesomeToken')); + $this->accessTokenMapper->insert($token); + $token->resetUpdatedFields(); + $this->accessTokenMapper->deleteByClientId(1234); + $this->accessTokenMapper->getByCode('MyAwesomeToken'); + } +} diff --git a/apps/oauth2/tests/Db/ClientMapperTest.php b/apps/oauth2/tests/Db/ClientMapperTest.php new file mode 100644 index 00000000000..2e8d20ad200 --- /dev/null +++ b/apps/oauth2/tests/Db/ClientMapperTest.php @@ -0,0 +1,82 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\OAuth2\Tests\Db; + +use OCA\OAuth2\Db\Client; +use OCA\OAuth2\Db\ClientMapper; +use OCA\OAuth2\Exceptions\ClientNotFoundException; +use OCP\IDBConnection; +use OCP\Server; +use Test\TestCase; + +/** + * @group DB + */ +class ClientMapperTest extends TestCase { + /** @var ClientMapper */ + private $clientMapper; + + protected function setUp(): void { + parent::setUp(); + $this->clientMapper = new ClientMapper(Server::get(IDBConnection::class)); + } + + protected function tearDown(): void { + $query = Server::get(IDBConnection::class)->getQueryBuilder(); + $query->delete('oauth2_clients')->execute(); + + parent::tearDown(); + } + + public function testGetByIdentifier(): void { + $client = new Client(); + $client->setClientIdentifier('MyAwesomeClientIdentifier'); + $client->setName('Client Name'); + $client->setRedirectUri('https://example.com/'); + $client->setSecret('TotallyNotSecret'); + $this->clientMapper->insert($client); + $client->resetUpdatedFields(); + $this->assertEquals($client, $this->clientMapper->getByIdentifier('MyAwesomeClientIdentifier')); + } + + public function testGetByIdentifierNotExisting(): void { + $this->expectException(ClientNotFoundException::class); + + $this->clientMapper->getByIdentifier('MyTotallyNotExistingClient'); + } + + public function testGetByUid(): void { + $client = new Client(); + $client->setClientIdentifier('MyNewClient'); + $client->setName('Client Name'); + $client->setRedirectUri('https://example.com/'); + $client->setSecret('TotallyNotSecret'); + $this->clientMapper->insert($client); + $client->resetUpdatedFields(); + $this->assertEquals($client, $this->clientMapper->getByUid($client->getId())); + } + + public function testGetByUidNotExisting(): void { + $this->expectException(ClientNotFoundException::class); + + $this->clientMapper->getByUid(1234); + } + + public function testGetClients(): void { + $this->assertSame('array', gettype($this->clientMapper->getClients())); + } + + public function testInsertLongEncryptedSecret(): void { + $client = new Client(); + $client->setClientIdentifier('MyNewClient'); + $client->setName('Client Name'); + $client->setRedirectUri('https://example.com/'); + $client->setSecret('b81dc8e2dc178817bf28ca7b37265aa96559ca02e6dcdeb74b42221d096ed5ef63681e836ae0ba1077b5fb5e6c2fa7748c78463f66fe0110c8dcb8dd7eb0305b16d0cd993e2ae275879994a2abf88c68|e466d9befa6b0102341458e45ecd551a|013af9e277374483123437f180a3b0371a411ad4f34c451547909769181a7d7cc191f0f5c2de78376d124dd7751b8c9660aabdd913f5e071fc6b819ba2e3d919|3'); + $this->clientMapper->insert($client); + $this->assertTrue(true); + } +} diff --git a/apps/oauth2/tests/Settings/AdminTest.php b/apps/oauth2/tests/Settings/AdminTest.php new file mode 100644 index 00000000000..0f08bb30276 --- /dev/null +++ b/apps/oauth2/tests/Settings/AdminTest.php @@ -0,0 +1,60 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\OAuth2\Tests\Settings; + +use OCA\OAuth2\Db\ClientMapper; +use OCA\OAuth2\Settings\Admin; +use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class AdminTest extends TestCase { + + /** @var Admin|MockObject */ + private $admin; + + /** @var IInitialState|MockObject */ + private $initialState; + + /** @var ClientMapper|MockObject */ + private $clientMapper; + + protected function setUp(): void { + parent::setUp(); + + $this->initialState = $this->createMock(IInitialState::class); + $this->clientMapper = $this->createMock(ClientMapper::class); + + $this->admin = new Admin( + $this->initialState, + $this->clientMapper, + $this->createMock(IURLGenerator::class), + $this->createMock(LoggerInterface::class) + ); + } + + public function testGetForm(): void { + $expected = new TemplateResponse( + 'oauth2', + 'admin', + [], + '' + ); + $this->assertEquals($expected, $this->admin->getForm()); + } + + public function testGetSection(): void { + $this->assertSame('security', $this->admin->getSection()); + } + + public function testGetPriority(): void { + $this->assertSame(100, $this->admin->getPriority()); + } +} |