Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

RFC: Separate authorization and token endpoints #42

Merged
11 changes: 8 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@ All notable changes to this project will be documented in this file, in reverse

### Added

- Nothing.
- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Adds `TokenEndpointHandler`,
`AuthorizationMiddleware` and `AuthorizationHandler` in the `Zend\Expressive\Authentication\OAuth2` namespace
to [implement an authorization server](docs/book/authorization-server.md).

### Changed

- Nothing.
- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Splits
`Zend\Expressive\Authentication\OAuth2\OAuth2Middleware` into individual implementations that allow
[OAuth RFC-6749](https://tools.ietf.org/html/rfc6749) compliant authorization server implementations.

### Deprecated

- Nothing.

### Removed

- Nothing.
- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Removes
`Zend\Expressive\Authentication\OAuth2\OAuth2Middleware`.

### Fixed

Expand Down
113 changes: 113 additions & 0 deletions docs/book/authorization-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Implement an authorization server

This library provides the basics to implement an authorization server
for your application.

Since there are authorization flows that require user interaction,
your application is expected to provide the middleware to handle this.

## Add the token endpoint

This is the most simple part, since this library provides
`Zend\Expressive\Authentication\OAuth2\TokenHandler` to deal with it.

This endpoint must accept POST requests.

For example:

```php
use Zend\Expressive\Authentication\OAuth2;

$app->route('/oauth2/token', OAuth2\TokenHandler::class, ['POST']);
```

## Add the authorization endpoint

The authorization endpoint is an url of to which the client redirects
to obtain an access token or authorization code.

This endpoint must accept GET requests and should:

- Validate the request (especially for a valid client id and redirect url)
- Make sure the User is authenticated (for example by showing a login
prompt if needed)
- Optionally request the users consent to grant access to the client
- Redirect to a specified url of the client with success or error information

The first and the last part is provided by this library.

For example, to add the authorization endpoint you can declare a middleware pipe
to compose these parts:

```php
use Zend\Expressive\Authentication\OAuth2;

$app->route('/oauth2/authorize', [
OAuth2\AuthorizatonMiddleware,

// The followig middleware is provided by your application (see below)
Application\OAuthAuthorizationMiddleware::class,

OAuth2\AuthorizationHandler
], ['GET', 'POST']);
```

In your `Application\OAuthAuthorizationMiddleware`, you'll have access
to the `League\OAuth2\Server\RequestTypes\AuthorizationRequest` via the
psr-7 request. Your middleware should populate the user entity with `setUser()` and the
user's consent decision with `setAuthorizationApproved()` to this authorization
request instance.

```php
<?php

namespace Application;

use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;

class OAuthAuthorizationMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Assume a middleware handled the authentication check and
// populates the user object which also implements the
// OAuth2 UserEntityInterface
$user = $request->getAttribute('authenticated_user');

// Assume some middleware handles and populates a session
// container
$session = $request->getAttribute('session');

// This is populated by the previous middleware
/** @var AuthorizationRequest $authRequest */
$authRequest = $request->getAttribute(AuthorizationRequest::class);

// the user is authenticated
if ($user) {
$authRequest->setUser($user);

// Assume all clients are trusted, but you could
// handle consent here or within the next middleware
// as needed
$authRequest->setAuthorizationApproved(true);

return $handler->handle($request);
}

// The user is not authenticated, show login form ...

// Store the auth request state
// NOTE: Do not attempt to serialize or store the authorization
// request object. Store the query parameters instead and redirect
// with these to this endpoint again to replay the request.
$session['oauth2_request_params'] = $request->getQueryParams();

return new RedirectResponse('/oauth2/login');
}
}
```

5 changes: 5 additions & 0 deletions docs/book/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ $app->post('/api/users', [
App\Action\AddUserAction::class,
], 'api.add.user');
```

# Providing an authorization server

See the chapter [Authorization server](authorization-server.md) for details on how
to implement this.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pages:
- index.md
- Introduction: intro.md
- Usage: usage.md
- "Authorization server": authorization-server.md
- Grant:
- "Client credentials": grant/client_credentials.md
- "Password": grant/password.md
Expand Down
54 changes: 54 additions & 0 deletions src/AuthorizationHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-authentication-oauth2 for the canonical source repository
* @copyright Copyright (c) 2017-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-authentication-oauth2/blob/master/LICENSE.md
* New BSD License
*/

declare(strict_types=1);

namespace Zend\Expressive\Authentication\OAuth2;

use League\OAuth2\Server\AuthorizationServer;
use phpDocumentor\Reflection\Types\This;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Handles the already validated and competed authorization request
*
* This will perform the required redirect to the requesting party.
* The request must provide an attribute `League\OAuth2\Server\AuthorizationServer`
* that contains the validated OAuth2 request
*
* @see https://tools.ietf.org/html/rfc6749#section-3.1.1
*/
class AuthorizationHandler implements RequestHandlerInterface
{
/**
* @var AuthorizationServer
*/
private $server;

/**
* @var callable
*/
private $responseFactory;

public function __construct(AuthorizationServer $server, callable $responseFactory)
{
$this->server = $server;
$this->responseFactory = function () use ($responseFactory): ResponseInterface {
return $responseFactory();
};
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$authRequest = $request->getAttribute(AuthorizationRequest::class);
return $this->server->completeAuthorizationRequest($authRequest, ($this->responseFactory)());
}
}
26 changes: 26 additions & 0 deletions src/AuthorizationHandlerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-authentication-oauth2 for the canonical source repository
* @copyright Copyright (c) 2017-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-authentication-oauth2/blob/master/LICENSE.md
* New BSD License
*/

declare(strict_types=1);

namespace Zend\Expressive\Authentication\OAuth2;

use League\OAuth2\Server\AuthorizationServer;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;

final class AuthorizationHandlerFactory
{
public function __invoke(ContainerInterface $container)
{
return new AuthorizationHandler(
$container->get(AuthorizationServer::class),
$container->get(ResponseInterface::class)
);
}
}
79 changes: 79 additions & 0 deletions src/AuthorizationMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive-authentication-oauth2 for the canonical source repository
* @copyright Copyright (c) 2017-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-authentication-oauth2/blob/master/LICENSE.md
* New BSD License
*/

declare(strict_types=1);

namespace Zend\Expressive\Authentication\OAuth2;

use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Implements OAuth2 authorization request validation
*
* Performs checks if the OAuth authorization request is valid and populates it
* to the next handler via the request object as attribute with the key
* `League\OAuth2\Server\AuthorizationServer`
*
* The next handler should take care of checking the resource owner's authentication and
* consent. It may intercept to ensure authentication and consent before populating it to
* the authorization request object
*
* @see https://oauth2.thephpleague.com/authorization-server/auth-code-grant/
* @see https://oauth2.thephpleague.com/authorization-server/implicit-grant/
*/
class AuthorizationMiddleware implements MiddlewareInterface
{
/**
* @var AuthorizationServer
*/
protected $server;

/**
* @var callable
*/
protected $responseFactory;

public function __construct(AuthorizationServer $server, callable $responseFactory)
{
$this->server = $server;
$this->responseFactory = function () use ($responseFactory) : ResponseInterface {
return $responseFactory();
};
}

/**
* {@inheritDoc}
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$response = ($this->responseFactory)();

try {
$authRequest = $this->server->validateAuthorizationRequest($request);

// The next handler must take care of providing the
// authenticated user and the approval
$authRequest->setAuthorizationApproved(false);

return $handler->handle($request->withAttribute(AuthorizationRequest::class, $authRequest));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass the the auth request via the server request object to the next handler to complete it.
(ensure authentication, giving consent, etc ...). This gives consumers the freedom to act according to their business needs

} catch (OAuthServerException $exception) {
// The validation throws this exception if the request is not valid
// for example when the client id is invalid
return $exception->generateHttpResponse($response);
} catch (\Exception $exception) {
return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500))
->generateHttpResponse($response);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,12 @@
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;

use function sprintf;

class OAuth2MiddlewareFactory
final class AuthorizationMiddlewareFactory
{
public function __invoke(ContainerInterface $container) : OAuth2Middleware
public function __invoke(ContainerInterface $container) : AuthorizationMiddleware
{
$authServer = $container->has(AuthorizationServer::class)
? $container->get(AuthorizationServer::class)
: null;

if (null === $authServer) {
throw new Exception\InvalidConfigException(sprintf(
"The %s service is missing",
AuthorizationServer::class
));
}

return new OAuth2Middleware(
$authServer,
return new AuthorizationMiddleware(
$container->get(AuthorizationServer::class),
$container->get(ResponseInterface::class)
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ public function getDependencies() : array
AuthenticationInterface::class => OAuth2Adapter::class
],
'factories' => [
OAuth2Middleware::class => OAuth2MiddlewareFactory::class,
AuthorizationMiddleware::class => AuthorizationMiddlewareFactory::class,
AuthorizationHandler::class => AuthorizationServerFactory::class,
TokenEndpointHandler::class => TokenEndpointHandlerFactory::class,
OAuth2Adapter::class => OAuth2AdapterFactory::class,
AuthorizationServer::class => AuthorizationServerFactory::class,
ResourceServer::class => ResourceServerFactory::class,
Expand Down Expand Up @@ -90,7 +92,7 @@ public function getRoutes() : array
[
'name' => 'oauth',
'path' => '/oauth',
'middleware' => OAuth2Middleware::class,
'middleware' => AuthorizationMiddleware::class,
'allowed_methods' => ['GET', 'POST']
],
];
Expand Down
Loading