RoundCube Webmail
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

406 lines
13 KiB

<?php
namespace Roundcube\Tests\Rcmail;
use Roundcube\Tests\ActionTestCase;
use Roundcube\Tests\ExitException;
use Roundcube\Tests\HttpClientMock;
use Roundcube\Tests\OutputHtmlMock;
use Roundcube\Tests\StderrMock;
use function Roundcube\Tests\getProperty;
use function Roundcube\Tests\setProperty;
/**
* Test class to test rcmail_oauth class
*/
class OauthTest extends ActionTestCase
{
// created a valid and enabled oauth instance
private $config = [
'provider' => 'test',
'token_uri' => 'https://test/token',
'auth_uri' => 'https://test/auth',
'identity_uri' => 'https://test/ident',
'issuer' => 'https://test/',
// Do not set JWKS
'client_id' => 'some-client',
'client_secret' => 'very-secure',
'scope' => 'plop',
];
private $identity = [
'sub' => '82c8f487-df95-4960-972c-4e680c3c72f5',
'name' => 'John Doe',
'preferred_username' => 'John D',
'given_name' => 'John',
'family_name' => 'Doe',
'email' => 'j.doe@test.fake',
'email_verified' => true,
'locale' => 'en',
];
private function generate_fake_id_token()
{
$id_token_payload = (array) [
'typ' => 'ID', // this is a token id
'exp' => (time() + 600),
'iat' => time(),
'auth_time' => time(),
'jti' => 'uniq-id',
'iss' => $this->config['issuer'],
'aud' => $this->config['client_id'],
'azp' => $this->config['client_id'],
'session_state' => 'fake-session',
'acr' => '1',
'nonce' => 'fake-nonce',
'sid' => '65f8d42c-dbbd-4f76-b5f3-44b540e4253a',
] + $this->identity;
// Right now our code does not check signature
$jwt_header = strtr(base64_encode(json_encode(['alg' => 'NONE', 'typ' => 'JWT'])), '+/', '-_');
$jwt_body = strtr(base64_encode(json_encode($id_token_payload)), '+/', '-_');
$jwt_signature = ''; // NONE alg
return implode('.', [$jwt_header, $jwt_body, $jwt_signature]);
}
/**
* Test jwt_decode() method with an invalid token
*/
public function test_jwt_decode_invalid()
{
$jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.EkN-DOsnsuRjRO6BxXemmJDm3HbxrbRzXglbN2S4sOkopdU4IsDxTI8jO19W_A4K8ZPJijNLis4EZsHeY559a4DFOd50_OqgHGuERTqYZyuhtF39yxJPAjUESwxk2J5k_4zM3O-vtd1Ghyo4IbqKKSy6J9mTniYJPenn5-HIirE';
$oauth = \rcmail_oauth::get_instance();
// We can't use expectException until we drop support for phpunit 4.8 (i.e. PHP 5.4)
// $this->expectException(RuntimeException::class);
try {
$oauth->jwt_decode($jwt);
} catch (\RuntimeException $e) {
}
$this->assertTrue(isset($e));
}
/**
* Test jwt_decode() method with an array aud
*/
public function test_jwt_decode_array()
{
$jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6WyJzb21lLWNsaWVudCJdfQ.signature';
$oauth = new \rcmail_oauth([
'client_id' => 'some-client',
]);
$body = $oauth->jwt_decode($jwt);
$this->assertSame($body['aud'], ['some-client']);
}
/**
* Test jwt_decode() method with a string aud
*/
public function test_jwt_decode_string()
{
$jwt = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImF1ZCI6InNvbWUtY2xpZW50In0.signature';
$oauth = new \rcmail_oauth([
'client_id' => 'some-client',
]);
$body = $oauth->jwt_decode($jwt);
$this->assertSame($body['aud'], 'some-client');
}
/**
* Test is_enabled() method
*/
public function test_is_enabled()
{
$oauth = \rcmail_oauth::get_instance();
$this->assertFalse($oauth->is_enabled());
}
/**
* Test is_enabled() method
*/
public function test_is_enabled_with_token_url()
{
$oauth = new \rcmail_oauth($this->config);
$oauth->init();
$this->assertTrue($oauth->is_enabled());
}
/**
* Test discovery method
*/
public function test_discovery()
{
// fake discovery response
$config_answer = [
'issuer' => 'https://test/issuer',
'authorization_endpoint' => 'https://test/auth',
'token_endpoint' => 'https://test/token',
'userinfo_endpoint' => 'https://test/userinfo',
'end_session_endpoint' => 'https://test/logout',
'jwks_uri' => 'https://test/jwks',
];
HttpClientMock::setResponses([
[200, ['Content-Type' => 'application/json'], json_encode($config_answer)],
]);
// provide only the config
$oauth = new \rcmail_oauth([
'provider' => 'example',
'config_uri' => 'https://test/config',
'client_id' => 'some-client',
]);
$oauth->init();
// if discovery succeed, should be enabled
$this->assertTrue($oauth->is_enabled());
}
/**
* Test get_redirect_uri() method
*/
public function test_get_redirect_uri()
{
$oauth = \rcmail_oauth::get_instance();
$this->assertMatchesRegularExpression('|^http://.*/index.php/login/oauth$|', $oauth->get_redirect_uri());
}
/**
* Test login_redirect() method
*/
public function test_login_redirect()
{
$output = $this->initOutput(\rcmail_action::MODE_HTTP, 'login', '');
$oauth = new \rcmail_oauth($this->config);
$oauth->init();
try {
$oauth->login_redirect();
$result = null;
$ecode = null;
} catch (ExitException $e) {
$result = $e->getMessage();
$ecode = $e->getCode();
}
$this->assertSame(OutputHtmlMock::E_REDIRECT, $ecode);
$this->assertMatchesRegularExpression('|^Location: https://test/auth\?.*|', $result);
[$base, $query] = explode('?', substr($result, 10));
parse_str($query, $map);
$this->assertSame($this->config['scope'], $map['scope']);
$this->assertSame($this->config['client_id'], $map['client_id']);
$this->assertSame('code', $map['response_type']);
$this->assertSame($_SESSION['oauth_state'], $map['state']);
$this->assertSame($_SESSION['oauth_nonce'], $map['nonce']);
$this->assertMatchesRegularExpression('!http.*/login/oauth!', $map['redirect_uri']);
}
/**
* Test request_access_token() method with a wrong state
*/
public function test_request_access_token_with_wrong_state()
{
$oauth = new \rcmail_oauth($this->config);
$oauth->init();
$_SESSION['oauth_state'] = 'random-state';
StderrMock::start();
$response = $oauth->request_access_token('fake-code', 'mismatch-state');
StderrMock::stop();
// should be false as state do not match
$this->assertFalse($response);
$this->assertSame('ERROR: OAuth token request failed: state parameter mismatch', trim(StderrMock::$output));
}
/**
* Test request_access_token()
*/
public function test_request_access_token_with_wrong_nonce()
{
$payload = [
'token_type' => 'Bearer',
'access_token' => 'FAKE-ACCESS-TOKEN',
'expires_in' => 300,
'refresh_token' => 'FAKE-REFRESH-TOKEN',
'refresh_expires_in' => 1800,
'id_token' => $this->generate_fake_id_token(), // inject a generated identity
'not-before-policy' => 0,
'session_state' => 'fake-session',
'scope' => 'openid profile email',
];
HttpClientMock::setResponses([
[200, ['Content-Type' => 'application/json'], json_encode($payload)],
]);
$oauth = new \rcmail_oauth((array) $this->config);
$oauth->init();
$_SESSION['oauth_state'] = 'random-state'; // ensure state identiquals
$_SESSION['oauth_nonce'] = 'wrong-nonce';
StderrMock::start();
$response = $oauth->request_access_token('fake-code', 'random-state');
StderrMock::stop();
$this->assertFalse($response);
$this->assertStringContainsString('identity\'s nonce mismatch', StderrMock::$output);
}
/**
* Test request_access_token() method
*/
public function test_request_access_token()
{
$payload = [
'token_type' => 'Bearer',
'access_token' => 'FAKE-ACCESS-TOKEN',
'expires_in' => 300,
'refresh_token' => 'FAKE-REFRESH-TOKEN',
'refresh_expires_in' => 1800,
'id_token' => $this->generate_fake_id_token(), // inject a generated identity
'not-before-policy' => 0,
'session_state' => 'fake-session',
'scope' => 'openid profile email',
];
HttpClientMock::setResponses([
[200, ['Content-Type' => 'application/json'], json_encode($payload)],
]);
$oauth = new \rcmail_oauth((array) $this->config);
$oauth->init();
$_SESSION['oauth_state'] = 'random-state'; // ensure state identiquals
$_SESSION['oauth_nonce'] = 'fake-nonce';
$response = $oauth->request_access_token('fake-code', 'random-state');
$this->assertTrue($response);
$login_phase = getProperty($oauth, 'login_phase');
$this->assertSame('Bearer FAKE-ACCESS-TOKEN', $login_phase['authorization']);
$this->assertSame($this->identity['email'], $login_phase['username']);
$this->assertTrue(isset($login_phase['token']));
$this->assertFalse(isset($login_phase['token']['access_token']));
}
/**
* Test request_access_token() method without identity, code will have to fetch the identity using the access token
*/
public function test_request_access_token_without_id_token()
{
$payload = [
'token_type' => 'Bearer',
'access_token' => 'FAKE-ACCESS-TOKEN',
'expires_in' => 300,
'refresh_token' => 'FAKE-REFRESH-TOKEN',
'refresh_expires_in' => 1800,
'not-before-policy' => 0,
'session_state' => 'fake-session',
'scope' => 'openid profile email',
];
// TODO should create a specific Mock to check request and validate it
HttpClientMock::setResponses([
[200, ['Content-Type' => 'application/json'], json_encode($payload)], // the request access
[200, ['Content-Type' => 'application/json'], json_encode($this->identity)], // call to userinfo
]);
$oauth = new \rcmail_oauth((array) $this->config);
$oauth->init();
$_SESSION['oauth_state'] = 'random-state'; // ensure state identiquals
$_SESSION['oauth_nonce'] = 'fake-nonce'; // ensure nonce identiquals
$response = $oauth->request_access_token('fake-code', 'random-state');
$this->assertTrue($response);
$login_phase = getProperty($oauth, 'login_phase');
$this->assertSame('Bearer FAKE-ACCESS-TOKEN', $login_phase['authorization']);
$this->assertSame($this->identity['email'], $login_phase['username']);
$this->assertTrue(isset($login_phase['token']));
$this->assertFalse(isset($login_phase['token']['access_token']));
}
/**
* Test user_create() method
*/
public function test_valid_user_create()
{
$oauth = new \rcmail_oauth();
$oauth->init();
// fake identity
setProperty($oauth, 'login_phase', [
'token' => [
'identity' => [
'email' => 'jdoe@faké.dômain',
'name' => 'John Doe',
'locale' => 'en-US',
],
],
]);
$answer = $oauth->user_create([]);
$this->assertSame($answer, [
'user_name' => 'John Doe',
'user_email' => 'jdoe@xn--fak-dma.xn--dmain-6ta',
'language' => 'en_US',
]);
}
/**
* Test invalid properties in user_create
*/
public function test_invalid_user_create()
{
$oauth = new \rcmail_oauth();
$oauth->init();
// fake identity
setProperty($oauth, 'login_phase', [
'token' => [
'identity' => [
'email' => 'bad-domain',
'name' => 'John Doe',
'locale' => '/martian',
],
],
]);
StderrMock::start();
$answer = $oauth->user_create([]);
StderrMock::stop();
// only user_name can be defined
$this->assertSame($answer, ['user_name' => 'John Doe']);
$this->assertStringContainsString('ignoring invalid email', StderrMock::$output);
$this->assertStringContainsString('ignoring language', StderrMock::$output);
}
/**
* Test refresh_access_token() method
*/
public function test_refresh_access_token()
{
// FIXME
$this->markTestIncomplete();
}
}