oauth2_server.test in OAuth2 Server 7
OAuth2 tests.
File
tests/oauth2_server.testView source
<?php
/**
* @file
* OAuth2 tests.
*/
/**
* Test basic API.
*/
class OAuth2ServerTestCase extends DrupalWebTestCase {
protected $profile = 'testing';
/**
* The client key of the test client.
*
* @var string
*/
protected $client_key = 'test_client';
/**
* The client secret of the test client.
*
* @var string
*/
protected $client_secret = 'test_secret';
/**
* The public key X.509 certificate used for all tests with encryption.
*
* @var string
*/
protected $public_key = '-----BEGIN CERTIFICATE-----
MIIDMDCCApmgAwIBAgIBADANBgkqhkiG9w0BAQQFADB0MS0wKwYDVQQDEyRodHRw
czovL21hcmtldHBsYWNlLmludGVybmFsLmMtZy5pby8xCzAJBgNVBAYTAkFVMRMw
EQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0
eSBMdGQwHhcNMTQwMTIxMTYyMzAyWhcNMTQwMTIzMTYyMzAyWjB0MS0wKwYDVQQD
EyRodHRwczovL21hcmtldHBsYWNlLmludGVybmFsLmMtZy5pby8xCzAJBgNVBAYT
AkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRn
aXRzIFB0eSBMdGQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANVMpjmyWlaD
6N1x1O4cf5PB6fXjq4dx1zKh/znG/zMJhkaT0TNJDD+3zfpJYFFxZGrbde+dYinL
jDK+ROvq7+h+93r0eWrld+R/kNWgILJtWwXQACPDd0pVtdOiSSd90QSEfRZyyYCl
n8RvVIPdPbGiPtDQGDwV5Dc5WcupdJNBAgMBAAGjgdEwgc4wHQYDVR0OBBYEFO4C
ZtCI7/REm9UO+PFpbAAsHHOUMIGeBgNVHSMEgZYwgZOAFO4CZtCI7/REm9UO+PFp
bAAsHHOUoXikdjB0MS0wKwYDVQQDEyRodHRwczovL21hcmtldHBsYWNlLmludGVy
bmFsLmMtZy5pby8xCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEw
HwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCAQAwDAYDVR0TBAUwAwEB
/zANBgkqhkiG9w0BAQQFAAOBgQCSCeFzNdUeFh0yNVatOdQpm2du1v7A4NXpdWL5
tXJQpv3Vgohc9f2GrVr1np270aJ3rzmSrWugZRHx0A3zhuYTNsapacvIOqmffPHd
0IZVnRgXnHPqwnWqMWuNtb8DglEEjKarjnOos/RbGvbirWsAJObxnt9kfI5wUOoA
0mYehA==
-----END CERTIFICATE-----';
/**
* The private key used for all tests with encryption.
*
* @var string
*/
protected $private_key = '-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDVTKY5slpWg+jdcdTuHH+Twen146uHcdcyof85xv8zCYZGk9Ez
SQw/t836SWBRcWRq23XvnWIpy4wyvkTr6u/ofvd69Hlq5Xfkf5DVoCCybVsF0AAj
w3dKVbXTokknfdEEhH0WcsmApZ/Eb1SD3T2xoj7Q0Bg8FeQ3OVnLqXSTQQIDAQAB
AoGAa/aEHKgd+bSC5bN8Z5mdKZj5ZzB53fDNUB+XJBOJkLe9c3PWa/MJdCcA5zLE
wfR3M28p3sL2sNkKeZS9JfyguU0QQzMhrnJZMSwPzrcUEVcRI/3vCvgnWr/4UFBW
JQpdWGvmk9MNg83y/ddnIBHEQRI9POz/dt/4L58Vq5YUy8ECQQDuWHV2nMmvuAiW
/s+D+S8arhfUyupNEVhNvpqMxK/25s4rUHGadIWm2TPStWEyxQGE4Om4bcw8KOLw
iAeKQ/qFAkEA5RlDJHz0CEgW4+bM+rOIi+tLB2C+TLzKH0eDGpeImAdsk4Z53Lxm
22iZm3DtkEqrrl+bYiaQVFovtbd5wmS4jQJBALFlcXfo1kxNA0evO7CUZLTM4rvk
k2LtB/ZFaS5grj9sJgMjCorVMyyt+N5ZVZC+BJVr+Ujln98e51nzRPlqAykCQQC/
9rT94/2O2ujjOcdT4g9uPk/19KhAIIi0QPWn2IVJ7h6aVrnRrcP54OGlD7DfkNHe
IJpQWcPiClejygMqUb8ZAkEA6SFArj46gwFaERr+D8wMizfZdxhzEuMMG3angAuV
1VPFI7qyv4rtDVATTk8RXeXUcP7l3JaQbqh+Jf0d1eSUpg==
-----END RSA PRIVATE KEY-----';
public static function getInfo() {
return array(
'name' => 'OAuth2 Server',
'description' => 'Tests basic OAuth2 Server functionality.',
'group' => 'OAuth2',
);
}
public function setUp() {
parent::setUp('oauth2_server', 'oauth2_server_test');
// Set the keys so that the module can see them.
$keys = array(
'public_key' => $this->public_key,
'private_key' => $this->private_key,
);
variable_set('oauth2_server_keys', $keys);
variable_set('oauth2_server_keys_last_generated', REQUEST_TIME);
// Create the server and client.
$server = entity_create('oauth2_server', array());
$server->name = 'test';
$server->label = 'Test';
$server->settings = array(
'default_scope' => 'basic',
'enforce_state' => TRUE,
'allow_implicit' => TRUE,
'use_openid_connect' => TRUE,
'use_crypto_tokens' => FALSE,
'store_encrypted_token_string' => FALSE,
'require_exact_redirect_uri' => TRUE,
'grant_types' => array(
'authorization_code' => 'authorization_code',
'client_credentials' => 'client_credentials',
'urn:ietf:params:oauth:grant-type:jwt-bearer' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'refresh_token' => 'refresh_token',
'password' => 'password',
),
'always_issue_new_refresh_token' => TRUE,
'access_lifetime' => 3600,
'id_lifetime' => 3600,
'refresh_token_lifetime' => 1209600,
);
$server
->save();
$client = entity_create('oauth2_server_client', array());
$client->server = $server->name;
$client->label = 'Test client';
$client->client_key = $this->client_key;
$client->client_secret = oauth2_server_hash_client_secret($this->client_secret);
$client->public_key = $this->public_key;
// The module supports entering multiple redirect uris separated by a
// newline. Both a dummy and the real uri are specified to confirm that
// validation passes.
$client->redirect_uri = 'https://google.com' . "\n" . url('authorized', array(
'absolute' => TRUE,
));
$client->automatic_authorization = TRUE;
$client
->save();
$scopes = array(
'basic' => 'Basic',
'admin' => 'Admin',
'forbidden' => 'Forbidden',
'openid' => 'OpenID Connect',
'email' => 'email',
'phone' => 'phone',
'profile' => 'Profile',
'offline_access' => 'Offline Access',
);
foreach ($scopes as $scope_name => $scope_label) {
$scope = entity_create('oauth2_server_scope', array());
$scope->server = $server->name;
$scope->name = $scope_name;
$scope->description = '';
$scope
->save();
}
}
/**
* Performs an authorization request and returns it.
*
* Used to test authorization, the implicit flow, and the authorization_code
* grant type.
*
* @return
* The return value of $this->httpRequest().
*/
protected function authorizationCodeRequest($response_type, $scope = NULL) {
$query = array(
'response_type' => $response_type,
'client_id' => $this->client_key,
'state' => drupal_get_token($this->client_key),
// The "authorized" url doesn't actually exist, but we don't need it.
'redirect_uri' => url('authorized', array(
'absolute' => TRUE,
)),
// OpenID Connect requests require a nonce. Others ignore it.
'nonce' => 'test',
);
if ($scope) {
$query['scope'] = $scope;
}
$authorize_url = url('oauth2/authorize', array(
'absolute' => TRUE,
'query' => $query,
));
return $this
->httpRequest($authorize_url);
}
/**
* Tests the authorization part of the flow.
*/
public function testAuthorization() {
// Create a user, log him in, and retry the request.
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
$result = $this
->authorizationCodeRequest('code');
// Test the redirect_uri and authorization code.
$authorize_redirect = FALSE;
$redirect_uri = url('authorized', array(
'absolute' => TRUE,
));
// Rather than assuming that clean URLs are enabled let's assume that if
// they are not enabled then the q argument is first.
if ($result->code == 302 && strpos($result->redirect_url, $redirect_uri, 0) === 0) {
$authorize_redirect = TRUE;
}
$this
->assertTrue($authorize_redirect, 'User was properly redirected to the "redirect_uri".');
$redirect_url_parts = explode('?', $result->redirect_url);
$redirect_url_params = drupal_get_query_array($redirect_url_parts[1]);
$redirect_url_params += array(
'code' => '',
);
$this
->assertTrue($redirect_url_params['code'], 'The server returned an authorization code');
$valid_token = drupal_valid_token($redirect_url_params['state'], $this->client_key);
$this
->assertTrue($valid_token, 'The server returned a valid state');
}
/**
* Tests the implicit flow.
*/
function testImplicitFlow() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
$result = $this
->authorizationCodeRequest('token');
$this
->assertEqual($result->code, 302, 'The implicit flow request completed successfully');
$redirect_url_parts = explode('#', $result->redirect_url);
$response = drupal_get_query_array($redirect_url_parts[1]);
$this
->assertTokenResponse($response, FALSE);
// We have received an access token. Verify it.
// See http://drupal.org/node/1958718.
if (!empty($response['access_token'])) {
$verification_url = url('oauth2/tokens/' . $response['access_token'], array(
'absolute' => TRUE,
));
$result = $this
->httpRequest($verification_url);
$verification_response = json_decode($result->data);
$this
->assertEqual($result->code, 200, 'The provided access token was successfully verified.');
$this
->assertEqual($verification_response->scope, urldecode($response['scope']), 'The provided scope matches the scope of the verified access token.');
}
}
/**
* Tests the "Authorization code" grant type.
*/
public function testAuthorizationCodeGrantType() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
// Perform authorization and get the code.
$result = $this
->authorizationCodeRequest('code');
$redirect_url_parts = explode('?', $result->redirect_url);
$redirect_url_params = drupal_get_query_array($redirect_url_parts[1]);
$authorization_code = $redirect_url_params['code'];
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'authorization_code',
'code' => $authorization_code,
'redirect_uri' => url('authorized', array(
'absolute' => TRUE,
)),
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
// Instead of the Authorization header, the server also supports
// passing the client key and client secret inside the request body
// ($data['client_id'] and $data['client_secret']) for all grant types,
// but it is not recommended and should be limited to clients unable
// to directly utilize the HTTP Basic authentication scheme.
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($token_url, $options);
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
$this
->assertTokenResponse($response);
}
/**
* Tests the "Client credentials" grant type.
*/
public function testClientCredentialsGrantType() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'client_credentials',
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($token_url, $options);
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
$this
->assertTokenResponse($response, FALSE);
}
/**
* Tests the "JWT bearer" grant type.
*/
public function testJwtBearerGrantType() {
$jwt_util = new OAuth2\Encryption\Jwt();
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$jwt_data = array(
'iss' => $this->client_key,
'exp' => time() + 1000,
'iat' => time(),
'sub' => $user->uid,
'aud' => $token_url,
'jti' => '123456',
);
$data = array(
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwt_util
->encode($jwt_data, $this->private_key, 'RS256'),
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
),
);
$result = $this
->httpRequest($token_url, $options);
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
$this
->assertTokenResponse($response, FALSE);
}
/**
* Tests the "User credentials" grant type.
*/
public function testPasswordGrantType() {
$result = $this
->passwordGrantRequest();
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
$this
->assertTokenResponse($response);
}
/**
* Tests the "Refresh token" grant type.
*/
public function testRefreshTokenGrantType() {
// Do a password grant first, in order to get the refresh token.
$result = $this
->passwordGrantRequest();
$response = json_decode($result->data);
$refresh_token = $response->refresh_token;
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'refresh_token',
'refresh_token' => $refresh_token,
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($token_url, $options);
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
// The response will include a new refresh_token because
// always_issue_new_refresh_token is TRUE.
$this
->assertTokenResponse($response);
}
/**
* Tests scopes.
*/
public function testScopes() {
// The default scope returned by oauth2_server_default_scope().
$result = $this
->passwordGrantRequest();
$response = json_decode($result->data);
$this
->assertEqual($response->scope, 'basic admin', 'The correct default scope was returned.');
// A non-existent scope.
$result = $this
->passwordGrantRequest('invalid_scope');
$response = json_decode($result->data);
$error = isset($response->error) && $response->error == 'invalid_scope';
$this
->assertTrue($error, 'Invalid scope correctly detected.');
// A scope forbidden by oauth2_server_scope_access.
// @see oauth2_server_test_entity_query_alter()
$result = $this
->passwordGrantRequest('forbidden');
$response = json_decode($result->data);
$error = isset($response->error) && $response->error == 'invalid_scope';
$this
->assertTrue($error, 'Inaccessible scope correctly detected.');
// A specific requested scope.
$result = $this
->passwordGrantRequest('admin');
$response = json_decode($result->data);
$this
->assertEqual($response->scope, 'admin', 'The correct scope was returned.');
}
/**
* Tests the OpenID Connect authorization code flow.
*/
function testOpenIdConnectAuthorizationCodeFlow() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
// Perform authorization without the offline_access scope.
// No refresh_token should be returned from the /token endpoint.
$result = $this
->authorizationCodeRequest('code', 'openid');
$redirect_url_parts = explode('?', $result->redirect_url);
$redirect_url_params = drupal_get_query_array($redirect_url_parts[1]);
$authorization_code = $redirect_url_params['code'];
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'authorization_code',
'code' => $authorization_code,
'redirect_uri' => url('authorized', array(
'absolute' => TRUE,
)),
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($token_url, $options);
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
$this
->assertTokenResponse($response, FALSE);
if (!empty($response->id_token)) {
$this
->assertIdToken($response->id_token);
}
else {
$this
->assertTrue(FALSE, 'The token request returned an id_token.');
}
// Perform authorization witho the offline_access scope.
// A refresh_token should be returned from the /token endpoint.
$result = $this
->authorizationCodeRequest('code', 'openid offline_access');
$redirect_url_parts = explode('?', $result->redirect_url);
$redirect_url_params = drupal_get_query_array($redirect_url_parts[1]);
$authorization_code = $redirect_url_params['code'];
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'authorization_code',
'code' => $authorization_code,
'redirect_uri' => url('authorized', array(
'absolute' => TRUE,
)),
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($token_url, $options);
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
$this
->assertTokenResponse($response);
if (!empty($response->id_token)) {
$this
->assertIdToken($response->id_token);
}
else {
$this
->assertTrue(FALSE, 'The token request returned an id_token.');
}
}
/**
* Tests the OpenID Connect implicit flow.
*/
function testOpenIdConnectImplicitFlow() {
$account = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($account);
$result = $this
->authorizationCodeRequest('id_token', 'openid email');
$this
->assertEqual($result->code, 302, 'The "id_token" implicit flow request completed successfully');
$redirect_url_parts = explode('#', $result->redirect_url);
$response = drupal_get_query_array($redirect_url_parts[1]);
if (!empty($response['id_token'])) {
$this
->assertIdToken($response['id_token'], FALSE, $account);
}
else {
$this
->assertTrue(FALSE, 'The token request returned an id_token.');
}
$result = $this
->authorizationCodeRequest('token id_token', 'openid email profile phone');
$this
->assertEqual($result->code, 302, 'The "token id_token" implicit flow request completed successfully');
$redirect_url_parts = explode('#', $result->redirect_url);
$response = drupal_get_query_array($redirect_url_parts[1]);
$this
->assertTokenResponse($response, FALSE);
if (!empty($response['id_token'])) {
$this
->assertIdToken($response['id_token'], TRUE);
}
else {
$this
->assertTrue(FALSE, 'The token request returned an id_token.');
}
// Add a timezone to the account, to test the 'zoneinfo' claim.
user_save($account, array(
'timezone' => 'Europe/London',
));
// Request OpenID Connect user information (claims).
$query = array(
'access_token' => $response['access_token'],
);
$info_url = url('oauth2/UserInfo', array(
'absolute' => TRUE,
'query' => $query,
));
$result = $this
->httpRequest($info_url);
$response = json_decode($result->data);
$expected_claims = array(
'sub' => $account->uid,
'email' => $account->mail,
'email_verified' => TRUE,
'phone_number' => '123456',
'phone_number_verified' => FALSE,
'preferred_username' => $account->name,
'name' => format_username($account),
'zoneinfo' => $account->timezone,
);
foreach ($expected_claims as $claim => $expected_value) {
$this
->assertEqual($response->{$claim}, $expected_value, 'The UserInfo endpoint returned a valid "' . $claim . '" claim');
}
}
/**
* Tests that the OpenID Connect 'sub' property affects user info 'sub' claim.
*/
public function testOpenIdConnectNonDefaultSub() {
variable_set('oauth2_server_user_sub_property', 'name');
$result = $this
->passwordGrantRequest('openid');
$response = json_decode($result->data);
$access_token = $response->access_token;
$query = array(
'access_token' => $access_token,
);
$info_url = url('oauth2/UserInfo', array(
'absolute' => TRUE,
'query' => $query,
));
$result = $this
->httpRequest($info_url);
$response = json_decode($result->data, TRUE);
$this
->assertEqual($this->loggedInUser->name, $response['sub'], 'The UserInfo "sub" is now the user\'s name.');
}
/**
* Tests that the OpenID Connect 'sub' property affects ID token 'sub' claim.
*/
public function testOpenIdConnectNonDefaultSubInIdToken() {
variable_set('oauth2_server_user_sub_property', 'name');
// This is the authorization code grant type flow.
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
$result = $this
->authorizationCodeRequest('code', 'openid offline_access');
$redirect_url_parts = explode('?', $result->redirect_url);
$redirect_url_params = drupal_get_query_array($redirect_url_parts[1]);
$authorization_code = $redirect_url_params['code'];
// Get tokens using the authorization code.
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'authorization_code',
'code' => $authorization_code,
'redirect_uri' => url('authorized', array(
'absolute' => TRUE,
)),
);
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($token_url, $options);
$response = json_decode($result->data);
$parts = explode('.', $response->id_token);
$claims = json_decode(oauth2_server_base64url_decode($parts[1]), TRUE);
$this
->assertEqual($this->loggedInUser->name, $claims['sub'], 'The ID token "sub" is now the user\'s name.');
}
/**
* Tests crypto tokens.
*/
public function testCryptoTokens() {
// Enable crypto tokens.
$server = oauth2_server_load('test');
$server->settings['use_crypto_tokens'] = TRUE;
$server
->save();
$result = $this
->passwordGrantRequest();
$this
->assertEqual($result->code, 200, 'The token request completed successfully');
$response = json_decode($result->data);
// The refresh token is contained inside the crypto token.
$this
->assertTokenResponse($response, FALSE);
$verified = FALSE;
if (substr_count($response->access_token, '.') == 2) {
// Verify the JTW Access token following the instructions from
// http://bshaffer.github.io/oauth2-server-php-docs/overview/jwt-access-tokens
list($header, $payload, $signature) = explode('.', $response->access_token);
// The signature is "url safe base64 encoded".
$signature = base64_decode(strtr($signature, '-_,', '+/'));
$payload_to_verify = utf8_decode($header . '.' . $payload);
$verified = openssl_verify($payload_to_verify, $signature, $this->public_key, 'sha256');
}
$this
->assertTrue($verified, 'The JWT Access Token is valid.');
}
/**
* Tests revoking an access token.
*/
public function testRevoke() {
$result = $this
->passwordGrantRequest();
$response = json_decode($result->data);
$access_token = $response->access_token;
$resource_request_options = array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
),
);
$resource_url = url('oauth2_test/resource/admin', array(
'absolute' => TRUE,
));
$result = $this
->httpRequest($resource_url, $resource_request_options);
$this
->assertEqual($result->code, 200, 'Token works before revocation.');
// Revoke the access token.
$revoke_url = url('oauth2/revoke', array(
'absolute' => TRUE,
));
$revoke_options = array(
'method' => 'POST',
'data' => http_build_query(array(
'token' => $access_token,
'token_type_hint' => 'access_token',
)),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
$result = $this
->httpRequest($revoke_url, $revoke_options);
// Note: a valid revoke request will always return 200 even if the token was
// invalid (see https://tools.ietf.org/html/rfc7009#section-2.2).
$this
->assertEqual(200, $result->code, 'Revoke request succeeded');
$result = $this
->httpRequest($resource_url, $resource_request_options);
$this
->assertEqual($result->code, 401, 'Token no longer works after revocation.');
}
/**
* Tests resource requests.
*/
public function testResourceRequests() {
$result = $this
->passwordGrantRequest('admin');
$response = json_decode($result->data);
$access_token = $response->access_token;
// Check resource access with no access token.
$resource_url = url('oauth2_test/resource/admin', array(
'absolute' => TRUE,
));
$result = $this
->httpRequest($resource_url);
$this
->assertEqual($result->code, 401, 'Missing access token correctly detected.');
// Check resource access with an insufficient scope.
$query = array(
'access_token' => $access_token,
);
$resource_url = url('oauth2_test/resource/forbidden', array(
'absolute' => TRUE,
'query' => $query,
));
$result = $this
->httpRequest($resource_url);
$response = json_decode($result->data);
$error = isset($response->error) && $response->error == 'insufficient_scope';
$this
->assertTrue($error, 'Insufficient scope correctly detected.');
// Check resource access with the access token in the url.
$query = array(
'access_token' => $access_token,
);
$resource_url = url('oauth2_test/resource/admin', array(
'absolute' => TRUE,
'query' => $query,
));
$result = $this
->httpRequest($resource_url);
$this
->assertEqual($result->code, 200, 'Access token in the URL correctly detected.');
// Check resource access with the access token in the header.
$resource_url = url('oauth2_test/resource/admin', array(
'absolute' => TRUE,
));
$options = array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
),
);
$result = $this
->httpRequest($resource_url, $options);
$this
->assertEqual($result->code, 200, 'Access token in the header correctly detected.');
}
/**
* Test that access is denied when using a token for a blocked user.
*/
public function testBlockedUserTokenFails() {
// Get a normal access token for a normal user.
$result = $this
->passwordGrantRequest('admin');
$response = json_decode($result->data);
$access_token = $response->access_token;
// Check resource access while the user is active.
$resource_url = url('oauth2_test/resource/admin', array(
'absolute' => TRUE,
));
$options = array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
),
);
$result = $this
->httpRequest($resource_url, $options);
$this
->assertEqual($result->code, 200, 'An active user is correctly authenticated.');
// Block the user.
user_save($this->loggedInUser, array(
'status' => 0,
));
// Check resource access while the user is blocked.
$resource_url = url('oauth2_test/resource/admin', array(
'absolute' => TRUE,
));
$options = array(
'headers' => array(
'Authorization' => 'Bearer ' . $access_token,
),
);
$result = $this
->httpRequest($resource_url, $options);
$this
->assertEqual($result->code, 403, 'A blocked user is denied access with 403 Forbidden.');
}
/**
* Performs a password grant request and returns it.
*
* Used to test the grant itself, as well as a helper for other tests
* (since it's a fast way of getting an access token).
*
* @param $scope
* An optional scope to request.
*
* @return
* The return value of $this->httpRequest().
*/
protected function passwordGrantRequest($scope = NULL) {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$this
->drupalLogin($user);
$token_url = url('oauth2/token', array(
'absolute' => TRUE,
));
$data = array(
'grant_type' => 'password',
'username' => $user->name,
'password' => $user->pass_raw,
);
if ($scope) {
$data['scope'] = $scope;
}
$options = array(
'method' => 'POST',
'data' => http_build_query($data),
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => 'Basic ' . base64_encode($this->client_key . ':' . $this->client_secret),
),
);
return $this
->httpRequest($token_url, $options);
}
/**
* Performs a drupal_http_request() with additional parameters.
*
* Passes along all cookies. This ensures that the test user has access
* to the oauth2 endpoints.
*
* @param $url
* $url: A string containing a fully qualified URI.
* @param $options
* The options array passed along to drupal_http_request().
*
* @return
* The result object as returned by drupal_http_request().
*/
protected function httpRequest($url, array $options = array()) {
// Forward cookies.
$cookie_string = '';
foreach ($this->cookies as $name => $data) {
$cookie_string .= $name . '=' . $data['value'] . ';';
}
$options['headers']['Cookie'] = $cookie_string;
// Set other general options.
$options += array(
'max_redirects' => 0,
);
return drupal_http_request($url, $options);
}
/**
* Assert that the given id_token response has the expected values.
*
* @param $id_token
* The id_token.
* @param $has_at_hash
* Whether the token is supposed to contain the at_hash claim.
* @param $account
* The account of the authenticated user, if the id_token is supposed
* to contain user claims.
*/
protected function assertIdToken($id_token, $has_at_hash = FALSE, $account = NULL) {
$parts = explode('.', $id_token);
list($headerb64, $claims64, $signatureb64) = $parts;
$claims = json_decode(oauth2_server_base64url_decode($claims64), TRUE);
$signature = oauth2_server_base64url_decode($signatureb64);
$payload = utf8_decode($headerb64 . '.' . $claims64);
$verified = openssl_verify($payload, $signature, $this->public_key, 'sha256');
$this
->assertTrue($verified, 'The id_token has a valid signature.');
$this
->assertTrue(array_key_exists('iss', $claims), 'The id_token contains an "iss" claim.');
$this
->assertTrue(array_key_exists('sub', $claims), 'The id_token contains a "sub" claim.');
$this
->assertTrue(array_key_exists('aud', $claims), 'The id_token contains an "aud" claim.');
$this
->assertTrue(array_key_exists('iat', $claims), 'The id_token contains an "iat" claim.');
$this
->assertTrue(array_key_exists('exp', $claims), 'The id_token contains an "exp" claim.');
$this
->assertTrue(array_key_exists('auth_time', $claims), 'The id_token contains an "auth_time" claim.');
$this
->assertTrue(array_key_exists('nonce', $claims), 'The id_token contains a "nonce" claim');
if ($has_at_hash) {
$this
->assertTrue(array_key_exists('at_hash', $claims), 'The id_token contains an "at_hash" claim.');
}
if ($account) {
$this
->assertTrue(array_key_exists('email', $claims), 'The id_token contains an "email" claim.');
$this
->assertTrue(array_key_exists('email_verified', $claims), 'The id_token contains an "email_verified" claim.');
}
$this
->assertEqual($claims['aud'], $this->client_key, 'The id_token "aud" claim contains the expected client_id.');
$this
->assertEqual($claims['nonce'], 'test', 'The id_token "nonce" claim contains the expected nonce.');
if ($account) {
$this
->assertEqual($claims['email'], $account->mail);
}
}
/**
* Assert that the given token response has the expected values.
*
* @param $response
* The response (either an object decoded from a json string or the
* query string taken from the url in case of the implicit flow).
* @param $has_refresh_token
* A boolean indicating whether this response should have a refresh token.
*/
protected function assertTokenResponse($response, $has_refresh_token = TRUE) {
// Make sure we have an array.
$response = (array) $response;
$this
->assertTrue(array_key_exists('access_token', $response), 'The "access token" value is present in the return values');
$this
->assertTrue(array_key_exists('expires_in', $response), 'The "expires_in" value is present in the return values');
$this
->assertTrue(array_key_exists('token_type', $response), 'The "token_type" value is present in the return values');
$this
->assertTrue(array_key_exists('scope', $response), 'The "scope" value is present in the return values');
if ($has_refresh_token) {
$this
->assertTrue(array_key_exists('refresh_token', $response), 'The "refresh_token" value is present in the return values');
}
}
}
/**
* Test \Drupal\oauth2_server\Storage.
*/
class OAuth2ServerStorageTestCase extends DrupalWebTestCase {
protected $profile = 'testing';
/**
* The client key of the test client.
*
* @var string
*/
protected $client_key = 'test_client';
/**
* The client secret of the test client.
*
* @var string
*/
protected $client_secret = 'test_secret';
/**
* The storage instance to be tested.
*
* @var \Drupal\oauth2_server\Storage
*/
protected $storage;
/**
* The test client.
*
* @var OAuth2ServerClient
*/
protected $client;
public static function getInfo() {
return array(
'name' => 'OAuth2 Server Storage',
'description' => 'Tests the Storage implementation for the oauth2-server-php library.',
'group' => 'OAuth2',
);
}
public function setUp() {
parent::setUp('oauth2_server');
// Create the server and client.
$server = entity_create('oauth2_server', array());
$server->name = 'test';
$server->label = 'Test';
$server->settings = array(
'default_scope' => '',
'allow_implicit' => TRUE,
'require_exact_redirect_uri' => TRUE,
'grant_types' => array(
'authorization_code' => 'authorization_code',
'client_credentials' => 'client_credentials',
'refresh_token' => 'refresh_token',
'password' => 'password',
),
'always_issue_new_refresh_token' => TRUE,
);
$server
->save();
$this->client = entity_create('oauth2_server_client', array());
$this->client->server = $server->name;
$this->client->label = 'Test client';
$this->client->client_key = $this->client_key;
$this->client->client_secret = oauth2_server_hash_client_secret($this->client_secret);
$this->client->redirect_uri = url('authorized', array(
'absolute' => TRUE,
));
$this->client->automatic_authorization = TRUE;
$this->client
->save();
$this->storage = new Drupal\oauth2_server\Storage();
}
public function testCheckClientCredentials() {
// Nonexistent client_id.
$result = $this->storage
->checkClientCredentials('fakeclient', 'testpass');
$this
->assertFalse($result, 'Invalid client credentials correctly detected.');
// Invalid client_secret.
$result = $this->storage
->checkClientCredentials($this->client_key, 'invalidcredentials');
$this
->assertFalse($result, 'Invalid client_secret correctly detected.');
// Valid credentials.
$result = $this->storage
->checkClientCredentials($this->client_key, $this->client_secret);
$this
->assertTrue($result, 'Valid client credentials correctly detected.');
// No client secret.
$result = $this->storage
->checkClientCredentials($this->client_key, '');
$this
->assertFalse($result, 'Empty client secret not accepted.');
// Allow empty client secret, try again.
$this->client->client_secret = '';
$this->client
->save();
$result = $this->storage
->checkClientCredentials($this->client_key, '');
$this
->assertTrue($result, 'Empty client secret accepted if none required.');
// Try again with a NULL client secret. This should be accepted too.
$result = $this->storage
->checkClientCredentials($this->client_key, NULL);
$this
->assertTrue($result, 'Null client secret accepted if none required.');
}
public function testGetClientDetails() {
// Nonexistent client_id.
$details = $this->storage
->getClientDetails('fakeclient');
$this
->assertFalse($details, 'Invalid client_id correctly detected.');
// Valid client_id.
$details = $this->storage
->getClientDetails($this->client_key);
$this
->assertNotNull($details, 'Client details successfully returned.');
$this
->assertTrue(array_key_exists('client_id', $details), 'The "client_id" value is present in the client details.');
$this
->assertTrue(array_key_exists('client_secret', $details), 'The "client_secret" value is present in the client details.');
$this
->assertTrue(array_key_exists('redirect_uri', $details), 'The "redirect_uri" value is present in the client details.');
}
public function testAccessToken() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$token = $this->storage
->getAccessToken('newtoken');
$this
->assertFalse($token, 'Trying to load a nonexistent token is unsuccessful.');
$expires = time() + 20;
$success = $this->storage
->setAccessToken('newtoken', $this->client_key, $user->uid, $expires);
$this
->assertTrue($success, 'A new access token has been successfully created.');
// Verify the return format of getAccessToken().
$token = $this->storage
->getAccessToken('newtoken');
$this
->assertTrue($token, 'An access token was successfully returned.');
$this
->assertTrue(array_key_exists('access_token', $token), 'The "access_token" value is present in the token array.');
$this
->assertTrue(array_key_exists('client_id', $token), 'The "client_id" value is present in the token array.');
$this
->assertTrue(array_key_exists('user_id', $token), 'The "user_id" value is present in the token array.');
$this
->assertTrue(array_key_exists('expires', $token), 'The "expires" value is present in the token array.');
$this
->assertEqual($token['access_token'], 'newtoken', 'The "access_token" key has the expected value.');
$this
->assertEqual($token['client_id'], $this->client_key, 'The "client_id" key has the expected value.');
$this
->assertEqual($token['user_id'], $user->uid, 'The "user_id" key has the expected value.');
$this
->assertEqual($token['expires'], $expires, 'The "expires" key has the expected value.');
// Update the token.
$expires = time() + 42;
$success = $this->storage
->setAccessToken('newtoken', $this->client_key, $user->uid, $expires);
$this
->assertTrue($success, 'The access token was successfully updated.');
$token = $this->storage
->getAccessToken('newtoken');
$this
->assertTrue($token, 'An access token was successfully returned.');
$this
->assertEqual($token['expires'], $expires, 'The expires timestamp matches the new value.');
}
public function testSetRefreshToken() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$token = $this->storage
->getRefreshToken('refreshtoken');
$this
->assertFalse($token, 'Trying to load a nonexistent token is unsuccessful.');
$expires = time() + 20;
$success = $this->storage
->setRefreshToken('refreshtoken', $this->client_key, $user->uid, $expires);
$this
->assertTrue($success, 'A new refresh token has been successfully created.');
// Verify the return format of getRefreshToken().
$token = $this->storage
->getRefreshToken('refreshtoken');
$this
->assertTrue($token, 'A refresh token was successfully returned.');
$this
->assertTrue(array_key_exists('refresh_token', $token), 'The "refresh_token" value is present in the token array.');
$this
->assertTrue(array_key_exists('client_id', $token), 'The "client_id" value is present in the token array.');
$this
->assertTrue(array_key_exists('user_id', $token), 'The "user_id" value is present in the token array.');
$this
->assertTrue(array_key_exists('expires', $token), 'The "expires" value is present in the token array.');
$this
->assertEqual($token['refresh_token'], 'refreshtoken', 'The "refresh_token" key has the expected value.');
$this
->assertEqual($token['client_id'], $this->client_key, 'The "client_id" key has the expected value.');
$this
->assertEqual($token['user_id'], $user->uid, 'The "user_id" key has the expected value.');
$this
->assertEqual($token['expires'], $expires, 'The "expires" key has the expected value.');
}
public function testAuthorizationCode() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
$code = $this->storage
->getAuthorizationCode('newcode');
$this
->assertFalse($code, 'Trying to load a nonexistent authorization code is unsuccessful.');
$expires = time() + 20;
$success = $this->storage
->setAuthorizationCode('newcode', $this->client_key, $user->uid, 'http://example.com', $expires);
$this
->assertTrue($success, 'A new authorization code was successfully created.');
// Verify the return format of getAuthorizationCode().
$code = $this->storage
->getAuthorizationCode('newcode');
$this
->assertTrue($code, 'An authorization code was successfully returned.');
$this
->assertTrue(array_key_exists('authorization_code', $code), 'The "authorization_code" value is present in the code array.');
$this
->assertTrue(array_key_exists('client_id', $code), 'The "client_id" value is present in the code array.');
$this
->assertTrue(array_key_exists('user_id', $code), 'The "user_id" value is present in the code array.');
$this
->assertTrue(array_key_exists('redirect_uri', $code), 'The "redirect_uri" value is present in the code array.');
$this
->assertTrue(array_key_exists('expires', $code), 'The "expires" value is present in the code array.');
$this
->assertEqual($code['authorization_code'], 'newcode', 'The "authorization_code" key has the expected value.');
$this
->assertEqual($code['client_id'], $this->client_key, 'The "client_id" key has the expected value.');
$this
->assertEqual($code['user_id'], $user->uid, 'The "user_id" key has the expected value.');
$this
->assertEqual($code['redirect_uri'], 'http://example.com', 'The "redirect_uri" key has the expected value.');
$this
->assertEqual($code['expires'], $expires, 'The "expires" key has the expected value.');
// Change an existing code
$expires = time() + 42;
$success = $this->storage
->setAuthorizationCode('newcode', $this->client_key, $user->uid, 'http://example.org', $expires);
$this
->assertTrue($success, 'The authorization code was successfully updated.');
$code = $this->storage
->getAuthorizationCode('newcode');
$this
->assertTrue($code, 'An authorization code was successfully returned.');
$this
->assertEqual($code['expires'], $expires, 'The expires timestamp matches the new value.');
}
public function testCheckUserCredentials() {
$user = $this
->drupalCreateUser(array(
'use oauth2 server',
));
// Correct credentials
$result = $this->storage
->checkUserCredentials($user->name, $user->pass_raw);
$this
->assertTrue($result, 'Valid user credentials correctly detected.');
// Invalid username.
$result = $this->storage
->checkUserCredentials('fakeusername', $user->pass_raw);
$this
->assertFalse($result, 'Invalid username correctly detected.');
// Invalid password.
$result = $this->storage
->checkUserCredentials($user->name, 'fakepass');
$this
->assertFalse($result, 'Invalid password correctly detected');
}
}
/**
* Test administration forms.
*/
class OAuth2ServerAdminTestCase extends DrupalWebTestCase {
public static function getInfo() {
return array(
'name' => 'OAuth2 Server Admin',
'description' => 'Tests administration functionality for OAuth2 Server.',
'group' => 'OAuth2',
);
}
public function setUp() {
parent::setUp('oauth2_server');
}
public function testEditingClientSecret() {
$account = $this
->drupalCreateUser(array(
'administer oauth2 server',
));
$this
->drupalLogin($account);
$server_name = strtolower($this
->randomName());
// Create a server in the UI.
$this
->drupalPost('admin/structure/oauth2-servers/add', array(
'label' => $this
->randomString(),
'name' => $server_name,
), t('Save server'));
// Create a client of the server in the UI, with a random secret.
$client_key = strtolower($this
->randomName());
$secret = $this
->randomString(32);
$this
->drupalPost('admin/structure/oauth2-servers/manage/' . $server_name . '/clients/add', array(
'label' => $this
->randomString(),
'client_key' => $client_key,
'redirect_uri' => 'http://localhost',
'require_client_secret' => TRUE,
'client_secret' => $secret,
), t('Save client'));
// Test that the raw secret does not match the saved (hashed) one.
$client = oauth2_server_client_load($client_key);
$this
->assertNotEqual($secret, $client->client_secret, 'Raw secret does not match hashed secret.');
// Test that the secret can be matched.
$this
->assertTrue(oauth2_server_check_client_secret($client->client_secret, $secret), 'Hashes match for known secret and stored secret.');
// Edit the client, and do not set a new secret. It should stay the same.
$old_hashed_secret = $client->client_secret;
$this
->updateClient($client, array(
'label' => $this
->randomString(),
));
$client_controller = entity_get_controller('oauth2_server_client');
$client_controller
->resetCache();
$client = oauth2_server_client_load($client_key);
$this
->assertEqual($old_hashed_secret, $client->client_secret, 'Secret is not changed accidentally when editing the client.');
// Edit the client, and set an empty secret.
$this
->updateClient($client, array(
'require_client_secret' => FALSE,
));
$client_controller
->resetCache();
$client = oauth2_server_client_load($client_key);
$this
->assertTrue($client->client_secret === '', 'Secret is set to empty if it is not required.');
// Edit the client, and set a new, non-empty secret.
$new_secret = $this
->randomString(32);
$this
->updateClient($client, array(
'require_client_secret' => TRUE,
'client_secret' => $new_secret,
));
$client_controller
->resetCache();
$client = oauth2_server_client_load($client_key);
$this
->assertTrue(oauth2_server_check_client_secret($client->client_secret, $new_secret), 'Hashes match for new secret and stored secret.');
}
/**
* Edit a client in the UI.
*
* @param \OAuth2ServerClient $client
* The client entity.
* @param array $values
* New values.
*/
protected function updateClient(\OAuth2ServerClient $client, array $values) {
$this
->drupalPost('admin/structure/oauth2-servers/manage/' . $client->server . '/clients/' . $client
->internalIdentifier() . '/edit', $values, t('Save client'));
}
}
Classes
Name | Description |
---|---|
OAuth2ServerAdminTestCase | Test administration forms. |
OAuth2ServerStorageTestCase | Test \Drupal\oauth2_server\Storage. |
OAuth2ServerTestCase | Test basic API. |