* @category class * @package core * @subpackage api * @copyright 2005-2017 Titan Framework * @license http://www.titanframework.com/license/ BSD License (3 Clause) * @see Api, ApiEntity, ApiException, ApiList * @link http://www.titanframework.com/docs/api/ */ abstract class ApiAuth { protected $name; protected $timeout = 36000; protected $token; protected $gcmApiKey = NULL; protected $sendAlerts = FALSE; protected $context = array (); protected static $headers = NULL; protected $user = NULL; protected $register = NULL; protected $endpoints = array (); const C_USER_LOGIN = 'USER'; const C_USER_ID = 'USER-BY-ID'; const C_USER_MAIL = 'USER-BY-MAIL'; const C_USER_BROWSER = 'USER-BROWSER'; const C_CLIENT = 'CLIENT'; const C_CLIENT_USER = 'CLIENT-AS-USER'; const C_APP = 'APP'; public function __construct ($app) { if (!array_key_exists ('name', $app)) return; $this->name = trim ($app ['name']); if (array_key_exists ('auth', $app)) { $this->context = explode ('|', $app ['auth']); array_walk ($this->context, 'cleanArray'); } if (array_key_exists ('token', $app)) $this->token = trim ($app ['token']); if (array_key_exists ('request-timeout', $app)) $this->timeout = (int) preg_replace ('/[^0-9]/i', '', $app ['request-timeout']); if (array_key_exists ('gcm-api-key', $app)) $this->gcmApiKey = trim ($app ['gcm-api-key']); if (array_key_exists ('send-alerts', $app)) $this->sendAlerts = strtoupper (trim ($app ['send-alerts'])) == 'TRUE' ? TRUE : FALSE; if (array_key_exists ('register-as', $app) && trim ($app ['register-as']) != '' && Security::singleton ()->userTypeExists ($app ['register-as'])) $this->register = trim ($app ['register-as']); if (array_key_exists ('endpoint', $app) && is_array ($app ['endpoint'])) foreach ($app ['endpoint'] as $trash => $endpoint) { if (!array_key_exists ($endpoint ['method'], $this->endpoints)) $this->endpoints [$endpoint ['method']] = array (); $this->endpoints [$endpoint ['method']][] = trim (trim ($endpoint ['uri']), '\/'); } } public function getUser () { return $this->user; } public function setUser ($id) { $this->user = (int) $id; User::singleton ()->loadById ($id); } public function getName () { return $this->name; } public function hasContext () { $args = func_get_args (); foreach ($args as $trash => $auth) if (in_array ($auth, $this->context)) return TRUE; return FALSE; } public function sendAlerts () { return $this->sendAlerts; } public function getRegisterType () { if (is_null ($this->register)) return NULL; return Security::singleton ()->getUserType ($this->register); } public function encrypt ($input) { return Api::encrypt ($input, md5 ($this->token . (string) $this->timestamp)); } public function decrypt ($input) { return Api::decrypt ($input, md5 ($this->token . (string) $this->timestamp)); } public function load () { $this->loadParamsByHeaders (); } public function isAccessibleEndpoint ($uri) { if (!sizeof ($this->endpoints)) return TRUE; foreach ($this->endpoints [Api::getHttpRequestMethod ()] as $trash => $value) { preg_match ('/'. $value .'/', $uri, $matches); // For debug: // throw new Exception (print_r ($matches, TRUE)); if (sizeof ($matches) != 1 || strlen ($matches [0]) != strlen ($uri)) continue; return TRUE; } return FALSE; } abstract public function authenticate (); abstract protected function loadParamsByHeaders (); abstract protected function requiredParamsIsFilled (); abstract public function isActive (); abstract static protected function signature ($timestamp, $id, $signature); abstract static protected function sanitizeParam ($param, $value); abstract public static function getHeaders (); abstract public function registerGoogleCloudMessage ($gcmRegistrationId); abstract public function sendNotification ($user, $message); } /** * This class contains implementation for Embrapa-Auth protocol. * * @author Jairo Rodrigues Filho * @author Camilo Carromeu * @todo Move to repos/auth/Embrapa.php * @link http://cloud.cnpgc.embrapa.br/embrapa-auth */ class EmbrapaAuth extends ApiAuth { const TIMESTAMP = 'x-embrapa-auth-timestamp'; const USER_ID = 'x-embrapa-auth-user-id'; const USER_SIGNATURE = 'x-embrapa-auth-user-signature'; const CLIENT_ID = 'x-embrapa-auth-client-id'; const CLIENT_SIGNATURE = 'x-embrapa-auth-client-signature'; const APP_ID = 'x-embrapa-auth-application-id'; const APP_SIGNATURE = 'x-embrapa-auth-application-signature'; protected $timestamp = 0; protected $userId = ''; protected $userSignature = ''; protected $clientId = ''; protected $clientSignature = ''; protected $appId = ''; protected $appSignature = ''; public function __construct ($app) { parent::__construct ($app); } public function authenticateForRegister () { if (!$this->timestamp) throw new ApiException (__ ('Has a problem with your device clock! Please, verify.'), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST, 'Invalid header parameter: UNIX timestamp is empty!'); if (sizeof (array_intersect (array (self::C_APP), $this->context)) && ($this->name == '' || $this->token == '')) throw new ApiException (__ ('Application credentials are incorrect or empty!'), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST, 'Invalid header parameter: Application credentials are incorrect or empty!'); if (time () < $this->timestamp - 180) throw new ApiException (__ ('The time of your device must be correct!'), ApiException::ERROR_REQUEST_TIMESTAMP, ApiException::BAD_REQUEST, 'UNIX timestamp of request is invalid (higher than server time)!'); if (time () - $this->timestamp > $this->timeout) throw new ApiException (__ ('Request timeout!'), ApiException::ERROR_REQUEST_TIMESTAMP, ApiException::REQUEST_TIME_OUT, 'UNIX timestamp of request is very old!'); if (sizeof (array_intersect (array (self::C_APP), $this->context)) && $this->appSignature != self::signature ($this->timestamp, $this->name, $this->token)) throw new ApiException (__ ('Invalid application credentials!'), ApiException::ERROR_APP_AUTH, ApiException::UNAUTHORIZED); } public function authenticate () { $this->requiredParamsIsFilled (); $db = Database::singleton (); if (time () < $this->timestamp - 180) throw new ApiException (__ ('The time of your device must be correct!'), ApiException::ERROR_REQUEST_TIMESTAMP, ApiException::BAD_REQUEST, 'UNIX timestamp of request is invalid (higher than server time)!'); if (time () - $this->timestamp > $this->timeout) throw new ApiException (__ ('Request timeout!'), ApiException::ERROR_REQUEST_TIMESTAMP, ApiException::REQUEST_TIME_OUT, 'UNIX timestamp of request is very old!'); if (sizeof (array_intersect (array (self::C_APP), $this->context)) && $this->appSignature != self::signature ($this->timestamp, $this->name, $this->token)) throw new ApiException (__ ('Invalid application credentials!'), ApiException::ERROR_APP_AUTH, ApiException::UNAUTHORIZED); if (sizeof (array_intersect (array (self::C_CLIENT, self::C_CLIENT_USER), $this->context))) { if (!MobileDevice::isActive ()) throw new ApiException (__ ('Register of credentials for mobile devices is not enabled!'), ApiException::ERROR_CLIENT_AUTH, ApiException::UNAUTHORIZED); $client = MobileDevice::getRegisteredDevice ($this->clientId); if (!is_object ($client)) throw new ApiException (__ ('This client is not registered!'), ApiException::ERROR_CLIENT_AUTH, ApiException::UNAUTHORIZED); if ($this->clientSignature != self::signature ($this->timestamp, $client->id, $client->pk)) throw new ApiException (__ ('Invalid client credentials!'), ApiException::ERROR_CLIENT_AUTH, ApiException::UNAUTHORIZED); if (in_array (self::C_CLIENT_USER, $this->context)) { $sth = $db->prepare ("SELECT _id FROM _user WHERE _id = :id AND _active = B'1' AND _deleted = B'0' LIMIT 1"); $sth->bindParam (':id', $client->user, PDO::PARAM_INT); $sth->execute (); if (!is_object ($sth->fetch (PDO::FETCH_OBJ))) throw new ApiException (__ ('User does not exist or is inactive!'), ApiException::ERROR_USER_AUTH, ApiException::UNAUTHORIZED); $this->setUser ($client->user); } MobileDevice::registerDeviceAccess ($this->clientId); } if (sizeof (array_intersect (array (self::C_USER_ID, self::C_USER_LOGIN, self::C_USER_MAIL, self::C_USER_BROWSER), $this->context))) { $user = NULL; if (in_array (self::C_USER_LOGIN, $this->context)) { $sth = $db->prepare ("SELECT _id, _login AS id, _password AS passwd FROM _user WHERE _login = :login AND _active = B'1' AND _deleted = B'0' LIMIT 1"); $sth->bindParam (':login', $this->userId, PDO::PARAM_STR); $sth->execute (); $user = $sth->fetch (PDO::FETCH_OBJ); } elseif (in_array (self::C_USER_ID, $this->context)) { $sth = $db->prepare ("SELECT _id, _id AS id, _password AS passwd FROM _user WHERE _id = :id AND _active = B'1' AND _deleted = B'0' LIMIT 1"); $uid = (int) preg_replace ('/[^0-9]/i', '', $this->userId); $sth->bindParam (':id', $uid, PDO::PARAM_INT); $sth->execute (); $user = $sth->fetch (PDO::FETCH_OBJ); } elseif (in_array (self::C_USER_MAIL, $this->context) || in_array (self::C_USER_BROWSER, $this->context)) { if (!Database::isUnique ('_user', '_email')) throw new ApiException (__ ('e-Mail must be unique to authenticate user! Please, report to system administrator.'), ApiException::ERROR_USER_AUTH, ApiException::UNAUTHORIZED); $sth = $db->prepare ("SELECT _id, _email AS id, _password AS passwd FROM _user WHERE _email = :mail AND _active = B'1' AND _deleted = B'0' LIMIT 1"); $sth->bindParam (':mail', $this->userId, PDO::PARAM_STR); $sth->execute (); $user = $sth->fetch (PDO::FETCH_OBJ); } if (!is_object ($user)) throw new ApiException (__ ('User does not exist or is inactive!'), ApiException::ERROR_USER_AUTH, ApiException::UNAUTHORIZED); if (in_array (self::C_USER_BROWSER, $this->context)) { $pk = BrowserDevice::getKeyForRegisteredUser ($user->_id); if ($this->userSignature != self::signature ($this->timestamp, $user->id, $pk)) throw new ApiException (__ ('Invalid user credentials!'), ApiException::ERROR_USER_AUTH, ApiException::UNAUTHORIZED); BrowserDevice::registerAccess ($user->_id); } elseif ($this->userSignature != self::signature ($this->timestamp, $user->id, $user->passwd)) throw new ApiException (__ ('Invalid user credentials!'), ApiException::ERROR_USER_AUTH, ApiException::UNAUTHORIZED); $this->setUser ($user->_id); } return TRUE; } protected function loadParamsByHeaders () { $headers = self::getHeaders (); $params = array (self::TIMESTAMP => 'timestamp', self::USER_ID => 'userId', self::USER_SIGNATURE => 'userSignature', self::CLIENT_ID => 'clientId', self::CLIENT_SIGNATURE => 'clientSignature', self::APP_ID => 'appId', self::APP_SIGNATURE => 'appSignature'); foreach ($params as $key => $param) $this->$param = array_key_exists ($key, $headers) ? self::sanitizeParam ($key, $headers [$key]) : NULL; } protected function requiredParamsIsFilled () { if (!$this->timestamp) throw new ApiException (__ ('Has a problem with your device clock! Please, verify.'), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST, 'Invalid header parameter: UNIX timestamp is empty!'); if (sizeof (array_intersect (array (self::C_USER_ID, self::C_USER_LOGIN, self::C_USER_MAIL), $this->context)) && ($this->userId == '' || $this->userSignature == '')) throw new ApiException (__ ('User credentials are incorrect or empty!'), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST, 'Invalid header parameter: User credentials are incorrect or empty!'); if (sizeof (array_intersect (array (self::C_CLIENT, self::C_CLIENT_USER), $this->context)) && ($this->clientId == '' || $this->clientSignature == '')) throw new ApiException (__ ('Client credentials are incorrect or empty!'), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST, 'Invalid header parameter: Client credentials are incorrect or empty!'); if (sizeof (array_intersect (array (self::C_APP), $this->context)) && ($this->name == '' || $this->token == '')) throw new ApiException (__ ('Application credentials are incorrect or empty!'), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST, 'Invalid header parameter: Application credentials are incorrect or empty!'); } public static function getHeaders () { if (is_null (self::$headers)) { self::$headers = apache_request_headers (); foreach (self::$headers as $key => $value) self::$headers [strtolower ($key)] = $value; } return self::$headers; } static protected function signature ($timestamp, $id, $signature) { return hash_hmac ('sha1', $timestamp . $id, $signature); } static protected function sanitizeParam ($param, $value) { $value = trim ($value); switch ($param) { case self::TIMESTAMP: return (int) preg_replace ('/[^0-9]/i', '', $value); case self::APP_SIGNATURE: case self::CLIENT_SIGNATURE: case self::USER_SIGNATURE: $value = preg_replace ('/[^0-9A-Fa-f]/i', '', $value); if (strlen ($value) != 40) return NULL; return $value; } return $value; } public function isActive () { $headers = self::getHeaders (); if (!is_array ($headers) || !array_key_exists (self::APP_ID, $headers) || trim ($headers [self::APP_ID]) != $this->name) return FALSE; return TRUE; } public function registerGoogleCloudMessage ($gcmRegistrationId) { try { return MobileDevice::registerGoogleCloudMessage ($this->clientId, $gcmRegistrationId); } catch (Exception $e) { throw new ApiException ($e->getMessage (), ApiException::ERROR_INVALID_PARAMETER, ApiException::BAD_REQUEST); } } public function sendNotification ($user, $message) { if (is_integer ($user)) $user = array ($user); if (!is_array ($user) || !sizeof ($user) || !is_array ($message) || !sizeof ($message)) return FALSE; $sth = Database::singleton ()->prepare ("SELECT _gcm FROM _mobile WHERE _user IN (". implode (",", $user) .") AND _gcm IS NOT NULL"); $sth->execute (); $ids = $sth->fetchAll (PDO::FETCH_COLUMN); if (sizeof ($ids)) MobileDevice::sendNotification ($this->gcmApiKey, $ids, $message); } }