Source of file TokenAuthenticator.php
Size: 14,855 Bytes - Last Modified: 2021-12-24T06:41:16+00:00
/var/www/docs.ssmods.com/process/src/src/Authenticators/TokenAuthenticator.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 | <?php namespace Colymba\RESTfulAPI\Authenticators; use Colymba\RESTfulAPI\RESTfulAPIError; use Colymba\RESTfulAPI\Authenticators\Authenticator; use Colymba\RESTfulAPI\Extensions\TokenAuthExtension; use SilverStripe\Control\HTTPRequest; use SilverStripe\Control\Session; use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Config\Config; use SilverStripe\Core\Convert; use SilverStripe\ORM\DataObject; use SilverStripe\Security\IdentityStore; use SilverStripe\Security\Member; use SilverStripe\Security\MemberAuthenticator\LostPasswordHandler; use SilverStripe\Security\MemberAuthenticator\MemberAuthenticator; use SilverStripe\Security\PasswordEncryptor; use SilverStripe\Security\RandomGenerator; use SilverStripe\Security\Security; /** * RESTfulAPI Token authenticator * handles login, logout and request authentication via token * * @author Thierry Francois @colymba thierry@colymba.com * @copyright Copyright (c) 2013, Thierry Francois * * @license http://opensource.org/licenses/BSD-3-Clause BSD Simplified * * @package RESTfulAPI * @subpackage Authentication */ class TokenAuthenticator implements Authenticator { /** * Authentication token life in seconds * * @var integer * @config */ private static $tokenLife = 10800; //3 * 60 * 60; /** * HTTP Header name storing authentication token * * @var string * @config */ private static $tokenHeader = 'X-Silverstripe-Apitoken'; /** * Fallback GET/POST HTTP query var storing authentication token * * @var string * @config */ private static $tokenQueryVar = 'token'; /** * Class name to query for token validation * * @var string * @config */ private static $tokenOwnerClass = Member::class; /** * Whether or not the token should auto-update on activity. * When set to true, the token will automatically update its lifetime, similar * to a session-ping. * * @var boolean * @config */ private static $autoRefreshLifetime = false; /** * Stores current token authentication configurations * header, var, class, db columns.... * * @var array */ protected $tokenConfig; const AUTH_CODE_LOGGED_IN = 0; const AUTH_CODE_LOGIN_FAIL = 1; const AUTH_CODE_TOKEN_INVALID = 2; const AUTH_CODE_TOKEN_EXPIRED = 3; /** * List of URL accessible actions * * @var array */ private static $allowed_actions = array( 'login', 'logout', 'lostPassword', ); /** * Instanciation + config aquisition */ public function __construct() { $config = array(); $configInstance = Config::inst(); $config['life'] = $configInstance->get(self::class, 'tokenLife'); $config['header'] = $configInstance->get(self::class, 'tokenHeader'); $config['queryVar'] = $configInstance->get(self::class, 'tokenQueryVar'); $config['owner'] = $configInstance->get(self::class, 'tokenOwnerClass'); $config['autoRefresh'] = $configInstance->get(self::class, 'autoRefreshLifetime'); $tokenDBColumns = $configInstance->get(TokenAuthExtension::class, 'db'); $tokenDBColumn = array_search('Varchar(160)', $tokenDBColumns); $expireDBColumn = array_search('Int', $tokenDBColumns); if ($tokenDBColumn !== false) { $config['DBColumn'] = $tokenDBColumn; } else { $config['DBColumn'] = 'ApiToken'; } if ($expireDBColumn !== false) { $config['expireDBColumn'] = $expireDBColumn; } else { $config['expireDBColumn'] = 'ApiTokenExpire'; } $this->tokenConfig = $config; } /** * Login a user into the Framework and generates API token * Only works if the token owner is a Member * * @param HTTPRequest $request HTTP request containing 'email' & 'pwd' vars * @return array login result with token */ public function login(HTTPRequest $request) { $response = array(); if ($this->tokenConfig['owner'] === Member::class) { $email = $request->requestVar('email'); $pwd = $request->requestVar('pwd'); $member = false; if ($email && $pwd) { $member = Injector::inst()->get(MemberAuthenticator::class)->authenticate( array( 'Email' => $email, 'Password' => $pwd, ), $request ); if ($member) { $tokenData = $this->generateToken(); $tokenDBColumn = $this->tokenConfig['DBColumn']; $expireDBColumn = $this->tokenConfig['expireDBColumn']; $member->{$tokenDBColumn} = $tokenData['token']; $member->{$expireDBColumn} = $tokenData['expire']; $member->write(); $member->login(); } } if (!$member) { $response['result'] = false; $response['message'] = 'Authentication fail.'; $response['code'] = self::AUTH_CODE_LOGIN_FAIL; } else { $response['result'] = true; $response['message'] = 'Logged in.'; $response['code'] = self::AUTH_CODE_LOGGED_IN; $response['token'] = $tokenData['token']; $response['expire'] = $tokenData['expire']; $response['userID'] = $member->ID; } } return $response; } /** * Logout a user from framework * and update token with an expired one * if token owner class is a Member * * @param HTTPRequest $request HTTP request containing 'email' var */ public function logout(HTTPRequest $request) { $email = $request->requestVar('email'); $member = Member::get()->filter(array('Email' => $email))->first(); if ($member) { //logout $member->logout(); if ($this->tokenConfig['owner'] === Member::class) { //generate expired token $tokenData = $this->generateToken(true); //write $tokenDBColumn = $this->tokenConfig['DBColumn']; $expireDBColumn = $this->tokenConfig['expireDBColumn']; $member->{$tokenDBColumn} = $tokenData['token']; $member->{$expireDBColumn} = $tokenData['expire']; $member->write(); } } } /** * Sends password recovery email * * @param HTTPRequest $request HTTP request containing 'email' vars * @return array 'email' => false if email fails (Member doesn't exist will not be reported) */ public function lostPassword(HTTPRequest $request) { $email = Convert::raw2sql($request->requestVar('email')); $member = DataObject::get_one(Member::class, "\"Email\" = '{$email}'"); if ($member) { $token = $member->generateAutologinTokenAndStoreHash(); $link = Security::lost_password_url(); $lostPasswordHandler = new LostPasswordHandler($link); $lostPasswordHandler->sendEmail($member, $token); } return array('done' => true); } /** * Return the stored API token for a specific owner * * @param integer $id ID of the token owner * @return string API token for the owner */ public function getToken($id) { if ($id) { $ownerClass = $this->tokenConfig['owner']; $owner = DataObject::get_by_id($ownerClass, $id); if ($owner) { $tokenDBColumn = $this->tokenConfig['DBColumn']; return $owner->{$tokenDBColumn}; } else { user_error("API Token owner '$ownerClass' not found with ID = $id", E_USER_WARNING); } } else { user_error("TokenAuthenticator::getToken() requires an ID as argument.", E_USER_WARNING); } } /** * Reset an owner's token * if $expired is set to true the owner's will have a new invalidated/expired token * * @param integer $id ID of the token owner * @param boolean $expired if true the token will be invalidated */ public function resetToken($id, $expired = false) { if ($id) { $ownerClass = $this->tokenConfig['owner']; $owner = DataObject::get_by_id($ownerClass, $id); if ($owner) { //generate token $tokenData = $this->generateToken($expired); //write $tokenDBColumn = $this->tokenConfig['DBColumn']; $expireDBColumn = $this->tokenConfig['expireDBColumn']; $owner->{$tokenDBColumn} = $tokenData['token']; $owner->{$expireDBColumn} = $tokenData['expire']; $owner->write(); } else { user_error("API Token owner '$ownerClass' not found with ID = $id", E_USER_WARNING); } } else { user_error("TokenAuthenticator::resetToken() requires an ID as argument.", E_USER_WARNING); } } /** * Generates an encrypted random token * and an expiry date * * @param boolean $expired Set to true to generate an outdated token * @return array token data array('token' => HASH, 'expire' => EXPIRY_DATE) */ private function generateToken($expired = false) { $life = $this->tokenConfig['life']; if (!$expired) { $expire = time() + $life; } else { $expire = time() - ($life * 2); } $generator = new RandomGenerator(); $tokenString = $generator->randomToken(); $e = PasswordEncryptor::create_for_algorithm('blowfish'); //blowfish isn't URL safe and maybe too long? $salt = $e->salt($tokenString); $token = $e->encrypt($tokenString, $salt); return array( 'token' => substr($token, 7), 'expire' => $expire, ); } /** * Returns the DataObject related to the token * that sent the authenticated request * * @param HTTPRequest $request HTTP API request * @return null|DataObject null if failed or the DataObject token owner related to the request */ public function getOwner(HTTPRequest $request) { $owner = null; //get the token $token = $request->getHeader($this->tokenConfig['header']); if (!$token) { $token = $request->requestVar($this->tokenConfig['queryVar']); } if ($token) { $SQLToken = Convert::raw2sql($token); $owner = DataObject::get_one( $this->tokenConfig['owner'], "\"" . $this->tokenConfig['DBColumn'] . "\"='" . $SQLToken . "'", false ); if (!$owner) { $owner = null; } } return $owner; } /** * Checks if a request to the API is authenticated * Gets API Token from HTTP Request and return Auth result * * @param HTTPRequest $request HTTP API request * @return true|RESTfulAPIError True if token is valid OR RESTfulAPIError with details */ public function authenticate(HTTPRequest $request) { //get the token $token = $request->getHeader($this->tokenConfig['header']); if (!$token) { $token = $request->requestVar($this->tokenConfig['queryVar']); } if ($token) { //check token validity return $this->validateAPIToken($token, $request); } else { //no token, bad news return new RESTfulAPIError(403, 'Token invalid.', array( 'message' => 'Token invalid.', 'code' => self::AUTH_CODE_TOKEN_INVALID, ) ); } } /** * Validate the API token * * @param string $token Authentication token * @param HTTPRequest $request HTTP API request * @return true|RESTfulAPIError True if token is valid OR RESTfulAPIError with details */ private function validateAPIToken($token, $request) { //get owner with that token $SQL_token = Convert::raw2sql($token); $tokenColumn = $this->tokenConfig['DBColumn']; $tokenOwner = DataObject::get_one( $this->tokenConfig['owner'], "\"" . $this->tokenConfig['DBColumn'] . "\"='" . $SQL_token . "'", false ); if ($tokenOwner) { //check token expiry $tokenExpire = $tokenOwner->{$this->tokenConfig['expireDBColumn']}; $now = time(); $life = $this->tokenConfig['life']; if ($tokenExpire > ($now - $life)) { // check if token should automatically be updated if ($this->tokenConfig['autoRefresh']) { $tokenOwner->setField($this->tokenConfig['expireDBColumn'], $now + $life); $tokenOwner->write(); } //all good, log Member in if (is_a($tokenOwner, Member::class)) { # this is a login without the logging Config::nest(); Config::modify()->set(Member::class, 'session_regenerate_id', true); $identityStore = Injector::inst()->get(IdentityStore::class); $identityStore->logIn($tokenOwner, false, $request); Config::unnest(); } return true; } else { //too old return new RESTfulAPIError(403, 'Token expired.', array( 'message' => 'Token expired.', 'code' => self::AUTH_CODE_TOKEN_EXPIRED, ) ); } } else { //token not found //not sure it's wise to say it doesn't exist. Let's be shady here return new RESTfulAPIError(403, 'Token invalid.', array( 'message' => 'Token invalid.', 'code' => self::AUTH_CODE_TOKEN_INVALID, ) ); } } } |