Auth controller implemented and 100% test coverage

This commit is contained in:
kiswa 2016-05-23 20:34:06 +00:00
parent 9fd81422f6
commit ec68921595
7 changed files with 262 additions and 62 deletions

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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');
}
}