Skip to content

Commit 65c3aec

Browse files
committed
Module 3
Now that users can authenticate, we want to provide a simpler way for them to identify themselves as they use the system. Rather than solicit for an email address and password on every request, we want to use server-side sessions to track the user's authentication information. Your task is to update our authentication routine to leverage server-side storage of session information and update the authentication handler to use Bearer tokens (i.e. session IDs) to authenticate requests rather than email/password combinations.
1 parent c16ad84 commit 65c3aec

8 files changed

Lines changed: 121 additions & 41 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.session

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"autoload": {
2222
"psr-4": {
2323
"Notes\\Module1\\": "module-1/server/",
24-
"Notes\\Module2\\": "module-2/server/"
24+
"Notes\\Module2\\": "module-2/server/",
25+
"Notes\\Module3\\": "module-3/server/"
2526
},
2627
"files": [
2728
"util/Database.php",

module-3/client/notes.php

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@
88
use splitbrain\phpcli\Options;
99
use splitbrain\phpcli\TableFormatter;
1010

11-
class Module2 extends CLI
11+
class Module3 extends CLI
1212
{
1313
/**
1414
* @var GuzzleHttp\Client
1515
*/
1616
private $client;
17+
private $token;
1718

1819
private $user;
1920

@@ -33,6 +34,7 @@ protected function setup(Options $options)
3334

3435
$options->registerCommand('register', 'Create a user account.');
3536
$options->registerCommand('changePassword', 'Change your user password');
37+
$options->registerCommand('login', 'Create a persistent authentication session');
3638

3739
$options->registerArgument('note', 'Actual note to store.', true, 'create');
3840
$options->registerArgument('noteId', 'Identify the note to retrieve.', true, 'get');
@@ -47,12 +49,13 @@ protected function setup(Options $options)
4749
$options->registerOption('new-password', 'New login password', 'p', true, 'changePassword');
4850
$options->registerOption('repeat-password', 'New login password (again)', 'r', true, 'changePassword');
4951

50-
foreach (['create', 'get', 'delete', 'all'] as $command) {
52+
foreach (['create', 'get', 'delete', 'all', 'login'] as $command) {
5153
$options->registerOption('email', 'Email address', 'e', true, $command);
5254
$options->registerOption('password', 'Login password', 'p', true, $command);
5355
}
5456

5557
$this->client = new GuzzleHttp\Client(['base_uri' => 'http://localhost:8888']);
58+
$this->token = file_get_contents('.session');
5659
}
5760

5861
protected function createNote($contents)
@@ -66,9 +69,11 @@ protected function createNote($contents)
6669
'note' => $contents
6770
];
6871

69-
$response = $this->client->request('POST', 'notes', [
72+
$response = $this->client->request(
73+
'POST',
74+
'notes', [
7075
'body' => json_encode($jsonBody),
71-
'auth' => [$this->user, $this->pass]
76+
'auth' => [$this->user, $this->pass] // @TODO Replace basic auth with Bearer auth
7277
]);
7378

7479
if ($response->getStatusCode() === 200) {
@@ -116,7 +121,11 @@ private function printNote(array $note, TableFormatter $tf)
116121

117122
protected function getNote($noteId)
118123
{
119-
$response = $this->client->request('GET', sprintf('notes/%s', $noteId), ['auth' => [$this->user, $this->pass]]);
124+
$response = $this->client->request(
125+
'GET',
126+
sprintf('notes/%s', $noteId),
127+
['auth' => [$this->user, $this->pass]] // @TODO Replace basic auth with Bearer auth
128+
);
120129

121130
if ($response->getStatusCode() === 200) {
122131
$return = json_decode($response->getBody(), true);
@@ -134,7 +143,11 @@ protected function getNote($noteId)
134143
protected function deleteNote($noteId)
135144
{
136145
try {
137-
$this->client->request('DELETE', sprintf('notes/%s', $noteId), ['auth' => [$this->user, $this->pass]]);
146+
$this->client->request(
147+
'DELETE',
148+
sprintf('notes/%s', $noteId),
149+
['auth' => [$this->user, $this->pass]] // @TODO Replace basic auth with Bearer auth
150+
);
138151
$this->info(sprintf('Note ID %s has been deleted.', $noteId));
139152
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
140153
$this->error(sprintf('Unable to delete note %s. It might not exist!', $noteId));
@@ -143,7 +156,11 @@ protected function deleteNote($noteId)
143156

144157
protected function getAllNotes()
145158
{
146-
$response = $this->client->request('GET', 'notes', ['auth' => [$this->user, $this->pass]]);
159+
$response = $this->client->request(
160+
'GET',
161+
'notes',
162+
['auth' => [$this->user, $this->pass]] // @TODO Replace basic auth with Bearer auth
163+
);
147164

148165
if ($response->getStatusCode() === 200) {
149166
$notes = json_decode($response->getBody(), true);
@@ -247,6 +264,35 @@ protected function changePassword(Options $options)
247264
}
248265
}
249266

267+
protected function login(Options $options)
268+
{
269+
try {
270+
$response = $this->client->request('GET', 'login', ['auth' => [$this->user, $this->pass]]);
271+
272+
if ($response->getStatusCode() === 200) {
273+
$auth = json_decode($response->getBody(), true);
274+
275+
if (empty($auth)) {
276+
$this->error('Could not establish a session!');
277+
} else {
278+
$token = $auth['token'];
279+
280+
try {
281+
$fp = fopen('.session', 'w');
282+
fwrite($fp, $token);
283+
fclose($fp);
284+
285+
$this->info('Established a persistent session. Commence note taking!');
286+
} catch (\Exception $e) {
287+
$this->error('Could not establish a session!');
288+
}
289+
}
290+
}
291+
} catch(\GuzzleHttp\Exception\BadResponseException $e) {
292+
$this->error('Could not establish a session!');
293+
}
294+
}
295+
250296
protected function main(Options $options)
251297
{
252298
$args = $options->getArgs();
@@ -272,6 +318,9 @@ protected function main(Options $options)
272318
case 'changePassword':
273319
$this->changePassword($options);
274320
break;
321+
case 'login':
322+
$this->login($options);
323+
break;
275324
default:
276325
$this->error('No known command was called, we show the default help instead:');
277326
echo $options->help();
@@ -280,5 +329,5 @@ protected function main(Options $options)
280329
}
281330
}
282331

283-
$cli = new Module2();
332+
$cli = new Module3();
284333
$cli->run();

module-3/server/AuthMiddleware.php

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,23 @@
11
<?php declare(strict_types=1);
22

3-
namespace Notes\Module2;
3+
namespace Notes\Module3;
44

5-
use League\Route\Http\Exception\BadRequestException;
65
use League\Route\Http\Exception\ForbiddenException;
7-
use Notes\Util\Database;
86
use Psr\Http\Message\ResponseInterface;
97
use Psr\Http\Message\ServerRequestInterface;
108
use Psr\Http\Server\MiddlewareInterface;
119
use Psr\Http\Server\RequestHandlerInterface;
12-
use Zend\Diactoros\Response\JsonResponse;
1310

1411
class AuthMiddleware implements MiddlewareInterface
1512
{
1613
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
1714
{
18-
$authentication = $request->getHeader('authorization')[0];
19-
$base64Auth = trim(substr($authentication, 5));
20-
$decoded = explode(':', base64_decode($base64Auth));
15+
if (Server::authenticate($request)) {
16+
return $handler->handle($request);
17+
}
2118

22-
$db = new Database();
23-
try {
24-
$user = $db->getUserByEmail($decoded[0]);
25-
26-
// @TODO Proper hash comparison
27-
if (hash_equals($user->password, $decoded[1])) {
28-
$_SESSION['userId'] = $user->userId;
29-
30-
return $handler->handle($request);
31-
}
32-
} catch (\Exception $e) {}
19+
// @TODO Check for a Bearer token and set up the session
20+
$authorization = $request->getHeader('authorization')[0];
3321

3422
throw new ForbiddenException();
3523
}

module-3/server/Server.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php declare(strict_types=1);
22

3-
namespace Notes\Module2;
3+
namespace Notes\Module3;
44

5+
use GuzzleHttp\Exception\BadResponseException;
56
use League\Route\Http\Exception\BadRequestException;
67
use League\Route\Http\Exception\ForbiddenException;
78
use Notes\Util\Database;
@@ -112,7 +113,7 @@ public function register(ServerRequestInterface $request): array
112113
// Create the user when we throw a not found exception!
113114
$user = new BaseUser();
114115
$user->email = $parsed['email'];
115-
$user->password = $parsed['password']; // @TODO This should be hashed!
116+
$user->password = password_hash($parsed['password'], PASSWORD_BCRYPT, ['cost' => 11]);
116117
$user->greeting = isset($parsed['greeting']) ? $parsed['greeting'] : 'Friend';
117118

118119
$userId = $this->db->createUser($user);
@@ -140,22 +141,55 @@ public function changePassword(ServerRequestInterface $request): ResponseInterfa
140141
throw new BadRequestException('No such user!');
141142
}
142143

143-
// @TODO Use better comparisons
144-
if (!hash_equals($user->password, $parsed['old_password'])) {
144+
if (!password_verify($parsed['old_password'], $user->password)) {
145145
throw new BadRequestException('Invalid password!');
146146
}
147147

148148
if (!hash_equals($parsed['new_password'], $parsed['repeat_password'])) {
149149
throw new BadRequestException('Passwords must match!');
150150
}
151151

152-
// @TODO This should be hashed!
153-
$user->password = $parsed['new_password'];
152+
$user->password = password_hash($parsed['new_password'], PASSWORD_BCRYPT, ['cost' => 11]);
154153

155154
if ($this->db->updateUserPassword($user)) {
156155
return new EmptyResponse();
157156
}
158157

159158
throw new \Exception('Server error while updating password.');
160159
}
160+
161+
public static function authenticate(ServerRequestInterface $request): bool
162+
{
163+
$authentication = $request->getHeader('authorization')[0];
164+
$base64Auth = trim(substr($authentication, 5));
165+
$decoded = explode(':', base64_decode($base64Auth));
166+
167+
$db = new Database();
168+
try {
169+
$user = $db->getUserByEmail($decoded[0]);
170+
171+
if (password_verify($decoded[1], $user->password)) {
172+
if (session_status() !== PHP_SESSION_ACTIVE) {
173+
session_start();
174+
}
175+
176+
$_SESSION['userId'] = $user->userId;
177+
return true;
178+
}
179+
} catch (\Exception $e) {}
180+
181+
return false;
182+
}
183+
184+
public function login(ServerRequestInterface $request): array
185+
{
186+
if (self::authenticate($request)) {
187+
return [
188+
'userId' => $_SESSION['userId'],
189+
'token' => session_id()
190+
];
191+
}
192+
193+
throw new \Exception('There was a problem in your login attempt');
194+
}
161195
}

module-3/server/index.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@
1313
$router->setStrategy($strategy);
1414

1515
$router->group('/notes', function(\League\Route\RouteGroup $router) {
16-
$router->get('/', 'Notes\Module2\Server::getNotes');
17-
$router->get('/{id}', 'Notes\Module2\Server::getNote');
18-
$router->post('/', 'Notes\Module2\Server::createNote');
19-
$router->delete('/{id}', 'Notes\Module2\Server::deleteNote');
20-
})->middleware(new Notes\Module2\AuthMiddleware());
21-
22-
$router->post('/account', 'Notes\Module2\Server::register');
23-
$router->put('/account', 'Notes\Module2\Server::changePassword');
16+
$router->get('/', 'Notes\Module3\Server::getNotes');
17+
$router->get('/{id}', 'Notes\Module3\Server::getNote');
18+
$router->post('/', 'Notes\Module3\Server::createNote');
19+
$router->delete('/{id}', 'Notes\Module3\Server::deleteNote');
20+
})->middleware(new Notes\Module3\AuthMiddleware());
21+
22+
$router->post('/account', 'Notes\Module3\Server::register');
23+
$router->put('/account', 'Notes\Module3\Server::changePassword');
24+
$router->get('/login', 'Notes\Module3\Server::login');
2425

2526
$response = $router->dispatch($request);
2627

vendor/composer/autoload_psr4.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
'Psr\\Http\\Server\\' => array($vendorDir . '/psr/http-server-handler/src', $vendorDir . '/psr/http-server-middleware/src'),
1515
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'),
1616
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
17+
'Notes\\Module3\\' => array($baseDir . '/module-3/server'),
1718
'Notes\\Module2\\' => array($baseDir . '/module-2/server'),
1819
'Notes\\Module1\\' => array($baseDir . '/module-1/server'),
1920
'League\\Route\\' => array($vendorDir . '/league/route/src'),

vendor/composer/autoload_static.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class ComposerStaticInite8d69a9c610684d4ceded15f33142591
5555
),
5656
'N' =>
5757
array (
58+
'Notes\\Module3\\' => 14,
5859
'Notes\\Module2\\' => 14,
5960
'Notes\\Module1\\' => 14,
6061
),
@@ -109,6 +110,10 @@ class ComposerStaticInite8d69a9c610684d4ceded15f33142591
109110
array (
110111
0 => __DIR__ . '/..' . '/psr/container/src',
111112
),
113+
'Notes\\Module3\\' =>
114+
array (
115+
0 => __DIR__ . '/../..' . '/module-3/server',
116+
),
112117
'Notes\\Module2\\' =>
113118
array (
114119
0 => __DIR__ . '/../..' . '/module-2/server',

0 commit comments

Comments
 (0)