closed #138 added new cookie based login

This commit is contained in:
Exodus4D
2016-05-01 17:20:16 +02:00
parent 7f23e31984
commit 2b58853bfb
20 changed files with 1016 additions and 219 deletions

View File

@@ -19,9 +19,9 @@ class AccessController extends Controller {
function beforeroute(\Base $f3) {
parent::beforeroute($f3);
// Any CMS route of a child class of this one, requires a
// valid logged in user!
$loginCheck = $this->checkLogIn($f3);
// Any route/endpoint of a child class of this one,
// requires a valid logged in user!
$loginCheck = $this->checkLogTimer($f3);
if( !$loginCheck ){
// no user found or LogIn timer expired
@@ -29,32 +29,4 @@ class AccessController extends Controller {
}
}
/**
* checks weather a user is currently logged in
* @param \Base $f3
* @return bool
*/
private function checkLogIn($f3){
$loginCheck = false;
if($f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) > 0){
// check logIn time
$logInTime = new \DateTime();
$logInTime->setTimestamp( $f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) );
$now = new \DateTime();
$timeDiff = $now->diff($logInTime);
$minutes = $timeDiff->days * 60 * 24 * 60;
$minutes += $timeDiff->h * 60;
$minutes += $timeDiff->i;
if($minutes <= $f3->get('PATHFINDER.TIMER.LOGGED')){
$loginCheck = true;
}
}
return $loginCheck;
}
}

View File

@@ -568,7 +568,6 @@ class Route extends \Controller\AccessController {
$item = false;
}
$data[0]->reset();
}
}, [$map, $validMaps, $activeCharacter]);

View File

@@ -84,6 +84,35 @@ class User extends Controller\Controller{
return $login;
}
/**
* validate cookie character information
* -> return character data (if valid)
* @param \Base $f3
*/
public function getCookieCharacter($f3){
$data = $f3->get('POST');
$return = (object) [];
$return->error = [];
if( !empty($data['cookie']) ){
if( !empty($cookieData = $this->getCookieByName($data['cookie']) )){
// cookie data is valid -> validate data against DB (security check!)
if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true))) ){
// character is valid and allowed to login
$return->character = reset($characters)->getData();
}else{
$characterError = (object) [];
$characterError->type = 'warning';
$characterError->message = 'This can happen through "invalid cookie data", "login restrictions", "CREST problems".';
$return->error[] = $characterError;
}
}
}
echo json_encode($return);
}
/**
* get captcha image and store key to session
* @param \Base $f3
@@ -149,64 +178,6 @@ class User extends Controller\Controller{
parent::logOut($f3);
}
/**
* save/update "map sharing" configurations for all map types
* the user has access to
* @param \Base $f3
*/
public function saveSharingConfig(\Base $f3){
$data = $f3->get('POST');
$return = (object) [];
$activeCharacter = $this->getCharacter();
if($activeCharacter){
$privateSharing = 0;
$corporationSharing = 0;
$allianceSharing = 0;
// form values
if(isset($data['formData'])){
$formData = $data['formData'];
if(isset($formData['privateSharing'])){
$privateSharing = 1;
}
if(isset($formData['corporationSharing'])){
$corporationSharing = 1;
}
if(isset($formData['allianceSharing'])){
$allianceSharing = 1;
}
}
$activeCharacter->shared = $privateSharing;
$activeCharacter = $activeCharacter->save();
// update corp/ally ---------------------------------------------------------------
$corporation = $activeCharacter->getCorporation();
$alliance = $activeCharacter->getAlliance();
if(is_object($corporation)){
$corporation->shared = $corporationSharing;
$corporation->save();
}
if(is_object($alliance)){
$alliance->shared = $allianceSharing;
$alliance->save();
}
$user = $activeCharacter->getUser();
$return->userData = $user->getData();
}
echo json_encode($return);
}
/**
* update user account data
* -> a fresh user automatically generated on first login with a new character

View File

@@ -8,6 +8,7 @@
namespace Controller;
use Controller\Ccp as Ccp;
use Model;
class AppController extends Controller {
@@ -43,6 +44,12 @@ class AppController extends Controller {
// JS main file
$f3->set('jsView', 'login');
// characters from cookies
$f3->set('cookieCharacters', $this->getCookieByName(self::COOKIE_PREFIX_CHARACTER, true));
$f3->set('getCharacterGrid', function($characters){
return ( ((12 / count($characters)) <= 4) ? 4 : (12 / count($characters)) );
});
}
}

View File

@@ -52,6 +52,7 @@ class Sso extends Api\User{
const ERROR_CHARACTER_FORBIDDEN = 'Character "%s" is not authorized to log in';
const ERROR_CHARACTER_MISMATCH = 'The character "%s" you tried to log in, does not match';
const ERROR_SERVICE_TIMEOUT = 'CCP SSO service timeout (%ss). Try again later';
const ERROR_COOKIE_LOGIN = 'Login from Cookie failed. Please retry by CCP SSO';
/**
* CREST "Scopes" are used by pathfinder
@@ -156,7 +157,7 @@ class Sso extends Api\User{
// get character data from CREST
$characterData = $this->getCharacterData($accessData->accessToken);
if(isset($characterData->character)){
if( isset($characterData->character) ){
// add "ownerHash" and CREST tokens
$characterData->character['ownerHash'] = $verificationCharacterData->CharacterOwnerHash;
$characterData->character['crestAccessToken'] = $accessData->accessToken;
@@ -203,6 +204,9 @@ class Sso extends Api\User{
$loginCheck = $this->loginByCharacter($characterModel);
if($loginCheck){
// set "login" cookie
$this->setLoginCookie($characterModel);
// route to "map"
$f3->reroute('@map');
}else{
@@ -235,6 +239,38 @@ class Sso extends Api\User{
}
}
/**
* login by cookie
* @param \Base $f3
*/
public function login(\Base $f3){
$data = (array)$f3->get('GET');
$character = null;
if( !empty($data['cookie']) ){
if( !empty($cookieData = $this->getCookieByName($data['cookie']) )){
// cookie data is valid -> validate data against DB (security check!)
if( !empty($characters = $this->getCookieCharacters(array_slice($cookieData, 0, 1, true))) ){
// character is valid and allowed to login
$character = $characters[$data['cookie']];
}
}
}
if( is_object($character)){
// login by character
$loginCheck = $this->loginByCharacter($character);
if($loginCheck){
// route to "map"
$f3->reroute('@map');
}
}
// on error -> route back to login form
$f3->set(self::SESSION_KEY_SSO_ERROR, self::ERROR_COOKIE_LOGIN);
$f3->reroute('@login');
}
/**
* get a valid "access_token" for oAuth 2.0 verification
* -> if $authCode is set -> request NEW "access_token"
@@ -357,7 +393,7 @@ class Sso extends Api\User{
* @param $accessToken
* @return mixed|null
*/
protected function verifyCharacterData($accessToken){
public function verifyCharacterData($accessToken){
$verifyUserUrl = self::getVerifyUserEndpoint();
$verifyUrlParts = parse_url($verifyUserUrl);
$characterData = null;
@@ -492,7 +528,7 @@ class Sso extends Api\User{
* @param array $additionalOptions
* @return object
*/
protected function getCharacterData($accessToken, $additionalOptions = []){
public function getCharacterData($accessToken, $additionalOptions = []){
$endpoints = $this->getEndpoints($accessToken, $additionalOptions);
$characterData = (object) [];

View File

@@ -13,6 +13,10 @@ use DB;
class Controller {
// cookie specific keys (names)
const COOKIE_NAME_STATE = 'cookie';
const COOKIE_PREFIX_CHARACTER = 'char';
/**
* @var \Base
*/
@@ -114,6 +118,200 @@ class Controller {
}
}
/**
* get cookies "state" information
* -> whether user accepts cookies
* @return bool
*/
protected function getCookieState(){
return (bool)count( $this->getCookieByName(self::COOKIE_NAME_STATE) );
}
/**
* search for existing cookies
* -> either a specific cookie by its name
* -> or get multiple cookies by their name (search by prefix)
* @param $cookieName
* @param bool $prefix
* @return array
*/
protected function getCookieByName($cookieName, $prefix = false){
$data = [];
if(!empty($cookieName)){
$cookieData = (array)$this->getF3()->get('COOKIE');
if($prefix === true){
// look for multiple cookies with same prefix
foreach($cookieData as $name => $value){
if(strpos($name, $cookieName) === 0){
$data[$name] = $value;
}
}
}elseif( isset($cookieData[$cookieName]) ){
// look for a single cookie
$data[$cookieName] = $cookieData[$cookieName];
}
}
return $data;
}
/**
* set/update logged in cookie by character model
* -> store validation data in DB
* @param Model\CharacterModel $character
*/
protected function setLoginCookie(Model\CharacterModel $character){
if( $this->getCookieState() ){
$expireSeconds = (int) $this->getF3()->get('PATHFINDER.LOGIN.COOKIE_EXPIRE');
$expireSeconds *= 24 * 60 * 60;
$timezone = new \DateTimeZone( $this->getF3()->get('TZ') );
$expireTime = new \DateTime('now', $timezone);
// add cookie expire time
$expireTime->add(new \DateInterval('PT' . $expireSeconds . 'S'));
// unique "selector" -> to facilitate database look-ups (small size)
// -> This is preferable to simply using the database id field,
// which leaks the number of active users on the application
$selector = bin2hex(mcrypt_create_iv(12, MCRYPT_DEV_URANDOM));
// generate unique "validator" (strong encryption)
// -> plaintext set to user (cookie), hashed version of this in DB
$size = mcrypt_get_iv_size(MCRYPT_CAST_256, MCRYPT_MODE_CFB);
$validator = bin2hex(mcrypt_create_iv($size, MCRYPT_DEV_URANDOM));
// generate unique cookie token
$token = hash('sha256', $validator);
// get unique cookie name for this character
$name = md5($character->name);
$authData = [
'characterId' => $character,
'selector' => $selector,
'token' => $token,
'expires' => $expireTime->format('Y-m-d H:i:s')
];
$authenticationModel = $character->rel('characterTokens');
$authenticationModel->copyfrom($authData);
$authenticationModel->save();
$cookieValue = implode(':', [$selector, $validator]);
// get cookie name -> save new one OR update existing cookie
$cookieName = 'COOKIE.' . self::COOKIE_PREFIX_CHARACTER . '_' . $name;
$this->getF3()->set($cookieName, $cookieValue, $expireSeconds);
}
}
/**
* get characters from given cookie data
* -> validate cookie data
* -> validate characters
* @param array $cookieData
* @return array
* @throws \Exception
*/
protected function getCookieCharacters($cookieData = []){
$characters = [];
if(
$this->getCookieState() &&
!empty($cookieData)
){
/**
* @var $characterAuth Model\CharacterAuthenticationModel
*/
$characterAuth = Model\BasicModel::getNew('CharacterAuthenticationModel');
$timezone = new \DateTimeZone( $this->getF3()->get('TZ') );
$currentTime = new \DateTime('now', $timezone);
foreach($cookieData as $name => $value){
// remove invalid cookies
$invalidCookie = false;
$data = explode(':', $value);
if(count($data) === 2){
// cookie data is well formatted
$characterAuth->getByForeignKey('selector', $data[0], ['limit' => 1]);
// validate expire data
// validate token
if(
!$characterAuth->dry() &&
strtotime($characterAuth->expires) >= $currentTime->getTimestamp() &&
hash_equals($characterAuth->token, hash('sha256', $data[1]))
){
// cookie information is valid
// -> try to update character information from CREST
// e.g. Corp has changed, this also ensures valid "access_token"
/**
* @var $character Model\CharacterModel
*/
$character = $characterAuth->characterId;
$updateStatus = $character->updateFromCrest();
// check if character still has user (is not the case of "ownerHash" changed
// check if character is still authorized to log in (e.g. corp/ally or config has changed
// -> do NOT remove cookie on failure. This can be a temporary problem (e.g. CREST is down,..)
if(
empty($updateStatus) &&
$character->hasUserCharacter() &&
$character->isAuthorized()
){
$characters[$name] = $character;
}
}else{
$invalidCookie = true;
}
$characterAuth->reset();
}else{
$invalidCookie = true;
}
// remove invalid cookie
if($invalidCookie){
$this->getF3()->clear('COOKIE.' . $name);
}
}
}
return $characters;
}
/**
* checks whether a user is currently logged in
* @param \Base $f3
* @return bool
*/
protected function checkLogTimer($f3){
$loginCheck = false;
if($f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) > 0){
// check logIn time
$logInTime = new \DateTime();
$logInTime->setTimestamp( $f3->get(Api\User::SESSION_KEY_CHARACTER_TIME) );
$now = new \DateTime();
$timeDiff = $now->diff($logInTime);
$minutes = $timeDiff->days * 60 * 24 * 60;
$minutes += $timeDiff->h * 60;
$minutes += $timeDiff->i;
if($minutes <= $f3->get('PATHFINDER.TIMER.LOGGED')){
$loginCheck = true;
}
}
return $loginCheck;
}
/**
* get current character model
* @param int $ttl
@@ -127,12 +325,15 @@ class Controller {
$characterId = (int)$this->getF3()->get(Api\User::SESSION_KEY_CHARACTER_ID);
if($characterId){
/**
* @var $characterModel \Model\CharacterModel
* @var $characterModel Model\CharacterModel
*/
$characterModel = Model\BasicModel::getNew('CharacterModel');
$characterModel->getById($characterId, $ttl);
if( !$characterModel->dry() ){
if(
!$characterModel->dry() &&
$characterModel->hasUserCharacter()
){
$character = &$characterModel;
}
}

View File

@@ -40,6 +40,7 @@ class Setup extends Controller {
'Model\UserCharacterModel',
'Model\CharacterModel',
'Model\CharacterAuthenticationModel',
'Model\CharacterLogModel',
'Model\SystemModel',