diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 84a9678..3a08114 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,4 +124,4 @@ jobs: - name: Run phpstan if: success() || failure() run: composer stan - continue-on-error: true \ No newline at end of file + continue-on-error: true diff --git a/.gitignore b/.gitignore index 4722bb4..f242c89 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.idea composer.lock .php_cs.cache +.phpunit.result.cache diff --git a/Docs/Documentation/Social.md b/Docs/Documentation/Social.md index c63d374..e5da800 100644 --- a/Docs/Documentation/Social.md +++ b/Docs/Documentation/Social.md @@ -1,7 +1,7 @@ Social Layer ============ The social layer provide a easier way to handle social provider authentication -with provides using OAuth1 or OAuth2. The idea is to provide a base +with provides using OAuth1 or OAuth2. The idea is to provide a base interface for both OAuth and OAuth2. ***Make sure to load the bootstap.php file of this plugin!*** @@ -12,9 +12,10 @@ We have mappers to allow you a quick start with these providers: - Facebook - Google - Instagram -- LinkedIn +- LinkedIn (Deprecated, they switched to OpenID-Connect) +- LinkedInOpenIDConnect (New, OIDC based authentication) - Pinterest -- Tumblr +- Tumblr - Twitter You must define 'options.redirectUri', 'options.clientId' and @@ -57,7 +58,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory; ->getAuthorizationUrl($this->request) ); } - + /** * Callback to get user information from provider * @@ -80,7 +81,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory; } $data = $server->getUser($this->request); $data = (new MapUser())($server, $data); - + //your code } catch (\Exception $e) { $this->log($log); @@ -92,4 +93,4 @@ Working with cakephp/authentication If you're using the new cakephp/authentication we recommend you to use the SocialAuthenticator and SocialMiddleware provided in this plugin. For more details of how to handle social authentication with cakephp/authentication, please check -how we implemented at CakeDC/Users plugins. \ No newline at end of file +how we implemented at CakeDC/Users plugins. diff --git a/composer.json b/composer.json index ce92741..1f35ce7 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,8 @@ "cakephp/authorization": "^3.0", "cakephp/cakephp-codesniffer": "^5.0", "cakephp/authentication": "^3.0", - "yubico/u2flib-server": "^1.0" + "yubico/u2flib-server": "^1.0", + "firebase/php-jwt": "^v6.8" }, "suggest": { }, diff --git a/config/auth.php b/config/auth.php index 2dafc60..86436e6 100644 --- a/config/auth.php +++ b/config/auth.php @@ -10,6 +10,7 @@ */ use Cake\Routing\Router; + return [ 'OAuth.path' => ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialLogin', 'prefix' => null], 'OAuth.providers' => [ @@ -23,7 +24,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/facebook', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/facebook', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/facebook', - ] + ], ], 'twitter' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth1Service', @@ -33,8 +34,9 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/twitter', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/twitter', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/twitter', - ] + ], ], + // Deprecated, LinkedIn switched to OpenID-Connect and OAuth2 is no longer working properly 'linkedIn' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', 'className' => 'League\OAuth2\Client\Provider\LinkedIn', @@ -43,7 +45,18 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/linkedIn', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedIn', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedIn', - ] + ], + ], + 'linkedInOpenIDConnect' => [ + 'service' => 'CakeDC\Auth\Social\Service\OpenIDConnectService', + 'className' => 'League\OAuth2\Client\Provider\LinkedIn', + 'mapper' => 'CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect', + 'options' => [ + 'redirectUri' => Router::fullBaseUrl() . '/auth/linkedInOpenIDConnect', + 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedInOpenIDConnect', + 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedInOpenIDConnect', + 'defaultScopes' => ['email', 'openid', 'profile'], + ], ], 'instagram' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -53,7 +66,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/instagram', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/instagram', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/instagram', - ] + ], ], 'google' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -64,7 +77,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/google', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/google', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/google', - ] + ], ], 'amazon' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -74,7 +87,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/amazon', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/amazon', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/amazon', - ] + ], ], 'azure' => [ 'service' => 'CakeDC\Auth\Social\Service\OAuth2Service', @@ -84,7 +97,7 @@ 'redirectUri' => Router::fullBaseUrl() . '/auth/azure', 'linkSocialUri' => Router::fullBaseUrl() . '/link-social/azure', 'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/azure', - ] + ], ], ], 'TwoFactorProcessors' => [ @@ -110,7 +123,7 @@ // QR-code provider (more on this later) 'qrcodeprovider' => new \RobThree\Auth\Providers\Qr\EndroidQrCodeProvider(), // Random Number Generator provider (more on this later) - 'rngprovider' => null + 'rngprovider' => null, ], 'Webauthn2fa' => [ 'enabled' => false, @@ -122,6 +135,6 @@ 'controller' => 'Users', 'action' => 'webauthn2fa', 'prefix' => false, - ] - ] + ], + ], ]; diff --git a/config/permissions.php b/config/permissions.php index 930b853..46d453a 100644 --- a/config/permissions.php +++ b/config/permissions.php @@ -105,14 +105,10 @@ 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'resetOneTimePasswordAuthenticator', - 'allowed' => function (array $user, $role, \Cake\Http\ServerRequest $request) { - $userId = \Cake\Utility\Hash::get($request->getAttribute('params'), 'pass.0'); - if (!empty($userId) && !empty($user)) { - return $userId === $user['id']; - } - - return false; - } + 'allowed' => [ + 'className' => \CakeDC\Auth\Rbac\Rules\OTPRule::class, + 'option' => [] + ], ], //all roles allowed to Pages/display [ diff --git a/src/Rbac/CachedRbac.php b/src/Rbac/CachedRbac.php new file mode 100644 index 0000000..b73d887 --- /dev/null +++ b/src/Rbac/CachedRbac.php @@ -0,0 +1,207 @@ +undasherize = $config['undasherize']; + } + $this->permissionsMap = Cache::remember('auth_permissions', function () { + return $this->buildPermissionsMap(); + }, '_cakedc_auth_'); + } + + /** + * @return array + */ + public function getPermissions(): array + { + return $this->permissions; + } + + /** + * @return array + */ + public function buildPermissionsMap() + { + $asArray = function ($permission, $key, $default = null) { + if ($default !== null && !array_key_exists($key, $permission)) { + return [$default, '_']; + } + if (!array_key_exists($key, $permission) || $permission[$key] == false || $permission[$key] == null) { + return ['_']; + } + $item = $permission[$key]; + if (is_string($item)) { + return [$item]; + } + return (array)$item; + }; + $map = []; + foreach ($this->permissions as $permission) { + if (isset($permission['role'])) { + $role = $permission['role']; + } else { + $role = '*'; + } + $roles = (array)$role; + foreach ($roles as $role) { + $prefixes = $asArray($permission, 'prefix', '*'); + $plugins = $asArray($permission, 'plugin', '*'); + $controllers = $asArray($permission, 'controller', '*'); + foreach ($prefixes as $prefix) { + foreach ($plugins as $plugin) { + foreach ($controllers as $controller) { + $key = "$prefix|$plugin|$controller"; + $map[$role][$key][] = $permission; + } + } + } + } + } + + return $map; + } + + /** + * @param array $permissions permissions + * @return void + */ + public function setPermissions($permissions): void + { + $this->permissions = $permissions; + } + + /** + * Match against permissions, return if matched + * Permissions are processed based on the 'permissions' config values + * + * @param array|\ArrayAccess $user current user array + * @param \Psr\Http\Message\ServerRequestInterface $request request + * @return bool true if there is a match in permissions + */ + public function checkPermissions($user, ServerRequestInterface $request): bool + { + $roleField = $this->getConfig('role_field'); + $defaultRole = $this->getConfig('default_role'); + $role = Hash::get($user, $roleField, $defaultRole); + + $keys = $this->permissionKeys($request); + foreach ([$role, '*'] as $checkRole) { + if (!array_key_exists($checkRole, $this->permissionsMap)) { + continue; + } + foreach ($keys as $key) { + if (!array_key_exists($key, $this->permissionsMap[$checkRole])) { + continue; + } + $permissions = $this->permissionsMap[$checkRole][$key]; + foreach ($permissions as $permission) { + $matchResult = $this->_matchPermission($permission, $user, $role, $request); + if ($matchResult !== null) { + if ($this->getConfig('log')) { + $this->log($matchResult->getReason(), LogLevel::DEBUG); + } + + return $matchResult->isAllowed(); + } + } + } + } + + return false; + } + + /** + * Build list of permission keys to search. + * + * @param \Psr\Http\Message\ServerRequestInterface $request request + * @return array list of key to search based on request. + */ + protected function permissionKeys(ServerRequestInterface $request) + { + $params = $request->getAttribute('params'); + $permission = [ + 'prefix' => $params['prefix'] ?? null, + 'plugin' => $params['plugin'] ?? null, + 'controller' => $params['controller'] ?? null, + ]; + $keys = []; + $getKeys = function ($permission, $key) { + if ($permission[$key] == false || $permission[$key] == null) { + return ['_', '*']; + } + $item = $permission[$key]; + if ($this->undasherize) { + $item = Inflector::camelize((string)$item, '-'); + } + return [$item, '*']; + }; + $prefixes = $getKeys($permission, 'prefix'); + $plugins = $getKeys($permission, 'plugin'); + $controllers = $getKeys($permission, 'controller'); + foreach ($prefixes as $prefix) { + foreach ($plugins as $plugin) { + foreach ($controllers as $controller) { + $keys[] = "$prefix|$plugin|$controller"; + } + } + } + + return $keys; + } + +} diff --git a/src/Rbac/Permissions/AbstractProvider.php b/src/Rbac/Permissions/AbstractProvider.php index 60a52d8..150697f 100644 --- a/src/Rbac/Permissions/AbstractProvider.php +++ b/src/Rbac/Permissions/AbstractProvider.php @@ -99,14 +99,10 @@ public function __construct(array $config = []) 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'resetOneTimePasswordAuthenticator', - 'allowed' => function (array $user, string $role, ServerRequest $request): bool { - $userId = Hash::get($request->getAttribute('params'), 'pass.0'); - if (!empty($userId) && !empty($user)) { - return $userId === $user['id']; - } - - return false; - }, + 'allowed' => [ + 'className' => \CakeDC\Auth\Rbac\Rules\OTPRule::class, + 'option' => [] + ], ], //all roles allowed to Pages/display [ diff --git a/src/Rbac/Rbac.php b/src/Rbac/Rbac.php index d65eebf..01cfa42 100644 --- a/src/Rbac/Rbac.php +++ b/src/Rbac/Rbac.php @@ -22,6 +22,7 @@ use CakeDC\Auth\Rbac\Permissions\AbstractProvider; use CakeDC\Auth\Rbac\Permissions\ConfigProvider; use CakeDC\Auth\Rbac\Rules\Rule; +use CakeDC\Auth\Rbac\Rules\RuleRegistry; use Psr\Http\Message\ServerRequestInterface; use Psr\Log\LogLevel; use RuntimeException; @@ -186,7 +187,12 @@ protected function _matchPermission(array $permission, array|ArrayAccess $user, if (!is_string($value) && is_callable($value)) { $return = (bool)call_user_func($value, $user, $role, $request); } elseif ($value instanceof Rule) { - $return = $value->allowed($user, $role, $request); + $return = (bool)$value->allowed($user, $role, $request); + } elseif (is_array($value) && array_key_exists('className', $value)) { + $rule = RuleRegistry::get($value['className'], $value['options'] ?? []); + if ($rule instanceof Rule) { + $return = (bool)$rule->allowed($user, $role, $request); + } } elseif ($key === 'bypassAuth' && $value === true) { $return = true; } elseif ($key === 'allowed') { diff --git a/src/Rbac/Rules/OTPRule.php b/src/Rbac/Rules/OTPRule.php new file mode 100644 index 0000000..58eadee --- /dev/null +++ b/src/Rbac/Rules/OTPRule.php @@ -0,0 +1,36 @@ +getAttribute('params'), 'pass.0'); + if (!empty($userId) && !empty($user)) { + return $userId === $user['id']; + } + + return false; + } +} diff --git a/src/Social/Mapper/LinkedInOpenIDConnect.php b/src/Social/Mapper/LinkedInOpenIDConnect.php new file mode 100644 index 0000000..28af68d --- /dev/null +++ b/src/Social/Mapper/LinkedInOpenIDConnect.php @@ -0,0 +1,38 @@ + 'picture', + 'first_name' => 'given_name', + 'last_name' => 'family_name', + 'email' => 'email', + 'link' => 'link', + 'id' => 'sub', + ]; + + protected function _link(): string + { + // no way to retrieve the public url from the users profile + + return 'https://www.linkedin.com'; + } +} diff --git a/src/Social/Service/OpenIDConnectService.php b/src/Social/Service/OpenIDConnectService.php new file mode 100644 index 0000000..bdd3d4c --- /dev/null +++ b/src/Social/Service/OpenIDConnectService.php @@ -0,0 +1,89 @@ + [ + 'baseUrl' => 'https://www.linkedin.com/', + 'url' => 'https://www.linkedin.com/oauth/.well-known/openid-configuration', + 'jwk' => [ + 'defaultAlgorithm' => 'RS256', + ], + ], + ]; + + public function getUser(ServerRequestInterface $request): array + { + if (!$request instanceof ServerRequest) { + throw new \BadMethodCallException('Request must be an instance of ServerRequest'); + } + if (!$this->validate($request)) { + throw new BadRequestException('Invalid OAuth2 state'); + } + + $code = $request->getQuery('code'); + /** @var \League\OAuth2\Client\Token\AccessToken $token */ + $token = $this->provider->getAccessToken('authorization_code', ['code' => $code]); + $tokenValues = $token->getValues(); + $idToken = $tokenValues['id_token'] ?? null; + if (!$idToken) { + throw new BadRequestException('Missing id_token in response'); + } + try { + $idTokenDecoded = JWT::decode($idToken, $this->getIdTokenKeys()); + + return ['token' => $token] + (array)$idTokenDecoded; + } catch (\Exception $ex) { + throw new BadRequestException('Invalid id token. ' . $ex->getMessage()); + } + } + + protected function getIdTokenKeys(): array + { + $discoverData = $this->discover(); + $jwksUri = $discoverData['jwks_uri'] ?? null; + if (!$jwksUri) { + throw new BadRequestException( + 'No `jwks_uri` in discover data. Unable to retrieve the JWT signature public key' + ); + } + if (strpos($jwksUri, $this->getConfig('openid.baseUrl')) !== 0) { + throw new BadRequestException( + 'Invalid `jwks_uri` in discover data. It is not pointing to ' . + $this->getConfig('openid.baseUrl') + ); + } + $client = $this->getHttpClient(); + $jwksData = $client->get($jwksUri)->getJson(); + if (!$jwksData) { + throw new BadRequestException( + 'Unable to retrieve jwks. Not found in the `jwks_uri` contents' + ); + } + + return JWK::parseKeySet($jwksData, $this->getConfig('openid.jwk.defaultAlgorithm')); + } + + public function discover(): array + { + $openidUrl = $this->getConfig('openid.url'); + $client = $this->getHttpClient(); + + return $client->get($openidUrl)->getJson(); + } + + protected function getHttpClient(): Client + { + return new Client(); + } +} diff --git a/tests/TestCase/Rbac/CachedRbacTest.php b/tests/TestCase/Rbac/CachedRbacTest.php new file mode 100644 index 0000000..055665f --- /dev/null +++ b/tests/TestCase/Rbac/CachedRbacTest.php @@ -0,0 +1,1120 @@ +defaultPermissions = [ + //all bypass + [ + 'prefix' => false, + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => [ + // LoginTrait + 'socialLogin', + 'login', + 'logout', + 'socialEmail', + 'verify', + // RegisterTrait + 'register', + 'validateEmail', + // PasswordManagementTrait used in RegisterTrait + 'changePassword', + 'resetPassword', + 'requestResetPassword', + // UserValidationTrait used in PasswordManagementTrait + 'resendTokenValidation', + 'linkSocial', + ], + 'bypassAuth' => true, + ], + [ + 'prefix' => false, + 'plugin' => 'CakeDC/Users', + 'controller' => 'SocialAccounts', + 'action' => [ + 'validateAccount', + 'resendValidation', + ], + 'bypassAuth' => true, + ], + //admin role allowed to all the things + [ + 'role' => 'admin', + 'prefix' => '*', + 'extension' => '*', + 'plugin' => '*', + 'controller' => '*', + 'action' => '*', + ], + //specific actions allowed for the all roles in Users plugin + [ + 'role' => '*', + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => ['profile', 'logout', 'linkSocial', 'callbackLinkSocial'], + ], + [ + 'role' => '*', + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'resetOneTimePasswordAuthenticator', + 'allowed' => [ + 'className' => \CakeDC\Auth\Rbac\Rules\OTPRule::class, + 'option' => [] + ], + ], + //all roles allowed to Pages/display + [ + 'role' => '*', + 'controller' => 'Pages', + 'action' => 'display', + ], + [ + 'role' => '*', + 'plugin' => 'DebugKit', + 'controller' => '*', + 'action' => '*', + 'bypassAuth' => true, + ], + ]; + $this->rbac = new CachedRbac([ + 'permissions' => $this->defaultPermissions, + ]); + \Cake\Cache\Cache::clear('_cakedc_auth_'); + } + + /** + * Tears down the fixture, for example, closes a network connection. + * This method is called after a test is executed. + */ + public function tearDown(): void + { + unset($this->rbac); + \Cake\Cache\Cache::clear('_cakedc_auth_'); + } + + public function testConstructGetDefaultPermissions() + { + $this->rbac = new CachedRbac(); + $result = $this->rbac->getPermissions(); + $this->assertSame($this->defaultPermissions, $result); + } + + public function testConstructBadProvider() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Class "\Exception" must extend AbstractProvider'); + $this->rbac = new CachedRbac([ + 'permissions_provider_class' => '\Exception', + ]); + } + + public function testConstructSetPermissions() + { + $this->rbac = new CachedRbac([ + 'permissions' => [], + ]); + $this->assertEmpty($this->rbac->getPermissions()); + } + + protected function assertConstructorPermissions($instance, $config, $permissions) + { + $reflectedClass = new ReflectionClass($instance); + $constructor = $reflectedClass->getConstructor(); + $constructor->invoke($this->simpleRbacAuthorize, $this->registry, $config); + + //we should have the default permissions + $resultPermissions = $this->simpleRbacAuthorize->getConfig('permissions'); + $this->assertEquals($permissions, $resultPermissions); + } + + /** + * @dataProvider providerAuthorize + */ + public function testAuthorize($permissions, $user, $requestParams, $expected) + { + $this->rbac = new CachedRbac(['permissions' => $permissions, 'undasherize' => true]); + $request = $this->_requestFromArray($requestParams); + + $result = $this->rbac->checkPermissions($user, $request); + $this->assertSame($expected, $result); + } + + public function testAuthorizeRuleClass() + { + $trueRuleMock = self::getMockBuilder(Owner::class) + ->onlyMethods(['allowed']) + ->getMock(); + $trueRuleMock->expects(self::any()) + ->method('allowed') + ->willReturn(true); + + $user = [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ]; + $requestParams = [ + 'controller' => 'Tests', + 'action' => 'one', + ]; + + $permissions = [ + [ + 'role' => ['test'], + 'controller' => '*', + 'action' => 'one', + 'allowed' => $trueRuleMock, + ], + ]; + + $this->rbac = new CachedRbac(['permissions' => $permissions, 'undasherize' => true]); + $request = $this->_requestFromArray($requestParams); + + $result = $this->rbac->checkPermissions($user, $request); + $this->assertSame(true, $result); + } + + public static function providerAuthorize() + { + return [ + 'deny-first-discard-after' => [ + //permissions + [ + [ + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'one', + 'allowed' => false, + ], + ], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + false, + ], + 'star-invert' => [ + //permissions + [[ + '*plugin' => 'Tests', + '*role' => 'test', + '*controller' => 'Tests', + '*action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'something', + ], + //request + [ + 'plugin' => 'something', + 'controller' => 'something', + 'action' => 'something', + ], + //expected + true, + ], + 'star-invert-deny' => [ + //permissions + [[ + '*plugin' => 'Tests', + '*role' => 'test', + '*controller' => 'Tests', + '*action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'something', + ], + //request + [ + 'plugin' => 'something', + 'controller' => 'something', + 'action' => 'test', + ], + //expected + false, + ], + 'user-arr' => [ + //permissions + [ + [ + 'username' => 'luke', + 'user.id' => 1, + 'profile.id' => 256, + 'user.profile.signature' => "Hi I'm luke", + 'user.allowed' => false, + 'controller' => 'Tests', + 'action' => 'one', + ], + ], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + 'profile' => [ + 'id' => 256, + 'signature' => "Hi I'm luke", + ], + 'allowed' => false, + ], + //request + [ + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'happy-strict-all' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + 'allowed' => true, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'happy-strict-all-deny' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + false, + ], + 'happy-plugin-null-allowed-null' => [ + //permissions + [[ + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => null, + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'happy-plugin-asterisk' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Any', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'happy-plugin-asterisk-main-app' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => null, + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'happy-role-asterisk' => [ + //permissions + [[ + 'role' => '*', + 'controller' => 'Tests', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'any-role', + ], + //request + [ + 'plugin' => null, + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'happy-controller-asterisk' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => '*', + 'action' => 'test', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'happy-action-asterisk' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'Tests', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'any', + ], + //expected + true, + ], + 'happy-some-asterisk-allowed' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => '*', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'any', + ], + //expected + true, + ], + 'happy-some-asterisk-deny' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => 'test', + 'controller' => '*', + 'action' => '*', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'any', + ], + //expected + false, + ], + 'all-deny' => [ + //permissions + [[ + 'plugin' => '*', + 'role' => '*', + 'controller' => '*', + 'action' => '*', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Any', + 'controller' => 'Any', + 'action' => 'any', + ], + //expected + false, + ], + 'dasherized' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'role' => 'test', + 'controller' => 'TestTests', + 'action' => 'TestAction', + 'allowed' => true, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'tests', + 'controller' => 'test-tests', + 'action' => 'test-action', + ], + //expected + true, + ], + 'happy-array' => [ + //permissions + [[ + 'plugin' => ['Tests'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'happy-array-deny' => [ + //permissions + [[ + 'plugin' => ['Tests'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'three', + ], + //expected + false, + ], + 'happy-callback-check-params' => [ + //permissions + [[ + 'plugin' => ['Tests'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + // 'allowed' => function ($user, $role, $request) { + // return $user['id'] === 1 && $role = 'test' && $request->getParam('plugin') == 'Tests'; + // }, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'happy-callback-deny' => [ + //permissions + [[ + 'plugin' => ['*'], + 'role' => ['test'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + 'allowed' => false, + // function ($user, $role, $request) { + // return false; + // }, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + false, + ], + 'happy-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['Admin'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'Admin', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'deny-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['admin'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + false, + ], + 'star-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => '*', + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'array-prefix' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['One', 'Admin'], + 'controller' => '*', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'Admin', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'array-prefix-deny' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['one', 'admin'], + 'controller' => '*', + 'action' => 'one', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + false, + ], + 'happy-ext' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => ['Admin'], + 'extension' => ['csv'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'Admin', + '_ext' => 'csv', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'deny-ext' => [ + //permissions + [[ + 'role' => ['test'], + 'extension' => ['csv'], + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'controller' => 'Tests', + '_ext' => 'csv', + 'action' => 'one', + ], + //expected + false, + ], + 'star-ext' => [ + //permissions + [[ + 'role' => ['test'], + 'prefix' => '*', + 'extension' => '*', + 'controller' => ['Tests'], + 'action' => ['one', 'two'], + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'admin', + '_ext' => 'other', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'array-ext' => [ + //permissions + [[ + 'role' => ['test'], + 'extension' => ['csv', 'pdf'], + 'controller' => '*', + 'action' => '*', + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + '_ext' => 'csv', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + true, + ], + 'array-ext-deny' => [ + //permissions + [[ + 'role' => ['test'], + 'extension' => ['csv', 'docx'], + 'controller' => '*', + 'action' => 'one', + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'prefix' => 'csv', + 'controller' => 'Tests', + 'action' => 'one', + ], + //expected + false, + ], + 'bypass-auth' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'bypassAuth' => true, + ]], + //user + [], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'bypass-auth-callable' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'bypassAuth' => false, + ]], + //user + [], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + false, + ], + 'bypass-auth-rule-not-allowed-order-matters' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'role' => '*', + 'bypassAuth' => true, + 'allowed' => false, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'rule-not-allowed-bypass-auth-order-matters' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'role' => '*', + 'allowed' => false, + 'bypassAuth' => true, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + false, + ], + 'bypass-auth-user-not-authorized-another-role' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'role' => 'admin', + 'bypassAuth' => true, + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'test', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + false, + ], + 'custom-rule-any-role' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'role' => 'admin', + // 'allowed' => new SampleRule(), + ]], + //user + [ + 'id' => 1, + 'username' => 'luke', + 'role' => 'admin', + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + true, + ], + 'custom-rule-no-role' => [ + //permissions + [[ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + 'role' => 'admin', + // 'allowed' => new SampleRule(), + ]], + //user + [ + ], + //request + [ + 'plugin' => 'Tests', + 'controller' => 'Tests', + 'action' => 'test', + ], + //expected + false, + ], + ]; + } + + /** + * @param array $params + * @return ServerRequest + */ + protected function _requestFromArray($params) + { + $request = new ServerRequest(); + + return $request + ->withParam('plugin', $params['plugin'] ?? null) + ->withParam('controller', $params['controller'] ?? null) + ->withParam('action', $params['action'] ?? null) + ->withParam('prefix', $params['prefix'] ?? null) + ->withParam('_ext', $params['_ext'] ?? null); + } + + public function testGetPermissions() + { + $permissions = ['test']; + $this->rbac->setPermissions($permissions); + $this->assertSame($permissions, $this->rbac->getPermissions()); + } +} diff --git a/tests/TestCase/Rbac/RbacTest.php b/tests/TestCase/Rbac/RbacTest.php index 7b0814b..9cd1e19 100644 --- a/tests/TestCase/Rbac/RbacTest.php +++ b/tests/TestCase/Rbac/RbacTest.php @@ -19,7 +19,6 @@ use CakeDC\Auth\Rbac\Rules\Owner; use CakeDC\Auth\Test\App\Auth\Rule\SampleRule; use Exception; -use PHPUnit\Framework\Attributes\DataProvider; use Psr\Log\LogLevel; use ReflectionClass; use RuntimeException; @@ -98,7 +97,10 @@ public function setUp(): void 'plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'resetOneTimePasswordAuthenticator', - 'allowed' => true, + 'allowed' => [ + 'className' => \CakeDC\Auth\Rbac\Rules\OTPRule::class, + 'option' => [] + ], ], //all roles allowed to Pages/display [ @@ -126,21 +128,13 @@ public function tearDown(): void unset($this->rbac); } - /** - * @covers \CakeDC\Auth\Rbac\Rbac::__construct - */ public function testConstructGetDefaultPermissions() { $this->rbac = new Rbac(); $result = $this->rbac->getPermissions(); - $this->assertTrue(is_callable($result[4]['allowed'])); - $result[4]['allowed'] = true; $this->assertSame($this->defaultPermissions, $result); } - /** - * @covers \CakeDC\Auth\Rbac\Rbac::__construct - */ public function testConstructBadProvider() { $this->expectException(RuntimeException::class); @@ -150,9 +144,6 @@ public function testConstructBadProvider() ]); } - /** - * @covers \CakeDC\Auth\Rbac\Rbac::__construct - */ public function testConstructSetPermissions() { $this->rbac = new Rbac([ diff --git a/tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php b/tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php new file mode 100644 index 0000000..5e95926 --- /dev/null +++ b/tests/TestCase/Social/Mapper/LinkedInOpenIDConnectTest.php @@ -0,0 +1,81 @@ + 'test-token', + 'expires' => 1490988496, + ]); + $rawData = [ + 'sub' => '1', + 'token' => $token, + 'email' => 'test@gmail.com', + 'given_name' => 'Test', + 'family_name' => 'User', + 'industry' => 'Computer Software', + 'location' => [ + 'country' => [ + 'code' => 'es', + ], + 'name' => 'Spain', + ], + 'picture' => 'https://media.licdn.com/mpr/mprx/test.jpg', + + + 'bio' => 'The best test user in the world.', + 'publicProfileUrl' => 'https://www.linkedin.com/in/test', + ]; + $providerMapper = new LinkedInOpenIDConnect(); + $user = $providerMapper($rawData); + + $this->assertEquals([ + 'id' => '1', + 'username' => null, + 'full_name' => null, + 'first_name' => 'Test', + 'last_name' => 'User', + 'email' => 'test@gmail.com', + 'avatar' => 'https://media.licdn.com/mpr/mprx/test.jpg', + 'gender' => null, + 'link' => 'https://www.linkedin.com', + 'bio' => 'The best test user in the world.', + 'locale' => null, + 'validated' => true, + 'credentials' => [ + 'token' => 'test-token', + 'secret' => null, + 'expires' => 1490988496, + ], + 'raw' => $rawData, + ], $user); + } +} diff --git a/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php b/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php new file mode 100644 index 0000000..72a51b9 --- /dev/null +++ b/tests/TestCase/Social/Service/OpenIDConnectServiceTest.php @@ -0,0 +1,315 @@ +Client = $this->getMockBuilder( + \Cake\Http\Client::class + )->onlyMethods([ + 'get', + ])->getMock(); + + $this->Provider = $this->getMockBuilder( + \League\OAuth2\Client\Provider\LinkedIn::class + )->setConstructorArgs([ + [ + 'id' => '1', + 'firstName' => 'first', + 'lastName' => 'last', + ], + [], + ])->onlyMethods([ + 'getAccessToken', 'getState', 'getAuthorizationUrl', 'getResourceOwner', + ])->getMock(); + + $config = [ + 'service' => OpenIDConnectService::class, + 'className' => $this->Provider, + 'mapper' => \CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect::class, + 'authParams' => ['scope' => ['public_profile', 'email', 'user_birthday', 'user_gender', 'user_link']], + 'options' => [ + 'state' => '__TEST_STATE__', + ], + 'collaborators' => [], + 'signature' => null, + 'mapFields' => [], + 'path' => [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'socialLogin', + 'prefix' => null, + ], + ]; + + $this->Service = $this->getMockBuilder( + \CakeDC\Auth\Social\Service\OpenIDConnectService::class + )->setConstructorArgs([ + $config, + ])->onlyMethods([ + 'getIdTokenKeys', + 'getHttpClient', + ])->getMock(); + + $this->Service->expects($this->any()) + ->method('getHttpClient') + ->will($this->returnValue($this->Client)); + + //new OpenIDConnectService($config); + $this->Request = ServerRequestFactory::fromGlobals(); + } + + /** + * teardown any static object changes and restore them. + * + * @return void + */ + public function tearDown(): void + { + parent::tearDown(); + + unset($this->Provider, $this->Service, $this->Request); + } + + /** + * Test construct + * + * @return void + */ + public function testConstruct() + { + $service = new OpenIDConnectService([ + 'className' => \League\OAuth2\Client\Provider\LinkedIn::class, + 'mapper' => \CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect::class, + 'authParams' => ['scope' => ['public_profile', 'email', 'user_birthday', 'user_gender', 'user_link']], + 'options' => [], + 'collaborators' => [], + 'signature' => null, + 'mapFields' => [], + 'path' => [ + 'plugin' => 'CakeDC/Users', + 'controller' => 'Users', + 'action' => 'socialLogin', + 'prefix' => null, + ], + ]); + $this->assertInstanceOf(ServiceInterface::class, $service); + } + + /** + * Test GetUser InvalidRequest + * + * @return void + */ + public function testGetUserInvalidRequest() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->expectException(BadRequestException::class, 'Invalid OAuth2 state'); + + $this->Service->getUser($this->Request); + } + + /** + * Test GetUser MisingIdToken + * + * @return void + */ + public function testGetUserMisingIdToken() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->Request->getSession()->write('oauth2state', '__TEST_STATE__'); + + $token = new \League\OAuth2\Client\Token\AccessToken([ + 'access_token' => 'test-token', + 'expires' => 1490988496, + ]); + + $this->Provider->expects($this->once()) + ->method('getAccessToken') + ->with( + $this->equalTo('authorization_code'), + $this->equalTo(['code' => 'ZPO9972j3092304230']) + ) + ->will($this->returnValue($token)); + + $this->expectException(BadRequestException::class, 'Missing id_token in response'); + + $this->Service->getUser($this->Request); + } + + /** + * Test GetUser InvalidIdToken + * + * @return void + */ + public function testGetUserInvalidIdToken() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->Request->getSession()->write('oauth2state', '__TEST_STATE__'); + + $token = new \League\OAuth2\Client\Token\AccessToken([ + 'access_token' => 'test-token', + 'expires' => 1490988496, + 'id_token' => 'invalid-jwt', + ]); + + $this->Provider->expects($this->once()) + ->method('getAccessToken') + ->with( + $this->equalTo('authorization_code'), + $this->equalTo(['code' => 'ZPO9972j3092304230']) + ) + ->will($this->returnValue($token)); + + $this->expectException(BadRequestException::class, 'Invalid id token. Key may not be empty'); + + $this->Service->getUser($this->Request); + } + + /** + * Test GetUser + * + * @return void + */ + public function testGetUser() + { + $this->Request = $this->Request->withQueryParams([ + 'code' => 'ZPO9972j3092304230', + 'state' => '__TEST_STATE__', + ]); + + $this->Request->getSession()->write('oauth2state', '__TEST_STATE__'); + + $key = 'example_key'; + $alg = 'HS256'; + $payload = [ + 'iat' => 1490988496, + 'iss' => 'https://www.linkedin.com/', + ]; + $kid = 'd929668a-bab1-4c69-9598-4373149723ff'; + $jwt = JWT::encode($payload, $key, $alg, $kid); + + $token = new \League\OAuth2\Client\Token\AccessToken([ + 'access_token' => 'test-token', + 'expires' => 1490988496, + 'id_token' => $jwt, + ]); + + $this->Provider->expects($this->once()) + ->method('getAccessToken') + ->with( + $this->equalTo('authorization_code'), + $this->equalTo(['code' => 'ZPO9972j3092304230']) + ) + ->will($this->returnValue($token)); + + $jwksData = [ + $kid => new Key($key, $alg), + ]; + + $this->Service->expects($this->once()) + ->method('getIdTokenKeys') + ->will($this->returnValue($jwksData)); + + $actual = $this->Service->getUser($this->Request); + + $expected = [ + 'token' => $token, + 'iat' => 1490988496, + 'iss' => 'https://www.linkedin.com/', + ]; + + $this->assertEquals($expected, $actual); + } + + /** + * Test discover + * + * @return void + */ + public function testDiscover() + { + $arrayTest = ['test' => 'test']; + $response = new \Cake\Http\Client\Response([], json_encode($arrayTest)); + + $this->Client->expects($this->once()) + ->method('get') + ->will($this->returnValue($response)); + + $actual = $this->Service->discover(); + + $this->assertEquals($arrayTest, $actual); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index fb2ad8f..39b3023 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -106,6 +106,13 @@ 'serialize' => 'File', 'duration' => '+10 seconds', ], + '_cakedc_auth_' => [ + 'className' => 'File', + 'prefix' => 'users_app_cakedc_auth_', + 'path' => CACHE . 'cakedc_auth/', + 'serialize' => 'File', + 'duration' => '+10 seconds', + ], ]; Cache::setConfig($cache);