From d35787079f975c3c9821c035893de06ffb10b4a0 Mon Sep 17 00:00:00 2001 From: Olav Morken <olav.morken@uninett.no> Date: Thu, 15 Jul 2010 10:29:51 +0000 Subject: [PATCH] New authentication module: authX509 git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2421 44740490-163a-0410-bde0-09ae8108e29a --- dictionaries/errors.definition.json | 18 ++ dictionaries/errors.translation.json | 18 ++ docs/simplesamlphp-idp.txt | 3 + modules/authX509/default-disable | 0 .../dictionaries/X509error.definition.json | 8 + .../dictionaries/X509error.translation.json | 8 + modules/authX509/docs/authX509.txt | 106 ++++++++ .../authX509/lib/Auth/Source/X509userCert.php | 233 ++++++++++++++++++ modules/authX509/templates/X509error.php | 42 ++++ modules/ldap/lib/ConfigHelper.php | 49 +++- 10 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 modules/authX509/default-disable create mode 100644 modules/authX509/dictionaries/X509error.definition.json create mode 100644 modules/authX509/dictionaries/X509error.translation.json create mode 100644 modules/authX509/docs/authX509.txt create mode 100644 modules/authX509/lib/Auth/Source/X509userCert.php create mode 100644 modules/authX509/templates/X509error.php diff --git a/dictionaries/errors.definition.json b/dictionaries/errors.definition.json index 7e10e8c87..5d62394ff 100644 --- a/dictionaries/errors.definition.json +++ b/dictionaries/errors.definition.json @@ -235,5 +235,23 @@ }, "descr_RESPONSESTATUSNOSUCCESS": { "en": "The Identity Provider responded with an error. (The status code in the SAML Response was not success)" + }, + "title_NOCERT": { + "en": "No certificate" + }, + "descr_NOCERT": { + "en": "Authentication failed: your browser did not send any certificate" + }, + "title_INVALIDCERT": { + "en": "Invalid certificate" + }, + "descr_INVALIDCERT": { + "en": "Authentication failed: the certificate your browser sent is invalid or cannot be read" + }, + "title_UNKNOWNCERT": { + "en": "Unknown certificate" + }, + "descr_UNKNOWNCERT": { + "en": "Authentication failed: the certificate your browser sent is unknown" } } diff --git a/dictionaries/errors.translation.json b/dictionaries/errors.translation.json index 20f0d0100..a89cee59a 100644 --- a/dictionaries/errors.translation.json +++ b/dictionaries/errors.translation.json @@ -1433,5 +1433,23 @@ "pt": "O Fornecedor de Identidade respondeu com um erro. (A resposta SAML cont\u00e9m um c\u00f3digo de insucesso)", "fr": "Le fournisseur d'identit\u00e9 a renvoy\u00e9 une erreur (le code de statut de la r\u00e9ponse SAML n'indiquait pas le succ\u00e8s)", "hr": "Sustav jedinstvene autentikacije poslao je odgovor koji sadr\u017ei informaciju o pojavi gre\u0161ke. (\u0160ifra statusa dostavljena u SAML odgovoru ne odgovara \u0161ifri uspje\u0161no obra\u0111enog zahtjeva)" + }, + "title_NOCERT": { + "fr": "Aucun certificat pr\u00e9sent\u00e9" + }, + "descr_NOCERT": { + "fr": "Echec de l'authentification: votre navigateur n'a pas pr\u00e9sent\u00e9 de certificat" + }, + "title_INVALIDCERT": { + "fr": "Certificat invalide" + }, + "descr_INVALIDCERT": { + "fr": "Echec de l'authentification: le certificat pr\u00e9sent\u00e9 par votre navigateur est invalide ou illisible" + }, + "title_UNKNOWNCERT": { + "fr": "Certificat inconnu" + }, + "descr_UNKNOWNCERT": { + "fr": "Echec de l'authentification: le certificat pr\u00e9sent\u00e9 par votre navigateur n'est pas connu" } } diff --git a/docs/simplesamlphp-idp.txt b/docs/simplesamlphp-idp.txt index eedc96578..805a8cbb5 100644 --- a/docs/simplesamlphp-idp.txt +++ b/docs/simplesamlphp-idp.txt @@ -28,6 +28,9 @@ Authentication module The next step is to configure the way users authenticate on your IdP. Various modules in the `modules/` directory provides methods for authenticating your users. This is an overview of those that are included in the simpleSAMLphp distribution: +[`authX509:authX509userCert`](./authX509:authX509) +: Authenticate against a LDAP database with a SSL client certificate. + `exampleauth:UserPass` : Authenticate against a list of usernames and passwords. diff --git a/modules/authX509/default-disable b/modules/authX509/default-disable new file mode 100644 index 000000000..e69de29bb diff --git a/modules/authX509/dictionaries/X509error.definition.json b/modules/authX509/dictionaries/X509error.definition.json new file mode 100644 index 000000000..9bd4897b9 --- /dev/null +++ b/modules/authX509/dictionaries/X509error.definition.json @@ -0,0 +1,8 @@ +{ + "certificate_header": { + "en": "X509 certificate authentication" + }, + "certificate_text": { + "en": "X509 certificate authentication is required to access this service." + } +} diff --git a/modules/authX509/dictionaries/X509error.translation.json b/modules/authX509/dictionaries/X509error.translation.json new file mode 100644 index 000000000..09b7b45d9 --- /dev/null +++ b/modules/authX509/dictionaries/X509error.translation.json @@ -0,0 +1,8 @@ +{ + "certificate_header": { + "fr": "Authentification par certificat X509" + }, + "certificate_text": { + "fr": "Un certificat X509 est requis pour acc\u00e9der \u00e0 ce service." + } +} diff --git a/modules/authX509/docs/authX509.txt b/modules/authX509/docs/authX509.txt new file mode 100644 index 000000000..0364cf296 --- /dev/null +++ b/modules/authX509/docs/authX509.txt @@ -0,0 +1,106 @@ +Using the X509 authentication source with simpleSAMLphp +======================================================= + +The authX509 module provide X509 authentication with certificate +validation. For now there is only one authentication source: + +* authX509userCert Validate against LDAP userCertificate attribute + +More validation schemes (OCSP, CRL, local list) might be added later. + +Configuring Apache +------------------ + +This module assumes that the server requests a client certificate, and +stores it in the environment variable SSL_CLIENT_CERT. This can be achieved +with such a configuration: + + SSLEngine on + SSLCertificateFile /etc/openssl/certs/server.crt + SSLCertificateKeyFile /etc/openssl/private/server.key + SSLCACertificateFile /etc/openssl/certs/ca.crt + SSLVerifyClient require + SSLVerifyDepth 2 + SSLOptions +ExportCertData + +Note that SSLVerifyClient can be set to optional if you want to support +both certificate and plain login authentication at the same time (more on +this later). + +If your server or your client (or both!) have TLS renegociation disabled +as a workaround for CVE-2009-3555, then the configuration directive above +must not appear in a <Directory>, <Location>, or in a name-based +<VirtualHost>. You can only use them server-wide, or in <VirtualHost> +with different IP address/port combinaisons. + + +Setting up the authX509 module +------------------------------ + +The first thing you need to do is to enable the cas module: + + touch modules/authX509/enable + +Then you must add it as an authentication source. Here is an +example authsource.php + + 'x509' => array( + 'authX509:authX509userCert', + 'hostname' => 'ldaps://ldap.example.net', + 'enable_tls' => FALSE, + 'attributes' => array("cn", "uid", "mail", "ou", "sn"), + 'search.enable' => TRUE, + 'search.attributes' => array('uid', 'mail'), + 'search.base' => 'dc=example,dc=net', + 'x509attributes' => array('UID' => 'uid'), + 'ldapusercert' => array('userCertificate;binary'), + ), + +The configuration is the same as for the LDAP module, except for +two options: + +* x509attributes is used to map a certificate subject attribute to + an LDAP attribute. It is used to find the certificate + owner in LDAP from the certificate subject. If multiple + mappings are provided, any mappping will match (this + is a logical OR). Default is array('UID' => 'uid') +* ldapusercert the LDAP attribute in which the user certificate will + be found. Default is userCertificate;binary + + +Uploading certificate in LDAP +----------------------------- + +Certificate are usually stored in LDAP as DER, in binary. Here is +how to convert from PEM to DER: + + openssl x509 -in cert.pem -inform PEM -outform DER -out cert.der + +Here is some LDIF to upload the certificate in the directory: + + dn: uid=jdoe,dc=example,dc=net + changetype: modify + add: userCertificate;binary + userCertificate;binary:< file:///path/to/cert.der + + +Supporting both certificate and login authentications +===================================================== + +In your Apache configuration, set SSLVerifyClient to optional. Then you +can hack your metadata/saml20-idp-hosted.php file that way: + + $auth_source = empty($_SERVER['SSL_CLIENT_CERT']) ? 'ldap' : 'x509'; + $metadata = array( + '__DYNAMIC:1__' => array( + 'host' => '__DEFAULT__', + 'privatekey' => 'server.key', + 'certificate' => 'server.crt', + 'auth' => $auth_source, + 'authority' => 'login', + 'userid.attribute' => 'uid', + 'logouttype' => 'iframe', + 'AttributeNameFormat' => + 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', + ) + diff --git a/modules/authX509/lib/Auth/Source/X509userCert.php b/modules/authX509/lib/Auth/Source/X509userCert.php new file mode 100644 index 000000000..3ddd7e411 --- /dev/null +++ b/modules/authX509/lib/Auth/Source/X509userCert.php @@ -0,0 +1,233 @@ +<?php + +/** + * This class implements x509 certificate authentication with + * certificate validation against an LDAP directory. + * + * @author Emmanuel Dreyfus <manu@netbsd.org> + * @package simpleSAMLphp + * @version $Id$ + */ +class sspmod_authX509_Auth_Source_X509userCert extends SimpleSAML_Auth_Source { + + /** + * x509 attributes to use from the certificate + * for searching the user in the LDAP directory. + */ + private $x509attributes = array('UID' => 'uid'); + + + /** + * LDAP attribute containing the user certificate + */ + private $ldapusercert = array('userCertificate;binary'); + + + /** + * LDAPConfigHelper object + */ + private $ldapcf; + + + /** + * Constructor for this authentication source. + * + * All subclasses who implement their own constructor must call this + * constructor before using $config for anything. + * + * @param array $info Information about this authentication source. + * @param array &$config Configuration for this authentication source. + */ + public function __construct($info, &$config) { + assert('is_array($info)'); + assert('is_array($config)'); + + if (isset($config['authX509:x509attributes'])) + $this->x509attributes = + $config['authX509:x509attributes']; + + if (isset($config['authX509:ldapusercert'])) + $this->ldapusercert = + $config['authX509:ldapusercert']; + + parent::__construct($info, $config); + + $this->ldapcf = new sspmod_ldap_ConfigHelper($config, + 'Authentication source ' . var_export($this->authId, TRUE)); + + return; + } + + + /** + * Convert certificate from PEM to DER + * + * @param array $pem_data PEM-encoded certificate + */ + private function pem2der($pem_data) { + $begin = "CERTIFICATE-----"; + $end = "-----END"; + $pem_data = substr($pem_data, + strpos($pem_data, $begin)+strlen($begin)); + $pem_data = substr($pem_data, 0, strpos($pem_data, $end)); + $der = base64_decode($pem_data); + return $der; + } + + + /** + * Convert certificate from DER to PEM + * + * @param array $der_data DER-encoded certificate + */ + private function der2pem($der_data) { + $pem = chunk_split(base64_encode($der_data), 64, "\n"); + $pem = "-----BEGIN CERTIFICATE-----\n".$pem. + "-----END CERTIFICATE-----\n"; + return $pem; + } + + + /** + * Finish a failed authentication. + * + * This function can be overloaded by a child authentication + * class that wish to perform some operations on failure + * + * @param array &$state Information about the current authentication. + */ + public function authFailed(&$state) { + $id = SimpleSAML_Auth_State::saveState($state, self::STAGEID); + $params = array('AuthState' => $id); + $config = SimpleSAML_Configuration::getInstance(); + + $t = new SimpleSAML_XHTML_Template($config, + 'authX509:X509error.php'); + $t->data['stateparams'] = $params; + $t->data['errorcode'] = $state['authX509.error']; + + $t->show(); + exit(); + } + + + /** + * Validate certificate and login + * + * This function try to validate the certificate. + * On success, the user is logged in without going through + * o login page. + * On failure, The authX509:X509error.php template is + * loaded. + * + * @param array &$state Information about the current authentication. + */ + public function authenticate(&$state) { + assert('is_array($state)'); + $ldapcf = $this->ldapcf; + + if (!isset($_SERVER['SSL_CLIENT_CERT']) || + ($_SERVER['SSL_CLIENT_CERT'] == '')) { + $state['authX509.error'] = "NOCERT"; + $this->authFailed($state); + assert('FALSE'); /* NOTREACHED */ + return; + } + + $client_cert = $_SERVER['SSL_CLIENT_CERT']; + $client_cert_data = openssl_x509_parse($client_cert); + if ($client_cert_data == FALSE) { + SimpleSAML_Logger::error('authX509: invalid cert'); + $state['authX509.error'] = "INVALIDCERT"; + $this->authFailed($state); + + assert('FALSE'); /* NOTREACHED */ + return; + } + + $dn = FALSE; + foreach ($this->x509attributes as $x509_attr => $ldap_attr) { + /* value is scalar */ + $value = $client_cert_data['subject'][$x509_attr]; + SimpleSAML_Logger::info('authX509: cert '. + $x509_attr.' = '.$value); + $dn = $ldapcf->searchfordn($ldap_attr, $value, TRUE); + if ($dn !== FALSE) + break; + } + + if ($dn === FALSE) { + SimpleSAML_Logger::error('authX509: cert has '. + 'no matching user in LDAP'); + $state['authX509.error'] = "UNKNOWNCERT"; + $this->authFailed($state); + + assert('FALSE'); /* NOTREACHED */ + return; + } + + $ldap_certs = $ldapcf->getAttributes($dn, $this->ldapusercert); + if ($ldap_certs === FALSE) { + SimpleSAML_Logger::error('authX509: no certificate '. + 'found in LDAP for dn='.$dn); + $state['authX509.error'] = "UNKNOWNCERT"; + $this->authFailed($state); + + assert('FALSE'); /* NOTREACHED */ + return; + } + + + $merged_ldapcerts = array(); + foreach ($this->ldapusercert as $attr) + $merged_ldapcerts = array_merge($merged_ldapcerts, + $ldap_certs[$attr]); + $ldap_certs = $merged_ldapcerts; + + foreach ($ldap_certs as $ldap_cert) { + $pem = $this->der2pem($ldap_cert); + $ldap_cert_data = openssl_x509_parse($pem); + if($ldap_cert_data == FALSE) { + SimpleSAML_Logger::error('authX509: cert in '. + 'LDAP in invalid for '. + 'dn = '.$dn); + continue; + } + + if ($ldap_cert_data === $client_cert_data) { + $attributes = $ldapcf->getAttributes($dn); + assert('is_array($attributes)'); + $state['Attributes'] = $attributes; + $this->authSuccesful($state); + + assert('FALSE'); /* NOTREACHED */ + return; + } + } + + SimpleSAML_Logger::error('authX509: no matching cert in '. + 'LDAP for dn = '.$dn); + $state['authX509.error'] = "UNKNOWNCERT"; + $this->authFailed($state); + + assert('FALSE'); /* NOTREACHED */ + return; + } + + + /** + * Finish a succesfull authentication. + * + * This function can be overloaded by a child authentication + * class that wish to perform some operations after login. + * + * @param array &$state Information about the current authentication. + */ + public function authSuccesful(&$state) { + SimpleSAML_Auth_Source::completeAuth($state); + + assert('FALSE'); /* NOTREACHED */ + return; + } + +} diff --git a/modules/authX509/templates/X509error.php b/modules/authX509/templates/X509error.php new file mode 100644 index 000000000..cded8b864 --- /dev/null +++ b/modules/authX509/templates/X509error.php @@ -0,0 +1,42 @@ +<?php +$this->data['header'] = $this->t('{authX509:X509error:certificate_header}'); + +$this->includeAtTemplateBase('includes/header.php'); + +?> + +<?php +if ($this->data['errorcode'] !== NULL) { +?> + <div style="border-left: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8; background: #f5f5f5"> + <img src="/<?php echo $this->data['baseurlpath']; ?>resources/icons/experience/gtk-dialog-error.48x48.png" style="float: left; margin: 15px " /> + <h2><?php echo $this->t('{login:error_header}'); ?></h2> + <p><b><?php echo $this->t('{errors:title_' . $this->data['errorcode'] . '}'); ?></b></p> + <p><?php echo $this->t('{errors:descr_' . $this->data['errorcode'] . '}'); ?></p> + </div> +<?php +} +?> + <h2 style="break: both"><?php echo $this->t('{authX509:X509error:certificate_header}'); ?></h2> + + <p><?php echo $this->t('{authX509:X509error:certificate_text}'); ?></p> + + <a href="<?php echo SimpleSAML_Utilities::selfURL(); ?>"> + <?php echo $this->t('{login:login_button}'); ?> + </a> + +<?php + +if(!empty($this->data['links'])) { + echo '<ul class="links" style="margin-top: 2em">'; + foreach($this->data['links'] AS $l) { + echo '<li><a href="' . htmlspecialchars($l['href']) . '">' . htmlspecialchars($this->t($l['text'])) . '</a></li>'; + } + echo '</ul>'; +} + + + + +$this->includeAtTemplateBase('includes/footer.php'); +?> diff --git a/modules/ldap/lib/ConfigHelper.php b/modules/ldap/lib/ConfigHelper.php index fea883ea5..f9aef64db 100644 --- a/modules/ldap/lib/ConfigHelper.php +++ b/modules/ldap/lib/ConfigHelper.php @@ -212,7 +212,52 @@ class sspmod_ldap_ConfigHelper { return $ldap->getAttributes($dn, $this->attributes); } -} + /** + * Search for a DN. + * + * @param string|array $attribute + * The attribute name(s) searched for. If set to NULL, values from + * configuration is used. + * @param string $value + * The attribute value searched for. + * @param bool $allowZeroHits + * Determines if the method will throw an exception if no + * hits are found. Defaults to FALSE. + * @return string + * The DN of the matching element, if found. If no element was + * found and $allowZeroHits is set to FALSE, an exception will + * be thrown; otherwise NULL will be returned. + * @throws SimpleSAML_Error_AuthSource if: + * - LDAP search encounter some problems when searching cataloge + * - Not able to connect to LDAP server + * @throws SimpleSAML_Error_UserNotFound if: + * - $allowZeroHits er TRUE and no result is found + * + */ + public function searchfordn($attribute, $value, $allowZeroHits) { + $ldap = new SimpleSAML_Auth_LDAP($this->hostname, + $this->enableTLS, + $this->debug, + $this->timeout); + + if ($attribute == NULL) + $attribute = $this->searchAttributes; -?> \ No newline at end of file + return $ldap->searchfordn($this->searchBase, $attribute, + $value, $allowZeroHits); + } + + public function getAttributes($dn, $attributes = NULL) { + if ($attributes == NULL) + $attributes = $this->attributes; + + $ldap = new SimpleSAML_Auth_LDAP($this->hostname, + $this->enableTLS, + $this->debug, + $this->timeout); + + return $ldap->getAttributes($dn, $attributes); + } + +} -- GitLab