Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #65 #66

Merged
merged 14 commits into from
Oct 26, 2018
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ cache:
install:
- composer install --no-interaction
- mkdir -p example/idp_metadata
- make -C example
- php bin/download_idp_metadata.php ./example/idp_metadata

script:
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,13 @@ $settings = array(
],
'sp_org_name' => 'your organization full name',
'sp_org_display_name' => 'your organization display name',
'sp_key_cert_values' => [
'countryName' => 'Your Country',
'stateOrProvinceName' => 'Your Province or State',
'localityName' => 'Locality',
'commonName' => 'Name',
'emailAddress' => '[email protected]',
]
'idp_metadata_folder' => '/path/to/idp_metadata/',
'sp_attributeconsumingservice' => [
// order is important ! the 0-base index in this array will be used as ID in the calls
Expand Down Expand Up @@ -196,10 +203,12 @@ The method will redirect to the IdP Single Logout page, or return false if you a
|getSPMetadata() : string|returns the SP metadata as a string|
|login(string $idpFilename, int $assertID, int $attrID, $level = 1, string $redirectTo = null, $shouldRedirect = true)|login with REDIRECT binding. Use `$idpFilename` to select in IdP for login by indicating the name (without extension) of an XML file in your `idp_metadata_folder`. `$assertID` and `$attrID` indicate respectively the array index of `sp_assertionconsumerservice` and `sp_attributeconsumingservice` provided in settings. Optional parameters: `$level` for SPID authentication level (1, 2 or 3), `$redirectTo` to indicate an url to redirect to after login, `$shouldRedirect` to indicate if the login function should automatically redirect to the IdP or should return the login url as a string|
|loginPost(string $idpName, int $ass, int $attr, $level = 1, string $redirectTo = null, $shouldRedirect = true)|like login, but uses POST binding|
|public function logout(int $slo, string $redirectTo = null, $shouldRedirect = true)|logout with REDIRECT binding. `$slo` indicates the array index of the `sp_singlelogoutservice` provided in settings. Optional parameters: `$redirectTo` to indicate an url to redirect to after login, `$shouldRedirect` to indicate if the login function should automatically redirect to the IdP or should return the login url as a string|
|logout(int $slo, string $redirectTo = null, $shouldRedirect = true)|logout with REDIRECT binding. `$slo` indicates the array index of the `sp_singlelogoutservice` provided in settings. Optional parameters: `$redirectTo` to indicate an url to redirect to after login, `$shouldRedirect` to indicate if the login function should automatically redirect to the IdP or should return the login url as a string|
|logoutPost(int $slo, string $redirectTo = null, $shouldRedirect = true)|like logout, but uses POST binding|
|isAuthenticated() : bool|checks if the user is authenticated. This method **MUST** be caled after login and logout to finalise the operation.|
|getAttributes() : array|If you requested attributes with an attribute consuming service during login, this method will return them in array format|
|isConfigured() : bool|Returns true if the SP certificates are found where the settings says they are, and they are valid (i.e. the library has been configured correctly)|
|configure(string $countryName, string $stateName, string $localityName, string $commonName, string $emailAddress)|Generates the SP key and certificate (validity = 10 years) where the settings says they should be; this function should be used with care because it requires write access to the filessystem, and invalidates the metadata|

### Example

Expand Down Expand Up @@ -339,7 +348,6 @@ cd vendor/italia/spid-php-lib
Install prerequisites with composer, generate key and certificate for the SP and download the metadata for all current production IdPs with:
```sh
composer install
make -C example/
bin/download_idp_metadata.php example/idp_metadata
```

Expand Down
7 changes: 7 additions & 0 deletions example/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
],
'sp_org_name' => 'test',
'sp_org_display_name' => 'Test',
'sp_key_cert_values' => [
'countryName' => 'IT',
'stateOrProvinceName' => 'Milan',
'localityName' => 'Milan',
'commonName' => 'Name',
'emailAddress' => '[email protected]',
],
'idp_metadata_folder' => './idp_metadata/',
'sp_attributeconsumingservice' => [
["name", "familyName", "fiscalNumber", "email"],
Expand Down
8 changes: 4 additions & 4 deletions src/Sp.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,25 @@ class Sp
*/
private $protocol;

public function __construct(array $settings, String $protocol = null)
public function __construct(array $settings, String $protocol = null, $autoconfigure = true)
{
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
switch ($protocol) {
case 'saml':
$this->protocol = new Spid\Saml($settings);
$this->protocol = new Spid\Saml($settings, $autoconfigure);
break;
default:
$this->protocol = new Spid\Saml($settings);
$this->protocol = new Spid\Saml($settings, $autoconfigure);
}
}

public function __call($method, $arguments)
{
$methods_implemented = get_class_methods($this->protocol);
if (!in_array($method, $methods_implemented)) {
throw new \Exception("Invalid method requested", 1);
throw new \Exception("Invalid method [$method] requested", 1);
}
return call_user_func_array(array($this->protocol, $method), $arguments);
}
Expand Down
12 changes: 11 additions & 1 deletion src/Spid/Interfaces/SAMLInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,24 @@ interface SAMLInterface
// ],
// 'sp_org_name' => 'your organization full name',
// 'sp_org_display_name' => 'your organization display name',
// 'sp_key_cert_values' => [
// 'countryName' => 'CN',
// 'stateOrProvinceName' => 'State',
// 'localityName' => 'Locality',
// 'commonName' => 'Name',
// 'emailAddress' => '[email protected]',
// ],
// 'idp_metadata_folder' => '/path/to/idp_metadata/',
// 'sp_attributeconsumingservice' => [
// // order is important ! the 0-base index in this array will be used as ID in the calls
// ["fiscalNumber"],
// ["name", "familyName", "fiscalNumber", "email", "spidCode"],
// ...
// ];
public function __construct(array $settings);
//
// $autoconfigure: boolean value, determines if SP key and cert files should be autogenerated base on values provided in settings
// If set to false this step will be skipped
public function __construct(array $settings, $autoconfigure = true);

// loads an Idp object by parsing the provided XML at $filename
// $filename: file name of the IdP to be loaded. Only the file, without the path, needs to be provided.
Expand Down
48 changes: 46 additions & 2 deletions src/Spid/Saml.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@
use Italia\Spid\Spid\Saml\Settings;
use Italia\Spid\Spid\Saml\SignatureUtils;
use Italia\Spid\Spid\Interfaces\SAMLInterface;
use Italia\Spid\Spid\Session;

class Saml implements SAMLInterface
{
public $settings;
private $idps = []; // contains filename -> Idp object array
private $session; // Session object

public function __construct(array $settings)
public function __construct(array $settings, $autoconfigure = true)
{
Settings::validateSettings($settings);
$this->settings = $settings;

// Do not attemp autoconfiguration if key and cert values have not been set
if (!array_key_exists('sp_key_cert_values', $this->settings)) {
$autoconfigure = false;
}
if ($autoconfigure && !$this->isConfigured()) {
$this->configure();
}
}

public function loadIdpFromFile(string $filename)
Expand Down Expand Up @@ -229,4 +238,39 @@ public function getAttributes() : array
}
return $this->session->attributes;
}
}

// returns true if the SP certificates are found where the settings says they are, and they are valid
// (i.e. the library has been configured correctly
private function isConfigured() : bool
{
if (!is_readable($this->settings['sp_key_file'])) {
return false;
}
if (!is_readable($this->settings['sp_cert_file'])) {
return false;
}
$key = file_get_contents($this->settings['sp_key_file']);
if (!openssl_get_privatekey($key)) {
return false;
}
$cert = file_get_contents($this->settings['sp_cert_file']);
if (!openssl_get_publickey($cert)) {
return false;
}
if (!SignatureUtils::certDNEquals($cert, $this->settings)) {
return false;
}
return true;
}

// Generates with openssl the SP certificates where the settings says they should be
// this function should be used with care because it requires write access to the filesystem, and invalidates the metadata
private function configure()
{
$keyCert = SignatureUtils::generateKeyCert($this->settings);
file_put_contents($this->settings['sp_key_file'], $keyCert['key']);
file_put_contents($this->settings['sp_cert_file'], $keyCert['cert']);
}


}
72 changes: 62 additions & 10 deletions src/Spid/Saml/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@ class Settings
const BINDING_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect';
const BINDING_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST';

const REQUIRED = 1;
const NOT_REQUIRED = 0;
// Settings with value 1 are mandatory
private static $validSettings = [
'sp_entityid' => 1,
'sp_key_file' => 1,
'sp_cert_file' => 1,
'sp_assertionconsumerservice' => 1,
'sp_singlelogoutservice' => 1,
'sp_attributeconsumingservice' => 0,
'sp_org_name' => 0,
'sp_org_display_name' => 0,
'idp_metadata_folder' => 1
'sp_entityid' => self::REQUIRED,
'sp_key_file' => self::REQUIRED,
'sp_cert_file' => self::REQUIRED,
'sp_assertionconsumerservice' => self::REQUIRED,
'sp_singlelogoutservice' => self::REQUIRED,
'sp_attributeconsumingservice' => self::NOT_REQUIRED,
'sp_org_name' => self::NOT_REQUIRED,
'sp_org_display_name' => self::NOT_REQUIRED,
'sp_key_cert_values' => [
self::NOT_REQUIRED => [
'countryName' => self::REQUIRED,
'stateOrProvinceName' => self::REQUIRED,
'localityName' => self::REQUIRED,
'commonName' => self::REQUIRED,
'emailAddress' => self::REQUIRED
]
],
'idp_metadata_folder' => self::REQUIRED
];

private static $validAttributeFields = [
Expand Down Expand Up @@ -45,8 +56,23 @@ public static function validateSettings(array $settings)
$missingSettings = array();
$msg = 'Missing settings fields: ';
array_walk(self::$validSettings, function ($v, $k) use (&$missingSettings, &$settings) {
if (self::$validSettings[$k] == 1 && !array_key_exists($k, $settings)) {
$settingRequired = self::$validSettings[$k];
$childSettings = [];
if (is_array($v) && isset($v[self::REQUIRED])) {
$settingRequired = self::REQUIRED;
$childSettings[$k] = $v[self::REQUIRED];
}
if ($settingRequired == self::REQUIRED && !array_key_exists($k, $settings)) {
$missingSettings[$k] = 1;
} else {
foreach ($childSettings as $key => $value) {
if (
$value == self::REQUIRED &&
!array_key_exists($key, $settings[$k])
) {
$missingSettings[$key] = 1;
}
}
}
});
foreach ($missingSettings as $k => $v) {
Expand All @@ -57,6 +83,16 @@ public static function validateSettings(array $settings)
}

$invalidFields = array_diff_key($settings, self::$validSettings);
// Check for settings that have child values
array_walk(self::$validSettings, function($v, $k) use (&$invalidFields) {
// Child values found, check if settings array is set for that key
if (is_array($v) && isset($settings[$k])) {
// $v has at most 2 keys, self::REQUIRED and self::NOT_REQUIRED
// do array_dif_key for both sub arrays
$invalidFields = array_merge($invalidFields, array_diff_key($settings[$k], reset($v)));
$invalidFields = array_merge($invalidFields, array_diff_key($settings[$k], end($v)));
}
});
$msg = 'Invalid settings fields: ';
foreach ($invalidFields as $k => $v) {
$msg .= $k . ', ';
Expand Down Expand Up @@ -147,5 +183,21 @@ private static function checkSettingsValues($settings)
', got ' . parse_url($slo[0], PHP_URL_HOST) . 'instead');
}
});
if (isset($settings['sp_key_cert_values'])) {
if (!is_array($settings['sp_key_cert_values'])) {
throw new \Exception('sp_key_cert_values should be an array');
}
if (count($settings['sp_key_cert_values']) != 5) {
throw new \Exception('sp_key_cert_values should contain 5 values: countryName, stateOrProvinceName, localityName, commonName, emailAddress');
}
foreach ($settings['sp_key_cert_values'] as $key => $value) {
if (!is_string($value)) {
throw new \Exception('sp_key_cert_values values should be strings. Valued provided for key ' . $key . ' is not a string');
}
}
if (strlen($settings['sp_key_cert_values']['countryName']) != 2) {
throw new \Exception('sp_key_cert_values countryName should be a 2 characters country code');
}
}
}
}
46 changes: 46 additions & 0 deletions src/Spid/Saml/SignatureUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,52 @@ public static function validateXmlSignature($xml, $cert) : bool
return false;
}

public static function certDNEquals($cert, $settings)
{
$parsed = openssl_x509_parse($cert);
$dn = $parsed['subject'];

$newDN = array();
$newDN[] = $settings['sp_org_name'] ?? [];
$newDN[] = $settings['sp_org_display_name'] ?? [];
$newDN = array_merge($newDN, $settings['sp_key_cert_values'] ?? []);
asort($dn);
asort($newDN);

if (array_values($dn) == array_values($newDN)) {
return true;
}
return false;
}

public static function generateKeyCert($settings) : array
{
$numberofdays = 3652 * 2;
$privkey = openssl_pkey_new(array(
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
));
$dn = array(
"countryName" => $settings['sp_key_cert_values']['countryName'],
"stateOrProvinceName" => $settings['sp_key_cert_values']['stateOrProvinceName'],
"localityName" => $settings['sp_key_cert_values']['localityName'],
"organizationName" => $orgName = $settings['sp_org_name'],
"organizationalUnitName" => $settings['sp_org_display_name'],
"commonName" => $settings['sp_key_cert_values']['commonName'],
"emailAddress" => $settings['sp_key_cert_values']['emailAddress']
);
$csr = openssl_csr_new($dn, $privkey, array('digest_alg' => 'sha256'));
$myserial = (int) hexdec(bin2hex(openssl_random_pseudo_bytes(8)));
$configArgs = array("digest_alg" => "sha256");
$sscert = openssl_csr_sign($csr, null, $privkey, $numberofdays, $configArgs, $myserial);
openssl_x509_export($sscert, $publickey);
openssl_pkey_export($privkey, $privatekey);
return [
'key' => $privatekey,
'cert' => $publickey
];
}

private static function query(\DOMDocument $dom, $query, \DOMElement $context = null)
{
$xpath = new \DOMXPath($dom);
Expand Down
32 changes: 32 additions & 0 deletions tests/SpTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ final class SpTest extends PHPUnit\Framework\TestCase
],
'sp_org_name' => 'test_simevo',
'sp_org_display_name' => 'Test Simevo',
'sp_key_cert_values' => [
'countryName' => 'IT',
'stateOrProvinceName' => 'Milan',
'localityName' => 'Milan',
'commonName' => 'Name',
'emailAddress' => '[email protected]',
],
'idp_metadata_folder' => './example/idp_metadata/',
'sp_attributeconsumingservice' => [
["name", "familyName", "fiscalNumber", "email"],
Expand All @@ -29,6 +36,31 @@ public function testCanBeCreatedFromValidSettings()
Italia\Spid\Sp::class,
new Italia\Spid\Sp(SpTest::$settings)
);
$this->assertTrue(is_readable(self::$settings['sp_key_file']));
$this->assertTrue(is_readable(self::$settings['sp_cert_file']));
}

public function testCanBeCreatedWithoutAutoconfigure()
{
$settings = SpTest::$settings;
$settings['sp_key_file'] = './wrong/location/sp.key';
$settings['sp_cert_file'] = './wrong/location/sp.crt';
$this->assertInstanceOf(
Italia\Spid\Sp::class,
new Italia\Spid\Sp(SpTest::$settings, null, false)
);
$this->assertFalse(is_readable($settings['sp_key_file']));
$this->assertFalse(is_readable($settings['sp_cert_file']));
}

public function testCannotCreateNoKeyCert()
{
$this->assertInstanceOf(
Italia\Spid\Sp::class,
new Italia\Spid\Sp(SpTest::$settings, null, false)
);
$this->assertTrue(is_readable(self::$settings['sp_key_file']));
$this->assertTrue(is_readable(self::$settings['sp_cert_file']));
}

private function validateXml($xmlString, $schemaFile, $valid = true)
Expand Down