closed #138 added new cookie based login
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -568,7 +568,6 @@ class Route extends \Controller\AccessController {
|
||||
$item = false;
|
||||
}
|
||||
$data[0]->reset();
|
||||
|
||||
}
|
||||
|
||||
}, [$map, $validMaps, $activeCharacter]);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) );
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) [];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class Setup extends Controller {
|
||||
|
||||
'Model\UserCharacterModel',
|
||||
'Model\CharacterModel',
|
||||
'Model\CharacterAuthenticationModel',
|
||||
'Model\CharacterLogModel',
|
||||
|
||||
'Model\SystemModel',
|
||||
|
||||
Reference in New Issue
Block a user