Auth controller implemented and 100% test coverage
This commit is contained in:
parent
9fd81422f6
commit
ec68921595
@ -4,22 +4,68 @@ use Firebase\JWT\JWT;
|
||||
|
||||
class Auth extends BaseController {
|
||||
|
||||
public static function CreateInitialAdmin($container) {
|
||||
$admin = new User($container, 1);
|
||||
|
||||
if ($admin->id === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$admin->security_level = new SecurityLevel(SecurityLevel::Admin);
|
||||
$admin->username = 'admin';
|
||||
$admin->password_hash = password_hash('admin', PASSWORD_BCRYPT);
|
||||
$admin->save();
|
||||
}
|
||||
|
||||
public static function CreateJwtKey() {
|
||||
$key = R::load('jwt', 1);
|
||||
|
||||
// Don't create more than one secret key
|
||||
if ($key->id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a JWT signing key by hashing the current time.
|
||||
$key->secret = hash('sha512', strval(time()));
|
||||
|
||||
R::store($key);
|
||||
}
|
||||
|
||||
// TODO: Determine if this endpoint is needed.
|
||||
// The new API should be varifying and updating the user's
|
||||
// token on each call so an authentication endpoint should not
|
||||
// be needed. The code will remain for now as an example of what
|
||||
// to do in the future.
|
||||
public function authenticate($request, $response, $args) {
|
||||
if (!$request->hasHeader('Authorization')) {
|
||||
return $response->withStatus(400); // Bad Request
|
||||
return $this->jsonResponse($response, 400);
|
||||
}
|
||||
|
||||
$jwt = $request->getHeader('Authorization');
|
||||
$payload = null;
|
||||
$jwt = $request->getHeader('Authorization')[0];
|
||||
$payload = $this->getJwtPayload($jwt);
|
||||
|
||||
try {
|
||||
$payload = JWT::decode($jwt, $this->getJwtKey(), array('HS256'));
|
||||
} catch (Exception $ex) {
|
||||
if ($payload === null) {
|
||||
return $this->jsonResponse($response, 401);
|
||||
}
|
||||
|
||||
// Issue new token with extended expiration
|
||||
$user = new User($this->container, (int) $payload->uid);
|
||||
if ($user->active_token !== $jwt) {
|
||||
$user->active_token = '';
|
||||
$user->save();
|
||||
|
||||
return $response->withJson(json_encode($jwt));
|
||||
$this->apiJson->addAlert('error', 'Invalid access token.');
|
||||
|
||||
return $this->jsonResponse($response, 401);
|
||||
}
|
||||
|
||||
$jwt = $this->createJwt($payload->uid);
|
||||
$user->active_token = $jwt;
|
||||
$user->save();
|
||||
|
||||
$this->apiJson->setSuccess();
|
||||
$this->apiJson->addData($jwt);
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
}
|
||||
|
||||
public function login($request, $response, $args) {
|
||||
@ -29,38 +75,77 @@ class Auth extends BaseController {
|
||||
if ($user === null) {
|
||||
$this->apiJson->addAlert('error', 'Invalid username or password.');
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
return $this->jsonResponse($response, 401);
|
||||
}
|
||||
|
||||
if (!password_verify($data->password, $user->password_hash)) {
|
||||
$this->apiJson->addAlert('error', 'Invalid username or password.');
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
return $this->jsonResponse($response, 401);
|
||||
}
|
||||
|
||||
// Username and password verified
|
||||
// Issue JWT
|
||||
$jwt = $this->createJwt($user->id);
|
||||
$user = new User($this->container, $user->id);
|
||||
|
||||
$user->active_token = $jwt;
|
||||
$user->last_login = time();
|
||||
$user->save();
|
||||
|
||||
$this->apiJson->setSuccess();
|
||||
$this->apiJson->addData($jwt);
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
}
|
||||
|
||||
public function logout($request, $response, $args) {
|
||||
if (!$request->hasHeader('Authorization')) {
|
||||
return $this->jsonResponse($response, 400);
|
||||
}
|
||||
|
||||
$jwt = $request->getHeader('Authorization')[0];
|
||||
$payload = $this->getJwtPayload($jwt);
|
||||
|
||||
if ($payload === null) {
|
||||
return $this->jsonResponse($response, 401);
|
||||
}
|
||||
|
||||
$user = new User($this->container, $payload->uid);
|
||||
|
||||
if ($user->id) {
|
||||
$user->active_token = '';
|
||||
$user->save();
|
||||
}
|
||||
|
||||
$this->apiJson->setSuccess();
|
||||
$this->apiJson->addAlert('success', 'You have been logged out.');
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
}
|
||||
|
||||
private function generateJwt() {
|
||||
private function getJwtPayload($jwt) {
|
||||
try {
|
||||
$payload = JWT::decode($jwt, $this->getJwtKey(), ['HS256']);
|
||||
} catch (Exception $ex) {
|
||||
$this->apiJson->addAlert('error', 'Invalid access token.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function createJwt($userId) {
|
||||
return JWT::encode(array(
|
||||
'exp' => time() + (60 * 30), // 30 minutes
|
||||
'uid' => $userId
|
||||
), $this->getJwtKey());
|
||||
}
|
||||
|
||||
private function getJwtKey() {
|
||||
self::CreateJwtKey();
|
||||
$key = R::load('jwt', 1);
|
||||
|
||||
if ($key->id === 0) {
|
||||
// Generate a JWT key by hashing the current time.
|
||||
// This should make (effectively) every instance of TaskBoard
|
||||
// have a unique secret key for JWTs.
|
||||
$key->token = password_hash(strval(time()), PASSWORD_BCRYPT);
|
||||
|
||||
R::store($key);
|
||||
}
|
||||
|
||||
return $key->token;
|
||||
return $key->secret;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,8 +13,8 @@ abstract class BaseController {
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function jsonResponse($response) {
|
||||
return $response->withJson($this->apiJson);
|
||||
public function jsonResponse($response, $status = 200) {
|
||||
return $response->withStatus($status)->withJson($this->apiJson);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ class Users extends BaseController {
|
||||
$user = new User($this->container);
|
||||
$user->loadFromBean($bean);
|
||||
|
||||
$this->apiJson->addData($user);
|
||||
$this->apiJson->addData($this->cleanUser($user));
|
||||
}
|
||||
} else {
|
||||
$this->logger->addInfo('No users in database.');
|
||||
@ -36,7 +36,7 @@ class Users extends BaseController {
|
||||
}
|
||||
|
||||
$this->apiJson->setSuccess();
|
||||
$this->apiJson->addData($user);
|
||||
$this->apiJson->addData($this->cleanUser($user));
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
}
|
||||
@ -120,5 +120,13 @@ class Users extends BaseController {
|
||||
|
||||
return $this->jsonResponse($response);
|
||||
}
|
||||
|
||||
private function cleanUser($user) {
|
||||
$user->security_level = $user->security_level->getValue();
|
||||
unset($user->password_hash);
|
||||
unset($user->active_token);
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,47 +7,50 @@ R::setup('sqlite:taskboard.sqlite');
|
||||
$app = new Slim\App();
|
||||
require 'app-setup.php';
|
||||
|
||||
Auth::CreateInitialAdmin($container);
|
||||
Auth::CreateJwtKey();
|
||||
|
||||
$app->get ('/', 'Invalid:noApi');
|
||||
|
||||
$app->get ('/boards', 'Boards:getAllBoards');
|
||||
$app->get ('/boards/{id}', 'Boards:getBoard');
|
||||
$app->post ('/boards', 'Boards:addBoard');
|
||||
$app->post ('/boards/{id}', 'Boards:updateBoard');
|
||||
$app->delete('/boards/{id}', 'Boards:removeBoard');
|
||||
$app->get ('/boards', 'Boards:getAllBoards');
|
||||
$app->get ('/boards/{id}', 'Boards:getBoard');
|
||||
$app->post ('/boards', 'Boards:addBoard');
|
||||
$app->post ('/boards/{id}', 'Boards:updateBoard');
|
||||
$app->delete('/boards/{id}', 'Boards:removeBoard');
|
||||
|
||||
$app->get ('/autoactions', 'AutoActions:getAllActions');
|
||||
$app->post ('/autoactions', 'AutoActions:addAction');
|
||||
$app->delete('/autoactions/{id}', 'AutoActions:removeAction');
|
||||
|
||||
$app->get ('/columns/{id}', 'Columns:getColumn');
|
||||
$app->post ('/columns', 'Columns:addColumn');
|
||||
$app->post ('/columns/{id}', 'Columns:updateColumn');
|
||||
$app->delete('/columns/{id}', 'Columns:removeColumn');
|
||||
$app->get ('/columns/{id}', 'Columns:getColumn');
|
||||
$app->post ('/columns', 'Columns:addColumn');
|
||||
$app->post ('/columns/{id}', 'Columns:updateColumn');
|
||||
$app->delete('/columns/{id}', 'Columns:removeColumn');
|
||||
|
||||
$app->get ('/tasks/{id}', 'Tasks:getTask');
|
||||
$app->post ('/tasks', 'Tasks:addTask');
|
||||
$app->post ('/tasks/{id}', 'Tasks:updateTask');
|
||||
$app->delete('/tasks/{id}', 'Tasks:removeTask');
|
||||
$app->get ('/tasks/{id}', 'Tasks:getTask');
|
||||
$app->post ('/tasks', 'Tasks:addTask');
|
||||
$app->post ('/tasks/{id}', 'Tasks:updateTask');
|
||||
$app->delete('/tasks/{id}', 'Tasks:removeTask');
|
||||
|
||||
$app->get ('/comments/{id}', 'Comments:getComment');
|
||||
$app->post ('/comments', 'Comments:addComment');
|
||||
$app->post ('/comments/{id}', 'Comments:updateComment');
|
||||
$app->delete('/comments/{id}', 'Comments:removeComment');
|
||||
$app->get ('/comments/{id}', 'Comments:getComment');
|
||||
$app->post ('/comments', 'Comments:addComment');
|
||||
$app->post ('/comments/{id}', 'Comments:updateComment');
|
||||
$app->delete('/comments/{id}', 'Comments:removeComment');
|
||||
|
||||
$app->get ('/attachments/{id}', 'Attachments:getAttachment');
|
||||
$app->post ('/attachments', 'Attachments:addAttachment');
|
||||
$app->post ('/attachments/{id}', 'Attachments:updateAttachment');
|
||||
$app->delete('/attachments/{id}', 'Attachments:removeAttachment');
|
||||
|
||||
$app->get ('/users', 'Users:getAllUsers');
|
||||
$app->get ('/users/{id}', 'Users:getUser');
|
||||
$app->post ('/users', 'Users:addUser');
|
||||
$app->post ('/users/{id}', 'Users:updateUser');
|
||||
$app->delete('/users/{id}', 'Users:removeUser');
|
||||
$app->get ('/users', 'Users:getAllUsers');
|
||||
$app->get ('/users/{id}', 'Users:getUser');
|
||||
$app->post ('/users', 'Users:addUser');
|
||||
$app->post ('/users/{id}', 'Users:updateUser');
|
||||
$app->delete('/users/{id}', 'Users:removeUser');
|
||||
|
||||
$app->post('/authenticate', 'Auth:authenticate');
|
||||
$app->post('/login', 'Auth:login');
|
||||
$app->post('/logout', 'Auth:logout');
|
||||
$app->post('/authenticate', 'Auth:authenticate');
|
||||
$app->post('/login', 'Auth:login');
|
||||
$app->post('/logout', 'Auth:logout');
|
||||
|
||||
$app->run();
|
||||
R::close();
|
||||
|
@ -16,6 +16,7 @@ class User extends BaseModel {
|
||||
public $default_board_id = 0;
|
||||
public $user_option_id = 0;
|
||||
public $last_login = 0;
|
||||
public $active_token = '';
|
||||
|
||||
public function __construct($container, $id = 0) {
|
||||
parent::__construct('user', $id, $container);
|
||||
@ -36,6 +37,7 @@ class User extends BaseModel {
|
||||
$bean->default_board_id = $this->default_board_id;
|
||||
$bean->user_option_id = $this->user_option_id;
|
||||
$bean->last_login = $this->last_login;
|
||||
$bean->active_token = $this->active_token;
|
||||
}
|
||||
|
||||
public function loadFromBean($bean) {
|
||||
@ -76,6 +78,7 @@ class User extends BaseModel {
|
||||
$this->default_board_id = (int) $obj->default_board_id;
|
||||
$this->user_option_id = (int) $obj->user_option_id;
|
||||
$this->last_login = (int) $obj->last_login;
|
||||
$this->active_token = $obj->active_token;
|
||||
} catch (Exception $ex) {
|
||||
$this->is_valid = false;
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ class DataMock {
|
||||
$user->default_board_id = 1;
|
||||
$user->user_option_id = 1;
|
||||
$user->last_login = 123456789;
|
||||
$user->active_token = '';
|
||||
|
||||
return $user;
|
||||
}
|
||||
@ -162,6 +163,7 @@ class RequestMock {
|
||||
public $invalidPayload = false;
|
||||
public $payload = null;
|
||||
public $hasHeader = true;
|
||||
public $header = null;
|
||||
|
||||
public function getBody() {
|
||||
if ($this->invalidPayload) {
|
||||
@ -180,7 +182,11 @@ class RequestMock {
|
||||
}
|
||||
|
||||
public function getHeader($header) {
|
||||
return $header;
|
||||
if ($this->header) {
|
||||
return $this->header;
|
||||
}
|
||||
|
||||
return (array) $header;
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,7 +197,7 @@ class ResponseMock {
|
||||
}
|
||||
|
||||
public function withStatus($status) {
|
||||
return $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,36 +1,131 @@
|
||||
<?php
|
||||
use RedBeanPHP\R;
|
||||
use Firebase\JWT\JWT;
|
||||
|
||||
class AuthTest extends PHPUnit_Framework_TestCase {
|
||||
private $auth;
|
||||
|
||||
public static function setupBeforeClass() {
|
||||
try {
|
||||
RedBeanPHP\R::setup('sqlite:tests.db');
|
||||
R::setup('sqlite:tests.db');
|
||||
} catch (Exception $ex) { }
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
RedBeanPHP\R::nuke();
|
||||
R::nuke();
|
||||
|
||||
$this->auth = new Auth(new ContainerMock());
|
||||
}
|
||||
|
||||
/**
|
||||
* @group single
|
||||
*/
|
||||
public function testAuthenticate() {
|
||||
public function testAuthenticateFailures() {
|
||||
$request = new RequestMock();
|
||||
$request->hasHeader = false;
|
||||
|
||||
$actual = $this->auth->authenticate($request,
|
||||
new ResponseMock(), null);
|
||||
|
||||
$this->assertTrue($actual === 400);
|
||||
$this->assertTrue($actual->status === 'failure');
|
||||
|
||||
$actual = $this->auth->authenticate(new RequestMock(),
|
||||
new ResponseMock(), null);
|
||||
|
||||
$this->assertTrue($actual === json_encode('Authorization'));
|
||||
$expected = new ApiJson();
|
||||
$expected->addAlert('error', 'Invalid access token.');
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testAuthenticate() {
|
||||
Auth::CreateInitialAdmin(new ContainerMock());
|
||||
// Called twice to verify coverage of the check for existing admin
|
||||
Auth::CreateInitialAdmin(new ContainerMock());
|
||||
Auth::CreateJwtKey();
|
||||
|
||||
$jwtKey = R::load('jwt', 1);
|
||||
$admin = R::load('user', 1);
|
||||
|
||||
$token = JWT::encode(array(
|
||||
'exp' => time() + 600,
|
||||
'uid' => 1
|
||||
), $jwtKey->secret);
|
||||
|
||||
$admin->active_token = $token;
|
||||
R::store($admin);
|
||||
|
||||
$request = new RequestMock();
|
||||
$request->header = [$token];
|
||||
|
||||
$actual = $this->auth->authenticate($request,
|
||||
new ResponseMock(), null);
|
||||
$this->assertTrue(strlen($actual->data[0]) > 0);
|
||||
|
||||
$this->auth = new Auth(new ContainerMock());
|
||||
|
||||
$admin->active_token = '';
|
||||
R::store($admin);
|
||||
|
||||
$actual = $this->auth->authenticate($request,
|
||||
new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'failure');
|
||||
}
|
||||
|
||||
public function testLogin() {
|
||||
$data = new stdClass();
|
||||
$data->username = 'admin';
|
||||
$data->password = 'admin';
|
||||
|
||||
$request = new RequestMock();
|
||||
$request->payload = $data;
|
||||
|
||||
$actual = $this->auth->login($request, new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'failure');
|
||||
|
||||
$this->auth = new Auth(new ContainerMock());
|
||||
Auth::CreateInitialAdmin(new ContainerMock());
|
||||
Auth::CreateJwtKey();
|
||||
|
||||
$actual = $this->auth->login($request, new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'success');
|
||||
|
||||
$this->auth = new Auth(new ContainerMock());
|
||||
$request->payload->password = 'asdf';
|
||||
|
||||
$actual = $this->auth->login($request, new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'failure');
|
||||
}
|
||||
|
||||
public function testLogout() {
|
||||
Auth::CreateInitialAdmin(new ContainerMock());
|
||||
$data = new stdClass();
|
||||
$data->username = 'admin';
|
||||
$data->password = 'admin';
|
||||
|
||||
$request = new RequestMock();
|
||||
$request->payload = $data;
|
||||
|
||||
$actual = $this->auth->login($request, new ResponseMock(), null);
|
||||
$jwt = $actual->data[0];
|
||||
$jwtKey = R::load('jwt', 1);
|
||||
|
||||
$this->auth = new Auth(new ContainerMock());
|
||||
$request = new RequestMock();
|
||||
$request->header = [$jwt];
|
||||
|
||||
$actual = $this->auth->logout($request, new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'success');
|
||||
}
|
||||
|
||||
public function testLogoutFailures() {
|
||||
$actual = $this->auth->logout(new RequestMock(),
|
||||
new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'failure');
|
||||
|
||||
$this->auth = new Auth(new ContainerMock());
|
||||
$request = new RequestMock();
|
||||
$request->hasHeader = false;
|
||||
|
||||
$actual = $this->auth->logout($request, new ResponseMock(), null);
|
||||
$this->assertTrue($actual->status === 'failure');
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user