openstackid/tests/OIDCPasswordlessTest.php

557 lines
20 KiB
PHP

<?php namespace Tests;
/**
* Copyright 2021 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Mail\OAuth2PasswordlessOTPMail;
use Illuminate\Support\Facades\Mail;
use jwe\IJWE;
use jwk\impl\RSAJWKFactory;
use jwk\JSONWebKeyPublicKeyUseValues;
use jws\IJWS;
use LaravelDoctrine\ORM\Facades\EntityManager;
use Models\OAuth2\Client;
use Models\OAuth2\OAuth2OTP;
use OAuth2\OAuth2Protocol;
use utils\factories\BasicJWTFactory;
use Utils\Services\UtilsServiceCatalog;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Session;
/**
* Class OIDCPasswordlessTest
* @package Tests
*/
class OIDCPasswordlessTest extends OpenStackIDBaseTest
{
/**
* @var Client
*/
public static $client = null;
protected function setUp():void
{
parent::setUp();
$client_repository = EntityManager::getRepository(Client::class);
$clients = $client_repository->findAll();
self::$client = $clients[0];
self::$client->enablePasswordless();
self::$client->setOtpLifetime(60 * 3);
self::$client->setOtpLength(6);
self::$client->setTokenEndpointAuthMethod(OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic);
EntityManager::persist(self::$client);
}
protected function tearDown():void
{
parent::tearDown();
}
/**
* @var string
*/
private $current_realm;
protected function prepareForTests()
{
parent::prepareForTests();
App::singleton(UtilsServiceCatalog::ServerConfigurationService, StubServerConfigurationService::class);
$this->current_realm = Config::get('app.url');
Session::start();
}
public function testCodeEmailFlowErrorScopes(){
$scope = sprintf('%s profile email',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// ask for wider scopes
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
$content = $response->getContent();
$response = json_decode($content);
$this->assertTrue(!empty($response->error));
}
public function testCodeEmailFlowError(){
$scope = sprintf('%s profile email',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendLink,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(400);
}
public function testCodeEmailFlowNoRefreshToken(){
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(200);
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$response = json_decode($content);
$access_token = $response->access_token;
$id_token = $response->id_token;
$this->assertTrue(!empty($access_token));
$this->assertTrue(!property_exists($response, "refresh_token"));
$this->assertTrue(!empty($id_token));
}
public function testCodeEmailFlowConsecutiveOTP(){
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp1 = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp1){
$otp1 = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp2 = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp2){
$otp2 = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
$repository = EntityManager::getRepository(OAuth2OTP::class);
$otp1 = $repository->getByValue($otp1);
$this->assertTrue(is_null($otp1));
$otp2 = $repository->getByValue($otp2);
$this->assertTrue(!is_null($otp2));
}
public function testCodeEmailFlowNarrowScopes(){
$scope = sprintf('%s profile email address',
OAuth2Protocol::OpenIdConnect_Scope,
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// ask for wider scopes
$scope = sprintf('%s profile email',
OAuth2Protocol::OpenIdConnect_Scope,
);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(200);
$content = $response->getContent();
$response = json_decode($content);
$this->assertTrue(!empty($response->id_token));
}
public function testCodeEmailFlow() {
$scope = sprintf('%s profile email address %s',
OAuth2Protocol::OpenIdConnect_Scope,
OAuth2Protocol::OfflineAccess_Scope
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp,
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(200);
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$response = json_decode($content);
$access_token = $response->access_token;
$refresh_token = $response->refresh_token;
$id_token = $response->id_token;
$this->assertTrue(!empty($access_token));
$this->assertTrue(!empty($refresh_token));
$this->assertTrue(!empty($id_token));
$jwt = BasicJWTFactory::build($id_token);
$use_enc = false;
if ($use_enc) {
$this->assertTrue($jwt instanceof IJWE);
$recipient_key = RSAJWKFactory::build
(
new RSAJWKPEMPrivateKeySpecification
(
TestSeeder::$client_private_key_1,
RSAJWKPEMPrivateKeySpecification::WithoutPassword,
$jwt->getJOSEHeader()->getAlgorithm()->getString()
)
);
$recipient_key->setKeyUse(JSONWebKeyPublicKeyUseValues::Encryption)->setId('recipient_public_key');
$jwt->setRecipientKey($recipient_key);
$payload = $jwt->getPlainText();
$jwt = BasicJWTFactory::build($payload);
$this->assertTrue($jwt instanceof IJWS);
}
return $access_token;
}
public function testInvalidRedeemCodeEmailFlow() {
$scope = sprintf('%s profile email address %s',
OAuth2Protocol::OpenIdConnect_Scope,
OAuth2Protocol::OfflineAccess_Scope
);
$params = [
'client_id' => self::$client->getClientId(),
'scope' => $scope,
OAuth2Protocol::OAuth2Protocol_Nonce => '123456',
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType_OTP,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessSend => OAuth2Protocol::OAuth2PasswordlessSendCode,
OAuth2Protocol::OAuth2PasswordlessEmail => "test@test.com",
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(200);
$otp = null;
Mail::assertNotQueued(OAuth2PasswordlessOTPMail::class, function(OAuth2PasswordlessOTPMail $email) use(&$otp){
$otp = $email->otp;
});
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$otp_response = json_decode($content);
$this->assertTrue($otp_response->scope == $scope);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp.'1',
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp.'2',
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
// exchange
$params = [
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_Passwordless,
OAuth2Protocol::OAuth2PasswordlessConnection => OAuth2Protocol::OAuth2PasswordlessConnectionEmail,
OAuth2Protocol::OAuth2PasswordlessEmail =>"test@test.com",
OAuth2Protocol::OAuth2Protocol_ResponseType_OTP => $otp.'3',
OAuth2Protocol::OAuth2Protocol_Scope => $scope
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
// Symfony interally prefixes headers with "HTTP", so
array("HTTP_Authorization" => " Basic " . base64_encode(self::$client->getClientId() . ':' . self::$client->getClientSecret())));
$this->assertResponseStatus(400);
$repository = EntityManager::getRepository(OAuth2OTP::class);
$otp = $repository->getByValue($otp);
$this->assertTrue(!is_null($otp));
$this->assertTrue(!$otp->isValid());
}
}