Skip to content

Commit

Permalink
Added Auth\Middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
jasny committed Dec 28, 2016
1 parent 78d0ab1 commit 0d68005
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 2 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,20 @@ Get current user
User user()


### Access control (middleware)

You can apply access control manually using the `is()` method. Alteratively, if you're using a PSR-7 compatible router
with middleware support (like [Jasny Router](https://github.com/jasny/router)]).

```php
$auth = new Auth(); // Implements the Jasny\Authz interface

$roure->add($auth->asMiddleware(function(ServerRequest $request) {
$route = $request->getAttribute('route');
return isset($route->auth) ? $route->auth : null;
}));
```

### Authorization

Check if a user has a specific role or superseding role
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@
"source": "https://github.com/jasny/auth"
},
"require": {
"php": ">=5.6.0"
"php": ">=5.6.0",
"psr/http-message": "^1.0"
},
"autoload": {
"psr-4": {
"Jasny\\": "src/"
}
},
"require-dev": {
"jasny/php-code-quality": "^2.1.1",
"jasny/php-code-quality": "^2.1.2",
"hashids/hashids": "~2.0.0"
},
"suggest": {
Expand Down
99 changes: 99 additions & 0 deletions src/Auth/Middleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace Jasny\Auth;

use Jasny\Auth;
use Jasny\Authz;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Access control middleware
*/
class Middleware
{
/**
* @var Auth|Authz
**/
protected $auth;

/**
* Function to get the required role from the request.
* @var callable
*/
protected $getRequiredRole;

/**
* Class constructor
*
* @param Authz $auth
* @param callable $getRequiredRole
*/
public function __construct(Authz $auth, $getRequiredRole)
{
$this->auth = $auth;

if (!is_callable($getRequiredRole)) {
throw new \InvalidArgumentException("'getRequiredRole' should be callable");
}

$this->getRequiredRole = $getRequiredRole;
}

/**
* Check if the current user has one of the roles
*
* @param array $roles
* @return
*/
protected function hasRole(array $roles)
{
$ret = false;

foreach ($roles as $role) {
$ret = $ret || $this->auth->is($role);
}

return $ret;
}

/**
* Respond with forbidden (or unauthorized)
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @return ResponseInterface
*/
protected function forbidden(ServerRequestInterface $request, ResponseInterface $response)
{
$unauthorized = $this->auth instanceof Auth && $this->auth->user() === null;

$forbiddenResponse = $response->withStatus($unauthorized ? 401 : 403);
$forbiddenResponse->getBody()->write('Access denied');

return $forbiddenResponse;
}

/**
* Run middleware action
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
if (!is_callable($next)) {
throw new \InvalidArgumentException("'next' should be callable");
}

$requiredRole = call_user_func($this->getRequiredRole, $request);

if (!empty($requiredRole) && !$this->hasRole((array)$requiredRole)) {
return $this->forbidden($request, $response);
}

return $next($request, $response);
}
}
162 changes: 162 additions & 0 deletions tests/Auth/MiddlewareTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
<?php

namespace Jasny\Auth;

use Jasny\Auth;
use Jasny\Authz;
use Jasny\Auth\Middleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit_Framework_MockObject_MockObject as MockObject;
use PHPUnit_Framework_MockObject_Builder_InvocationMocker as InvocationMocker;
use Jasny\TestHelper;

/**
* @covers Jasny\Auth\Middleware
*/
class MiddlewareTest extends TestCase
{
use TestHelper;

/**
* @var Authz|MockObject
*/
protected $auth;

/**
* @var Middleware
*/
protected $middleware;

public function setUp()
{
$this->auth = $this->createMock(Authz::class);
$this->middleware = new Middleware($this->auth, function(ServerRequestInterface $request) {
return $request->getAttribute('auth');
});
}

/**
* @expectedException \InvalidArgumentException
*/
public function testConstructInvalidArgument()
{
new Middleware($this->auth, 'foo bar zoo');
}

public function testInvokeWithoutRequiredRole()
{
$this->auth->expects($this->never())->method('is');

$request = $this->createMock(ServerRequestInterface::class);
$request->expects($this->once())->method('getAttribute')->with('auth')->willReturn(null);

$finalResponse = $this->createMock(ResponseInterface::class);

$response = $this->createMock(ResponseInterface::class);
$response->expects($this->never())->method('withStatus');

$next = $this->createCallbackMock(
$this->once(),
function(InvocationMocker $invoke) use ($request, $response, $finalResponse) {
$invoke->with($this->identicalTo($request), $this->identicalTo($response))->willReturn($finalResponse);
}
);

$result = call_user_func($this->middleware, $request, $response, $next);

$this->assertSame($finalResponse, $result);
}

public function testInvokeWithRequiredRole()
{
$this->auth->expects($this->once())->method('is')->with('user')->willReturn(true);

$request = $this->createMock(ServerRequestInterface::class);
$request->expects($this->once())->method('getAttribute')->with('auth')->willReturn('user');

$finalResponse = $this->createMock(ResponseInterface::class);

$response = $this->createMock(ResponseInterface::class);
$response->expects($this->never())->method('withStatus');

$next = $this->createCallbackMock(
$this->once(),
function(InvocationMocker $invoke) use ($request, $response, $finalResponse) {
$invoke->with($this->identicalTo($request), $this->identicalTo($response))->willReturn($finalResponse);
}
);

$result = call_user_func($this->middleware, $request, $response, $next);

$this->assertSame($finalResponse, $result);
}

public function testInvokeForbidden()
{
$this->auth->expects($this->once())->method('is')->with('user')->willReturn(false);

$request = $this->createMock(ServerRequestInterface::class);
$request->expects($this->once())->method('getAttribute')->with('auth')->willReturn('user');

$stream = $this->createMock(StreamInterface::class);
$stream->expects($this->once())->method('write')->with('Access denied');

$forbiddenResponse = $this->createMock(ResponseInterface::class);
$forbiddenResponse->expects($this->once())->method('getBody')->willReturn($stream);

$response = $this->createMock(ResponseInterface::class);
$response->expects($this->once())->method('withStatus')->with(403)->willReturn($forbiddenResponse);

$next = $this->createCallbackMock($this->never());

$result = call_user_func($this->middleware, $request, $response, $next);

$this->assertSame($forbiddenResponse, $result);
}

public function testInvokeUnauthorized()
{
$this->auth = $this->getMockBuilder(Auth::class)
->disableProxyingToOriginalMethods()
->setMethods(['user', 'is', 'persistCurrentUser', 'getCurrentUserId', 'fetchUserById',
'fetchUserByUsername'])
->getMock();

$this->auth->expects($this->once())->method('user')->willReturn(null);
$this->auth->expects($this->once())->method('is')->with('user')->willReturn(false);

$this->setPrivateProperty($this->middleware, 'auth', $this->auth);

$request = $this->createMock(ServerRequestInterface::class);
$request->expects($this->once())->method('getAttribute')->with('auth')->willReturn('user');

$stream = $this->createMock(StreamInterface::class);
$stream->expects($this->once())->method('write')->with('Access denied');

$forbiddenResponse = $this->createMock(ResponseInterface::class);
$forbiddenResponse->expects($this->once())->method('getBody')->willReturn($stream);

$response = $this->createMock(ResponseInterface::class);
$response->expects($this->once())->method('withStatus')->with(401)->willReturn($forbiddenResponse);

$next = $this->createCallbackMock($this->never());

$result = call_user_func($this->middleware, $request, $response, $next);

$this->assertSame($forbiddenResponse, $result);
}

/**
* @expectedException \InvalidArgumentException
*/
public function testInvokeInvalidArgument()
{
$request = $this->createMock(ServerRequestInterface::class);
$response = $this->createMock(ResponseInterface::class);

$result = call_user_func($this->middleware, $request, $response, 'foo bar zoo');
}
}

0 comments on commit 0d68005

Please sign in to comment.