Skip to content

Commit 49c87cf

Browse files
authored
Primary Key only Session auth (#154)
* Use only id for Session storage. * Add tests * Simplify. * Use key. * Fixes. * Docs * Fix up name.
1 parent 181d7e2 commit 49c87cf

File tree

4 files changed

+670
-2
lines changed

4 files changed

+670
-2
lines changed

docs/AuthenticationPlugin.md

+111-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,124 @@ $this->loadComponent('TinyAuth.Authentication', [
1010
]);
1111
```
1212

13-
Make sure you load the middleware:
13+
Make sure you load the middleware in your `Application` class:
1414
```php
1515
use Authentication\Middleware\AuthenticationMiddleware;
1616

1717
// in Application::middleware()
1818
$middlewareQueue->add(new AuthenticationMiddleware($this));
1919
```
2020

21-
You can always get the identity from the AuthUser component and helper:
21+
This plugin ships with an improved session authenticator:
22+
23+
- PrimaryKeySession authenticator (extending the Authentication.Session one):
24+
stores only the ID and fetches the rest from DB (keeping it always up to date)
25+
26+
It also ships with an enhanced redirect handler:
27+
28+
- ForbiddenCakeRedirect: Allows an `unauthorizedMessage` to be set as error flash message.
29+
30+
31+
Now let's set up `getAuthenticationService()` and make sure to load all needed Authenticators, e.g.:
32+
33+
```php
34+
/**
35+
* @param \Psr\Http\Message\ServerRequestInterface $request Request
36+
*
37+
* @return \Authentication\AuthenticationServiceInterface
38+
*/
39+
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
40+
{
41+
$service = new AuthenticationService();
42+
43+
// Define where users should be redirected to when they are not authenticated
44+
$service->setConfig([
45+
'unauthenticatedRedirect' => Router::url([
46+
'prefix' => false,
47+
'plugin' => false,
48+
'controller' => 'Account',
49+
'action' => 'login',
50+
]),
51+
'queryParam' => 'redirect',
52+
]);
53+
54+
$fields = [
55+
AbstractIdentifier::CREDENTIAL_USERNAME => 'email',
56+
AbstractIdentifier::CREDENTIAL_PASSWORD => 'password',
57+
];
58+
59+
// Load identifiers
60+
$service->loadIdentifier('Authentication.Password', [
61+
'fields' => $fields,
62+
'resolver' => [
63+
'className' => 'Authentication.Orm',
64+
'finder' => 'active',
65+
],
66+
]);
67+
$service->loadIdentifier('Authentication.Token', [
68+
'tokenField' => 'id',
69+
'dataField' => 'key',
70+
'resolver' => [
71+
'className' => 'Authentication.Orm',
72+
'finder' => 'active',
73+
],
74+
]);
75+
76+
// Load the authenticators. Session should be first.
77+
$service->loadAuthenticator('TinyAuth.PrimaryKeySession', [
78+
'urlChecker' => 'Authentication.CakeRouter',
79+
]);
80+
$service->loadAuthenticator('Authentication.Form', [
81+
'urlChecker' => 'Authentication.CakeRouter',
82+
'fields' => $fields,
83+
'loginUrl' => [
84+
'prefix' => false,
85+
'plugin' => false,
86+
'controller' => 'Account',
87+
'action' => 'login',
88+
],
89+
]);
90+
$service->loadAuthenticator('Authentication.Cookie', [
91+
'urlChecker' => 'Authentication.CakeRouter',
92+
'rememberMeField' => 'remember_me',
93+
'fields' => [
94+
'username' => 'email',
95+
'password' => 'password',
96+
],
97+
'loginUrl' => [
98+
'prefix' => false,
99+
'plugin' => false,
100+
'controller' => 'Account',
101+
'action' => 'login',
102+
],
103+
]);
104+
105+
// This is a one click token login as optional addition
106+
$service->loadIdentifier('Tools.LoginLink', [
107+
'resolver' => [
108+
'className' => 'Authentication.Orm',
109+
'finder' => 'active',
110+
],
111+
'preCallback' => function (int $id) {
112+
TableRegistry::getTableLocator()->get('Users')->confirmEmail($id);
113+
},
114+
]);
115+
$service->loadAuthenticator('Tools.LoginLink', [
116+
'urlChecker' => 'Authentication.CakeRouter',
117+
'loginUrl' => [
118+
'prefix' => false,
119+
'plugin' => false,
120+
'controller' => 'Account',
121+
'action' => 'login',
122+
],
123+
]);
124+
125+
return $service;
126+
}
127+
```
128+
129+
130+
You can always get the identity result (User entity) from the AuthUser component and helper:
22131
```php
23132
$this->AuthUser->identity();
24133
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace TinyAuth\Authenticator;
5+
6+
use ArrayAccess;
7+
use Authentication\Authenticator\Result;
8+
use Authentication\Authenticator\ResultInterface;
9+
use Authentication\Authenticator\SessionAuthenticator as AuthenticationSessionAuthenticator;
10+
use Authentication\Identifier\IdentifierInterface;
11+
use Cake\Http\Exception\UnauthorizedException;
12+
use Psr\Http\Message\ResponseInterface;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
15+
/**
16+
* Session Authenticator with only ID
17+
*/
18+
class PrimaryKeySessionAuthenticator extends AuthenticationSessionAuthenticator {
19+
20+
/**
21+
* @param \Authentication\Identifier\IdentifierInterface $identifier
22+
* @param array<string, mixed> $config
23+
*/
24+
public function __construct(IdentifierInterface $identifier, array $config = []) {
25+
$config += [
26+
'identifierKey' => 'key',
27+
'idField' => 'id',
28+
];
29+
30+
parent::__construct($identifier, $config);
31+
}
32+
33+
/**
34+
* Authenticate a user using session data.
35+
*
36+
* @param \Psr\Http\Message\ServerRequestInterface $request The request to authenticate with.
37+
* @return \Authentication\Authenticator\ResultInterface
38+
*/
39+
public function authenticate(ServerRequestInterface $request): ResultInterface {
40+
$sessionKey = $this->getConfig('sessionKey');
41+
/** @var \Cake\Http\Session $session */
42+
$session = $request->getAttribute('session');
43+
44+
$userId = $session->read($sessionKey);
45+
if (!$userId) {
46+
return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
47+
}
48+
49+
$user = $this->_identifier->identify([$this->getConfig('identifierKey') => $userId]);
50+
if (!$user) {
51+
return new Result(null, Result::FAILURE_IDENTITY_NOT_FOUND);
52+
}
53+
54+
return new Result($user, Result::SUCCESS);
55+
}
56+
57+
/**
58+
* @inheritDoc
59+
*/
60+
public function persistIdentity(ServerRequestInterface $request, ResponseInterface $response, $identity): array {
61+
$sessionKey = $this->getConfig('sessionKey');
62+
/** @var \Cake\Http\Session $session */
63+
$session = $request->getAttribute('session');
64+
65+
if (!$session->check($sessionKey)) {
66+
$session->renew();
67+
$session->write($sessionKey, $identity[$this->getConfig('idField')]);
68+
}
69+
70+
return [
71+
'request' => $request,
72+
'response' => $response,
73+
];
74+
}
75+
76+
/**
77+
* Impersonates a user
78+
*
79+
* @param \Psr\Http\Message\ServerRequestInterface $request The request
80+
* @param \Psr\Http\Message\ResponseInterface $response The response
81+
* @param \ArrayAccess $impersonator User who impersonates
82+
* @param \ArrayAccess $impersonated User impersonated
83+
* @return array
84+
*/
85+
public function impersonate(
86+
ServerRequestInterface $request,
87+
ResponseInterface $response,
88+
ArrayAccess $impersonator,
89+
ArrayAccess $impersonated,
90+
): array {
91+
$sessionKey = $this->getConfig('sessionKey');
92+
$impersonateSessionKey = $this->getConfig('impersonateSessionKey');
93+
/** @var \Cake\Http\Session $session */
94+
$session = $request->getAttribute('session');
95+
if ($session->check($impersonateSessionKey)) {
96+
throw new UnauthorizedException(
97+
'You are impersonating a user already. ' .
98+
'Stop the current impersonation before impersonating another user.',
99+
);
100+
}
101+
$session->write($impersonateSessionKey, $impersonator[$this->getConfig('idField')]);
102+
$session->write($sessionKey, $impersonated[$this->getConfig('idField')]);
103+
$this->setConfig('identify', true);
104+
105+
return [
106+
'request' => $request,
107+
'response' => $response,
108+
];
109+
}
110+
111+
}

0 commit comments

Comments
 (0)