Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,22 @@ DirectoryIndex index.php
</IfModule>
```

### Authentication
### Authentication and Authorization

Authentication is unique for each application. But tying your authentication mechanisms into RestServer is easy. By simply adding a method named `authorize` to your `Controller` all requests will call that method first. If `authorize()` returns false, the server will issue a 401 Unauthorized response. If `authorize()` returns true, the request continues on to call the correct controller action. All actions will run the authorization first unless you add `@noAuth` in the action's docs (I usually put it above the `@url` mappings).
Authentication is unique for each application. But tying your authentication mechanisms into RestServer is easy. By simply adding `authenticate` and `authorize` methods to your `Controller` all requests will call these methods first. If `authenticate()` or `authorize()` returns false, the server will issue a **401 Invalid credentials** or **403 Unauthorized** response respectively. If both `authenticate()` and `authorize()` returns true, the request continues on to call the correct controller action. All actions will run the authorization first unless you add `@noAuth` in the action's docs (I usually put it above the `@url` mappings).

Inside your authentication method you can use PHP's [`getallheaders`](http://php.net/manual/en/function.getallheaders.php) function or `$_COOKIE` depending on how you want to authorize your users. This is where you would load the user object from your database, and set it to `$this->user = getUserFromDatabase()` so that your action will have access to it later when it gets called.
You can select authentication and authorization methods as per your requirements you can implement only `autenticate` if you want to confirm client identity. Or you can implement both, then `authorize` can help to confirm if current client is allowed to access a certain action. For more details about authentication. and how to use `JWT` token as bearer header please check example file `TestAuthControll.php`.

RestServer is meant to be a simple mechanism to map your application into a REST API. The rest of the details are up to you.
Currently default authentication handler support **Basic** and **Bearer** headers based authentication. and pass `[username, password]` or `bearer token` respectively to `authenticate()` method in your controller. In case you want to authenticate clients using some other method like cookies, You can do that inside `authenticate` method. You may replace default authentication handler by passing your own implementation of `AuthServer` interface to RestServer instance. like

```php
/**
* include following lines after $server = new RestServer($mode);
*/
$server->authHandler = new myAuthServer();
```

RestServer is meant to be a simple mechanism to map your application into a REST API and pass requested data or headers. The rest of the details are up to you.

### Cross-origin resource sharing

Expand All @@ -181,6 +190,17 @@ For security reasons, browsers restrict cross-origin HTTP or REST requests initi
$server->allowedOrigin = '*';
```

### Working with files
Using Multipart with REST APIs is a bad idea and neither it is supported by `RestServer`. RestServer uses direct file upload approach like S3 services. you can upload one file per request without any additional form data.

* **Upload:**
In file uploads action you may use two special parameters in method definition. `$data` and `$mime` first parameter will hold file content and the `$mime` parameter can provide details about file content type.

* **Download:**
RestServer will start a file download in case a action return `SplFileInfo` object.

For more details please check `upload` and `download` methods in example.

### Throwing and Handling Errors

You may provide errors to your API users easily by throwing an excetion with the class `RestException`. Example:
Expand Down
152 changes: 152 additions & 0 deletions example/TestAuthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php
/*
*********************************************************************
****************************** NOTICE ******************************
*********************************************************************

Before testing this example, you must full fill following requirements

1. Generate private and public keys pair in same directory as (testkey and testkey.pub)
2. install php-jwt ie. run `composer require firebase/php-jwt`

*/

use \Firebase\JWT\JWT;
use \Jacwright\RestServer\RestException;

class TestController
{
/**
* Mocking up user table
*/
private $listUser = array(
'admin@domain.tld' => array('email' => 'admin@domain.tld', 'password' => 'adminPass', 'role' => 'admin'),
'user@domain.tld' => array('email' => 'user@domain.tld', 'password' => 'userPass', 'role' => 'user')
);

/**
* Security
*/
public $private_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey';
public $public_key = __DIR__ . DIRECTORY_SEPARATOR . 'testkey.pub';
public $hash_type = 'RS256';

/**
* Logged in user
*/
private $loggedUser = null;

/**
* Check client credentials and return true if found valid, false otherwise
*/
public function authenticate($credentials, $auth_type)
{
switch ($auth_type) {
case 'Bearer':
$public_key = file_get_contents($this->public_key);
$token = JWT::decode($credentials, $public_key, array($this->hash_type));
if ($token && !empty($token->username) && $this->listUser[$token->username]) {
$this->loggedUser = $this->listUser[$token->username];
return true;
}
break;

case 'Basic':
default:
$email = $credentials['username'];
if (isset($this->listUser[$email]) && $this->listUser[$email]['password'] == $credentials['password']) {
$this->loggedUser = $this->listUser[$email];
return true;
}
break;
}

return false;
}

/**
* Check if current user is allowed to access a certain method
*/
public function authorize($method)
{
if ('admin' == $this->loggedUser['role']) {
return true; // admin can access everthing

} else if ('user' == $this->loggedUser['role']) {
// user can access selected methods only
if (in_array($method, array('download'))) {
return true;
}
}

return false;
}

/**
* To get JWT token client can post his username and password to this method
*
* @url POST /login
* @noAuth
*/
public function login($data = array())
{
$username = isset($data['username']) ? $data['username'] : null;
$password = isset($data['password']) ? $data['password'] : null;

// only if we have valid user
if (isset($this->listUser[$username]) && $this->listUser[$username] == $password) {
$token = array(
"iss" => 'My Website',
"iat" => time(),
"nbf" => time(),
"exp" => time() + (60 * 60 * 24 * 30 * 12 * 1), // valid for one year
"username" => $this->listUser[$username]['email'];
);

// return jwt token
$private_key = file_get_contents($this->private_key);
return JWT::encode($token, $private_key, $this->hash_type);
}

throw new RestException(401, "Invalid username or password");
}

/**
* Upload a file
*
* @url PUT /files/$filename
*/
public function upload($filename, $data, $mime)
{
$storage_dir = sys_get_temp_dir();
$allowedTypes = array('pdf' => 'application/pdf', 'html' => 'plain/html', 'wav' => 'audio/wav');
if (in_array($mime, $allowedTypes)) {
if (!empty($data)) {
$file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename;
file_put_contents($file_path, $data);
return $filename;
} else {
throw new RestException(411, "Empty file");
}
} else {
throw new RestException(415, "Unsupported File Type");
}
}

/**
* Download a file
*
* @url GET /files/$filename
*/
public function download($filename)
{
$storage_dir = sys_get_temp_dir();
$file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename;
if (file_exists($file_path)) {
return SplFileInfo($file_path);
} else {
throw new RestException(404, "File not found");
}
}

}
38 changes: 38 additions & 0 deletions example/TestController.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,44 @@ public function listUsers($query)
return $users; // serializes object into JSON
}

/**
* Upload a file
*
* @url PUT /files/$filename
*/
public function upload($filename, $data, $mime)
{
$storage_dir = sys_get_temp_dir();
$allowedTypes = array('pdf' => 'application/pdf', 'html' => 'plain/html', 'wav' => 'audio/wav');
if (in_array($mime, $allowedTypes)) {
if (!empty($data)) {
$file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename;
file_put_contents($file_path, $data);
return $filename;
} else {
throw new RestException(411, "Empty file");
}
} else {
throw new RestException(415, "Unsupported File Type");
}
}

/**
* Download a file
*
* @url GET /files/$filename
*/
public function download($filename)
{
$storage_dir = sys_get_temp_dir();
$file_path = $storage_dir . DIRECTORY_SEPARATOR . $filename;
if (file_exists($file_path)) {
return SplFileInfo($file_path);
} else {
throw new RestException(404, "File not found");
}
}

/**
* Get Charts
*
Expand Down
97 changes: 92 additions & 5 deletions source/Jacwright/RestServer/Auth/HTTPAuthServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,103 @@ public function __construct($realm = 'Rest Server') {
$this->realm = $realm;
}

public function isAuthorized($classObj) {
public function isAuthenticated($classObj) {
$auth_headers = $this->getAuthHeaders();

// Try to use bearer token as default
$auth_method = 'Bearer';
$credentials = $this->getBearer($auth_headers);

// TODO: add digest method

// In case bearer token is not present try with Basic autentication
if (empty($credentials)) {
$auth_method = 'Basic';
$credentials = $this->getBasic($auth_headers);
}

if (method_exists($classObj, 'authenticate')) {
return $classObj->authenticate($credentials, $auth_method);
}

return true; // original behavior
}

public function unauthenticated($path) {
header("WWW-Authenticate: Basic realm=\"$this->realm\"");
throw new \Jacwright\RestServer\RestException(401, "Invalid credentials, access is denied to $path.");
}

public function isAuthorized($classObj, $method) {
if (method_exists($classObj, 'authorize')) {
return $classObj->authorize();
return $classObj->authorize($method);
}

return true;
}

public function unauthorized($classObj) {
header("WWW-Authenticate: Basic realm=\"$this->realm\"");
throw new \Jacwright\RestServer\RestException(401, "You are not authorized to access this resource.");
public function unauthorized($path) {
throw new \Jacwright\RestServer\RestException(403, "You are not authorized to access $path.");
}

/**
* Get username and password from header
*/
protected function getBasic($headers) {
// mod_php
if (isset($_SERVER['PHP_AUTH_USER'])) {
return array(
'username' => $this->server_get('PHP_AUTH_USER'),
'password' => $this->server_get('PHP_AUTH_PW')
);
} else { // most other servers
if (!empty($headers)) {
list ($username, $password) = explode(':',base64_decode(substr($headers, 6)));
return array('username' => $username, 'password' => $password);
}
}
return array('username' => null, 'password' => null);
}

/**
* Get access token from header
*/
protected function getBearer($headers) {
if (!empty($headers)) {
if (preg_match('/Bearer\s(\S+)/', $headers, $matches)) {
return $matches[1];
}
}
return null;
}

/**
* Get username and password from header via Digest method
*/
protected function getDigest() {
if (false) { // TODO // currently not functional
return array('username' => null, 'password' => null);
}
return null;
}

/**
* Get authorization header
*/
protected function getAuthHeaders() {
$headers = null;
if (isset($_SERVER['Authorization'])) {
$headers = trim($_SERVER["Authorization"]);
} else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { //Nginx or fast CGI
$headers = trim($_SERVER["HTTP_AUTHORIZATION"]);
} else if (function_exists('apache_request_headers')) {
$requestHeaders = apache_request_headers();
$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
if (isset($requestHeaders['Authorization'])) {
$headers = trim($requestHeaders['Authorization']);
}
}
return $headers;
}

}
27 changes: 24 additions & 3 deletions source/Jacwright/RestServer/AuthServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,36 @@
namespace Jacwright\RestServer;

interface AuthServer {
/**
* Indicates whether the requesting client is a recognized and authenticated party.
*
* @param object $classObj An instance of the controller for the path.
*
* @return bool True if authenticated, false if not.
*/
public function isAuthenticated($classObj);

/**
* Handles the case where the client is not recognized party.
* This method must either return data or throw a RestException.
*
* @param string $path The requested path.
*
* @return mixed The response to send to the client
*
* @throws RestException
*/
public function unauthenticated($path);

/**
* Indicates whether the client is authorized to access the resource.
*
* @param string $path The requested path.
* @param object $classObj An instance of the controller for the path.
* @param string $method The requested method.
*
* @return bool True if authorized, false if not.
*/
public function isAuthorized($classObj);
public function isAuthorized($classObj, $method);

/**
* Handles the case where the client is not authorized.
Expand All @@ -22,5 +43,5 @@ public function isAuthorized($classObj);
*
* @throws RestException
*/
public function unauthorized($classObj);
public function unauthorized($path);
}
Loading