aboutsummaryrefslogtreecommitdiffstats
path: root/library/oauth2/src/OAuth2/Controller
diff options
context:
space:
mode:
authorzotlabs <mike@macgirvin.com>2016-10-07 14:11:24 -0700
committerzotlabs <mike@macgirvin.com>2016-10-07 14:11:24 -0700
commit10863a5949cc59771424cb809af5c9f279f78a58 (patch)
tree7a86223b830c1ae784bd4557bbefee9f60169542 /library/oauth2/src/OAuth2/Controller
parentbf02e0428347350126abdd1726aa3e58c9ed63bb (diff)
downloadvolse-hubzilla-10863a5949cc59771424cb809af5c9f279f78a58.tar.gz
volse-hubzilla-10863a5949cc59771424cb809af5c9f279f78a58.tar.bz2
volse-hubzilla-10863a5949cc59771424cb809af5c9f279f78a58.zip
add oauth2/oidc lib
Diffstat (limited to 'library/oauth2/src/OAuth2/Controller')
-rw-r--r--library/oauth2/src/OAuth2/Controller/AuthorizeController.php388
-rw-r--r--library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php43
-rw-r--r--library/oauth2/src/OAuth2/Controller/ResourceController.php111
-rw-r--r--library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php26
-rw-r--r--library/oauth2/src/OAuth2/Controller/TokenController.php278
-rw-r--r--library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php32
6 files changed, 878 insertions, 0 deletions
diff --git a/library/oauth2/src/OAuth2/Controller/AuthorizeController.php b/library/oauth2/src/OAuth2/Controller/AuthorizeController.php
new file mode 100644
index 000000000..a9a722587
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/AuthorizeController.php
@@ -0,0 +1,388 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\Storage\ClientInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController implements AuthorizeControllerInterface
+{
+ private $scope;
+ private $state;
+ private $client_id;
+ private $redirect_uri;
+ private $response_type;
+
+ protected $clientStorage;
+ protected $responseTypes;
+ protected $config;
+ protected $scopeUtil;
+
+ /**
+ * @param OAuth2\Storage\ClientInterface $clientStorage REQUIRED Instance of OAuth2\Storage\ClientInterface to retrieve client information
+ * @param array $responseTypes OPTIONAL Array of OAuth2\ResponseType\ResponseTypeInterface objects. Valid array
+ * keys are "code" and "token"
+ * @param array $config OPTIONAL Configuration options for the server
+ * <code>
+ * $config = array(
+ * 'allow_implicit' => false, // if the controller should allow the "implicit" grant type
+ * 'enforce_state' => true // if the controller should require the "state" parameter
+ * 'require_exact_redirect_uri' => true, // if the controller should require an exact match on the "redirect_uri" parameter
+ * 'redirect_status_code' => 302, // HTTP status code to use for redirect responses
+ * );
+ * </code>
+ * @param OAuth2\ScopeInterface $scopeUtil OPTIONAL Instance of OAuth2\ScopeInterface to validate the requested scope
+ */
+ public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null)
+ {
+ $this->clientStorage = $clientStorage;
+ $this->responseTypes = $responseTypes;
+ $this->config = array_merge(array(
+ 'allow_implicit' => false,
+ 'enforce_state' => true,
+ 'require_exact_redirect_uri' => true,
+ 'redirect_status_code' => 302,
+ ), $config);
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null)
+ {
+ if (!is_bool($is_authorized)) {
+ throw new \InvalidArgumentException('Argument "is_authorized" must be a boolean. This method must know if the user has granted access to the client.');
+ }
+
+ // We repeat this, because we need to re-validate. The request could be POSTed
+ // by a 3rd-party (because we are not internally enforcing NONCEs, etc)
+ if (!$this->validateAuthorizeRequest($request, $response)) {
+ return;
+ }
+
+ // If no redirect_uri is passed in the request, use client's registered one
+ if (empty($this->redirect_uri)) {
+ $clientData = $this->clientStorage->getClientDetails($this->client_id);
+ $registered_redirect_uri = $clientData['redirect_uri'];
+ }
+
+ // the user declined access to the client's application
+ if ($is_authorized === false) {
+ $redirect_uri = $this->redirect_uri ?: $registered_redirect_uri;
+ $this->setNotAuthorizedResponse($request, $response, $redirect_uri, $user_id);
+
+ return;
+ }
+
+ // build the parameters to set in the redirect URI
+ if (!$params = $this->buildAuthorizeParameters($request, $response, $user_id)) {
+ return;
+ }
+
+ $authResult = $this->responseTypes[$this->response_type]->getAuthorizeResponse($params, $user_id);
+
+ list($redirect_uri, $uri_params) = $authResult;
+
+ if (empty($redirect_uri) && !empty($registered_redirect_uri)) {
+ $redirect_uri = $registered_redirect_uri;
+ }
+
+ $uri = $this->buildUri($redirect_uri, $uri_params);
+
+ // return redirect response
+ $response->setRedirect($this->config['redirect_status_code'], $uri);
+ }
+
+ protected function setNotAuthorizedResponse(RequestInterface $request, ResponseInterface $response, $redirect_uri, $user_id = null)
+ {
+ $error = 'access_denied';
+ $error_message = 'The user denied access to your application';
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, $error, $error_message);
+ }
+
+ /*
+ * We have made this protected so this class can be extended to add/modify
+ * these parameters
+ */
+ protected function buildAuthorizeParameters($request, $response, $user_id)
+ {
+ // @TODO: we should be explicit with this in the future
+ $params = array(
+ 'scope' => $this->scope,
+ 'state' => $this->state,
+ 'client_id' => $this->client_id,
+ 'redirect_uri' => $this->redirect_uri,
+ 'response_type' => $this->response_type,
+ );
+
+ return $params;
+ }
+
+ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ // Make sure a valid client id was supplied (we can not redirect because we were unable to verify the URI)
+ if (!$client_id = $request->query('client_id', $request->request('client_id'))) {
+ // We don't have a good URI to use
+ $response->setError(400, 'invalid_client', "No client id supplied");
+
+ return false;
+ }
+
+ // Get client details
+ if (!$clientData = $this->clientStorage->getClientDetails($client_id)) {
+ $response->setError(400, 'invalid_client', 'The client id supplied is invalid');
+
+ return false;
+ }
+
+ $registered_redirect_uri = isset($clientData['redirect_uri']) ? $clientData['redirect_uri'] : '';
+
+ // Make sure a valid redirect_uri was supplied. If specified, it must match the clientData URI.
+ // @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+ // @see http://tools.ietf.org/html/rfc6749#section-4.1.2.1
+ // @see http://tools.ietf.org/html/rfc6749#section-4.2.2.1
+ if ($supplied_redirect_uri = $request->query('redirect_uri', $request->request('redirect_uri'))) {
+ // validate there is no fragment supplied
+ $parts = parse_url($supplied_redirect_uri);
+ if (isset($parts['fragment']) && $parts['fragment']) {
+ $response->setError(400, 'invalid_uri', 'The redirect URI must not contain a fragment');
+
+ return false;
+ }
+
+ // validate against the registered redirect uri(s) if available
+ if ($registered_redirect_uri && !$this->validateRedirectUri($supplied_redirect_uri, $registered_redirect_uri)) {
+ $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI provided is missing or does not match', '#section-3.1.2');
+
+ return false;
+ }
+ $redirect_uri = $supplied_redirect_uri;
+ } else {
+ // use the registered redirect_uri if none has been supplied, if possible
+ if (!$registered_redirect_uri) {
+ $response->setError(400, 'invalid_uri', 'No redirect URI was supplied or stored');
+
+ return false;
+ }
+
+ if (count(explode(' ', $registered_redirect_uri)) > 1) {
+ $response->setError(400, 'invalid_uri', 'A redirect URI must be supplied when multiple redirect URIs are registered', '#section-3.1.2.3');
+
+ return false;
+ }
+ $redirect_uri = $registered_redirect_uri;
+ }
+
+ // Select the redirect URI
+ $response_type = $request->query('response_type', $request->request('response_type'));
+
+ // for multiple-valued response types - make them alphabetical
+ if (false !== strpos($response_type, ' ')) {
+ $types = explode(' ', $response_type);
+ sort($types);
+ $response_type = ltrim(implode(' ', $types));
+ }
+
+ $state = $request->query('state', $request->request('state'));
+
+ // type and client_id are required
+ if (!$response_type || !in_array($response_type, $this->getValidResponseTypes())) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_request', 'Invalid or missing response type', null);
+
+ return false;
+ }
+
+ if ($response_type == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+ if (!isset($this->responseTypes['code'])) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'authorization code grant type not supported', null);
+
+ return false;
+ }
+ if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'authorization_code')) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+ return false;
+ }
+ if ($this->responseTypes['code']->enforceRedirect() && !$redirect_uri) {
+ $response->setError(400, 'redirect_uri_mismatch', 'The redirect URI is mandatory and was not supplied');
+
+ return false;
+ }
+ } else {
+ if (!$this->config['allow_implicit']) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unsupported_response_type', 'implicit grant type not supported', null);
+
+ return false;
+ }
+ if (!$this->clientStorage->checkRestrictedGrantType($client_id, 'implicit')) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'unauthorized_client', 'The grant type is unauthorized for this client_id', null);
+
+ return false;
+ }
+ }
+
+ // validate requested scope if it exists
+ $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+
+ if ($requestedScope) {
+ // restrict scope by client specific scope if applicable,
+ // otherwise verify the scope exists
+ $clientScope = $this->clientStorage->getClientScope($client_id);
+ if ((empty($clientScope) && !$this->scopeUtil->scopeExists($requestedScope))
+ || (!empty($clientScope) && !$this->scopeUtil->checkScope($requestedScope, $clientScope))) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_scope', 'An unsupported scope was requested', null);
+
+ return false;
+ }
+ } else {
+ // use a globally-defined default scope
+ $defaultScope = $this->scopeUtil->getDefaultScope($client_id);
+
+ if (false === $defaultScope) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $state, 'invalid_client', 'This application requires you specify a scope parameter', null);
+
+ return false;
+ }
+
+ $requestedScope = $defaultScope;
+ }
+
+ // Validate state parameter exists (if configured to enforce this)
+ if ($this->config['enforce_state'] && !$state) {
+ $response->setRedirect($this->config['redirect_status_code'], $redirect_uri, null, 'invalid_request', 'The state parameter is required');
+
+ return false;
+ }
+
+ // save the input data and return true
+ $this->scope = $requestedScope;
+ $this->state = $state;
+ $this->client_id = $client_id;
+ // Only save the SUPPLIED redirect URI (@see http://tools.ietf.org/html/rfc6749#section-4.1.3)
+ $this->redirect_uri = $supplied_redirect_uri;
+ $this->response_type = $response_type;
+
+ return true;
+ }
+
+ /**
+ * Build the absolute URI based on supplied URI and parameters.
+ *
+ * @param $uri An absolute URI.
+ * @param $params Parameters to be append as GET.
+ *
+ * @return
+ * An absolute URI with supplied parameters.
+ *
+ * @ingroup oauth2_section_4
+ */
+ private function buildUri($uri, $params)
+ {
+ $parse_url = parse_url($uri);
+
+ // Add our params to the parsed uri
+ foreach ($params as $k => $v) {
+ if (isset($parse_url[$k])) {
+ $parse_url[$k] .= "&" . http_build_query($v, '', '&');
+ } else {
+ $parse_url[$k] = http_build_query($v, '', '&');
+ }
+ }
+
+ // Put humpty dumpty back together
+ return
+ ((isset($parse_url["scheme"])) ? $parse_url["scheme"] . "://" : "")
+ . ((isset($parse_url["user"])) ? $parse_url["user"]
+ . ((isset($parse_url["pass"])) ? ":" . $parse_url["pass"] : "") . "@" : "")
+ . ((isset($parse_url["host"])) ? $parse_url["host"] : "")
+ . ((isset($parse_url["port"])) ? ":" . $parse_url["port"] : "")
+ . ((isset($parse_url["path"])) ? $parse_url["path"] : "")
+ . ((isset($parse_url["query"]) && !empty($parse_url['query'])) ? "?" . $parse_url["query"] : "")
+ . ((isset($parse_url["fragment"])) ? "#" . $parse_url["fragment"] : "")
+ ;
+ }
+
+ protected function getValidResponseTypes()
+ {
+ return array(
+ self::RESPONSE_TYPE_ACCESS_TOKEN,
+ self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+ );
+ }
+
+ /**
+ * Internal method for validating redirect URI supplied
+ *
+ * @param string $inputUri The submitted URI to be validated
+ * @param string $registeredUriString The allowed URI(s) to validate against. Can be a space-delimited string of URIs to
+ * allow for multiple URIs
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-3.1.2
+ */
+ protected function validateRedirectUri($inputUri, $registeredUriString)
+ {
+ if (!$inputUri || !$registeredUriString) {
+ return false; // if either one is missing, assume INVALID
+ }
+
+ $registered_uris = explode(' ', $registeredUriString);
+ foreach ($registered_uris as $registered_uri) {
+ if ($this->config['require_exact_redirect_uri']) {
+ // the input uri is validated against the registered uri using exact match
+ if (strcmp($inputUri, $registered_uri) === 0) {
+ return true;
+ }
+ } else {
+ $registered_uri_length = strlen($registered_uri);
+ if ($registered_uri_length === 0) {
+ return false;
+ }
+
+ // the input uri is validated against the registered uri using case-insensitive match of the initial string
+ // i.e. additional query parameters may be applied
+ if (strcasecmp(substr($inputUri, 0, $registered_uri_length), $registered_uri) === 0) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Convenience methods to access the parameters derived from the validated request
+ */
+
+ public function getScope()
+ {
+ return $this->scope;
+ }
+
+ public function getState()
+ {
+ return $this->state;
+ }
+
+ public function getClientId()
+ {
+ return $this->client_id;
+ }
+
+ public function getRedirectUri()
+ {
+ return $this->redirect_uri;
+ }
+
+ public function getResponseType()
+ {
+ return $this->response_type;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php b/library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php
new file mode 100644
index 000000000..fa07ae8d2
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/AuthorizeControllerInterface.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when a user should be authorized
+ * by an authorization server. As OAuth2 does not handle
+ * authorization directly, this controller ensures the request is valid, but
+ * requires the application to determine the value of $is_authorized
+ *
+ * ex:
+ * > $user_id = $this->somehowDetermineUserId();
+ * > $is_authorized = $this->somehowDetermineUserAuthorization();
+ * > $response = new OAuth2\Response();
+ * > $authorizeController->handleAuthorizeRequest(
+ * > OAuth2\Request::createFromGlobals(),
+ * > $response,
+ * > $is_authorized,
+ * > $user_id);
+ * > $response->send();
+ *
+ */
+interface AuthorizeControllerInterface
+{
+ /**
+ * List of possible authentication response types.
+ * The "authorization_code" mechanism exclusively supports 'code'
+ * and the "implicit" mechanism exclusively supports 'token'.
+ *
+ * @var string
+ * @see http://tools.ietf.org/html/rfc6749#section-4.1.1
+ * @see http://tools.ietf.org/html/rfc6749#section-4.2.1
+ */
+ const RESPONSE_TYPE_AUTHORIZATION_CODE = 'code';
+ const RESPONSE_TYPE_ACCESS_TOKEN = 'token';
+
+ public function handleAuthorizeRequest(RequestInterface $request, ResponseInterface $response, $is_authorized, $user_id = null);
+
+ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/library/oauth2/src/OAuth2/Controller/ResourceController.php b/library/oauth2/src/OAuth2/Controller/ResourceController.php
new file mode 100644
index 000000000..e8588188f
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/ResourceController.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+use OAuth2\Scope;
+
+/**
+ * @see OAuth2\Controller\ResourceControllerInterface
+ */
+class ResourceController implements ResourceControllerInterface
+{
+ private $token;
+
+ protected $tokenType;
+ protected $tokenStorage;
+ protected $config;
+ protected $scopeUtil;
+
+ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null)
+ {
+ $this->tokenType = $tokenType;
+ $this->tokenStorage = $tokenStorage;
+
+ $this->config = array_merge(array(
+ 'www_realm' => 'Service',
+ ), $config);
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null)
+ {
+ $token = $this->getAccessTokenData($request, $response);
+
+ // Check if we have token data
+ if (is_null($token)) {
+ return false;
+ }
+
+ /**
+ * Check scope, if provided
+ * If token doesn't have a scope, it's null/empty, or it's insufficient, then throw 403
+ * @see http://tools.ietf.org/html/rfc6750#section-3.1
+ */
+ if ($scope && (!isset($token["scope"]) || !$token["scope"] || !$this->scopeUtil->checkScope($scope, $token["scope"]))) {
+ $response->setError(403, 'insufficient_scope', 'The request requires higher privileges than provided by the access token');
+ $response->addHttpHeaders(array(
+ 'WWW-Authenticate' => sprintf('%s realm="%s", scope="%s", error="%s", error_description="%s"',
+ $this->tokenType->getTokenType(),
+ $this->config['www_realm'],
+ $scope,
+ $response->getParameter('error'),
+ $response->getParameter('error_description')
+ )
+ ));
+
+ return false;
+ }
+
+ // allow retrieval of the token
+ $this->token = $token;
+
+ return (bool) $token;
+ }
+
+ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response)
+ {
+ // Get the token parameter
+ if ($token_param = $this->tokenType->getAccessTokenParameter($request, $response)) {
+ // Get the stored token data (from the implementing subclass)
+ // Check we have a well formed token
+ // Check token expiration (expires is a mandatory paramter)
+ if (!$token = $this->tokenStorage->getAccessToken($token_param)) {
+ $response->setError(401, 'invalid_token', 'The access token provided is invalid');
+ } elseif (!isset($token["expires"]) || !isset($token["client_id"])) {
+ $response->setError(401, 'malformed_token', 'Malformed token (missing "expires")');
+ } elseif (time() > $token["expires"]) {
+ $response->setError(401, 'expired_token', 'The access token provided has expired');
+ } else {
+ return $token;
+ }
+ }
+
+ $authHeader = sprintf('%s realm="%s"', $this->tokenType->getTokenType(), $this->config['www_realm']);
+
+ if ($error = $response->getParameter('error')) {
+ $authHeader = sprintf('%s, error="%s"', $authHeader, $error);
+ if ($error_description = $response->getParameter('error_description')) {
+ $authHeader = sprintf('%s, error_description="%s"', $authHeader, $error_description);
+ }
+ }
+
+ $response->addHttpHeaders(array('WWW-Authenticate' => $authHeader));
+
+ return null;
+ }
+
+ // convenience method to allow retrieval of the token
+ public function getToken()
+ {
+ return $this->token;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php b/library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php
new file mode 100644
index 000000000..611421935
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/ResourceControllerInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when a "resource" is requested.
+ * call verifyResourceRequest in order to determine if the request
+ * contains a valid token.
+ *
+ * ex:
+ * > if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) {
+ * > $response->send(); // authorization failed
+ * > die();
+ * > }
+ * > return json_encode($resource); // valid token! Send the stuff!
+ *
+ */
+interface ResourceControllerInterface
+{
+ public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response, $scope = null);
+
+ public function getAccessTokenData(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/library/oauth2/src/OAuth2/Controller/TokenController.php b/library/oauth2/src/OAuth2/Controller/TokenController.php
new file mode 100644
index 000000000..42dab892f
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/TokenController.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
+use OAuth2\GrantType\GrantTypeInterface;
+use OAuth2\ScopeInterface;
+use OAuth2\Scope;
+use OAuth2\Storage\ClientInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\TokenControllerInterface
+ */
+class TokenController implements TokenControllerInterface
+{
+ protected $accessToken;
+ protected $grantTypes;
+ protected $clientAssertionType;
+ protected $scopeUtil;
+ protected $clientStorage;
+
+ public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null)
+ {
+ if (is_null($clientAssertionType)) {
+ foreach ($grantTypes as $grantType) {
+ if (!$grantType instanceof ClientAssertionTypeInterface) {
+ throw new \InvalidArgumentException('You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface');
+ }
+ }
+ }
+ $this->clientAssertionType = $clientAssertionType;
+ $this->accessToken = $accessToken;
+ $this->clientStorage = $clientStorage;
+ foreach ($grantTypes as $grantType) {
+ $this->addGrantType($grantType);
+ }
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if ($token = $this->grantAccessToken($request, $response)) {
+ // @see http://tools.ietf.org/html/rfc6749#section-5.1
+ // server MUST disable caching in headers when tokens are involved
+ $response->setStatusCode(200);
+ $response->addParameters($token);
+ $response->addHttpHeaders(array(
+ 'Cache-Control' => 'no-store',
+ 'Pragma' => 'no-cache',
+ 'Content-Type' => 'application/json'
+ ));
+ }
+ }
+
+ /**
+ * Grant or deny a requested access token.
+ * This would be called from the "/token" endpoint as defined in the spec.
+ * You can call your endpoint whatever you want.
+ *
+ * @param $request - RequestInterface
+ * Request object to grant access token
+ *
+ * @throws InvalidArgumentException
+ * @throws LogicException
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4
+ * @see http://tools.ietf.org/html/rfc6749#section-10.6
+ * @see http://tools.ietf.org/html/rfc6749#section-4.1.3
+ *
+ * @ingroup oauth2_section_4
+ */
+ public function grantAccessToken(RequestInterface $request, ResponseInterface $response)
+ {
+ if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+ $response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2');
+ $response->addHttpHeaders(array('Allow' => 'POST'));
+
+ return null;
+ }
+
+ /**
+ * Determine grant type from request
+ * and validate the request for that grant type
+ */
+ if (!$grantTypeIdentifier = $request->request('grant_type')) {
+ $response->setError(400, 'invalid_request', 'The grant type was not specified in the request');
+
+ return null;
+ }
+
+ if (!isset($this->grantTypes[$grantTypeIdentifier])) {
+ /* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */
+ $response->setError(400, 'unsupported_grant_type', sprintf('Grant type "%s" not supported', $grantTypeIdentifier));
+
+ return null;
+ }
+
+ $grantType = $this->grantTypes[$grantTypeIdentifier];
+
+ /**
+ * Retrieve the client information from the request
+ * ClientAssertionTypes allow for grant types which also assert the client data
+ * in which case ClientAssertion is handled in the validateRequest method
+ *
+ * @see OAuth2\GrantType\JWTBearer
+ * @see OAuth2\GrantType\ClientCredentials
+ */
+ if (!$grantType instanceof ClientAssertionTypeInterface) {
+ if (!$this->clientAssertionType->validateRequest($request, $response)) {
+ return null;
+ }
+ $clientId = $this->clientAssertionType->getClientId();
+ }
+
+ /**
+ * Retrieve the grant type information from the request
+ * The GrantTypeInterface object handles all validation
+ * If the object is an instance of ClientAssertionTypeInterface,
+ * That logic is handled here as well
+ */
+ if (!$grantType->validateRequest($request, $response)) {
+ return null;
+ }
+
+ if ($grantType instanceof ClientAssertionTypeInterface) {
+ $clientId = $grantType->getClientId();
+ } else {
+ // validate the Client ID (if applicable)
+ if (!is_null($storedClientId = $grantType->getClientId()) && $storedClientId != $clientId) {
+ $response->setError(400, 'invalid_grant', sprintf('%s doesn\'t exist or is invalid for the client', $grantTypeIdentifier));
+
+ return null;
+ }
+ }
+
+ /**
+ * Validate the client can use the requested grant type
+ */
+ if (!$this->clientStorage->checkRestrictedGrantType($clientId, $grantTypeIdentifier)) {
+ $response->setError(400, 'unauthorized_client', 'The grant type is unauthorized for this client_id');
+
+ return false;
+ }
+
+ /**
+ * Validate the scope of the token
+ *
+ * requestedScope - the scope specified in the token request
+ * availableScope - the scope associated with the grant type
+ * ex: in the case of the "Authorization Code" grant type,
+ * the scope is specified in the authorize request
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-3.3
+ */
+
+ $requestedScope = $this->scopeUtil->getScopeFromRequest($request);
+ $availableScope = $grantType->getScope();
+
+ if ($requestedScope) {
+ // validate the requested scope
+ if ($availableScope) {
+ if (!$this->scopeUtil->checkScope($requestedScope, $availableScope)) {
+ $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this request');
+
+ return null;
+ }
+ } else {
+ // validate the client has access to this scope
+ if ($clientScope = $this->clientStorage->getClientScope($clientId)) {
+ if (!$this->scopeUtil->checkScope($requestedScope, $clientScope)) {
+ $response->setError(400, 'invalid_scope', 'The scope requested is invalid for this client');
+
+ return false;
+ }
+ } elseif (!$this->scopeUtil->scopeExists($requestedScope)) {
+ $response->setError(400, 'invalid_scope', 'An unsupported scope was requested');
+
+ return null;
+ }
+ }
+ } elseif ($availableScope) {
+ // use the scope associated with this grant type
+ $requestedScope = $availableScope;
+ } else {
+ // use a globally-defined default scope
+ $defaultScope = $this->scopeUtil->getDefaultScope($clientId);
+
+ // "false" means default scopes are not allowed
+ if (false === $defaultScope) {
+ $response->setError(400, 'invalid_scope', 'This application requires you specify a scope parameter');
+
+ return null;
+ }
+
+ $requestedScope = $defaultScope;
+ }
+
+ return $grantType->createAccessToken($this->accessToken, $clientId, $grantType->getUserId(), $requestedScope);
+ }
+
+ /**
+ * addGrantType
+ *
+ * @param grantType - OAuth2\GrantTypeInterface
+ * the grant type to add for the specified identifier
+ * @param identifier - string
+ * a string passed in as "grant_type" in the response that will call this grantType
+ */
+ public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
+ {
+ if (is_null($identifier) || is_numeric($identifier)) {
+ $identifier = $grantType->getQuerystringIdentifier();
+ }
+
+ $this->grantTypes[$identifier] = $grantType;
+ }
+
+ public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if ($this->revokeToken($request, $response)) {
+ $response->setStatusCode(200);
+ $response->addParameters(array('revoked' => true));
+ }
+ }
+
+ /**
+ * Revoke a refresh or access token. Returns true on success and when tokens are invalid
+ *
+ * Note: invalid tokens do not cause an error response since the client
+ * cannot handle such an error in a reasonable way. Moreover, the
+ * purpose of the revocation request, invalidating the particular token,
+ * is already achieved.
+ *
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @return bool|null
+ */
+ public function revokeToken(RequestInterface $request, ResponseInterface $response)
+ {
+ if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
+ $response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2');
+ $response->addHttpHeaders(array('Allow' => 'POST'));
+
+ return null;
+ }
+
+ $token_type_hint = $request->request('token_type_hint');
+ if (!in_array($token_type_hint, array(null, 'access_token', 'refresh_token'), true)) {
+ $response->setError(400, 'invalid_request', 'Token type hint must be either \'access_token\' or \'refresh_token\'');
+
+ return null;
+ }
+
+ $token = $request->request('token');
+ if ($token === null) {
+ $response->setError(400, 'invalid_request', 'Missing token parameter to revoke');
+
+ return null;
+ }
+
+ // @todo remove this check for v2.0
+ if (!method_exists($this->accessToken, 'revokeToken')) {
+ $class = get_class($this->accessToken);
+ throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method");
+ }
+
+ $this->accessToken->revokeToken($token, $token_type_hint);
+
+ return true;
+ }
+}
diff --git a/library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php b/library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php
new file mode 100644
index 000000000..72d72570f
--- /dev/null
+++ b/library/oauth2/src/OAuth2/Controller/TokenControllerInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OAuth2\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when a token is being requested.
+ * it is called to handle all grant types the application supports.
+ * It also validates the client's credentials
+ *
+ * ex:
+ * > $tokenController->handleTokenRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response());
+ * > $response->send();
+ *
+ */
+interface TokenControllerInterface
+{
+ /**
+ * handleTokenRequest
+ *
+ * @param $request
+ * OAuth2\RequestInterface - The current http request
+ * @param $response
+ * OAuth2\ResponseInterface - An instance of OAuth2\ResponseInterface to contain the response data
+ *
+ */
+ public function handleTokenRequest(RequestInterface $request, ResponseInterface $response);
+
+ public function grantAccessToken(RequestInterface $request, ResponseInterface $response);
+}