summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.travis.yml9
-rw-r--r--CHANGELOG.md35
-rw-r--r--LICENSE2
-rw-r--r--src/OAuth2/Controller/AuthorizeController.php45
-rw-r--r--src/OAuth2/Controller/ResourceController.php2
-rw-r--r--src/OAuth2/Encryption/EncryptionInterface.php2
-rw-r--r--src/OAuth2/Encryption/Jwt.php42
-rw-r--r--src/OAuth2/GrantType/GrantTypeInterface.php1
-rw-r--r--src/OAuth2/GrantType/JwtBearer.php3
-rw-r--r--src/OAuth2/OpenID/Controller/AuthorizeController.php86
-rw-r--r--src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php12
-rw-r--r--src/OAuth2/OpenID/Controller/UserInfoController.php57
-rw-r--r--src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php23
-rw-r--r--src/OAuth2/OpenID/GrantType/AuthorizationCode.php36
-rw-r--r--src/OAuth2/OpenID/ResponseType/AuthorizationCode.php60
-rw-r--r--src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php32
-rw-r--r--src/OAuth2/OpenID/ResponseType/IdToken.php125
-rw-r--r--src/OAuth2/OpenID/ResponseType/IdTokenInterface.php29
-rw-r--r--src/OAuth2/OpenID/ResponseType/TokenIdToken.php28
-rw-r--r--src/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php9
-rw-r--r--src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php44
-rw-r--r--src/OAuth2/OpenID/Storage/UserClaimsInterface.php38
-rw-r--r--src/OAuth2/Request.php6
-rw-r--r--src/OAuth2/Response.php4
-rw-r--r--src/OAuth2/ResponseType/AccessToken.php6
-rw-r--r--src/OAuth2/ResponseType/AuthorizationCode.php10
-rw-r--r--src/OAuth2/Scope.php27
-rw-r--r--src/OAuth2/Server.php170
-rw-r--r--src/OAuth2/Storage/AccessTokenInterface.php3
-rw-r--r--src/OAuth2/Storage/Cassandra.php9
-rw-r--r--src/OAuth2/Storage/CryptoToken.php2
-rw-r--r--src/OAuth2/Storage/Memory.php54
-rw-r--r--src/OAuth2/Storage/Mongo.php13
-rw-r--r--src/OAuth2/Storage/Pdo.php82
-rw-r--r--src/OAuth2/Storage/Redis.php9
-rw-r--r--src/OAuth2/TokenType/Bearer.php19
-rw-r--r--test/OAuth2/Controller/AuthorizeControllerTest.php46
-rw-r--r--test/OAuth2/Controller/ResourceControllerTest.php4
-rw-r--r--test/OAuth2/GrantType/JwtBearerTest.php (renamed from test/OAuth2/GrantType/JWTBearerTest.php)2
-rw-r--r--test/OAuth2/OpenID/Controller/UserInfoControllerTest.php33
-rw-r--r--test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php57
-rw-r--r--test/OAuth2/OpenID/ResponseType/IdTokenTest.php184
-rw-r--r--test/OAuth2/OpenID/ResponseType/TokenIdTokenTest.php93
-rw-r--r--test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php95
-rw-r--r--test/OAuth2/OpenID/Storage/UserClaimsTest.php41
-rw-r--r--test/OAuth2/RequestTest.php39
-rw-r--r--test/OAuth2/ServerTest.php228
-rw-r--r--test/OAuth2/Storage/PdoTest.php41
-rw-r--r--test/OAuth2/TokenType/BearerTest.php22
-rw-r--r--test/config/storage.json22
-rw-r--r--test/lib/OAuth2/Request/TestRequest.php9
-rw-r--r--test/lib/OAuth2/Storage/BaseTest.php2
-rw-r--r--test/lib/OAuth2/Storage/Bootstrap.php108
54 files changed, 2039 insertions, 123 deletions
diff --git a/.gitignore b/.gitignore
index fad4cdf..c43a667 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
# Test Files #
test/config/test.sqlite
vendor
+composer.lock
+.idea
diff --git a/.travis.yml b/.travis.yml
index d68d047..cfa65e9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,6 +4,8 @@ php:
- 5.3
- 5.4
- 5.5
+ - 5.6
+ - hhvm
env:
- SKIP_MONGO_TESTS=1
@@ -14,11 +16,6 @@ services:
- cassandra
before_script:
- - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
+ - psql -c 'create database oauth2_server_php;' -U postgres
- composer require predis/predis:dev-master
- composer require thobbs/phpcassa:dev-master
-
-after_failure:
- - ps ax | grep mongo
- - cat /etc/mongodb.conf
- - cat /var/log/mongodb/mongodb.log
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d92834..00813f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,8 +4,41 @@ CHANGELOG for 1.x
This changelog references the relevant changes (bug and security fixes) done
in 1.x minor versions.
-To get the diff for a specific change, go to https://github.com/bshaffer/oauth2-server-php/commit/XXX where XXX is the change hash
+To see the files changed for a given bug, go to https://github.com/bshaffer/oauth2-server-php/issues/### where ### is the bug number
To get the diff between two versions, go to https://github.com/bshaffer/oauth2-server-php/compare/v1.0...v1.1
+To get the diff for a specific change, go to https://github.com/bshaffer/oauth2-server-php/commit/XXX where XXX is the change hash
+
+* 1.4 (2014-06-12)
+
+ PR: https://github.com/bshaffer/oauth2-server-php/pull/392
+
+ * bug #189 Storage\PDO - allows DSN string in constructor
+ * bug #233 Bearer Tokens - allows token in request body for PUT requests
+ * bug #346 Fixes open_basedir warning
+ * bug #351 Adds OpenID Connect support
+ * bug #355 Adds php 5.6 and HHVM to travis.ci testing
+ * [BC] bug #358 Adds `getQuerystringIdentifier()` to the GrantType interface
+ * bug #363 Encryption\JWT - Allows for subclassing JWT Headers
+ * bug #349 Bearer Tokens - adds requestHasToken method for when access tokens are optional
+ * bug #301 Encryption\JWT - fixes urlSafeB64Encode(): ensures newlines are replaced as expected
+ * bug #323 ResourceController - client_id is no longer required to be returned when calling getAccessToken
+ * bug #367 Storage\PDO - adds Postgres support
+ * bug #368 Access Tokens - use mcrypt_create_iv or openssl_random_pseudo_bytes to create token string
+ * bug #376 Request - allows case insensitive headers
+ * bug #384 Storage\PDO - can pass in PDO options in constructor of PDO storage
+ * misc fixes #361, #292, #373, #374, #379, #396
+* 1.3 (2014-02-27)
+
+ PR: https://github.com/bshaffer/oauth2-server-php/pull/325
+
+ * bug #311 adds cassandra storage
+ * bug #298 fixes response code for user credentials grant type
+ * bug #318 adds 'use_crypto_tokens' config to Server class for better DX
+ * [BC] bug #320 pass client_id to getDefaultScope
+ * bug #324 better feedback when running tests
+ * bug #335 adds support for non-expiring refresh tokens
+ * bug #333 fixes Pdo storage for getClientKey
+ * bug #336 fixes Redis storage for expireAuthorizationCode
* 1.3 (2014-02-27)
diff --git a/LICENSE b/LICENSE
index 6365aa2..d7ece84 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2010 Brent Shaffer
+Copyright (c) 2014 Brent Shaffer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php
index 74d2423..e669824 100644
--- a/src/OAuth2/Controller/AuthorizeController.php
+++ b/src/OAuth2/Controller/AuthorizeController.php
@@ -78,6 +78,7 @@ class AuthorizeController implements AuthorizeControllerInterface
$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;
$response->setRedirect($this->config['redirect_status_code'], $redirect_uri, $this->state, 'access_denied', "The user denied access to your application");
@@ -85,14 +86,10 @@ class AuthorizeController implements AuthorizeControllerInterface
return;
}
- // @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,
- );
+ // 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);
@@ -108,6 +105,24 @@ class AuthorizeController implements AuthorizeControllerInterface
$response->setRedirect($this->config['redirect_status_code'], $uri);
}
+ /*
+ * 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)
@@ -168,7 +183,7 @@ class AuthorizeController implements AuthorizeControllerInterface
$state = $request->query('state');
// type and client_id are required
- if (!$response_type || !in_array($response_type, array(self::RESPONSE_TYPE_AUTHORIZATION_CODE, self::RESPONSE_TYPE_ACCESS_TOKEN))) {
+ 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;
@@ -189,9 +204,7 @@ class AuthorizeController implements AuthorizeControllerInterface
return false;
}
- }
-
- if ($response_type == self::RESPONSE_TYPE_ACCESS_TOKEN) {
+ } 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);
@@ -287,6 +300,14 @@ class AuthorizeController implements AuthorizeControllerInterface
;
}
+ protected function getValidResponseTypes()
+ {
+ return array(
+ self::RESPONSE_TYPE_ACCESS_TOKEN,
+ self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+ );
+ }
+
/**
* Internal method for validating redirect URI supplied
*
diff --git a/src/OAuth2/Controller/ResourceController.php b/src/OAuth2/Controller/ResourceController.php
index 317d1d5..9623b9f 100644
--- a/src/OAuth2/Controller/ResourceController.php
+++ b/src/OAuth2/Controller/ResourceController.php
@@ -81,7 +81,7 @@ class ResourceController implements ResourceControllerInterface
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, 'invalid_token', 'Malformed token (missing "expires" or "client_id")');
+ $response->setError(401, 'invalid_token', 'Malformed token (missing "expires")');
} elseif (time() > $token["expires"]) {
$response->setError(401, 'invalid_token', 'The access token provided has expired');
} else {
diff --git a/src/OAuth2/Encryption/EncryptionInterface.php b/src/OAuth2/Encryption/EncryptionInterface.php
index dc8e0d2..2d336c6 100644
--- a/src/OAuth2/Encryption/EncryptionInterface.php
+++ b/src/OAuth2/Encryption/EncryptionInterface.php
@@ -6,4 +6,6 @@ interface EncryptionInterface
{
public function encode($payload, $key, $algorithm = null);
public function decode($payload, $key, $algorithm = null);
+ public function urlSafeB64Encode($data);
+ public function urlSafeB64Decode($b64);
}
diff --git a/src/OAuth2/Encryption/Jwt.php b/src/OAuth2/Encryption/Jwt.php
index 6bde68e..824ac13 100644
--- a/src/OAuth2/Encryption/Jwt.php
+++ b/src/OAuth2/Encryption/Jwt.php
@@ -10,11 +10,11 @@ class Jwt implements EncryptionInterface
{
public function encode($payload, $key, $algo = 'HS256')
{
- $header = array('typ' => 'JWT', 'alg' => $algo);
+ $header = $this->generateJwtHeader($payload, $algo);
$segments = array(
- $this->urlsafeB64Encode(json_encode($header)),
- $this->urlsafeB64Encode(json_encode($payload))
+ $this->urlSafeB64Encode(json_encode($header)),
+ $this->urlSafeB64Encode(json_encode($payload))
);
$signing_input = implode('.', $segments);
@@ -39,15 +39,15 @@ class Jwt implements EncryptionInterface
list($headb64, $payloadb64, $cryptob64) = $tks;
- if (null === ($header = json_decode($this->urlsafeB64Decode($headb64), true))) {
+ if (null === ($header = json_decode($this->urlSafeB64Decode($headb64), true))) {
return false;
}
- if (null === $payload = json_decode($this->urlsafeB64Decode($payloadb64), true)) {
+ if (null === $payload = json_decode($this->urlSafeB64Decode($payloadb64), true)) {
return false;
}
- $sig = $this->urlsafeB64Decode($cryptob64);
+ $sig = $this->urlSafeB64Decode($cryptob64);
if ($verify) {
if (!isset($header['alg'])) {
@@ -64,6 +64,7 @@ class Jwt implements EncryptionInterface
private function verifySignature($signature, $input, $key, $algo = 'HS256')
{
+ // use constants when possible, for HipHop support
switch ($algo) {
case'HS256':
case'HS384':
@@ -71,13 +72,13 @@ class Jwt implements EncryptionInterface
return $this->sign($input, $key, $algo) === $signature;
case 'RS256':
- return openssl_verify($input, $signature, $key, 'sha256') === 1;
+ return openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256') === 1;
case 'RS384':
- return @openssl_verify($input, $signature, $key, 'sha384') === 1;
+ return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384') === 1;
case 'RS512':
- return @openssl_verify($input, $signature, $key, 'sha512') === 1;
+ return @openssl_verify($input, $signature, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512') === 1;
default:
throw new \InvalidArgumentException("Unsupported or invalid signing algorithm.");
@@ -97,13 +98,13 @@ class Jwt implements EncryptionInterface
return hash_hmac('sha512', $input, $key, true);
case 'RS256':
- return $this->generateRSASignature($input, $key, 'sha256');
+ return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 'sha256');
case 'RS384':
- return $this->generateRSASignature($input, $key, 'sha384');
+ return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA384') ? OPENSSL_ALGO_SHA384 : 'sha384');
case 'RS512':
- return $this->generateRSASignature($input, $key, 'sha512');
+ return $this->generateRSASignature($input, $key, defined('OPENSSL_ALGO_SHA512') ? OPENSSL_ALGO_SHA512 : 'sha512');
default:
throw new \Exception("Unsupported or invalid signing algorithm.");
@@ -119,17 +120,17 @@ class Jwt implements EncryptionInterface
return $signature;
}
- private function urlSafeB64Encode($data)
+ public function urlSafeB64Encode($data)
{
$b64 = base64_encode($data);
- $b64 = str_replace(array('+', '/', '\r', '\n', '='),
+ $b64 = str_replace(array('+', '/', "\r", "\n", '='),
array('-', '_'),
$b64);
return $b64;
}
- private function urlSafeB64Decode($b64)
+ public function urlSafeB64Decode($b64)
{
$b64 = str_replace(array('-', '_'),
array('+', '/'),
@@ -137,4 +138,15 @@ class Jwt implements EncryptionInterface
return base64_decode($b64);
}
+
+ /**
+ * Override to create a custom header
+ */
+ protected function generateJwtHeader($payload, $algorithm)
+ {
+ return array(
+ 'typ' => 'JWT',
+ 'alg' => $algorithm,
+ );
+ }
}
diff --git a/src/OAuth2/GrantType/GrantTypeInterface.php b/src/OAuth2/GrantType/GrantTypeInterface.php
index e70bb23..98489e9 100644
--- a/src/OAuth2/GrantType/GrantTypeInterface.php
+++ b/src/OAuth2/GrantType/GrantTypeInterface.php
@@ -11,6 +11,7 @@ use OAuth2\ResponseInterface;
*/
interface GrantTypeInterface
{
+ public function getQuerystringIdentifier();
public function validateRequest(RequestInterface $request, ResponseInterface $response);
public function getClientId();
public function getUserId();
diff --git a/src/OAuth2/GrantType/JwtBearer.php b/src/OAuth2/GrantType/JwtBearer.php
index 04ac144..534feae 100644
--- a/src/OAuth2/GrantType/JwtBearer.php
+++ b/src/OAuth2/GrantType/JwtBearer.php
@@ -5,6 +5,7 @@ namespace OAuth2\GrantType;
use OAuth2\ClientAssertionType\ClientAssertionTypeInterface;
use OAuth2\Storage\JwtBearerInterface;
use OAuth2\Encryption\Jwt;
+use OAuth2\Encryption\EncryptionInterface;
use OAuth2\ResponseType\AccessTokenInterface;
use OAuth2\RequestInterface;
use OAuth2\ResponseInterface;
@@ -35,7 +36,7 @@ class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
* @param OAuth2\Encryption\JWT OPTIONAL $jwtUtil
* The class used to decode, encode and verify JWTs.
*/
- public function __construct(JwtBearerInterface $storage, $audience, Jwt $jwtUtil = null)
+ public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null)
{
$this->storage = $storage;
$this->audience = $audience;
diff --git a/src/OAuth2/OpenID/Controller/AuthorizeController.php b/src/OAuth2/OpenID/Controller/AuthorizeController.php
new file mode 100644
index 0000000..4850ebe
--- /dev/null
+++ b/src/OAuth2/OpenID/Controller/AuthorizeController.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Controller\AuthorizeController as BaseAuthorizeController;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\AuthorizeControllerInterface
+ */
+class AuthorizeController extends BaseAuthorizeController implements AuthorizeControllerInterface
+{
+ private $nonce;
+
+ protected function buildAuthorizeParameters($request, $response, $user_id)
+ {
+ if (!$params = parent::buildAuthorizeParameters($request, $response, $user_id)) {
+ return;
+ }
+
+ // Generate an id token if needed.
+ if ($this->needsIdToken($this->getScope()) && $this->getResponseType() == self::RESPONSE_TYPE_AUTHORIZATION_CODE) {
+ $params['id_token'] = $this->responseTypes['id_token']->createIdToken($this->getClientId(), $user_id, $this->nonce);
+ }
+
+ // add the nonce to return with the redirect URI
+ $params['nonce'] = $this->nonce;
+
+ return $params;
+ }
+
+ public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!parent::validateAuthorizeRequest($request, $response)) {
+ return false;
+ }
+
+ $nonce = $request->query('nonce');
+
+ // Validate required nonce for "id_token" and "token id_token"
+ if (!$nonce && in_array($this->getResponseType(), array(self::RESPONSE_TYPE_ID_TOKEN, self::RESPONSE_TYPE_TOKEN_ID_TOKEN))) {
+ $response->setError(400, 'invalid_nonce', 'This application requires you specify a nonce parameter');
+
+ return false;
+ }
+
+ $this->nonce = $nonce;
+
+ return true;
+ }
+
+ protected function getValidResponseTypes()
+ {
+ return array(
+ self::RESPONSE_TYPE_ACCESS_TOKEN,
+ self::RESPONSE_TYPE_AUTHORIZATION_CODE,
+ self::RESPONSE_TYPE_ID_TOKEN,
+ self::RESPONSE_TYPE_TOKEN_ID_TOKEN,
+ );
+ }
+
+ /**
+ * Returns whether the current request needs to generate an id token.
+ *
+ * ID Tokens are a part of the OpenID Connect specification, so this
+ * method checks whether OpenID Connect is enabled in the server settings
+ * and whether the openid scope was requested.
+ *
+ * @param $request_scope
+ * A space-separated string of scopes.
+ *
+ * @return
+ * TRUE if an id token is needed, FALSE otherwise.
+ */
+ public function needsIdToken($request_scope)
+ {
+ // see if the "openid" scope exists in the requested scope
+ return $this->scopeUtil->checkScope('openid', $request_scope);
+ }
+
+ public function getNonce()
+ {
+ return $this->nonce;
+ }
+}
diff --git a/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php b/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php
new file mode 100644
index 0000000..af47cd0
--- /dev/null
+++ b/src/OAuth2/OpenID/Controller/AuthorizeControllerInterface.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+interface AuthorizeControllerInterface
+{
+ const RESPONSE_TYPE_ID_TOKEN = 'id_token';
+ const RESPONSE_TYPE_TOKEN_ID_TOKEN = 'token id_token';
+}
diff --git a/src/OAuth2/OpenID/Controller/UserInfoController.php b/src/OAuth2/OpenID/Controller/UserInfoController.php
new file mode 100644
index 0000000..28e429e
--- /dev/null
+++ b/src/OAuth2/OpenID/Controller/UserInfoController.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\TokenType\TokenTypeInterface;
+use OAuth2\Storage\AccessTokenInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\Controller\ResourceController;
+use OAuth2\ScopeInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * @see OAuth2\Controller\UserInfoControllerInterface
+ */
+class UserInfoController extends ResourceController implements UserInfoControllerInterface
+{
+ private $token;
+
+ protected $tokenType;
+ protected $tokenStorage;
+ protected $userClaimsStorage;
+ protected $config;
+ protected $scopeUtil;
+
+ public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null)
+ {
+ $this->tokenType = $tokenType;
+ $this->tokenStorage = $tokenStorage;
+ $this->userClaimsStorage = $userClaimsStorage;
+
+ $this->config = array_merge(array(
+ 'www_realm' => 'Service',
+ ), $config);
+
+ if (is_null($scopeUtil)) {
+ $scopeUtil = new Scope();
+ }
+ $this->scopeUtil = $scopeUtil;
+ }
+
+ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response)
+ {
+ if (!$this->verifyResourceRequest($request, $response, 'openid')) {
+ return;
+ }
+
+ $token = $this->getToken();
+ $claims = $this->userClaimsStorage->getUserClaims($token['user_id'], $token['scope']);
+ // The sub Claim MUST always be returned in the UserInfo Response.
+ // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
+ $claims += array(
+ 'sub' => $token['user_id'],
+ );
+ $response->setParameters($claims);
+ }
+}
diff --git a/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php b/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php
new file mode 100644
index 0000000..a89049d
--- /dev/null
+++ b/src/OAuth2/OpenID/Controller/UserInfoControllerInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ * This controller is called when the user claims for OpenID Connect's
+ * UserInfo endpoint should be returned.
+ *
+ * ex:
+ * > $response = new OAuth2\Response();
+ * > $userInfoController->handleUserInfoRequest(
+ * > OAuth2\Request::createFromGlobals(),
+ * > $response;
+ * > $response->send();
+ *
+ */
+interface UserInfoControllerInterface
+{
+ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response);
+}
diff --git a/src/OAuth2/OpenID/GrantType/AuthorizationCode.php b/src/OAuth2/OpenID/GrantType/AuthorizationCode.php
new file mode 100644
index 0000000..4a5da01
--- /dev/null
+++ b/src/OAuth2/OpenID/GrantType/AuthorizationCode.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace OAuth2\OpenID\GrantType;
+
+use OAuth2\GrantType\AuthorizationCode as BaseAuthorizationCode;
+use OAuth2\Storage\AuthorizationCodeInterface;
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\RequestInterface;
+use OAuth2\ResponseInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode extends BaseAuthorizationCode
+{
+ public function createAccessToken(AccessTokenInterface $accessToken, $client_id, $user_id, $scope)
+ {
+ $includeRefreshToken = true;
+ if (isset($this->authCode['id_token'])) {
+ // OpenID Connect requests include the refresh token only if the
+ // offline_access scope has been requested and granted.
+ $scopes = explode(' ', trim($scope));
+ $includeRefreshToken = in_array('offline_access', $scopes);
+ }
+
+ $token = $accessToken->createAccessToken($client_id, $user_id, $scope, $includeRefreshToken);
+ if (isset($this->authCode['id_token'])) {
+ $token['id_token'] = $this->authCode['id_token'];
+ }
+
+ $this->storage->expireAuthorizationCode($this->authCode['code']);
+
+ return $token;
+ }
+}
diff --git a/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php b/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php
new file mode 100644
index 0000000..8971954
--- /dev/null
+++ b/src/OAuth2/OpenID/ResponseType/AuthorizationCode.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AuthorizationCode as BaseAuthorizationCode;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as AuthorizationCodeStorageInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCodeInterface
+{
+ public function __construct(AuthorizationCodeStorageInterface $storage, array $config = array())
+ {
+ parent::__construct($storage, $config);
+ }
+
+ public function getAuthorizeResponse($params, $user_id = null)
+ {
+ // build the URL to redirect to
+ $result = array('query' => array());
+
+ $params += array('scope' => null, 'state' => null, 'id_token' => null);
+
+ $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']);
+
+ if (isset($params['state'])) {
+ $result['query']['state'] = $params['state'];
+ }
+
+ return array($params['redirect_uri'], $result);
+ }
+
+ /**
+ * Handle the creation of the authorization code.
+ *
+ * @param $client_id
+ * Client identifier related to the authorization code
+ * @param $user_id
+ * User ID associated with the authorization code
+ * @param $redirect_uri
+ * An absolute URI to which the authorization server will redirect the
+ * user-agent to when the end-user authorization step is completed.
+ * @param $scope
+ * (optional) Scopes to be stored in space-separated string.
+ * @param $id_token
+ * (optional) The OpenID Connect id_token.
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4
+ * @ingroup oauth2_section_4
+ */
+ public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null)
+ {
+ $code = $this->generateAuthorizationCode();
+ $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token);
+
+ return $code;
+ }
+}
diff --git a/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php b/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php
new file mode 100644
index 0000000..7b8de7c
--- /dev/null
+++ b/src/OAuth2/OpenID/ResponseType/AuthorizationCodeInterface.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AuthorizationCodeInterface as BaseAuthorizationCodeInterface;
+
+/**
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface
+{
+ /**
+ * Handle the creation of the authorization code.
+ *
+ * @param $client_id
+ * Client identifier related to the authorization code
+ * @param $user_id
+ * User ID associated with the authorization code
+ * @param $redirect_uri
+ * An absolute URI to which the authorization server will redirect the
+ * user-agent to when the end-user authorization step is completed.
+ * @param $scope
+ * (optional) Scopes to be stored in space-separated string.
+ * @param $id_token
+ * (optional) The OpenID Connect id_token.
+ *
+ * @see http://tools.ietf.org/html/rfc6749#section-4
+ * @ingroup oauth2_section_4
+ */
+ public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null);
+}
diff --git a/src/OAuth2/OpenID/ResponseType/IdToken.php b/src/OAuth2/OpenID/ResponseType/IdToken.php
new file mode 100644
index 0000000..8d907bc
--- /dev/null
+++ b/src/OAuth2/OpenID/ResponseType/IdToken.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Encryption\EncryptionInterface;
+use OAuth2\Encryption\Jwt;
+use OAuth2\Storage\RefreshTokenInterface;
+use OAuth2\Storage\PublicKeyInterface;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+
+class IdToken implements IdTokenInterface
+{
+ protected $userClaimsStorage;
+ protected $publicKeyStorage;
+ protected $config;
+ protected $encryptionUtil;
+
+ public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null)
+ {
+ $this->userClaimsStorage = $userClaimsStorage;
+ $this->publicKeyStorage = $publicKeyStorage;
+ if (is_null($encryptionUtil)) {
+ $encryptionUtil = new Jwt();
+ }
+ $this->encryptionUtil = $encryptionUtil;
+
+ if (!isset($config['issuer'])) {
+ throw new \LogicException('config parameter "issuer" must be set');
+ }
+ $this->config = array_merge(array(
+ 'id_lifetime' => 3600,
+ ), $config);
+ }
+
+ public function getAuthorizeResponse($params, $userInfo = null)
+ {
+ // build the URL to redirect to
+ $result = array('query' => array());
+ $params += array('scope' => null, 'state' => null, 'nonce' => null);
+
+ // create the id token.
+ list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+ $userClaims = $this->userClaimsStorage->getUserClaims($user_id, $params['scope']);
+
+ $id_token = $this->createIdToken($params['client_id'], $userInfo, $params['nonce'], $userClaims, null);
+ $result["fragment"] = array('id_token' => $id_token);
+ if (isset($params['state'])) {
+ $result["fragment"]["state"] = $params['state'];
+ }
+
+ return array($params['redirect_uri'], $result);
+ }
+
+ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null)
+ {
+ // pull auth_time from user info if supplied
+ list($user_id, $auth_time) = $this->getUserIdAndAuthTime($userInfo);
+
+ $token = array(
+ 'iss' => $this->config['issuer'],
+ 'sub' => $user_id,
+ 'aud' => $client_id,
+ 'iat' => time(),
+ 'exp' => time() + $this->config['id_lifetime'],
+ 'auth_time' => $auth_time,
+ );
+
+ if ($nonce) {
+ $token['nonce'] = $nonce;
+ }
+
+ if ($userClaims) {
+ $token += $userClaims;
+ }
+
+ if ($access_token) {
+ $token['at_hash'] = $this->createAtHash($access_token, $client_id);
+ }
+
+ return $this->encodeToken($token, $client_id);
+ }
+
+ protected function createAtHash($access_token, $client_id = null)
+ {
+ // maps HS256 and RS256 to sha256, etc.
+ $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+ $hash_algorithm = 'sha' . substr($algorithm, 2);
+ $hash = hash($hash_algorithm, $access_token);
+ $at_hash = substr($hash, 0, strlen($hash) / 2);
+
+ return $this->encryptionUtil->urlSafeB64Encode($at_hash);
+ }
+
+ protected function encodeToken(array $token, $client_id = null)
+ {
+ $private_key = $this->publicKeyStorage->getPrivateKey($client_id);
+ $algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
+
+ return $this->encryptionUtil->encode($token, $private_key, $algorithm);
+ }
+
+ private function getUserIdAndAuthTime($userInfo)
+ {
+ $auth_time = null;
+
+ // support an array for user_id / auth_time
+ if (is_array($userInfo)) {
+ if (!isset($userInfo['user_id'])) {
+ throw new \LogicException('if $user_id argument is an array, user_id index must be set');
+ }
+
+ $auth_time = isset($userInfo['auth_time']) ? $userInfo['auth_time'] : null;
+ $user_id = $userInfo['user_id'];
+ } else {
+ $user_id = $userInfo;
+ }
+
+ if (is_null($auth_time)) {
+ $auth_time = time();
+ }
+
+ // userInfo is a scalar, and so this is the $user_id. Auth Time is null
+ return array($user_id, $auth_time);
+ }
+}
diff --git a/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php
new file mode 100644
index 0000000..3dfb57e
--- /dev/null
+++ b/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface IdTokenInterface extends ResponseTypeInterface
+{
+ /**
+ * Create the id token.
+ *
+ * If Authorization Code Flow is used, the id_token is generated when the
+ * authorization code is issued, and later returned from the token endpoint
+ * together with the access_token.
+ * If the Implicit Flow is used, the token and id_token are generated and
+ * returned together.
+ *
+ * @param string $client_id The client id.
+ * @param string $user_id The user id.
+ * @param string $nonce OPTIONAL The nonce.
+ * @param string $userClaims OPTIONAL Claims about the user.
+ * @param string $access_token OPTIONAL The access token, if known.
+ *
+ * @return string The ID Token represented as a JSON Web Token (JWT).
+ *
+ * @see http://openid.net/specs/openid-connect-core-1_0.html#IDToken
+ */
+ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims = null, $access_token = null);
+}
diff --git a/src/OAuth2/OpenID/ResponseType/TokenIdToken.php b/src/OAuth2/OpenID/ResponseType/TokenIdToken.php
new file mode 100644
index 0000000..67f2a4a
--- /dev/null
+++ b/src/OAuth2/OpenID/ResponseType/TokenIdToken.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\AccessTokenInterface;
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+class TokenIdToken implements TokenIdTokenInterface
+{
+ protected $accessToken;
+ protected $idToken;
+
+ public function __construct(AccessTokenInterface $accessToken, IdToken $idToken)
+ {
+ $this->accessToken = $accessToken;
+ $this->idToken = $idToken;
+ }
+
+ public function getAuthorizeResponse($params, $user_id = null)
+ {
+ $result = $this->accessToken->getAuthorizeResponse($params, $user_id);
+ $access_token = $result[1]['fragment']['access_token'];
+ $id_token = $this->idToken->createIdToken($params['client_id'], $user_id, $params['nonce'], null, $access_token);
+ $result[1]['fragment']['id_token'] = $id_token;
+
+ return $result;
+ }
+}
diff --git a/src/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php b/src/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php
new file mode 100644
index 0000000..1b4c513
--- /dev/null
+++ b/src/OAuth2/OpenID/ResponseType/TokenIdTokenInterface.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\ResponseType\ResponseTypeInterface;
+
+interface TokenIdTokenInterface extends ResponseTypeInterface
+{
+}
diff --git a/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php b/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php
new file mode 100644
index 0000000..3c6d00b
--- /dev/null
+++ b/src/OAuth2/OpenID/Storage/AuthorizationCodeInterface.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+use OAuth2\Storage\AuthorizationCodeInterface as BaseAuthorizationCodeInterface;
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should get/save authorization codes for the "Authorization Code"
+ * grant type
+ *
+ * @author Brent Shaffer <bshafs at gmail dot com>
+ */
+interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface
+{
+ /**
+ * Take the provided authorization code values and store them somewhere.
+ *
+ * This function should be the storage counterpart to getAuthCode().
+ *
+ * If storage fails for some reason, we're not currently checking for
+ * any sort of success/failure, so you should bail out of the script
+ * and provide a descriptive fail message.
+ *
+ * Required for OAuth2::GRANT_TYPE_AUTH_CODE.
+ *
+ * @param $code
+ * Authorization code to be stored.
+ * @param $client_id
+ * Client identifier to be stored.
+ * @param $user_id
+ * User identifier to be stored.
+ * @param string $redirect_uri
+ * Redirect URI(s) to be stored in a space-separated string.
+ * @param int $expires
+ * Expiration to be stored as a Unix timestamp.
+ * @param string $scope
+ * (optional) Scopes to be stored in space-separated string.
+ * @param string $id_token
+ * (optional) The OpenID Connect id_token.
+ *
+ * @ingroup oauth2_section_4
+ */
+ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null);
+}
diff --git a/src/OAuth2/OpenID/Storage/UserClaimsInterface.php b/src/OAuth2/OpenID/Storage/UserClaimsInterface.php
new file mode 100644
index 0000000..f230bef
--- /dev/null
+++ b/src/OAuth2/OpenID/Storage/UserClaimsInterface.php
@@ -0,0 +1,38 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+/**
+ * Implement this interface to specify where the OAuth2 Server
+ * should retrieve user claims for the OpenID Connect id_token.
+ */
+interface UserClaimsInterface
+{
+ // valid scope values to pass into the user claims API call
+ const VALID_CLAIMS = 'profile email address phone';
+
+ // fields returned for the claims above
+ const PROFILE_CLAIM_VALUES = 'name family_name given_name middle_name nickname preferred_username profile picture website gender birthdate zoneinfo locale updated_at';
+ const EMAIL_CLAIM_VALUES = 'email email_verified';
+ const ADDRESS_CLAIM_VALUES = 'formatted street_address locality region postal_code country';
+ const PHONE_CLAIM_VALUES = 'phone_number phone_number_verified';
+
+ /**
+ * Return claims about the provided user id.
+ *
+ * Groups of claims are returned based on the requested scopes. No group
+ * is required, and no claim is required.
+ *
+ * @param $user_id
+ * The id of the user for which claims should be returned.
+ * @param $scope
+ * The requested scope.
+ * Scopes with matching claims: profile, email, address, phone.
+ *
+ * @return
+ * An array in the claim => value format.
+ *
+ * @see http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
+ */
+ public function getUserClaims($user_id, $scope);
+}
diff --git a/src/OAuth2/Request.php b/src/OAuth2/Request.php
index b49a42a..b37c591 100644
--- a/src/OAuth2/Request.php
+++ b/src/OAuth2/Request.php
@@ -80,7 +80,9 @@ class Request implements RequestInterface
public function headers($name, $default = null)
{
- return isset($this->headers[$name]) ? $this->headers[$name] : $default;
+ $headers = array_change_key_case($this->headers);
+ $name = strtolower($name);
+ return isset($headers[$name]) ? $headers[$name] : $default;
}
public function getAllQueryParameters()
@@ -149,7 +151,7 @@ class Request implements RequestInterface
} elseif (isset($server['REDIRECT_HTTP_AUTHORIZATION'])) {
$authorizationHeader = $server['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (function_exists('apache_request_headers')) {
- $requestHeaders = apache_request_headers();
+ $requestHeaders = (array) apache_request_headers();
// Server-side fix for bug in old Android versions (a nice side-effect of this fix means we don't care about capitalization for Authorization)
$requestHeaders = array_combine(array_map('ucwords', array_keys($requestHeaders)), array_values($requestHeaders));
diff --git a/src/OAuth2/Response.php b/src/OAuth2/Response.php
index c16b476..d746789 100644
--- a/src/OAuth2/Response.php
+++ b/src/OAuth2/Response.php
@@ -180,7 +180,9 @@ class Response implements ResponseInterface
case 'xml':
// this only works for single-level arrays
$xml = new \SimpleXMLElement('<response/>');
- array_walk($this->parameters, array($xml, 'addChild'));
+ foreach ($this->parameters as $key => $param) {
+ $xml->addChild($param, $key);
+ }
return $xml->asXML();
}
diff --git a/src/OAuth2/ResponseType/AccessToken.php b/src/OAuth2/ResponseType/AccessToken.php
index 078a457..0fb1121 100644
--- a/src/OAuth2/ResponseType/AccessToken.php
+++ b/src/OAuth2/ResponseType/AccessToken.php
@@ -122,7 +122,11 @@ class AccessToken implements AccessTokenInterface
protected function generateAccessToken()
{
$tokenLen = 40;
- if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
+ if (function_exists('mcrypt_create_iv')) {
+ $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM);
+ } else if (function_exists('openssl_random_pseudo_bytes')) {
+ $randomData = openssl_random_pseudo_bytes(100);
+ } else if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
$randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true);
} else {
$randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
diff --git a/src/OAuth2/ResponseType/AuthorizationCode.php b/src/OAuth2/ResponseType/AuthorizationCode.php
index c39d6a0..51bd437 100644
--- a/src/OAuth2/ResponseType/AuthorizationCode.php
+++ b/src/OAuth2/ResponseType/AuthorizationCode.php
@@ -29,10 +29,10 @@ class AuthorizationCode implements AuthorizationCodeInterface
$params += array('scope' => null, 'state' => null);
- $result["query"]["code"] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']);
+ $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']);
if (isset($params['state'])) {
- $result["query"]["state"] = $params['state'];
+ $result['query']['state'] = $params['state'];
}
return array($params['redirect_uri'], $result);
@@ -85,7 +85,11 @@ class AuthorizationCode implements AuthorizationCodeInterface
protected function generateAuthorizationCode()
{
$tokenLen = 40;
- if (file_exists('/dev/urandom')) { // Get 100 bytes of random data
+ if (function_exists('mcrypt_create_iv')) {
+ $randomData = mcrypt_create_iv(100, MCRYPT_DEV_URANDOM);
+ } else if (function_exists('openssl_random_pseudo_bytes')) {
+ $randomData = openssl_random_pseudo_bytes(100);
+ } else if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
$randomData = file_get_contents('/dev/urandom', false, null, 0, 100) . uniqid(mt_rand(), true);
} else {
$randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);
diff --git a/src/OAuth2/Scope.php b/src/OAuth2/Scope.php
index 1e2e339..5c792bb 100644
--- a/src/OAuth2/Scope.php
+++ b/src/OAuth2/Scope.php
@@ -62,7 +62,18 @@ class Scope implements ScopeInterface
*/
public function scopeExists($scope)
{
- return $this->storage->scopeExists($scope);
+ // Check reserved scopes first.
+ $scope = explode(' ', trim($scope));
+ $reservedScope = $this->getReservedScopes();
+ $nonReservedScopes = array_diff($scope, $reservedScope);
+ if (count($nonReservedScopes) == 0) {
+ return true;
+ }
+ else {
+ // Check the storage for non-reserved scopes.
+ $nonReservedScopes = implode(' ', $nonReservedScopes);
+ return $this->storage->scopeExists($nonReservedScopes);
+ }
}
public function getScopeFromRequest(RequestInterface $request)
@@ -75,4 +86,18 @@ class Scope implements ScopeInterface
{
return $this->storage->getDefaultScope($client_id);
}
+
+ /**
+ * Get reserved scopes needed by the server.
+ *
+ * In case OpenID Connect is used, these scopes must include:
+ * 'openid', offline_access'.
+ *
+ * @return
+ * An array of reserved scopes.
+ */
+ public function getReservedScopes()
+ {
+ return array('openid', 'offline_access');
+ }
}
diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php
index f07b5fb..b138a55 100644
--- a/src/OAuth2/Server.php
+++ b/src/OAuth2/Server.php
@@ -4,6 +4,12 @@ namespace OAuth2;
use OAuth2\Controller\ResourceControllerInterface;
use OAuth2\Controller\ResourceController;
+use OAuth2\OpenID\Controller\UserInfoControllerInterface;
+use OAuth2\OpenID\Controller\UserInfoController;
+use OAuth2\OpenID\Controller\AuthorizeController as OpenIDAuthorizeController;
+use OAuth2\OpenID\ResponseType\AuthorizationCode as OpenIDAuthorizationCodeResponseType;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+use OAuth2\OpenID\GrantType\AuthorizationCode as OpenIDAuthorizationCodeGrantType;
use OAuth2\Controller\AuthorizeControllerInterface;
use OAuth2\Controller\AuthorizeController;
use OAuth2\Controller\TokenControllerInterface;
@@ -14,6 +20,8 @@ use OAuth2\ResponseType\ResponseTypeInterface;
use OAuth2\ResponseType\AuthorizationCode as AuthorizationCodeResponseType;
use OAuth2\ResponseType\AccessToken;
use OAuth2\ResponseType\CryptoToken;
+use OAuth2\OpenID\ResponseType\IdToken;
+use OAuth2\OpenID\ResponseType\TokenIdToken;
use OAuth2\TokenType\TokenTypeInterface;
use OAuth2\TokenType\Bearer;
use OAuth2\GrantType\GrantTypeInterface;
@@ -34,7 +42,8 @@ use OAuth2\Storage\CryptoTokenInterface;
*/
class Server implements ResourceControllerInterface,
AuthorizeControllerInterface,
- TokenControllerInterface
+ TokenControllerInterface,
+ UserInfoControllerInterface
{
// misc properties
protected $response;
@@ -45,6 +54,7 @@ class Server implements ResourceControllerInterface,
protected $authorizeController;
protected $tokenController;
protected $resourceController;
+ protected $userInfoController;
// config classes
protected $grantTypes;
@@ -60,6 +70,7 @@ class Server implements ResourceControllerInterface,
'client' => 'OAuth2\Storage\ClientInterface',
'refresh_token' => 'OAuth2\Storage\RefreshTokenInterface',
'user_credentials' => 'OAuth2\Storage\UserCredentialsInterface',
+ 'user_claims' => 'OAuth2\OpenID\Storage\UserClaimsInterface',
'public_key' => 'OAuth2\Storage\PublicKeyInterface',
'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface',
'scope' => 'OAuth2\Storage\ScopeInterface',
@@ -67,6 +78,8 @@ class Server implements ResourceControllerInterface,
protected $responseTypeMap = array(
'token' => 'OAuth2\ResponseType\AccessTokenInterface',
'code' => 'OAuth2\ResponseType\AuthorizationCodeInterface',
+ 'id_token' => 'OAuth2\OpenID\ResponseType\IdTokenInterface',
+ 'token id_token' => 'OAuth2\OpenID\ResponseType\TokenIdTokenInterface',
);
/**
@@ -101,6 +114,8 @@ class Server implements ResourceControllerInterface,
$this->config = array_merge(array(
'use_crypto_tokens' => false,
'store_encrypted_token_string' => true,
+ 'use_openid_connect' => false,
+ 'id_lifetime' => 3600,
'access_lifetime' => 3600,
'www_realm' => 'Service',
'token_param_name' => 'access_token',
@@ -151,6 +166,15 @@ class Server implements ResourceControllerInterface,
return $this->resourceController;
}
+ public function getUserInfoController()
+ {
+ if (is_null($this->userInfoController)) {
+ $this->userInfoController = $this->createDefaultUserInfoController();
+ }
+
+ return $this->userInfoController;
+ }
+
/**
* every getter deserves a setter
*/
@@ -176,6 +200,37 @@ class Server implements ResourceControllerInterface,
}
/**
+ * every getter deserves a setter
+ */
+ public function setUserInfoController(UserInfoControllerInterface $userInfoController)
+ {
+ $this->userInfoController = $userInfoController;
+ }
+
+ /**
+ * Return claims about the authenticated end-user.
+ * This would be called from the "/UserInfo" endpoint as defined in the spec.
+ *
+ * @param $request - OAuth2\RequestInterface
+ * Request object to grant access token
+ *
+ * @param $response - OAuth2\ResponseInterface
+ * Response object containing error messages (failure) or user claims (success)
+ *
+ * @throws InvalidArgumentException
+ * @throws LogicException
+ *
+ * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
+ */
+ public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null)
+ {
+ $this->response = is_null($response) ? new Response() : $response;
+ $this->getUserInfoController()->handleUserInfoRequest($request, $this->response);
+
+ return $this->response;
+ }
+
+ /**
* Grant or deny a requested access token.
* This would be called from the "/token" endpoint as defined in the spec.
* Obviously, you can call your endpoint whatever you want.
@@ -295,7 +350,7 @@ class Server implements ResourceControllerInterface,
if (is_string($key)) {
$this->grantTypes[$key] = $grantType;
} else {
- $this->grantTypes[] = $grantType;
+ $this->grantTypes[$grantType->getQuerystringIdentifier()] = $grantType;
}
// persist added grant type down to TokenController
@@ -318,7 +373,7 @@ class Server implements ResourceControllerInterface,
{
// if explicitly set to a valid key, do not "magically" set below
if (isset($this->storageMap[$key])) {
- if (!$storage instanceof $this->storageMap[$key]) {
+ if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) {
throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key]));
}
$this->storages[$key] = $storage;
@@ -400,8 +455,19 @@ class Server implements ResourceControllerInterface,
if (0 == count($this->responseTypes)) {
$this->responseTypes = $this->getDefaultResponseTypes();
}
+ if ($this->config['use_openid_connect'] && !isset($this->responseTypes['id_token'])) {
+ $this->responseTypes['id_token'] = $this->createDefaultIdTokenResponseType();
+ if ($this->config['allow_implicit']) {
+ $this->responseTypes['token id_token'] = $this->createDefaultTokenIdTokenResponseType();
+ }
+ }
+
$config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri')));
+ if ($this->config['use_openid_connect']) {
+ return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
+ }
+
return new AuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
}
@@ -429,11 +495,7 @@ class Server implements ResourceControllerInterface,
throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\ClientInterface to use the token server");
}
- if ($this->config['use_crypto_tokens']) {
- $accessTokenResponseType = $this->getCryptoTokenResponseType();
- } else {
- $accessTokenResponseType = $this->getAccessTokenResponseType();
- }
+ $accessTokenResponseType = $this->getAccessTokenResponseType();
return new TokenController($accessTokenResponseType, $this->storages['client'], $this->grantTypes, $this->clientAssertionType, $this->getScopeUtil());
}
@@ -458,6 +520,30 @@ class Server implements ResourceControllerInterface,
return new ResourceController($this->tokenType, $this->storages['access_token'], $config, $this->getScopeUtil());
}
+ protected function createDefaultUserInfoController()
+ {
+ if ($this->config['use_crypto_tokens']) {
+ // overwrites access token storage with crypto token storage if "use_crypto_tokens" is set
+ if (!isset($this->storages['access_token']) || !$this->storages['access_token'] instanceof CryptoTokenInterface) {
+ $this->storages['access_token'] = $this->createDefaultCryptoTokenStorage();
+ }
+ } elseif (!isset($this->storages['access_token'])) {
+ throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\AccessTokenInterface or use CryptoTokens to use the UserInfo server");
+ }
+
+ if (!isset($this->storages['user_claims'])) {
+ throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use the UserInfo server");
+ }
+
+ if (!$this->tokenType) {
+ $this->tokenType = $this->getDefaultTokenType();
+ }
+
+ $config = array_intersect_key($this->config, array('www_realm' => ''));
+
+ return new UserInfoController($this->tokenType, $this->storages['access_token'], $this->storages['user_claims'], $config, $this->getScopeUtil());
+ }
+
protected function getDefaultTokenType()
{
$config = array_intersect_key($this->config, array_flip(explode(' ', 'token_param_name token_bearer_header_name')));
@@ -470,16 +556,26 @@ class Server implements ResourceControllerInterface,
$responseTypes = array();
if ($this->config['allow_implicit']) {
- if ($this->config['use_crypto_tokens']) {
- $responseTypes['token'] = $this->getCryptoTokenResponseType();
- } elseif (isset($this->storages['access_token'])) {
- $responseTypes['token'] = $this->getAccessTokenResponseType();
+ $responseTypes['token'] = $this->getAccessTokenResponseType();
+ }
+
+ if ($this->config['use_openid_connect']) {
+ $responseTypes['id_token'] = $this->getIdTokenResponseType();
+ if ($this->config['allow_implicit']) {
+ $responseTypes['token id_token'] = $this->getTokenIdTokenResponseType();
}
}
if (isset($this->storages['authorization_code'])) {
$config = array_intersect_key($this->config, array_flip(explode(' ', 'enforce_redirect auth_code_lifetime')));
- $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+ if ($this->config['use_openid_connect']) {
+ if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) {
+ throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true");
+ }
+ $responseTypes['code'] = new OpenIDAuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+ } else {
+ $responseTypes['code'] = new AuthorizationCodeResponseType($this->storages['authorization_code'], $config);
+ }
}
if (count($responseTypes) == 0) {
@@ -508,7 +604,14 @@ class Server implements ResourceControllerInterface,
}
if (isset($this->storages['authorization_code'])) {
- $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']);
+ if ($this->config['use_openid_connect']) {
+ if (!$this->storages['authorization_code'] instanceof OpenIDAuthorizationCodeInterface) {
+ throw new \LogicException("Your authorization_code storage must implement OAuth2\OpenID\Storage\AuthorizationCodeInterface to work when 'use_openid_connect' is true");
+ }
+ $grantTypes['authorization_code'] = new OpenIDAuthorizationCodeGrantType($this->storages['authorization_code']);
+ } else {
+ $grantTypes['authorization_code'] = new AuthorizationCode($this->storages['authorization_code']);
+ }
}
if (count($grantTypes) == 0) {
@@ -524,16 +627,29 @@ class Server implements ResourceControllerInterface,
return $this->responseTypes['token'];
}
+ if ($this->config['use_crypto_tokens']) {
+ return $this->createDefaultCryptoTokenResponseType();
+ }
+
return $this->createDefaultAccessTokenResponseType();
}
- protected function getCryptoTokenResponseType()
+ protected function getIdTokenResponseType()
{
- if (isset($this->responseTypes['token'])) {
- return $this->responseTypes['token'];
+ if (isset($this->responseTypes['id_token'])) {
+ return $this->responseTypes['id_token'];
+ }
+
+ return $this->createDefaultIdTokenResponseType();
+ }
+
+ protected function getTokenIdTokenResponseType()
+ {
+ if (isset($this->responseTypes['token id_token'])) {
+ return $this->responseTypes['token id_token'];
}
- return $this->createDefaultCryptoTokenResponseType();
+ return $this->createDefaultTokenIdTokenResponseType();
}
/**
@@ -593,6 +709,24 @@ class Server implements ResourceControllerInterface,
return new AccessToken($this->storages['access_token'], $refreshStorage, $config);
}
+ protected function createDefaultIdTokenResponseType()
+ {
+ if (!isset($this->storages['user_claims'])) {
+ throw new \LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\UserClaimsInterface to use openid connect");
+ }
+ if (!isset($this->storages['public_key'])) {
+ throw new \LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect");
+ }
+
+ $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime')));
+ return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config);
+ }
+
+ protected function createDefaultTokenIdTokenResponseType()
+ {
+ return new TokenIdToken($this->getAccessTokenResponseType(), $this->getIdTokenResponseType());
+ }
+
public function getResponse()
{
return $this->response;
diff --git a/src/OAuth2/Storage/AccessTokenInterface.php b/src/OAuth2/Storage/AccessTokenInterface.php
index dd687e4..b55c3d6 100644
--- a/src/OAuth2/Storage/AccessTokenInterface.php
+++ b/src/OAuth2/Storage/AccessTokenInterface.php
@@ -21,10 +21,11 @@ interface AccessTokenInterface
* @return
* An associative array as below, and return NULL if the supplied oauth_token
* is invalid:
- * - client_id: Stored client identifier.
* - expires: Stored expiration in unix timestamp.
+ * - client_id: (optional) Stored client identifier.
* - user_id: (optional) Stored user identifier.
* - scope: (optional) Stored scope values in space-separated string.
+ * - id_token: (optional) Stored id_token (if "use_openid_connect" is true).
*
* @ingroup oauth2_section_7
*/
diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php
index b63a752..f6d1e05 100644
--- a/src/OAuth2/Storage/Cassandra.php
+++ b/src/OAuth2/Storage/Cassandra.php
@@ -1,9 +1,11 @@
<?php
namespace OAuth2\Storage;
+
use phpcassa\ColumnFamily;
use phpcassa\ColumnSlice;
use phpcassa\Connection\ConnectionPool;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
/**
* Cassandra storage for all storage types
@@ -32,7 +34,8 @@ class Cassandra implements AuthorizationCodeInterface,
UserCredentialsInterface,
RefreshTokenInterface,
JwtBearerInterface,
- ScopeInterface
+ ScopeInterface,
+ OpenIDAuthorizationCodeInterface
{
private $cache;
@@ -145,11 +148,11 @@ class Cassandra implements AuthorizationCodeInterface,
return $this->getValue($this->config['code_key'] . $code);
}
- public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null)
+ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
{
return $this->setValue(
$this->config['code_key'] . $authorization_code,
- compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope'),
+ compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
$expires
);
}
diff --git a/src/OAuth2/Storage/CryptoToken.php b/src/OAuth2/Storage/CryptoToken.php
index 6cad23b..f107e45 100644
--- a/src/OAuth2/Storage/CryptoToken.php
+++ b/src/OAuth2/Storage/CryptoToken.php
@@ -61,7 +61,7 @@ class CryptoToken implements CryptoTokenInterface
$algorithm = $this->publicKeyStorage->getEncryptionAlgorithm($client_id);
// now that we have the client_id, verify the token
- if (false === $this->encryptionUtil->decode($oauth_token, $public_key, $algorithm)) {
+ if (false === $this->encryptionUtil->decode($oauth_token, $public_key, true)) {
return false;
}
diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php
index 2c9dc3d..35e6cba 100644
--- a/src/OAuth2/Storage/Memory.php
+++ b/src/OAuth2/Storage/Memory.php
@@ -2,6 +2,9 @@
namespace OAuth2\Storage;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
/**
* Simple in-memory storage for all storage types
*
@@ -12,12 +15,14 @@ namespace OAuth2\Storage;
*/
class Memory implements AuthorizationCodeInterface,
UserCredentialsInterface,
+ UserClaimsInterface,
AccessTokenInterface,
ClientCredentialsInterface,
RefreshTokenInterface,
JwtBearerInterface,
ScopeInterface,
- PublicKeyInterface
+ PublicKeyInterface,
+ OpenIDAuthorizationCodeInterface
{
public $authorizationCodes;
public $userCredentials;
@@ -69,9 +74,9 @@ class Memory implements AuthorizationCodeInterface,
), $this->authorizationCodes[$code]);
}
- public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null)
+ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
{
- $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope');
+ $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token');
return true;
}
@@ -119,6 +124,45 @@ class Memory implements AuthorizationCodeInterface,
), $this->userCredentials[$username]);
}
+ /* UserClaimsInterface */
+ public function getUserClaims($user_id, $claims)
+ {
+ if (!$userDetails = $this->getUserDetails($user_id)) {
+ return false;
+ }
+
+ $claims = explode(' ', trim($claims));
+ $userClaims = array();
+
+ // for each requested claim, if the user has the claim, set it in the response
+ $validClaims = explode(' ', self::VALID_CLAIMS);
+ foreach ($validClaims as $validClaim) {
+ if (in_array($validClaim, $claims)) {
+ if ($validClaim == 'address') {
+ // address is an object with subfields
+ $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+ } else {
+ $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails));
+ }
+ }
+ }
+
+ return $userClaims;
+ }
+
+ protected function getUserClaim($claim, $userDetails)
+ {
+ $userClaims = array();
+ $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+ $claimValues = explode(' ', $claimValuesString);
+
+ foreach ($claimValues as $value) {
+ $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+ }
+
+ return $userClaims;
+ }
+
/* ClientCredentialsInterface */
public function checkClientCredentials($client_id, $client_secret = null)
{
@@ -206,9 +250,9 @@ class Memory implements AuthorizationCodeInterface,
return isset($this->accessTokens[$access_token]) ? $this->accessTokens[$access_token] : false;
}
- public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null)
+ public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null, $id_token = null)
{
- $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope');
+ $this->accessTokens[$access_token] = compact('access_token', 'client_id', 'user_id', 'expires', 'scope', 'id_token');
return true;
}
diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php
index 60db771..87935fe 100644
--- a/src/OAuth2/Storage/Mongo.php
+++ b/src/OAuth2/Storage/Mongo.php
@@ -2,6 +2,8 @@
namespace OAuth2\Storage;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
/**
* Simple MongoDB storage for all storage types
*
@@ -19,7 +21,8 @@ class Mongo implements AuthorizationCodeInterface,
ClientCredentialsInterface,
UserCredentialsInterface,
RefreshTokenInterface,
- JwtBearerInterface
+ JwtBearerInterface,
+ OpenIDAuthorizationCodeInterface
{
protected $db;
protected $config;
@@ -171,7 +174,7 @@ class Mongo implements AuthorizationCodeInterface,
return is_null($code) ? false : $code;
}
- public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null)
+ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
{
// if it exists, update it.
if ($this->getAuthorizationCode($code)) {
@@ -182,7 +185,8 @@ class Mongo implements AuthorizationCodeInterface,
'user_id' => $user_id,
'redirect_uri' => $redirect_uri,
'expires' => $expires,
- 'scope' => $scope
+ 'scope' => $scope,
+ 'id_token' => $id_token,
))
);
} else {
@@ -193,7 +197,8 @@ class Mongo implements AuthorizationCodeInterface,
'user_id' => $user_id,
'redirect_uri' => $redirect_uri,
'expires' => $expires,
- 'scope' => $scope
+ 'scope' => $scope,
+ 'id_token' => $id_token,
)
);
}
diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php
index 6f0eac3..0cdd52e 100644
--- a/src/OAuth2/Storage/Pdo.php
+++ b/src/OAuth2/Storage/Pdo.php
@@ -2,6 +2,8 @@
namespace OAuth2\Storage;
+use OAuth2\OpenID\Storage\UserClaimsInterface;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
/**
* Simple PDO storage for all storage types
*
@@ -21,7 +23,9 @@ class Pdo implements AuthorizationCodeInterface,
RefreshTokenInterface,
JwtBearerInterface,
ScopeInterface,
- PublicKeyInterface
+ PublicKeyInterface,
+ UserClaimsInterface,
+ OpenIDAuthorizationCodeInterface
{
protected $db;
protected $config;
@@ -29,8 +33,11 @@ class Pdo implements AuthorizationCodeInterface,
public function __construct($connection, $config = array())
{
if (!$connection instanceof \PDO) {
+ if (is_string($connection)) {
+ $connection = array('dsn' => $connection);
+ }
if (!is_array($connection)) {
- throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO or a configuration array');
+ throw new \InvalidArgumentException('First argument to OAuth2\Storage\Pdo must be an instance of PDO, a DSN string, or a configuration array');
}
if (!isset($connection['dsn'])) {
throw new \InvalidArgumentException('configuration array must contain "dsn"');
@@ -39,8 +46,9 @@ class Pdo implements AuthorizationCodeInterface,
$connection = array_merge(array(
'username' => null,
'password' => null,
+ 'options' => array(),
), $connection);
- $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password']);
+ $connection = new \PDO($connection['dsn'], $connection['username'], $connection['password'], $connection['options']);
}
$this->db = $connection;
@@ -159,8 +167,13 @@ class Pdo implements AuthorizationCodeInterface,
return $code;
}
- public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null)
+ public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
{
+ if (func_num_args() > 6) {
+ // we are calling with an id token
+ return call_user_func_array(array($this, 'setAuthorizationCodeWithIdToken'), func_get_args());
+ }
+
// convert expires to datestring
$expires = date('Y-m-d H:i:s', $expires);
@@ -174,6 +187,21 @@ class Pdo implements AuthorizationCodeInterface,
return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope'));
}
+ private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
+ {
+ // convert expires to datestring
+ $expires = date('Y-m-d H:i:s', $expires);
+
+ // if it exists, update it.
+ if ($this->getAuthorizationCode($code)) {
+ $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table']));
+ } else {
+ $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table']));
+ }
+
+ return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'));
+ }
+
public function expireAuthorizationCode($code)
{
$stmt = $this->db->prepare(sprintf('DELETE FROM %s WHERE authorization_code = :code', $this->config['code_table']));
@@ -196,6 +224,45 @@ class Pdo implements AuthorizationCodeInterface,
return $this->getUser($username);
}
+ /* UserClaimsInterface */
+ public function getUserClaims($user_id, $claims)
+ {
+ if (!$userDetails = $this->getUserDetails($user_id)) {
+ return false;
+ }
+
+ $claims = explode(' ', trim($claims));
+ $userClaims = array();
+
+ // for each requested claim, if the user has the claim, set it in the response
+ $validClaims = explode(' ', self::VALID_CLAIMS);
+ foreach ($validClaims as $validClaim) {
+ if (in_array($validClaim, $claims)) {
+ if ($validClaim == 'address') {
+ // address is an object with subfields
+ $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails);
+ } else {
+ $userClaims = array_merge($this->getUserClaim($validClaim, $userDetails));
+ }
+ }
+ }
+
+ return $userClaims;
+ }
+
+ protected function getUserClaim($claim, $userDetails)
+ {
+ $userClaims = array();
+ $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim)));
+ $claimValues = explode(' ', $claimValuesString);
+
+ foreach ($claimValues as $value) {
+ $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null;
+ }
+
+ return $userClaims;
+ }
+
/* OAuth2\Storage\RefreshTokenInterface */
public function getRefreshToken($refresh_token)
{
@@ -267,8 +334,9 @@ class Pdo implements AuthorizationCodeInterface,
public function scopeExists($scope)
{
$scope = explode(' ', $scope);
- $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN ("%s")', $this->config['scope_table'], implode('","', $scope)));
- $stmt->execute();
+ $whereIn = implode(',', array_fill(0, count($scope), '?'));
+ $stmt = $this->db->prepare(sprintf('SELECT count(scope) as count FROM %s WHERE scope IN (%s)', $this->config['scope_table'], $whereIn));
+ $stmt->execute($scope);
if ($result = $stmt->fetch()) {
return $result['count'] == count($scope);
@@ -318,7 +386,7 @@ class Pdo implements AuthorizationCodeInterface,
public function getJti($client_id, $subject, $audience, $expires, $jti)
{
- $stmt = $this->db->prepare($sql = sprintf('SELECT* FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table']));
+ $stmt = $this->db->prepare($sql = sprintf('SELECT * FROM %s WHERE issuer=:client_id AND subject=:subject AND audience=:audience AND expires=:expires AND jti=:jti', $this->config['jti_table']));
$stmt->execute(compact('client_id', 'subject', 'audience', 'expires', 'jti'));
diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php
index ee6a4ca..fd94f47 100644
--- a/src/OAuth2/Storage/Redis.php
+++ b/src/OAuth2/Storage/Redis.php
@@ -2,6 +2,8 @@
namespace OAuth2\Storage;
+use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
+
/**
* redis storage for all storage types
*
@@ -19,7 +21,8 @@ class Redis implements AuthorizationCodeInterface,
UserCredentialsInterface,
RefreshTokenInterface,
JwtBearerInterface,
- ScopeInterface
+ ScopeInterface,
+ OpenIDAuthorizationCodeInterface
{
private $cache;
@@ -92,11 +95,11 @@ class Redis implements AuthorizationCodeInterface,
return $this->getValue($this->config['code_key'] . $code);
}
- public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null)
+ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null)
{
return $this->setValue(
$this->config['code_key'] . $authorization_code,
- compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope'),
+ compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'),
$expires
);
}
diff --git a/src/OAuth2/TokenType/Bearer.php b/src/OAuth2/TokenType/Bearer.php
index f437a0b..0672572 100644
--- a/src/OAuth2/TokenType/Bearer.php
+++ b/src/OAuth2/TokenType/Bearer.php
@@ -26,6 +26,19 @@ class Bearer implements TokenTypeInterface
}
/**
+ * Check if the request has supplied token
+ *
+ * @see https://github.com/bshaffer/oauth2-server-php/issues/349#issuecomment-37993588
+ */
+ public function requestHasToken(RequestInterface $request)
+ {
+ $headers = $request->headers('AUTHORIZATION');
+
+ // check the header, then the querystring, then the request body
+ return !empty($headers) || (bool)($request->request($this->config['token_param_name'])) || (bool)($request->query($this->config['token_param_name']));
+ }
+
+ /**
* This is a convenience function that can be used to get the token, which can then
* be passed to getAccessTokenData(). The constraints specified by the draft are
* attempted to be adheared to in this method.
@@ -88,9 +101,9 @@ class Bearer implements TokenTypeInterface
}
if ($request->request($this->config['token_param_name'])) {
- // POST: Get the token from POST data
- if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
- $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST');
+ // // POST: Get the token from POST data
+ if (!in_array(strtolower($request->server('REQUEST_METHOD')), array('post', 'put'))) {
+ $response->setError(400, 'invalid_request', 'When putting the token in the body, the method must be POST or PUT', '#section-2.2');
return null;
}
diff --git a/test/OAuth2/Controller/AuthorizeControllerTest.php b/test/OAuth2/Controller/AuthorizeControllerTest.php
index 08e1d2f..1bce0ef 100644
--- a/test/OAuth2/Controller/AuthorizeControllerTest.php
+++ b/test/OAuth2/Controller/AuthorizeControllerTest.php
@@ -354,6 +354,11 @@ class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase
$this->assertNotNull($query);
$this->assertArrayHasKey('code', $query);
+ // ensure no id_token was saved, since the openid scope wasn't requested
+ $storage = $server->getStorage('authorization_code');
+ $code = $storage->getAuthorizationCode($query['code']);
+ $this->assertTrue(empty($code['id_token']));
+
// ensure no error was returned
$this->assertFalse(isset($query['error']));
$this->assertFalse(isset($query['error_description']));
@@ -411,6 +416,47 @@ class AuthorizeControllerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($query['state'], 'test');
}
+ public function testSuccessfulOpenidConnectRequest()
+ {
+ $server = $this->getTestServer(array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'bojanz',
+ ));
+
+ $request = new Request(array(
+ 'client_id' => 'Test Client ID',
+ 'redirect_uri' => 'http://adobe.com',
+ 'response_type' => 'code',
+ 'state' => 'xyz',
+ 'scope' => 'openid',
+ ));
+ $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+ $this->assertEquals($response->getStatusCode(), 302);
+ $location = $response->getHttpHeader('Location');
+ $parts = parse_url($location);
+ parse_str($parts['query'], $query);
+
+ $location = $response->getHttpHeader('Location');
+ $parts = parse_url($location);
+ $this->assertArrayHasKey('query', $parts);
+ $this->assertFalse(isset($parts['fragment']));
+
+ // assert fragment is in "application/x-www-form-urlencoded" format
+ parse_str($parts['query'], $query);
+ $this->assertNotNull($query);
+ $this->assertArrayHasKey('code', $query);
+
+ // ensure no error was returned
+ $this->assertFalse(isset($query['error']));
+ $this->assertFalse(isset($query['error_description']));
+
+ // confirm that the id_token has been created.
+ $storage = $server->getStorage('authorization_code');
+ $code = $storage->getAuthorizationCode($query['code']);
+ $this->assertTrue(!empty($code['id_token']));
+ }
+
public function testCreateController()
{
$storage = Bootstrap::getInstance()->getMemoryStorage();
diff --git a/test/OAuth2/Controller/ResourceControllerTest.php b/test/OAuth2/Controller/ResourceControllerTest.php
index 962737f..fb25550 100644
--- a/test/OAuth2/Controller/ResourceControllerTest.php
+++ b/test/OAuth2/Controller/ResourceControllerTest.php
@@ -60,7 +60,7 @@ class ResourceControllerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($response->getStatusCode(), 400);
$this->assertEquals($response->getParameter('error'), 'invalid_request');
- $this->assertEquals($response->getParameter('error_description'), 'When putting the token in the body, the method must be POST');
+ $this->assertEquals($response->getParameter('error_description'), 'When putting the token in the body, the method must be POST or PUT');
}
public function testInvalidContentType()
@@ -133,7 +133,7 @@ class ResourceControllerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($response->getStatusCode(), 401);
$this->assertEquals($response->getParameter('error'), 'invalid_token');
- $this->assertEquals($response->getParameter('error_description'), 'Malformed token (missing "expires" or "client_id")');
+ $this->assertEquals($response->getParameter('error_description'), 'Malformed token (missing "expires")');
}
public function testValidToken()
diff --git a/test/OAuth2/GrantType/JWTBearerTest.php b/test/OAuth2/GrantType/JwtBearerTest.php
index 1ac50f5..0a6c4b8 100644
--- a/test/OAuth2/GrantType/JWTBearerTest.php
+++ b/test/OAuth2/GrantType/JwtBearerTest.php
@@ -353,7 +353,7 @@ EOD;
{
$storage = Bootstrap::getInstance()->getMemoryStorage();
$server = new Server($storage);
- $server->addGrantType(new JwtBearer($storage, $audience));
+ $server->addGrantType(new JwtBearer($storage, $audience, new Jwt()));
return $server;
}
diff --git a/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php b/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php
new file mode 100644
index 0000000..939b715
--- /dev/null
+++ b/test/OAuth2/OpenID/Controller/UserInfoControllerTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace OAuth2\OpenID\Controller;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\GrantType\AuthorizationCode;
+use OAuth2\Request;
+use OAuth2\Response;
+
+class UserInfoControllerTest extends \PHPUnit_Framework_TestCase
+{
+ public function testValidToken()
+ {
+ $server = $this->getTestServer();
+ $request = Request::createFromGlobals();
+ $request->headers['AUTHORIZATION'] = 'Bearer accesstoken-openid-connect';
+ $response = new Response();
+
+ $server->handleUserInfoRequest($request, $response);
+ $parameters = $response->getParameters();
+ $this->assertEquals($parameters['sub'], 'testuser');
+ $this->assertEquals($parameters['email'], 'testuser@test.com');
+ $this->assertEquals($parameters['email_verified'], true);
+ }
+
+ private function getTestServer($config = array())
+ {
+ $storage = Bootstrap::getInstance()->getMemoryStorage();
+ $server = new Server($storage, $config);
+ return $server;
+ }
+}
diff --git a/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php b/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php
new file mode 100644
index 0000000..776002d
--- /dev/null
+++ b/test/OAuth2/OpenID/GrantType/AuthorizationCodeTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace OAuth2\OpenID\GrantType;
+
+use OAuth2\Storage\Bootstrap;
+use OAuth2\Server;
+use OAuth2\Request\TestRequest;
+use OAuth2\Response;
+
+class AuthorizationCodeTest extends \PHPUnit_Framework_TestCase
+{
+ public function testValidCode()
+ {
+ $server = $this->getTestServer();
+ $request = TestRequest::createPost(array(
+ 'grant_type' => 'authorization_code', // valid grant type
+ 'client_id' => 'Test Client ID', // valid client id
+ 'client_secret' => 'TestSecret', // valid client secret
+ 'code' => 'testcode-openid', // valid code
+ ));
+ $token = $server->grantAccessToken($request, new Response());
+
+ $this->assertNotNull($token);
+ $this->assertArrayHasKey('id_token', $token);
+ $this->assertEquals('test_id_token', $token['id_token']);
+
+ // this is only true if "offline_access" was requested
+ $this->assertFalse(isset($token['refresh_token']));
+ }
+
+ public function testOfflineAccess()
+ {
+ $server = $this->getTestServer();
+ $request = TestRequest::createPost(array(
+ 'grant_type' => 'authorization_code', // valid grant type
+ 'client_id' => 'Test Client ID', // valid client id
+ 'client_secret' => 'TestSecret', // valid client secret
+ 'code' => 'testcode-openid', // valid code
+ 'scope' => 'offline_access', // valid code
+ ));
+ $token = $server->grantAccessToken($request, new Response());
+
+ $this->assertNotNull($token);
+ $this->assertArrayHasKey('id_token', $token);
+ $this->assertEquals('test_id_token', $token['id_token']);
+ $this->assertTrue(isset($token['refresh_token']));
+ }
+
+ private function getTestServer()
+ {
+ $storage = Bootstrap::getInstance()->getMemoryStorage();
+ $server = new Server($storage, array('use_openid_connect' => true));
+ $server->addGrantType(new AuthorizationCode($storage));
+
+ return $server;
+ }
+}
diff --git a/test/OAuth2/OpenID/ResponseType/IdTokenTest.php b/test/OAuth2/OpenID/ResponseType/IdTokenTest.php
new file mode 100644
index 0000000..e772f6b
--- /dev/null
+++ b/test/OAuth2/OpenID/ResponseType/IdTokenTest.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\Encryption\Jwt;
+
+class IdTokenTest extends \PHPUnit_Framework_TestCase
+{
+ public function testValidateAuthorizeRequest()
+ {
+ $query = array(
+ 'response_type' => 'id_token',
+ 'redirect_uri' => 'http://adobe.com',
+ 'client_id' => 'Test Client ID',
+ 'scope' => 'openid',
+ 'state' => 'test',
+ );
+
+ // attempt to do the request without a nonce.
+ $server = $this->getTestServer(array('allow_implicit' => true));
+ $request = new Request($query);
+ $valid = $server->validateAuthorizeRequest($request, $response = new Response());
+
+ // Add a nonce and retry.
+ $query['nonce'] = 'test';
+ $request = new Request($query);
+ $valid = $server->validateAuthorizeRequest($request, $response = new Response());
+ $this->assertTrue($valid);
+ }
+
+ public function testHandleAuthorizeRequest()
+ {
+ // add the test parameters in memory
+ $server = $this->getTestServer(array('allow_implicit' => true));
+ $request = new Request(array(
+ 'response_type' => 'id_token',
+ 'redirect_uri' => 'http://adobe.com',
+ 'client_id' => 'Test Client ID',
+ 'scope' => 'openid email',
+ 'state' => 'test',
+ 'nonce' => 'test',
+ ));
+
+ $user_id = 'testuser';
+ $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id);
+
+ $this->assertEquals($response->getStatusCode(), 302);
+ $location = $response->getHttpHeader('Location');
+ $this->assertNotContains('error', $location);
+
+ $parts = parse_url($location);
+ $this->assertArrayHasKey('fragment', $parts);
+ $this->assertFalse(isset($parts['query']));
+
+ // assert fragment is in "application/x-www-form-urlencoded" format
+ parse_str($parts['fragment'], $params);
+ $this->assertNotNull($params);
+ $this->assertArrayHasKey('id_token', $params);
+ $this->assertArrayNotHasKey('access_token', $params);
+ $this->validateIdToken($params['id_token']);
+ }
+
+ public function testPassInAuthTime()
+ {
+ $server = $this->getTestServer(array('allow_implicit' => true));
+ $request = new Request(array(
+ 'response_type' => 'id_token',
+ 'redirect_uri' => 'http://adobe.com',
+ 'client_id' => 'Test Client ID',
+ 'scope' => 'openid email',
+ 'state' => 'test',
+ 'nonce' => 'test',
+ ));
+
+ // test with a scalar user id
+ $user_id = 'testuser123';
+ $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id);
+
+ list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response);
+
+ $this->assertTrue(is_array($payload));
+ $this->assertArrayHasKey('sub', $payload);
+ $this->assertEquals($user_id, $payload['sub']);
+ $this->assertArrayHasKey('auth_time', $payload);
+
+ // test with an array of user info
+ $userInfo = array(
+ 'user_id' => 'testuser1234',
+ 'auth_time' => date('Y-m-d H:i:s', strtotime('20 minutes ago')
+ ));
+
+ $server->handleAuthorizeRequest($request, $response = new Response(), true, $userInfo);
+
+ list($header, $payload, $signature) = $this->extractTokenDataFromResponse($response);
+
+ $this->assertTrue(is_array($payload));
+ $this->assertArrayHasKey('sub', $payload);
+ $this->assertEquals($userInfo['user_id'], $payload['sub']);
+ $this->assertArrayHasKey('auth_time', $payload);
+ $this->assertEquals($userInfo['auth_time'], $payload['auth_time']);
+ }
+
+ private function extractTokenDataFromResponse(Response $response)
+ {
+ $this->assertEquals($response->getStatusCode(), 302);
+ $location = $response->getHttpHeader('Location');
+ $this->assertNotContains('error', $location);
+
+ $parts = parse_url($location);
+ $this->assertArrayHasKey('fragment', $parts);
+ $this->assertFalse(isset($parts['query']));
+
+ parse_str($parts['fragment'], $params);
+ $this->assertNotNull($params);
+ $this->assertArrayHasKey('id_token', $params);
+ $this->assertArrayNotHasKey('access_token', $params);
+
+ list($headb64, $payloadb64, $signature) = explode('.', $params['id_token']);
+
+ $jwt = new Jwt();
+ $header = json_decode($jwt->urlSafeB64Decode($headb64), true);
+ $payload = json_decode($jwt->urlSafeB64Decode($payloadb64), true);
+
+ return array($header, $payload, $signature);
+ }
+
+ private function validateIdToken($id_token)
+ {
+ $parts = explode('.', $id_token);
+ foreach ($parts as &$part) {
+ // Each part is a base64url encoded json string.
+ $part = str_replace(array('-', '_'), array('+', '/'), $part);
+ $part = base64_decode($part);
+ $part = json_decode($part, true);
+ }
+ list($header, $claims, $signature) = $parts;
+
+ $this->assertArrayHasKey('iss', $claims);
+ $this->assertArrayHasKey('sub', $claims);
+ $this->assertArrayHasKey('aud', $claims);
+ $this->assertArrayHasKey('iat', $claims);
+ $this->assertArrayHasKey('exp', $claims);
+ $this->assertArrayHasKey('auth_time', $claims);
+ $this->assertArrayHasKey('nonce', $claims);
+ $this->assertArrayHasKey('email', $claims);
+ $this->assertArrayHasKey('email_verified', $claims);
+
+ $this->assertEquals($claims['iss'], 'test');
+ $this->assertEquals($claims['aud'], 'Test Client ID');
+ $this->assertEquals($claims['nonce'], 'test');
+ $this->assertEquals($claims['email'], 'testuser@test.com');
+ $duration = $claims['exp'] - $claims['iat'];
+ $this->assertEquals($duration, 3600);
+ }
+
+ private function getTestServer($config = array())
+ {
+ $config += array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'test',
+ 'id_lifetime' => 3600,
+ );
+
+ $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+ $memoryStorage->supportedScopes[] = 'email';
+ $storage = array(
+ 'client' => $memoryStorage,
+ 'scope' => $memoryStorage,
+ );
+ $responseTypes = array(
+ 'id_token' => new IdToken($memoryStorage, $memoryStorage, $config),
+ );
+
+ $server = new Server($storage, $config, array(), $responseTypes);
+ $server->addGrantType(new ClientCredentials($memoryStorage));
+
+ return $server;
+ }
+}
diff --git a/test/OAuth2/OpenID/ResponseType/TokenIdTokenTest.php b/test/OAuth2/OpenID/ResponseType/TokenIdTokenTest.php
new file mode 100644
index 0000000..488eb08
--- /dev/null
+++ b/test/OAuth2/OpenID/ResponseType/TokenIdTokenTest.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace OAuth2\OpenID\ResponseType;
+
+use OAuth2\Server;
+use OAuth2\Request;
+use OAuth2\Response;
+use OAuth2\Storage\Bootstrap;
+use OAuth2\GrantType\ClientCredentials;
+use OAuth2\ResponseType\AccessToken;
+
+class TokenIdTokenTest extends \PHPUnit_Framework_TestCase
+{
+
+ public function testHandleAuthorizeRequest()
+ {
+ // add the test parameters in memory
+ $server = $this->getTestServer(array('allow_implicit' => true));
+ $request = new Request(array(
+ 'response_type' => 'token id_token',
+ 'redirect_uri' => 'http://adobe.com',
+ 'client_id' => 'Test Client ID',
+ 'scope' => 'openid',
+ 'state' => 'test',
+ 'nonce' => 'test',
+ ));
+
+ $server->handleAuthorizeRequest($request, $response = new Response(), true);
+
+ $this->assertEquals($response->getStatusCode(), 302);
+ $location = $response->getHttpHeader('Location');
+ $this->assertNotContains('error', $location);
+
+ $parts = parse_url($location);
+ $this->assertArrayHasKey('fragment', $parts);
+ $this->assertFalse(isset($parts['query']));
+
+ // assert fragment is in "application/x-www-form-urlencoded" format
+ parse_str($parts['fragment'], $params);
+ $this->assertNotNull($params);
+ $this->assertArrayHasKey('id_token', $params);
+ $this->assertArrayHasKey('access_token', $params);
+ $this->validateIdToken($params['id_token']);
+ }
+
+ private function validateIdToken($id_token)
+ {
+ $parts = explode('.', $id_token);
+ foreach ($parts as &$part) {
+ // Each part is a base64url encoded json string.
+ $part = str_replace(array('-', '_'), array('+', '/'), $part);
+ $part = base64_decode($part);
+ $part = json_decode($part, TRUE);
+ }
+ list($header, $claims, $signature) = $parts;
+
+ $this->assertArrayHasKey('iss', $claims);
+ $this->assertArrayHasKey('sub', $claims);
+ $this->assertArrayHasKey('aud', $claims);
+ $this->assertArrayHasKey('iat', $claims);
+ $this->assertArrayHasKey('exp', $claims);
+ $this->assertArrayHasKey('auth_time', $claims);
+ $this->assertArrayHasKey('nonce', $claims);
+ $this->assertArrayHasKey('at_hash', $claims);
+
+ $this->assertEquals($claims['iss'], 'test');
+ $this->assertEquals($claims['aud'], 'Test Client ID');
+ $this->assertEquals($claims['nonce'], 'test');
+ $duration = $claims['exp'] - $claims['iat'];
+ $this->assertEquals($duration, 3600);
+ }
+
+ private function getTestServer($config = array())
+ {
+ $config += array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'test',
+ 'id_lifetime' => 3600,
+ );
+
+ $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
+ $responseTypes = array(
+ 'token' => new AccessToken($memoryStorage, $memoryStorage),
+ 'id_token' => new IdToken($memoryStorage, $memoryStorage, $config),
+ );
+ $responseTypes['token id_token'] = new TokenIdToken($responseTypes['token'], $responseTypes['id_token']);
+
+ $server = new Server($memoryStorage, $config, array(), $responseTypes);
+ $server->addGrantType(new ClientCredentials($memoryStorage));
+
+ return $server;
+ }
+}
diff --git a/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php b/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php
new file mode 100644
index 0000000..bdfb085
--- /dev/null
+++ b/test/OAuth2/OpenID/Storage/AuthorizationCodeTest.php
@@ -0,0 +1,95 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+use OAuth2\Storage\BaseTest;
+use OAuth2\Storage\NullStorage;
+
+class AuthorizationCodeTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testCreateAuthorizationCode($storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ if (!$storage instanceof AuthorizationCodeInterface) {
+ return;
+ }
+
+ // assert code we are about to add does not exist
+ $code = $storage->getAuthorizationCode('new-openid-code');
+ $this->assertFalse($code);
+
+ // add new code
+ $expires = time() + 20;
+ $scope = null;
+ $id_token = 'fake_id_token';
+ $success = $storage->setAuthorizationCode('new-openid-code', 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token);
+ $this->assertTrue($success);
+
+ $code = $storage->getAuthorizationCode('new-openid-code');
+ $this->assertNotNull($code);
+ $this->assertArrayHasKey('authorization_code', $code);
+ $this->assertArrayHasKey('client_id', $code);
+ $this->assertArrayHasKey('user_id', $code);
+ $this->assertArrayHasKey('redirect_uri', $code);
+ $this->assertArrayHasKey('expires', $code);
+ $this->assertEquals($code['authorization_code'], 'new-openid-code');
+ $this->assertEquals($code['client_id'], 'client ID');
+ $this->assertEquals($code['user_id'], 'SOMEUSERID');
+ $this->assertEquals($code['redirect_uri'], 'http://example.com');
+ $this->assertEquals($code['expires'], $expires);
+ $this->assertEquals($code['id_token'], $id_token);
+
+ // change existing code
+ $expires = time() + 42;
+ $new_id_token = 'fake_id_token-2';
+ $success = $storage->setAuthorizationCode('new-openid-code', 'client ID2', 'SOMEOTHERID', 'http://example.org', $expires, $scope, $new_id_token);
+ $this->assertTrue($success);
+
+ $code = $storage->getAuthorizationCode('new-openid-code');
+ $this->assertNotNull($code);
+ $this->assertArrayHasKey('authorization_code', $code);
+ $this->assertArrayHasKey('client_id', $code);
+ $this->assertArrayHasKey('user_id', $code);
+ $this->assertArrayHasKey('redirect_uri', $code);
+ $this->assertArrayHasKey('expires', $code);
+ $this->assertEquals($code['authorization_code'], 'new-openid-code');
+ $this->assertEquals($code['client_id'], 'client ID2');
+ $this->assertEquals($code['user_id'], 'SOMEOTHERID');
+ $this->assertEquals($code['redirect_uri'], 'http://example.org');
+ $this->assertEquals($code['expires'], $expires);
+ $this->assertEquals($code['id_token'], $new_id_token);
+ }
+
+ /** @dataProvider provideStorage */
+ public function testRemoveIdTokenFromAuthorizationCode($storage)
+ {
+ // add new code
+ $expires = time() + 20;
+ $scope = null;
+ $id_token = 'fake_id_token_to_remove';
+ $authcode = 'new-openid-code-'.rand();
+ $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, $id_token);
+ $this->assertTrue($success);
+
+ // verify params were set
+ $code = $storage->getAuthorizationCode($authcode);
+ $this->assertNotNull($code);
+ $this->assertArrayHasKey('id_token', $code);
+ $this->assertEquals($code['id_token'], $id_token);
+
+ // remove the id_token
+ $success = $storage->setAuthorizationCode($authcode, 'client ID', 'SOMEUSERID', 'http://example.com', $expires, $scope, null);
+
+ // verify the "id_token" is now null
+ $code = $storage->getAuthorizationCode($authcode);
+ $this->assertNotNull($code);
+ $this->assertArrayHasKey('id_token', $code);
+ $this->assertEquals($code['id_token'], null);
+ }
+}
diff --git a/test/OAuth2/OpenID/Storage/UserClaimsTest.php b/test/OAuth2/OpenID/Storage/UserClaimsTest.php
new file mode 100644
index 0000000..840f6c5
--- /dev/null
+++ b/test/OAuth2/OpenID/Storage/UserClaimsTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace OAuth2\OpenID\Storage;
+
+use OAuth2\Storage\BaseTest;
+use OAuth2\Storage\NullStorage;
+
+class UserClaimsTest extends BaseTest
+{
+ /** @dataProvider provideStorage */
+ public function testGetUserClaims($storage)
+ {
+ if ($storage instanceof NullStorage) {
+ $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage());
+
+ return;
+ }
+
+ if (!$storage instanceof UserClaimsInterface) {
+ // incompatible storage
+ return;
+ }
+
+ // invalid user
+ $claims = $storage->getUserClaims('fake-user', '');
+ $this->assertFalse($claims);
+
+ // valid user (no scope)
+ $claims = $storage->getUserClaims('testuser', '');
+
+ /* assert the decoded token is the same */
+ $this->assertFalse(isset($claims['email']));
+
+ // valid user
+ $claims = $storage->getUserClaims('testuser', 'email');
+
+ /* assert the decoded token is the same */
+ $this->assertEquals($claims['email'], "testuser@test.com");
+ $this->assertEquals($claims['email_verified'], true);
+ }
+}
diff --git a/test/OAuth2/RequestTest.php b/test/OAuth2/RequestTest.php
index 138c2c2..a7bcc4e 100644
--- a/test/OAuth2/RequestTest.php
+++ b/test/OAuth2/RequestTest.php
@@ -36,6 +36,45 @@ class RequestTest extends \PHPUnit_Framework_TestCase
$this->assertNotNUll($response->getParameter('access_token'));
}
+ public function testHeadersReturnsValueByKey()
+ {
+ $request = new Request(
+ array(),
+ array(),
+ array(),
+ array(),
+ array(),
+ array(),
+ array(),
+ array('AUTHORIZATION' => 'Basic secret')
+ );
+
+ $this->assertEquals('Basic secret', $request->headers('AUTHORIZATION'));
+ }
+
+ public function testHeadersReturnsDefaultIfHeaderNotPresent()
+ {
+ $request = new Request();
+
+ $this->assertEquals('Bearer', $request->headers('AUTHORIZATION', 'Bearer'));
+ }
+
+ public function testHeadersIsCaseInsensitive()
+ {
+ $request = new Request(
+ array(),
+ array(),
+ array(),
+ array(),
+ array(),
+ array(),
+ array(),
+ array('AUTHORIZATION' => 'Basic secret')
+ );
+
+ $this->assertEquals('Basic secret', $request->headers('Authorization'));
+ }
+
private function getTestServer($config = array())
{
$storage = Bootstrap::getInstance()->getMemoryStorage();
diff --git a/test/OAuth2/ServerTest.php b/test/OAuth2/ServerTest.php
index d2710ac..d3c8069 100644
--- a/test/OAuth2/ServerTest.php
+++ b/test/OAuth2/ServerTest.php
@@ -199,6 +199,40 @@ class ServerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($client_credentials, $memory);
}
+ public function testAddStorageWithNullValue()
+ {
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+ $server = new Server($memory);
+ $server->addStorage(null, 'refresh_token');
+
+ $client_credentials = $server->getStorage('client_credentials');
+
+ $this->assertNotNull($client_credentials);
+ $this->assertEquals($client_credentials, $memory);
+
+ $refresh_token = $server->getStorage('refresh_token');
+
+ $this->assertNull($refresh_token);
+ }
+
+ public function testNewServerWithNullStorageValue()
+ {
+ $memory = $this->getMock('OAuth2\Storage\Memory');
+ $server = new Server(array(
+ 'client_credentials' => $memory,
+ 'refresh_token' => null,
+ ));
+
+ $client_credentials = $server->getStorage('client_credentials');
+
+ $this->assertNotNull($client_credentials);
+ $this->assertEquals($client_credentials, $memory);
+
+ $refresh_token = $server->getStorage('refresh_token');
+
+ $this->assertNull($refresh_token);
+ }
+
public function testAddingClientCredentialsStorageSetsClientStorageByDefault()
{
$server = new Server();
@@ -405,7 +439,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase
$server->getTokenController();
}
- public function testUsingCryptoTokenAndClientStorageWithAuthorizeControllerIsOk()
+ public function testUsingCryptoTokenAndClientStorageWithAuthorizeControllerIsOkay()
{
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
$client = $this->getMock('OAuth2\Storage\ClientInterface');
@@ -414,4 +448,196 @@ class ServerTest extends \PHPUnit_Framework_TestCase
$this->assertInstanceOf('OAuth2\ResponseType\CryptoToken', $server->getResponseType('token'));
}
+
+ /**
+ * @expectedException LogicException UserClaims
+ **/
+ public function testUsingOpenIDConnectWithoutUserClaimsThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $server = new Server($client, array('use_openid_connect' => true));
+
+ $server->getAuthorizeController();
+ }
+
+ /**
+ * @expectedException LogicException PublicKeyInterface
+ **/
+ public function testUsingOpenIDConnectWithoutPublicKeyThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OPenID\Storage\UserClaimsInterface');
+ $server = new Server(array($client, $userclaims), array('use_openid_connect' => true));
+
+ $server->getAuthorizeController();
+ }
+
+ /**
+ * @expectedException LogicException issuer
+ **/
+ public function testUsingOpenIDConnectWithoutIssuerThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true));
+
+ $server->getAuthorizeController();
+ }
+
+ public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ ));
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertNull($server->getResponseType('token id_token'));
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\ResponseType\AccessTokenInterface
+ **/
+ public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ ));
+
+ $server->getAuthorizeController();
+ }
+
+ public function testUsingOpenIDConnectWithAllowImplicitAndUseCryptoTokensIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ 'use_crypto_tokens' => true,
+ ));
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\TokenIdTokenInterface', $server->getResponseType('token id_token'));
+ }
+
+ public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $server = new Server(array($client, $userclaims, $pubkey, $token), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ ));
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\TokenIdTokenInterface', $server->getResponseType('token id_token'));
+ }
+
+ public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ // $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $server = new Server(array($client, $userclaims, $pubkey), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy',
+ 'allow_implicit' => true,
+ ));
+
+ $token = $this->getMock('OAuth2\ResponseType\AccessTokenInterface');
+ $server->addResponseType($token, 'token');
+
+ $server->getAuthorizeController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenInterface', $server->getResponseType('id_token'));
+ $this->assertInstanceOf('OAuth2\OpenID\ResponseType\TokenIdTokenInterface', $server->getResponseType('token id_token'));
+ }
+
+ /**
+ * @expectedException LogicException OAuth2\OpenID\Storage\AuthorizationCodeInterface
+ **/
+ public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsException()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $authcode = $this->getMock('OAuth2\Storage\AuthorizationCodeInterface');
+
+ $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy'
+ ));
+
+ $server->getTokenController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code'));
+ }
+
+ public function testUsingOpenIDConnectWithOpenIDAuthorizationCodeStorageCreatesOpenIDAuthorizationCodeGrantType()
+ {
+ $client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface');
+ $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface');
+ $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface');
+ $token = $this->getMock('OAuth2\Storage\AccessTokenInterface');
+ $authcode = $this->getMock('OAuth2\OpenID\Storage\AuthorizationCodeInterface');
+
+ $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array(
+ 'use_openid_connect' => true,
+ 'issuer' => 'someguy'
+ ));
+
+ $server->getTokenController();
+
+ $this->assertInstanceOf('OAuth2\OpenID\GrantType\AuthorizationCode', $server->getGrantType('authorization_code'));
+ }
+
+ public function testAddGrantTypeWithoutKey()
+ {
+ $server = new Server();
+ $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')));
+
+ $grantTypes = $server->getGrantTypes();
+ $this->assertEquals('authorization_code', key($grantTypes));
+ }
+
+ public function testAddGrantTypeWithKey()
+ {
+ $server = new Server();
+ $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 'ac');
+
+ $grantTypes = $server->getGrantTypes();
+ $this->assertEquals('ac', key($grantTypes));
+ }
+
+ public function testAddGrantTypeWithKeyNotString()
+ {
+ $server = new Server();
+ $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 42);
+
+ $grantTypes = $server->getGrantTypes();
+ $this->assertEquals('authorization_code', key($grantTypes));
+ }
}
diff --git a/test/OAuth2/Storage/PdoTest.php b/test/OAuth2/Storage/PdoTest.php
new file mode 100644
index 0000000..f6c0549
--- /dev/null
+++ b/test/OAuth2/Storage/PdoTest.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace OAuth2\Storage;
+
+use OAuth2\Encryption\Jwt;
+
+class PdoTest extends BaseTest
+{
+ public function testCreatePdoStorageUsingPdoClass()
+ {
+ $pdo = new \PDO(sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir()));
+ $storage = new Pdo($pdo);
+
+ $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
+ }
+
+ public function testCreatePdoStorageUsingDSN()
+ {
+ $dsn = sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir());
+ $storage = new Pdo($dsn);
+
+ $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
+ }
+
+ public function testCreatePdoStorageUsingConfig()
+ {
+ $config = array('dsn' => sprintf('sqlite://%s', Bootstrap::getInstance()->getSqliteDir()));
+ $storage = new Pdo($config);
+
+ $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
+ }
+
+ /**
+ * @expectedException InvalidArgumentException dsn
+ */
+ public function testCreatePdoStorageWithoutDSNThrowsException()
+ {
+ $config = array('username' => 'brent', 'password' => 'brentisaballer');
+ $storage = new Pdo($config);
+ }
+}
diff --git a/test/OAuth2/TokenType/BearerTest.php b/test/OAuth2/TokenType/BearerTest.php
index f126823..a2e000e 100644
--- a/test/OAuth2/TokenType/BearerTest.php
+++ b/test/OAuth2/TokenType/BearerTest.php
@@ -33,4 +33,26 @@ class BearerTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($response->getParameter('error'), 'invalid_request');
$this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"');
}
+
+ public function testValidRequestUsingAuthorizationHeader()
+ {
+ $bearer = new Bearer();
+ $request = new TestRequest();
+ $request->headers['AUTHORIZATION'] = 'Bearer MyToken';
+ $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8';
+
+ $param = $bearer->getAccessTokenParameter($request, $response = new Response());
+ $this->assertEquals('MyToken', $param);
+ }
+
+ public function testValidRequestUsingAuthorizationHeaderCaseInsensitive()
+ {
+ $bearer = new Bearer();
+ $request = new TestRequest();
+ $request->server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=UTF-8';
+ $request->headers['Authorization'] = 'Bearer MyToken';
+
+ $param = $bearer->getAccessTokenParameter($request, $response = new Response());
+ $this->assertEquals('MyToken', $param);
+ }
}
diff --git a/test/config/storage.json b/test/config/storage.json
index a5470fb..82a7924 100644
--- a/test/config/storage.json
+++ b/test/config/storage.json
@@ -4,7 +4,8 @@
"client_id": "Test Client ID",
"user_id": "",
"redirect_uri": "",
- "expires": "9999999999"
+ "expires": "9999999999",
+ "id_token": "IDTOKEN"
},
"testcode-with-scope": {
"client_id": "Test Client ID",
@@ -24,6 +25,13 @@
"user_id": "",
"redirect_uri": "",
"expires": "9999999999"
+ },
+ "testcode-openid": {
+ "client_id": "Test Client ID",
+ "user_id": "",
+ "redirect_uri": "",
+ "expires": "9999999999",
+ "id_token": "test_id_token"
}
},
"client_credentials" : {
@@ -68,6 +76,11 @@
"testusername": {
"password": "testpass"
},
+ "testuser": {
+ "password": "password",
+ "email": "testuser@test.com",
+ "email_verified": true
+ },
"johndoe": {
"password": "password"
}
@@ -101,6 +114,13 @@
"expires": 99999999900,
"scope": "testscope"
},
+ "accesstoken-openid-connect": {
+ "access_token": "accesstoken-openid-connect",
+ "client_id": "Test Client ID",
+ "user_id": "testuser",
+ "expires": 99999999900,
+ "scope": "openid email"
+ },
"accesstoken-malformed": {
"access_token": "accesstoken-mallformed",
"expires": 99999999900,
diff --git a/test/lib/OAuth2/Request/TestRequest.php b/test/lib/OAuth2/Request/TestRequest.php
index 5e0d081..7bbce28 100644
--- a/test/lib/OAuth2/Request/TestRequest.php
+++ b/test/lib/OAuth2/Request/TestRequest.php
@@ -2,12 +2,13 @@
namespace OAuth2\Request;
+use OAuth2\Request;
use OAuth2\RequestInterface;
/**
*
*/
-class TestRequest implements RequestInterface
+class TestRequest extends Request implements RequestInterface
{
public $query, $request, $server, $headers;
@@ -16,6 +17,7 @@ class TestRequest implements RequestInterface
$this->query = $_GET;
$this->request = $_POST;
$this->server = $_SERVER;
+ $this->headers = array();
}
public function query($name, $default = null)
@@ -33,11 +35,6 @@ class TestRequest implements RequestInterface
return isset($this->server[$name]) ? $this->server[$name] : $default;
}
- public function headers($name, $default = null)
- {
- return isset($this->headers[$name]) ? $this->headers[$name] : $default;
- }
-
public function getAllQueryParameters()
{
return $this->query;
diff --git a/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php
index 90919b8..f7de3c7 100644
--- a/test/lib/OAuth2/Storage/BaseTest.php
+++ b/test/lib/OAuth2/Storage/BaseTest.php
@@ -7,6 +7,7 @@ abstract class BaseTest extends \PHPUnit_Framework_TestCase
public function provideStorage()
{
$mysql = Bootstrap::getInstance()->getMysqlPdo();
+ $postgres = Bootstrap::getInstance()->getPostgresPdo();
$sqlite = Bootstrap::getInstance()->getSqlitePdo();
$mongo = Bootstrap::getInstance()->getMongo();
$redis = Bootstrap::getInstance()->getRedisStorage();
@@ -20,6 +21,7 @@ abstract class BaseTest extends \PHPUnit_Framework_TestCase
array($memory),
array($sqlite),
array($mysql),
+ array($postgres),
array($mongo),
array($redis),
array($cassandra),
diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php
index 52508c1..8d7fe0d 100644
--- a/test/lib/OAuth2/Storage/Bootstrap.php
+++ b/test/lib/OAuth2/Storage/Bootstrap.php
@@ -7,6 +7,7 @@ class Bootstrap
protected static $instance;
private $mysql;
private $sqlite;
+ private $postgres;
private $mongo;
private $redis;
private $cassandra;
@@ -40,6 +41,38 @@ class Bootstrap
return $this->sqlite;
}
+ public function getPostgresPdo()
+ {
+ if (!$this->postgres) {
+ if (in_array('pgsql', \PDO::getAvailableDrivers())) {
+ $this->removePostgresDb();
+ $this->createPostgresDb();
+ if ($pdo = $this->getPostgresDriver()) {
+ $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
+ $this->populatePostgresDb($pdo);
+ $this->postgres = new Pdo($pdo);
+ } else {
+ $this->postgres = new NullStorage('Postgres', 'Unable to connect to postgres on localhost with user "postgres"');
+ }
+ } else {
+ $this->postgres = new NullStorage('Postgres', 'Missing postgres PDO extension.');
+ }
+ }
+
+ return $this->postgres;
+ }
+
+ public function getPostgresDriver()
+ {
+ try {
+ $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres');
+
+ return $pdo;
+ } catch (\PDOException $e) {
+ exit($e->getMessage());
+ }
+ }
+
public function getMemoryStorage()
{
return new Memory(json_decode(file_get_contents($this->configDir. '/storage.json'), true));
@@ -222,47 +255,78 @@ class Bootstrap
$this->runPdoSql($pdo);
}
+ private function removeMysqlDb(\PDO $pdo)
+ {
+ $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php');
+ }
+
+ private function createPostgresDb()
+ {
+ `createdb -O postgres oauth2_server_php`;
+ }
+
+ private function populatePostgresDb(\PDO $pdo)
+ {
+ $this->runPdoSql($pdo);
+ }
+
+ private function removePostgresDb()
+ {
+ `dropdb oauth2_server_php`;
+ }
+
public function runPdoSql(\PDO $pdo)
{
$pdo->exec('CREATE TABLE oauth_clients (client_id TEXT, client_secret TEXT, redirect_uri TEXT, grant_types TEXT, scope TEXT, user_id TEXT, public_key TEXT)');
- $pdo->exec('CREATE TABLE oauth_access_tokens (access_token TEXT, client_id TEXT, user_id TEXT, expires DATETIME, scope TEXT)');
- $pdo->exec('CREATE TABLE oauth_authorization_codes (authorization_code TEXT, client_id TEXT, user_id TEXT, redirect_uri TEXT, expires DATETIME, scope TEXT)');
- $pdo->exec('CREATE TABLE oauth_users (username TEXT, password TEXT, first_name TEXT, last_name TEXT, scope TEXT)');
- $pdo->exec('CREATE TABLE oauth_refresh_tokens (refresh_token TEXT, client_id TEXT, user_id TEXT, expires DATETIME, scope TEXT)');
+ $pdo->exec('CREATE TABLE oauth_access_tokens (access_token TEXT, client_id TEXT, user_id TEXT, expires TIMESTAMP, scope TEXT)');
+ $pdo->exec('CREATE TABLE oauth_authorization_codes (authorization_code TEXT, client_id TEXT, user_id TEXT, redirect_uri TEXT, expires TIMESTAMP, scope TEXT, id_token TEXT)');
+ $pdo->exec('CREATE TABLE oauth_users (username TEXT, password TEXT, first_name TEXT, last_name TEXT, scope TEXT, email TEXT, email_verified BOOLEAN)');
+ $pdo->exec('CREATE TABLE oauth_refresh_tokens (refresh_token TEXT, client_id TEXT, user_id TEXT, expires TIMESTAMP, scope TEXT)');
$pdo->exec('CREATE TABLE oauth_scopes (scope TEXT, is_default BOOLEAN)');
- $pdo->exec('CREATE TABLE oauth_public_keys (client_id TEXT, public_key TEXT, private_key TEXT, encryption_algorithm VARCHAR(100) DEFAULT "RS256")');
+ $pdo->exec('CREATE TABLE oauth_public_keys (client_id TEXT, public_key TEXT, private_key TEXT, encryption_algorithm VARCHAR(100) DEFAULT \'RS256\')');
$pdo->exec('CREATE TABLE oauth_jwt (client_id VARCHAR(80), subject VARCHAR(80), public_key VARCHAR(2000))');
// set up scopes
+ $sql = 'INSERT INTO oauth_scopes (scope) VALUES (?)';
foreach (explode(' ', 'supportedscope1 supportedscope2 supportedscope3 supportedscope4 clientscope1 clientscope2 clientscope3') as $supportedScope) {
- $pdo->exec(sprintf('INSERT INTO oauth_scopes (scope) VALUES ("%s")', $supportedScope));
+ $pdo->prepare($sql)->execute(array($supportedScope));
}
+ $sql = 'INSERT INTO oauth_scopes (scope, is_default) VALUES (?, ?)';
foreach (array('defaultscope1', 'defaultscope2') as $defaultScope) {
- $pdo->exec(sprintf('INSERT INTO oauth_scopes (scope, is_default) VALUES ("%s", 1)', $defaultScope));
+ $pdo->prepare($sql)->execute(array($defaultScope, true));
}
// set up clients
- $pdo->exec('INSERT INTO oauth_clients (client_id, client_secret, scope) VALUES ("Test Client ID", "TestSecret", "clientscope1 clientscope2")');
- $pdo->exec('INSERT INTO oauth_clients (client_id, client_secret, scope) VALUES ("Test Client ID 2", "TestSecret", "clientscope1 clientscope2 clientscope3")');
- $pdo->exec('INSERT INTO oauth_clients (client_id, client_secret, scope) VALUES ("Test Default Scope Client ID", "TestSecret", "clientscope1 clientscope2")');
- $pdo->exec('INSERT INTO oauth_clients (client_id, client_secret, grant_types) VALUES ("oauth_test_client", "testpass", "implicit password")');
+ $sql = 'INSERT INTO oauth_clients (client_id, client_secret, scope, grant_types) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'clientscope1 clientscope2', null));
+ $pdo->prepare($sql)->execute(array('Test Client ID 2', 'TestSecret', 'clientscope1 clientscope2 clientscope3', null));
+ $pdo->prepare($sql)->execute(array('Test Default Scope Client ID', 'TestSecret', 'clientscope1 clientscope2', null));
+ $pdo->prepare($sql)->execute(array('oauth_test_client', 'testpass', null, 'implicit password'));
// set up misc
- $pdo->exec('INSERT INTO oauth_access_tokens (access_token, client_id) VALUES ("testtoken", "Some Client")');
- $pdo->exec('INSERT INTO oauth_authorization_codes (authorization_code, client_id) VALUES ("testcode", "Some Client")');
- $pdo->exec('INSERT INTO oauth_users (username, password) VALUES ("testuser", "password")');
- $pdo->exec('INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES ("ClientID_One", "client_1_public", "client_1_private", "RS256")');
- $pdo->exec('INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES ("ClientID_Two", "client_2_public", "client_2_private", "RS256")');
- $pdo->exec(sprintf('INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (NULL, "%s", "%s", "RS256")', $this->getTestPublicKey(), $this->getTestPrivateKey()));
- $pdo->exec(sprintf('INSERT INTO oauth_jwt (client_id, subject, public_key) VALUES ("oauth_test_client", "test_subject", "%s")', $this->getTestPublicKey()));
- }
+ $sql = 'INSERT INTO oauth_access_tokens (access_token, client_id, user_id) VALUES (?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('testtoken', 'Some Client', null));
+ $pdo->prepare($sql)->execute(array('accesstoken-openid-connect', 'Some Client', 'testuser'));
- public function removeMysqlDb(\PDO $pdo)
- {
- $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php');
+ $sql = 'INSERT INTO oauth_authorization_codes (authorization_code, client_id) VALUES (?, ?)';
+ $pdo->prepare($sql)->execute(array('testcode', 'Some Client'));
+
+ $sql = 'INSERT INTO oauth_users (username, password, email, email_verified) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('testuser', 'password', 'testuser@test.com', true));
+
+ $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('ClientID_One', 'client_1_public', 'client_1_private', 'RS256'));
+ $pdo->prepare($sql)->execute(array('ClientID_Two', 'client_2_public', 'client_2_private', 'RS256'));
+
+ $sql = 'INSERT INTO oauth_public_keys (client_id, public_key, private_key, encryption_algorithm) VALUES (?, ?, ?, ?)';
+ $pdo->prepare($sql)->execute(array(null, $this->getTestPublicKey(), $this->getTestPrivateKey(), 'RS256'));
+
+ $sql = 'INSERT INTO oauth_jwt (client_id, subject, public_key) VALUES (?, ?, ?)';
+ $pdo->prepare($sql)->execute(array('oauth_test_client', 'test_subject', $this->getTestPublicKey()));
}
+
public function getSqliteDir()
{
return $this->configDir. '/test.sqlite';