From 1fed9ea77ad8942041a87c62c7a601dd51463dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20=C3=85kre=20Solberg?= <andreas.solberg@uninett.no> Date: Fri, 14 Sep 2007 11:20:59 +0000 Subject: [PATCH] Initial commit of version 0.4 of simplesamlphp git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@2 44740490-163a-0410-bde0-09ae8108e29a --- COPYING | 22 + cert/server.crt | 16 + cert/server.pem | 15 + config/config-template.php | 48 + docs/README | 29 + docs/simplesamlphp-install.html | 182 +++ docs/simplesamlphp-install.pdf | Bin 0 -> 43359 bytes docs/source/simplesamlphp-install.xml | 588 +++++++ lib/SimpleSAML/Bindings/SAML20/HTTPPost.php | 213 +++ .../Bindings/SAML20/HTTPRedirect.php | 125 ++ lib/SimpleSAML/Bindings/Shib13/HTTPPost.php | 198 +++ lib/SimpleSAML/Configuration.php | 53 + lib/SimpleSAML/Session.php | 223 +++ lib/SimpleSAML/Utilities.php | 92 ++ lib/SimpleSAML/XHTML/Template.php | 45 + lib/SimpleSAML/XML/AuthnResponse.php | 115 ++ lib/SimpleSAML/XML/MetaDataStore.php | 105 ++ lib/SimpleSAML/XML/SAML20/AuthnRequest.php | 195 +++ lib/SimpleSAML/XML/SAML20/AuthnResponse.php | 528 +++++++ lib/SimpleSAML/XML/SAML20/LogoutRequest.php | 182 +++ lib/SimpleSAML/XML/SAML20/LogoutResponse.php | 152 ++ lib/SimpleSAML/XML/Shib13/AuthnRequest.php | 114 ++ lib/SimpleSAML/XML/Shib13/AuthnResponse.php | 398 +++++ lib/xmlseclibs.php | 1387 +++++++++++++++++ metadata-templates/saml20-idp-hosted.php | 37 + metadata-templates/saml20-idp-remote.php | 41 + metadata-templates/saml20-sp-hosted.php | 37 + metadata-templates/saml20-sp-remote.php | 84 + metadata-templates/shib13-idp-hosted.php | 16 + metadata-templates/shib13-idp-remote.php | 29 + metadata-templates/shib13-sp-hosted.php | 35 + metadata-templates/shib13-sp-remote.php | 32 + templates/error.php | 97 ++ templates/login.php | 124 ++ templates/post-debug.php | 91 ++ templates/post.php | 24 + templates/status.php | 111 ++ www/_include.php | 18 + www/auth/login-auto.php | 95 ++ www/auth/login-radius.php | 95 ++ www/auth/login.php | 98 ++ www/example-simple/saml2-example.php | 52 + www/example-simple/shib13-example.php | 35 + www/index.html | 88 ++ www/resources/icons/bino.png | Bin 0 -> 15118 bytes www/resources/icons/bomb.png | Bin 0 -> 3439 bytes www/resources/icons/bomb_l.png | Bin 0 -> 8960 bytes www/resources/icons/compass_l.png | Bin 0 -> 17694 bytes www/resources/icons/debug.png | Bin 0 -> 10724 bytes www/resources/icons/lock.png | Bin 0 -> 14639 bytes www/resources/icons/pencil.png | Bin 0 -> 3004 bytes www/resources/sprites.gif | Bin 0 -> 12536 bytes www/saml2/idp/SSOService.php | 140 ++ www/saml2/idp/SingleLogoutService.php | 146 ++ www/saml2/sp/AssertionConsumerService.php | 52 + www/saml2/sp/SingleLogoutService.php | 78 + www/saml2/sp/initSLO.php | 66 + www/saml2/sp/initSSO.php | 97 ++ www/shib13/sp/AssertionConsumerService.php | 69 + www/shib13/sp/initSSO.php | 99 ++ 60 files changed, 7011 insertions(+) create mode 100644 COPYING create mode 100755 cert/server.crt create mode 100755 cert/server.pem create mode 100644 config/config-template.php create mode 100644 docs/README create mode 100644 docs/simplesamlphp-install.html create mode 100644 docs/simplesamlphp-install.pdf create mode 100644 docs/source/simplesamlphp-install.xml create mode 100644 lib/SimpleSAML/Bindings/SAML20/HTTPPost.php create mode 100644 lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php create mode 100644 lib/SimpleSAML/Bindings/Shib13/HTTPPost.php create mode 100644 lib/SimpleSAML/Configuration.php create mode 100644 lib/SimpleSAML/Session.php create mode 100644 lib/SimpleSAML/Utilities.php create mode 100644 lib/SimpleSAML/XHTML/Template.php create mode 100644 lib/SimpleSAML/XML/AuthnResponse.php create mode 100644 lib/SimpleSAML/XML/MetaDataStore.php create mode 100644 lib/SimpleSAML/XML/SAML20/AuthnRequest.php create mode 100644 lib/SimpleSAML/XML/SAML20/AuthnResponse.php create mode 100644 lib/SimpleSAML/XML/SAML20/LogoutRequest.php create mode 100644 lib/SimpleSAML/XML/SAML20/LogoutResponse.php create mode 100644 lib/SimpleSAML/XML/Shib13/AuthnRequest.php create mode 100644 lib/SimpleSAML/XML/Shib13/AuthnResponse.php create mode 100644 lib/xmlseclibs.php create mode 100644 metadata-templates/saml20-idp-hosted.php create mode 100644 metadata-templates/saml20-idp-remote.php create mode 100644 metadata-templates/saml20-sp-hosted.php create mode 100644 metadata-templates/saml20-sp-remote.php create mode 100644 metadata-templates/shib13-idp-hosted.php create mode 100644 metadata-templates/shib13-idp-remote.php create mode 100644 metadata-templates/shib13-sp-hosted.php create mode 100644 metadata-templates/shib13-sp-remote.php create mode 100644 templates/error.php create mode 100644 templates/login.php create mode 100644 templates/post-debug.php create mode 100644 templates/post.php create mode 100644 templates/status.php create mode 100644 www/_include.php create mode 100644 www/auth/login-auto.php create mode 100644 www/auth/login-radius.php create mode 100644 www/auth/login.php create mode 100644 www/example-simple/saml2-example.php create mode 100644 www/example-simple/shib13-example.php create mode 100644 www/index.html create mode 100644 www/resources/icons/bino.png create mode 100644 www/resources/icons/bomb.png create mode 100644 www/resources/icons/bomb_l.png create mode 100644 www/resources/icons/compass_l.png create mode 100644 www/resources/icons/debug.png create mode 100644 www/resources/icons/lock.png create mode 100644 www/resources/icons/pencil.png create mode 100644 www/resources/sprites.gif create mode 100644 www/saml2/idp/SSOService.php create mode 100644 www/saml2/idp/SingleLogoutService.php create mode 100644 www/saml2/sp/AssertionConsumerService.php create mode 100644 www/saml2/sp/SingleLogoutService.php create mode 100644 www/saml2/sp/initSLO.php create mode 100644 www/saml2/sp/initSSO.php create mode 100644 www/shib13/sp/AssertionConsumerService.php create mode 100644 www/shib13/sp/initSSO.php diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..7cd1553ad --- /dev/null +++ b/COPYING @@ -0,0 +1,22 @@ +simpleSAMLphp is based on code from Sun OpenSSO Extensions (formerly known as Lightbulb). +The initial versions of the SAML 2.0 part was written by Pat Patterson, Sun. + +The functionality have been extended and Andreas Ă…kre Solberg, UNINETT, has rewritten the +library and added support for Shibboleth IdP/SP, and SAML 2.0 IdP. The product is used to +bridge AAI protocols in the GÉANT project, http://geant2.net + +---- +simpleSAMLphp is subject to the terms +of the Common Development and Distribution License +(the License). You may not use this file except in +compliance with the License. + +You can obtain a copy of the License at +https://opensso.dev.java.net/public/CDDLv1.0.html or +opensso/legal/CDDLv1.0.txt +See the License for the specific language governing +permission and limitations under the License. + +When distributing Covered Code, include this CDDL +Header Notice in each file and include the License file +at opensso/legal/CDDLv1.0.txt. diff --git a/cert/server.crt b/cert/server.crt new file mode 100755 index 000000000..b4147e516 --- /dev/null +++ b/cert/server.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMC +Tk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYD +VQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG +9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4 +MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xi +ZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2Zl +aWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5v +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LO +NoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHIS +KOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d +1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8 +BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7n +bK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2Qar +Q4/67OZfHd7R+POBXhophSMv1ZOo +-----END CERTIFICATE----- diff --git a/cert/server.pem b/cert/server.pem new file mode 100755 index 000000000..673196b17 --- /dev/null +++ b/cert/server.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9 +IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+ +PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQAB +AoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0V +J5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8J +K2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV +6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF +3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq6 +6PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6 +rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL +8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4uk +XSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLc +PQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg== +-----END RSA PRIVATE KEY----- diff --git a/config/config-template.php b/config/config-template.php new file mode 100644 index 000000000..49833941a --- /dev/null +++ b/config/config-template.php @@ -0,0 +1,48 @@ +<?php +/* + * The configuration of simpleSAMLphp + * + * + */ + +$config = array ( + + /* + * Setup the following parameters to match the directory of your installation. + * See the user manual for more details. + */ + 'basedir' => '/var/www/simplesamlphp/', + 'baseurlpath' => 'simplesamlphp/', + 'templatedir' => '/var/www/simplesamlphp/templates', + 'metadatadir' => '/var/www/simplesamlphp/metadata', + + /* + * If you set the debug parameter to true, all SAML messages will be visible in the + * browser, and require the user to click the submit button. If debug is set to false, + * Browser/POST SAML messages will be automaticly submitted. + */ + 'debug' => false, + + /* + * This value is the duration of the session in seconds. Make sure that the time duration of + * cookies both at the SP and the IdP exceeds this duration. + */ + 'session.duration' => 8 * (60*60), // 8 hours. + + /* + * Default IdPs. If you do not enter an idpentityid in the SSO initialization endpoints, + * the default IdP configured here will be used. + */ + 'default-saml20-idp' => 'sam.feide.no', + 'default-shib13-idp' => 'urn:mace:switch.ch:aaitest:dukono.switch.ch', + + /* + * LDAP configuration. This is only relevant if you use the LDAP authentication plugin. + */ + 'auth.ldap.dnpattern' => 'uid=%username%,dc=feide,dc=no,ou=feide,dc=uninett,dc=no', + 'auth.ldap.hostname' => 'ldap.uninett.no', + 'auth.ldap.attributes' => 'objectclass=*' +); + + +?> \ No newline at end of file diff --git a/docs/README b/docs/README new file mode 100644 index 000000000..5c9503318 --- /dev/null +++ b/docs/README @@ -0,0 +1,29 @@ +README + + +Installation instructions: +========================== + +Store the simplesamlphp directory somewhere... + +In there there is a www directory, it have to be accessible from web, on the root of a vhost. The www can be moved outside the simplesamlphp folder. You can in example drop the content of the www folder into your existing web site folder. + +IF you decide to move the www folder out of the simplesamlphp folder, then you need to update the www/_include.php file properly. + +Next, configure config.php: +- set the path and hostnames. +- Use sam.feide.no as default idp. +- Set the default duration of a session to be in example 3 hours. + + +Then, configure saml20-sp-hosted to match your SP metadata. Change dev.andreas.feide.no to your hostname. Contact feide to ensure that your meta data is added to the Feide IdP. + +Then configure the saml20-idp-remote to match Feide. If there exists an entry for sam.feide.no it is probably already there. + +Then test the /example-simple/saml2-example.php log in with the feide test user, and look at the attributes. then test sp initated logout. + +Look at the example code of how to integrate with a service. + +Contact Andreas for questions: +andreas@uninett.no + diff --git a/docs/simplesamlphp-install.html b/docs/simplesamlphp-install.html new file mode 100644 index 000000000..3c13eb2c8 --- /dev/null +++ b/docs/simplesamlphp-install.html @@ -0,0 +1,182 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /><title>simpleSAMLphp Installation and Configuration</title><link rel="stylesheet" href="html.css" type="text/css" /><meta name="generator" content="DocBook XSL Stylesheets V1.69.1" /></head><body><div class="article" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title"><a id="id721994"></a>simpleSAMLphp Installation and Configuration</h2></div><div><div class="author"><h3 class="author"><span class="firstname">Andreas Ă…kre</span> <span class="surname">Solberg</span></h3><code class="email"><<a href="mailto:andreas.solberg@uninett.no">andreas.solberg@uninett.no</a>></code></div></div><div><p class="pubdate">Fri Sep 14 10:49:49 2007</p></div></div><hr /></div><div class="toc"><p><b>Table of Contents</b></p><dl><dt><span class="section"><a href="#id856632">The history of simpleSAMLphp</a></span></dt><dt><span class="section"><a href="#id856682">Changelog</a></span></dt><dd><dl><dt><span class="section"><a href="#id856693">Version 0.4</a></span></dt></dl></dd><dt><span class="section"><a href="#id856807">Download and get simpleSAMLphp</a></span></dt><dd><dl><dt><span class="section"><a href="#id856826">Getting a working copy of simpleSAMLphp from subversion</a></span></dt></dl></dd><dt><span class="section"><a href="#id856866">Installing simpleSAMLphp</a></span></dt><dd><dl><dt><span class="section"><a href="#id856941">The simpleSAMLphp installation webpage</a></span></dt></dl></dd><dt><span class="section"><a href="#id856967">Making configuration and metadata files</a></span></dt><dt><span class="section"><a href="#sect.config">Configuring simpleSAMLphp</a></span></dt><dd><dl><dt><span class="section"><a href="#id857037">Configuration for LDAP authentication plugin</a></span></dt></dl></dd><dt><span class="section"><a href="#id857095">Setting up a SAML 2.0 SP</a></span></dt><dd><dl><dt><span class="section"><a href="#id857107">Configuring metadata for a SAML 2.0 SP</a></span></dt><dt><span class="section"><a href="#id857190">Test the SAML 2.0 SP example</a></span></dt></dl></dd><dt><span class="section"><a href="#id857224">Setting up a Shibboleth 1.3 SP</a></span></dt><dd><dl><dt><span class="section"><a href="#id857235">Configuring metadata for Shibboleth 1.3 SP</a></span></dt><dt><span class="section"><a href="#id857296">Test the Shibboleth 1.3 SP example</a></span></dt></dl></dd><dt><span class="section"><a href="#id857331">Setting up a SAML 2.0 IdP</a></span></dt><dd><dl><dt><span class="section"><a href="#id857342">Configuring the SAML 2.0 IdP</a></span></dt><dt><span class="section"><a href="#id857376">Adding a SAML IdP signing certificate</a></span></dt><dt><span class="section"><a href="#id857440">Test SAML 2.0 IdP</a></span></dt></dl></dd><dt><span class="section"><a href="#id857453">Using the built-in SP WAYF functionality</a></span></dt><dt><span class="section"><a href="#id857466">Setting up WebSSO bridges</a></span></dt><dd><dl><dt><span class="section"><a href="#id857477">Bridging SAML 2.0 <-> SAML 2.0</a></span></dt><dt><span class="section"><a href="#id857522">Bridging Shibboleth 1.3 <-> Shibboleth 1.3</a></span></dt><dt><span class="section"><a href="#id857533">Bridging Shibboleth 1.3 <-> SAML 2.0</a></span></dt><dt><span class="section"><a href="#id857544">Bridging SAML 2.0 <-> Shibboleth 1.3</a></span></dt><dt><span class="section"><a href="#id857554">Bridging SAML 2.0 <-> OpenID</a></span></dt><dt><span class="section"><a href="#id857564">Bridging Shibboelth 1.3 <-> OpenID</a></span></dt></dl></dd><dt><span class="section"><a href="#id857576">Authentication API</a></span></dt></dl></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id856632"></a>The history of simpleSAMLphp</h2></div></div></div><p>simpleSAMLphp is based on code from <a href="https://opensso.dev.java.net/public/extensions/" target="_top">Sun OpenSSO + Extensions</a> (formerly known as Lightbulb).</p><p>The initial versions of the SAML 2.0 SP part was written by <a href="http://blogs.sun.com/superpat/" target="_top">Pat Patterson, Sun</a>.</p><p>The functionality has been extended and <a href="http://claimid.com/erlang" target="_top">Andreas Ă…kre Solberg</a>, <a href="http://uninett.no" target="_top">UNINETT</a>, has rewritten the library and + added support for Shibboleth. The product is used to bridge AAI protocols + in the GÉANT project, <a href="http://geant2.net" target="_top">http://geant2.net</a>.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id856682"></a>Changelog</h2></div></div></div><p>Here is changes between simpleSAML versions. Look here if you are + upgrading, to see if there are any changes to the config format.</p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id856693"></a>Version 0.4</h3></div></div></div><p>Released 2007-09-14. Revision X.</p><div class="itemizedlist"><ul type="disc"><li><p>Improved documentation</p></li><li><p>Authentication plugin API. Only LDAP authenticaiton plugin is + included, but it is now easier to implement your own plugin.</p></li><li><p>Added support for SAML 2.0 IdP to work with Google Apps for + Education. Tested.</p></li><li><p>Initial implementation of SAML 2.0 Single Log-Out + functionality both for SP and IdP. Seems to work, but not yet + well-tested.</p></li><li><p>Added support for bridging SAML 2.0 to SAML 2.0.</p></li><li><p>Added some time skew offset to the NotBefore timestamp on the + assertion, to allow some time skew between the SP and IdP.</p></li><li><p>Fixed Browser/POST page to automaticly submit, and have fall + back functionality for user agents with no javascript + support.</p></li><li><p>Fixed some bug with warning traversing Shibboleth 1.3 + Assertions.</p></li><li><p>Fixed tabindex on the login page of the LDAP authentication + module to allow you to tab from username, to password and then to + submit.</p></li><li><p>Fixed bug on autodiscovering hostname in multihost + environments.</p></li><li><p>Cleaned out some debug messages, and added a debug option in + the configuration file. This debug option let's you turn on the + possibility of showing all SAML messages to users in the web + browser, and manually submit them.</p></li><li><p>Several minor bugfixes.</p></li></ul></div></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id856807"></a>Download and get simpleSAMLphp</h2></div></div></div><p>You can go to <a href="http://rnd.feide.no/category/simplesamlphp/" target="_top">http://rnd.feide.no/category/simplesamlphp/</a> + to find the most recent release of simpleSAMLphp. Download the zipped + file, and unzip it on your webserver. However I hightly reccomend running + from a subversion checkout instead.</p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id856826"></a>Getting a working copy of simpleSAMLphp from subversion</h3></div></div></div><div class="warning" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Warning</h3><p>Right now the subversion repository is requiring a username / + password. I'll update the access control, so that everyone can get + read access without authentication. I'll announce it on the rnd blog + when it is ready.</p></div><p>If you want a working copy from subversion enter:</p><pre class="screen">svn co https://svn.uninett.no/svn/feidernd/simplesamlphp</pre><p>If you know subversion you know how to view logs and review + changes to the files. To update the version you have checked out, + enter:</p><pre class="screen">cd simplesamlphp +svn up</pre></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id856866"></a>Installing simpleSAMLphp</h2></div></div></div><p>First find an appropriate place for the <code class="filename">simplesamlphp + </code>folder. In example + <code class="filename">/var/simplesamlphp</code>.</p><p>Of the folders inside simplesamlphp, only the www folder needs to be + accessible from the web. There are several ways of putting the + simpleSAMLphp depending on the way web sites are structured on your apache + web server. Here is what I believe is the best configuration.</p><p>Find the apache configuration file for the virtual hosts that you + want to run simpleSAML on. The configuration may look like this:</p><pre class="programlisting"><VirtualHost *> + ServerName service.example.com + DocumentRoot /var/www/service.example.com + + Alias /simplesamlphp /var/simplesamlphp/www +</VirtualHost> +</pre><p>What is special is tha Alias directive. That directive will give + control to simplesamlphp to all urls that matches + <code class="literal">http(s)://service.example.com/simplesamlphp/*</code>. + SimpleSAML will need to have several SAML interfaces available on the web, + and all these interfaces are included in the www subdirectory of your + simplesamlphp installation. You can set the alias to whatever you want, + but this alias must be set in the config.php file of simpleSAML as + described in <a href="#sect.config" title="Configuring simpleSAMLphp">the section called “Configuring simpleSAMLphp”</a>. Here is an example of how + this configuration may look like in config.php:</p><pre class="programlisting">$config = array ( + 'basedir' => '/var/simplesamlphp/', + 'baseurl' => 'http://service.example.com', + 'baseurlpath' => 'simplesamlphp/',</pre><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id856941"></a>The simpleSAMLphp installation webpage</h3></div></div></div><p>When you have installed simpleSAMLphp, you can access the homepage + of your installation, which contains some information and a few links to + the test services. The url of an installation can be in example:</p><div class="literallayout"><p>https://service.example.com/simplesamlphp/</p></div><p>But it depends on how you set it up with apache.</p></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id856967"></a>Making configuration and metadata files</h2></div></div></div><p>Configuration and metadata files are stored in a template format, + you need to copy them to have your local copies. The reason why it is done + this way, is that when you upgrade you can do svn up in subversion or just + copy the whole directory over your installation, without replacing your + existing configuration. When you are updating, you should investigate + whether the config format is changed, this should be documented in the + changelog.</p><p>Here are the steps you need to do to create local configuration + files:</p><pre class="screen">cd /var/simplesamlphp +cp config/config-template.php config/config.php +cp -r metadata-templates/*.php metadata/ +</pre></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="sect.config"></a>Configuring simpleSAMLphp</h2></div></div></div><p>First configure all the paths in the beginning of the config file, + to correspond to your organization of the apache web server, and where you + place simpleSAMLphp.</p><p>You will need to set the entityid of a default IdP in + <code class="literal">default-saml20-idp</code> or + <code class="literal">default-shib13-idp</code> depending on whether you use + shibboleth or SAML 2.0.</p><p>There is one parameter debug that may be set to true or false. If + you set it to true, then all Browser/POST SAML messages will be printed to + the web browser, and the user will have to manually submit it. </p><p>The session.duration parameter says how many seconds that a session + should be valid. After this amont of time, the session is not valid + anymore.</p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857037"></a>Configuration for LDAP authentication plugin</h3></div></div></div><p>If you want to perform local authentication on this server, and + you want to use the LDAP authenticaiton plugin, then you need to + configure the following parameters:</p><div class="itemizedlist"><ul type="disc"><li><p><code class="literal">auth.ldap.dnpattern</code>: What DN should you + bind to? Replacing %username% with the username the user types + in.</p></li><li><p><code class="literal">auth.ldap.hostname</code>: The hostname of the + LDAP server</p></li><li><p><code class="literal">auth.ldap.attributes</code>: Search parameter to + LDAP. What attributes should be extracted? + <code class="literal">objectclass=*</code> gives you all.</p></li></ul></div></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id857095"></a>Setting up a SAML 2.0 SP</h2></div></div></div><p>This functionality is relevant if you want to integrate SAML 2.0 + authentication on a service of yours, and you know one or more IdPs that + you can connect to. You would need metadata for those IdPs.</p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857107"></a>Configuring metadata for a SAML 2.0 SP</h3></div></div></div><p>To configure a SAML 2.0 SP, you first need to configure the SP + data for all your vhosts. If you run only one host, you need only one + entry. This metadata is stored in the + <code class="filename">metadata/saml20-sp-hosted.php</code> file. Here is an + example of a metadata:</p><pre class="programlisting"> "dev.andreas.feide.no" => array( + 'host' => 'dev.andreas.feide.no', + "assertionConsumerServiceURL" => "http://dev.andreas.feide.no/saml2/sp/AssertionConsumerService.php", + "issuer" => "dev.andreas.feide.no", + "spNameQualifier" => "dev.andreas.feide.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ),</pre><p>Note that you should fill in the host field matching the hostname + of your vhost. That way simpleSAMLphp can automatically detect what SP + metadata to use based on the <code class="literal">Host:</code> header sent by the + HTTP user agent.</p><p>You also need to configure the metadata for the IdP that you want + to use. Here is a metadata example for the Feide IdP:</p><pre class="programlisting"> "sam.feide.no" => array( + "SingleSignOnUrl" => "https://sam.feide.no/amserver/SSORedirect/metaAlias/idp", + "SingleLogOutUrl" => "https://sam.feide.no/amserver/IDPSloRedirect/metaAlias/idp", + "certFingerprint" => "3a:e7:d3:d3:06:ba:57:fd:7f:62:6a:4b:a8:64:b3:4a:53:d9:5d:d0", + "base64attributes" => true),</pre><p>The IdP metadata is stored in the + <code class="filename">metadata/saml20-idp-remote.php</code> file. Configure the + correct URLs of the endpoints, the hash of the certificate, and whether + the IdP is base64 encoding attributes or not. Most IdPs don't use + base64, so if you do not connect to Feide you should turn this parameter + to <code class="literal">false</code>. Notice that the key of the array is the + entity id of the IdP, in this example: + <code class="literal">sam.feide.no</code>.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857190"></a>Test the SAML 2.0 SP example</h3></div></div></div><p>Go to the URL of the test page, similar to:</p><div class="literallayout"><p>http://service.example.com/simplesamlphp/example-simple/saml2-example.php</p></div><div class="note" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Note</h3><p>The simpleSAMLphp installation homepage will link you to this + example, so you do not need to type in the full url.</p></div><p>You should be redirected to the IdP. Login, and you should be sent + back and shown all the attributes sent form the IdP.</p></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id857224"></a>Setting up a Shibboleth 1.3 SP</h2></div></div></div><p>If you want to configure a service with authentication towards an + external Shibboleth 1.3 IdP, this section describes you how to proceed. + </p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857235"></a>Configuring metadata for Shibboleth 1.3 SP</h3></div></div></div><p>Configure Shibboleth 1.3 SP metadata for all your vhosts. If you + run only one host, you need only one entry. This metadata is stored in + the <code class="filename">metadata/shib13-sp-hosted.php</code> file. Here is an + example:</p><pre class="programlisting"> 'http://dev.andreas.feide.no' => array( + 'AssertionConsumerService' => 'http://dev.andreas.feide.no/shib13/sp/AssertionConsumerService.php', + 'host' => 'dev.andreas.feide.no' + ),</pre><p>Note that you should fill in the host field matching the hostname + of your vhost. That way simpleSAMLphp can automatically detect what SP + metadata to use based on the <code class="literal">Host:</code> header sent by the + HTTP user agent.</p><p>You also need to configure the metadata for the Shibboleth 1.3 + IdPs that you want to connect to. Here is an example:</p><pre class="programlisting"> 'urn:mace:switch.ch:aaitest:dukono.switch.ch' => array( + 'SingleSignOnUrl' => 'https://dukono.switch.ch/shibboleth-idp/SSO', + 'certFingerprint' => 'c7279a9f28f11380509e075441e3dc55fb9ab864' + ),</pre><p>Notice that the key of the array is the entity ID.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857296"></a>Test the Shibboleth 1.3 SP example</h3></div></div></div><p>Go to the URL of the shibboleth test page, similar to:</p><div class="literallayout"><p>http://service.example.com/example-simple/shib13-example.php</p></div><p>You should be redirected to the IdP. Login, and you should be sent + back and shown all the attributes sent form the IdP.</p><div class="note" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Note</h3><p>simpleSAMLphp does not support the attribute profile that + Shibboleth is using by default. To make attributes work, you need to + configure the IdP to perform attribute push.</p></div></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id857331"></a>Setting up a SAML 2.0 IdP</h2></div></div></div><p>If you have a user database and want to offer a SAML 2.0 IdP + functinoality towards external services, here is how you set it up.</p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857342"></a>Configuring the SAML 2.0 IdP</h3></div></div></div><p>Setup idp metadata in saml20-idp-hosted. Then for all the SP the + IdP shold trust in saml20-sp-remote. Then configure in config.php, ldap + DN patterns, ldap host etc. Next add a certificate with openssl.</p><p>Example config.php:</p><pre class="programlisting"> 'auth.ldap.dnpattern' => 'uid=%username%,dc=feide,dc=no,ou=feide,dc=uninett,dc=no', + 'auth.ldap.hostname' => 'ldap.uninett.no', + 'auth.ldap.attributes' => 'objectclass=*'</pre><p>Example IdP Metadata saml20-idp-hosted:</p><pre class="programlisting"> 'dev2.andreas.feide.no' => array( + 'host' => 'dev2.andreas.feide.no', + 'SingleSignOnUrl' => "http://dev2.andreas.feide.no/saml2/idp/SSOService.php", + 'SingleLogOutUrl' => "http://dev2.andreas.feide.no/saml2/idp/LogoutService.php", + 'privatekey' => 'server.pem', + 'certificate' => 'server.crt', + 'base64attributes' => true, + 'auth' => 'auth/login.php' + )</pre><p>The server.pem and server.crt is an example certificate shipped + with the package, and be used for demo purposes, but you must generate + your own to use in production services.</p><p>You also need to configure metadata for trusted SPs, here is an + example:</p><pre class="programlisting">_ "dev.andreas.feide.no" => array( + 'host' => 'dev.andreas.feide.no', + "assertionConsumerServiceURL" => "http://dev.andreas.feide.no/saml2/sp/AssertionConsumerService.php", + "issuer" => "dev.andreas.feide.no", + "spNameQualifier" => "dev.andreas.feide.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ),</pre></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857376"></a>Adding a SAML IdP signing certificate</h3></div></div></div><p>You should generate a new certificate for your IdP.</p><div class="warning" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Warning</h3><p>There is a certificate that follows this package that you can + use for test purposes, but off course NEVER use this in production as + the private key is also included in the package and can be downloaded + by anyone.</p></div><p>Here is an examples of openssl commands to generate a new key and + a selfsigned certificate to use for signing SAML messages:</p><pre class="screen">openssl genrsa -des3 -out server2.key 1024 +openssl rsa -in server2.key -out server2.pem +openssl req -new -key server.key -out server2.csr +openssl x509 -req -days 60 -in server2.csr -signkey server2.key -out server2.crt</pre><p>The certificate above will be valid for 60 days.</p><div class="note" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Note</h3><p>simpleSAMLphp will only work with RSA and not DSA + certificates.</p></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857440"></a>Test SAML 2.0 IdP</h3></div></div></div><p>To test the SAML 2.0 IdP, it is best to configure two hosts with + simpleSAMLphp, and use the SAML 2.0 SP demo example to test the + IdP.</p></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id857453"></a>Using the built-in SP WAYF functionality</h2></div></div></div><p>The WAYF is not yet a part of the simpleSAMLphp release. This + functionality will be added soon.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id857466"></a>Setting up WebSSO bridges</h2></div></div></div><p>simpleSAMLphp can be used to bridge between two WebSSO protocols. + Here is some short descriptions of how to setup the different bridge + configurations.</p><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857477"></a>Bridging SAML 2.0 <-> SAML 2.0</h3></div></div></div><p>In this setup you can bridge between two federations using SAML + 2.0.</p><p>To approach this, you must configure both saml 2.0 IdP and SP + hosted metadata, and in the IdP hosted metadata configure the auth + parameter to be the SP initialization endpoint, like this:</p><pre class="screen"> 'auth' => 'saml2/sp/initSSO.php?idpentityid=sam.feide.no'</pre><p>As you can see you specify the IdP in the remote federation as a + parameter to the initalization endpoint.</p><div class="note" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Note</h3><p>This section of the documentation is only a placeholder. There + will be more detailed information added later. For now, ask the author + if you want more details of such a setup.</p><p>Briding SAML 2.0 SLO is not implemented. Will be improved + soon.</p></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857522"></a>Bridging Shibboleth 1.3 <-> Shibboleth 1.3</h3></div></div></div><p>Will be supported soon.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857533"></a>Bridging Shibboleth 1.3 <-> SAML 2.0</h3></div></div></div><p>Will be supported soon.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857544"></a>Bridging SAML 2.0 <-> Shibboleth 1.3</h3></div></div></div><p>Will be supported soon.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857554"></a>Bridging SAML 2.0 <-> OpenID</h3></div></div></div><p>Will be supported soon.</p></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h3 class="title"><a id="id857564"></a>Bridging Shibboelth 1.3 <-> OpenID</h3></div></div></div><p>Will be supported soon.</p></div></div><div class="section" lang="en" xml:lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a id="id857576"></a>Authentication API</h2></div></div></div><p>The authentication plugin should be placed in the auth directory. + </p><p>The following parameters must be accepted in the incomming + URL:</p><div class="itemizedlist"><ul type="disc"><li><p><code class="literal">RelayState</code>: This is the URL that the user + should be sent back to after authentication within the plugin.</p></li><li><p><code class="literal">RequestID</code>: This is the ID of an incomming + request.</p></li></ul></div><p>The initSSO.php takes in addition the following parameters:</p><div class="itemizedlist"><ul type="disc"><li><p><code class="literal">idpentityid</code>: This is the entityid of the IdP + to authenticate with. This parameter is optional, if not set the + default for this host will be used.</p></li><li><p><code class="literal">spentityid</code>: This is which SP config to use. + This parameter is optional, if not set the default for this host will + be used.</p></li></ul></div><p>In hosted IdP metadata there is a config parameter auth that will + tell simpleSAML which authentication plugin that can be used.</p><div class="tip" style="margin-left: 0.5in; margin-right: 0.5in;"><h3 class="title">Tip</h3><p>The authentication API is pretty basic. The easiest way to + understand how it works is to look at one of the existing plugins that + is located in the auth directory of your installation.</p></div></div></div></body></html> diff --git a/docs/simplesamlphp-install.pdf b/docs/simplesamlphp-install.pdf new file mode 100644 index 0000000000000000000000000000000000000000..aecd7d045e1c15b4f8840ed6fb1ff8a9fc2d4b30 GIT binary patch literal 43359 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ℜX^KkZZDh)JAgzRLfT|seO55p}&p(2g{C4(ww)Ti!2SVc!2qJ6ND5@!m{6Cj_ Bn05dF literal 0 HcmV?d00001 diff --git a/docs/source/simplesamlphp-install.xml b/docs/source/simplesamlphp-install.xml new file mode 100644 index 000000000..69e644b30 --- /dev/null +++ b/docs/source/simplesamlphp-install.xml @@ -0,0 +1,588 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" +"http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd"> +<article> + <title>simpleSAMLphp Installation and Configuration</title> + + <articleinfo> + <date>2007-08-30</date> + + <pubdate>Fri Sep 14 10:49:49 2007</pubdate> + + <author> + <firstname>Andreas Ă…kre</firstname> + + <surname>Solberg</surname> + + <email>andreas.solberg@uninett.no</email> + </author> + </articleinfo> + + <section> + <title>The history of simpleSAMLphp</title> + + <para>simpleSAMLphp is based on code from <ulink + url="https://opensso.dev.java.net/public/extensions/">Sun OpenSSO + Extensions</ulink> (formerly known as Lightbulb).</para> + + <para>The initial versions of the SAML 2.0 SP part was written by <ulink + url="http://blogs.sun.com/superpat/">Pat Patterson, Sun</ulink>.</para> + + <para>The functionality has been extended and <ulink + url="http://claimid.com/erlang">Andreas Ă…kre Solberg</ulink>, <ulink + url="http://uninett.no">UNINETT</ulink>, has rewritten the library and + added support for Shibboleth. The product is used to bridge AAI protocols + in the GÉANT project, <ulink + url="http://geant2.net">http://geant2.net</ulink>.</para> + </section> + + <section> + <title>Changelog</title> + + <para>Here is changes between simpleSAML versions. Look here if you are + upgrading, to see if there are any changes to the config format.</para> + + <section> + <title>Version 0.4</title> + + <para>Released 2007-09-14. Revision X.</para> + + <itemizedlist> + <listitem> + <para>Improved documentation</para> + </listitem> + + <listitem> + <para>Authentication plugin API. Only LDAP authenticaiton plugin is + included, but it is now easier to implement your own plugin.</para> + </listitem> + + <listitem> + <para>Added support for SAML 2.0 IdP to work with Google Apps for + Education. Tested.</para> + </listitem> + + <listitem> + <para>Initial implementation of SAML 2.0 Single Log-Out + functionality both for SP and IdP. Seems to work, but not yet + well-tested.</para> + </listitem> + + <listitem> + <para>Added support for bridging SAML 2.0 to SAML 2.0.</para> + </listitem> + + <listitem> + <para>Added some time skew offset to the NotBefore timestamp on the + assertion, to allow some time skew between the SP and IdP.</para> + </listitem> + + <listitem> + <para>Fixed Browser/POST page to automaticly submit, and have fall + back functionality for user agents with no javascript + support.</para> + </listitem> + + <listitem> + <para>Fixed some bug with warning traversing Shibboleth 1.3 + Assertions.</para> + </listitem> + + <listitem> + <para>Fixed tabindex on the login page of the LDAP authentication + module to allow you to tab from username, to password and then to + submit.</para> + </listitem> + + <listitem> + <para>Fixed bug on autodiscovering hostname in multihost + environments.</para> + </listitem> + + <listitem> + <para>Cleaned out some debug messages, and added a debug option in + the configuration file. This debug option let's you turn on the + possibility of showing all SAML messages to users in the web + browser, and manually submit them.</para> + </listitem> + + <listitem> + <para>Several minor bugfixes.</para> + </listitem> + </itemizedlist> + </section> + </section> + + <section> + <title>Download and get simpleSAMLphp</title> + + <para>You can go to <ulink + url="http://rnd.feide.no/category/simplesamlphp/">http://rnd.feide.no/category/simplesamlphp/</ulink> + to find the most recent release of simpleSAMLphp. Download the zipped + file, and unzip it on your webserver. However I hightly reccomend running + from a subversion checkout instead.</para> + + <section> + <title>Getting a working copy of simpleSAMLphp from subversion</title> + + <warning> + <para>Right now the subversion repository is requiring a username / + password. I'll update the access control, so that everyone can get + read access without authentication. I'll announce it on the rnd blog + when it is ready.</para> + </warning> + + <para>If you want a working copy from subversion enter:</para> + + <screen>svn co https://svn.uninett.no/svn/feidernd/simplesamlphp</screen> + + <para>If you know subversion you know how to view logs and review + changes to the files. To update the version you have checked out, + enter:</para> + + <screen>cd simplesamlphp +svn up</screen> + </section> + </section> + + <section> + <title>Installing simpleSAMLphp</title> + + <para>First find an appropriate place for the <filename>simplesamlphp + </filename>folder. In example + <filename>/var/simplesamlphp</filename>.</para> + + <para>Of the folders inside simplesamlphp, only the www folder needs to be + accessible from the web. There are several ways of putting the + simpleSAMLphp depending on the way web sites are structured on your apache + web server. Here is what I believe is the best configuration.</para> + + <para>Find the apache configuration file for the virtual hosts that you + want to run simpleSAML on. The configuration may look like this:</para> + + <programlisting><VirtualHost *> + ServerName service.example.com + DocumentRoot /var/www/service.example.com + + Alias /simplesamlphp /var/simplesamlphp/www +</VirtualHost> +</programlisting> + + <para>What is special is tha Alias directive. That directive will give + control to simplesamlphp to all urls that matches + <literal>http(s)://service.example.com/simplesamlphp/*</literal>. + SimpleSAML will need to have several SAML interfaces available on the web, + and all these interfaces are included in the www subdirectory of your + simplesamlphp installation. You can set the alias to whatever you want, + but this alias must be set in the config.php file of simpleSAML as + described in <xref linkend="sect.config" />. Here is an example of how + this configuration may look like in config.php:</para> + + <programlisting>$config = array ( + 'basedir' => '/var/simplesamlphp/', + 'baseurl' => 'http://service.example.com', + 'baseurlpath' => 'simplesamlphp/',</programlisting> + + <section> + <title>The simpleSAMLphp installation webpage</title> + + <para>When you have installed simpleSAMLphp, you can access the homepage + of your installation, which contains some information and a few links to + the test services. The url of an installation can be in example:</para> + + <literallayout>https://service.example.com/simplesamlphp/</literallayout> + + <para>But it depends on how you set it up with apache.</para> + </section> + </section> + + <section> + <title>Making configuration and metadata files</title> + + <para>Configuration and metadata files are stored in a template format, + you need to copy them to have your local copies. The reason why it is done + this way, is that when you upgrade you can do svn up in subversion or just + copy the whole directory over your installation, without replacing your + existing configuration. When you are updating, you should investigate + whether the config format is changed, this should be documented in the + changelog.</para> + + <para>Here are the steps you need to do to create local configuration + files:</para> + + <screen>cd /var/simplesamlphp +cp config/config-template.php config/config.php +cp -r metadata-templates/*.php metadata/ +</screen> + </section> + + <section id="sect.config"> + <title>Configuring simpleSAMLphp</title> + + <para>First configure all the paths in the beginning of the config file, + to correspond to your organization of the apache web server, and where you + place simpleSAMLphp.</para> + + <para>You will need to set the entityid of a default IdP in + <literal>default-saml20-idp</literal> or + <literal>default-shib13-idp</literal> depending on whether you use + shibboleth or SAML 2.0.</para> + + <para>There is one parameter debug that may be set to true or false. If + you set it to true, then all Browser/POST SAML messages will be printed to + the web browser, and the user will have to manually submit it. </para> + + <para>The session.duration parameter says how many seconds that a session + should be valid. After this amont of time, the session is not valid + anymore.</para> + + <section> + <title>Configuration for LDAP authentication plugin</title> + + <para>If you want to perform local authentication on this server, and + you want to use the LDAP authenticaiton plugin, then you need to + configure the following parameters:</para> + + <itemizedlist> + <listitem> + <para><literal>auth.ldap.dnpattern</literal>: What DN should you + bind to? Replacing %username% with the username the user types + in.</para> + </listitem> + + <listitem> + <para><literal>auth.ldap.hostname</literal>: The hostname of the + LDAP server</para> + </listitem> + + <listitem> + <para><literal>auth.ldap.attributes</literal>: Search parameter to + LDAP. What attributes should be extracted? + <literal>objectclass=*</literal> gives you all.</para> + </listitem> + </itemizedlist> + </section> + </section> + + <section> + <title>Setting up a SAML 2.0 SP</title> + + <para>This functionality is relevant if you want to integrate SAML 2.0 + authentication on a service of yours, and you know one or more IdPs that + you can connect to. You would need metadata for those IdPs.</para> + + <section> + <title>Configuring metadata for a SAML 2.0 SP</title> + + <para>To configure a SAML 2.0 SP, you first need to configure the SP + data for all your vhosts. If you run only one host, you need only one + entry. This metadata is stored in the + <filename>metadata/saml20-sp-hosted.php</filename> file. Here is an + example of a metadata:</para> + + <programlisting> "dev.andreas.feide.no" => array( + 'host' => 'dev.andreas.feide.no', + "assertionConsumerServiceURL" => "http://dev.andreas.feide.no/saml2/sp/AssertionConsumerService.php", + "issuer" => "dev.andreas.feide.no", + "spNameQualifier" => "dev.andreas.feide.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ),</programlisting> + + <para>Note that you should fill in the host field matching the hostname + of your vhost. That way simpleSAMLphp can automatically detect what SP + metadata to use based on the <literal>Host:</literal> header sent by the + HTTP user agent.</para> + + <para>You also need to configure the metadata for the IdP that you want + to use. Here is a metadata example for the Feide IdP:</para> + + <programlisting> "sam.feide.no" => array( + "SingleSignOnUrl" => "https://sam.feide.no/amserver/SSORedirect/metaAlias/idp", + "SingleLogOutUrl" => "https://sam.feide.no/amserver/IDPSloRedirect/metaAlias/idp", + "certFingerprint" => "3a:e7:d3:d3:06:ba:57:fd:7f:62:6a:4b:a8:64:b3:4a:53:d9:5d:d0", + "base64attributes" => true),</programlisting> + + <para>The IdP metadata is stored in the + <filename>metadata/saml20-idp-remote.php</filename> file. Configure the + correct URLs of the endpoints, the hash of the certificate, and whether + the IdP is base64 encoding attributes or not. Most IdPs don't use + base64, so if you do not connect to Feide you should turn this parameter + to <literal>false</literal>. Notice that the key of the array is the + entity id of the IdP, in this example: + <literal>sam.feide.no</literal>.</para> + </section> + + <section> + <title>Test the SAML 2.0 SP example</title> + + <para>Go to the URL of the test page, similar to:</para> + + <literallayout>http://service.example.com/simplesamlphp/example-simple/saml2-example.php</literallayout> + + <note> + <para>The simpleSAMLphp installation homepage will link you to this + example, so you do not need to type in the full url.</para> + </note> + + <para>You should be redirected to the IdP. Login, and you should be sent + back and shown all the attributes sent form the IdP.</para> + </section> + </section> + + <section> + <title>Setting up a Shibboleth 1.3 SP</title> + + <para>If you want to configure a service with authentication towards an + external Shibboleth 1.3 IdP, this section describes you how to proceed. + </para> + + <section> + <title>Configuring metadata for Shibboleth 1.3 SP</title> + + <para>Configure Shibboleth 1.3 SP metadata for all your vhosts. If you + run only one host, you need only one entry. This metadata is stored in + the <filename>metadata/shib13-sp-hosted.php</filename> file. Here is an + example:</para> + + <programlisting> 'http://dev.andreas.feide.no' => array( + 'AssertionConsumerService' => 'http://dev.andreas.feide.no/shib13/sp/AssertionConsumerService.php', + 'host' => 'dev.andreas.feide.no' + ),</programlisting> + + <para>Note that you should fill in the host field matching the hostname + of your vhost. That way simpleSAMLphp can automatically detect what SP + metadata to use based on the <literal>Host:</literal> header sent by the + HTTP user agent.</para> + + <para>You also need to configure the metadata for the Shibboleth 1.3 + IdPs that you want to connect to. Here is an example:</para> + + <programlisting> 'urn:mace:switch.ch:aaitest:dukono.switch.ch' => array( + 'SingleSignOnUrl' => 'https://dukono.switch.ch/shibboleth-idp/SSO', + 'certFingerprint' => 'c7279a9f28f11380509e075441e3dc55fb9ab864' + ),</programlisting> + + <para>Notice that the key of the array is the entity ID.</para> + </section> + + <section> + <title>Test the Shibboleth 1.3 SP example</title> + + <para>Go to the URL of the shibboleth test page, similar to:</para> + + <literallayout>http://service.example.com/example-simple/shib13-example.php</literallayout> + + <para>You should be redirected to the IdP. Login, and you should be sent + back and shown all the attributes sent form the IdP.</para> + + <note> + <para>simpleSAMLphp does not support the attribute profile that + Shibboleth is using by default. To make attributes work, you need to + configure the IdP to perform attribute push.</para> + </note> + </section> + </section> + + <section> + <title>Setting up a SAML 2.0 IdP</title> + + <para>If you have a user database and want to offer a SAML 2.0 IdP + functinoality towards external services, here is how you set it up.</para> + + <section> + <title>Configuring the SAML 2.0 IdP</title> + + <para>Setup idp metadata in saml20-idp-hosted. Then for all the SP the + IdP shold trust in saml20-sp-remote. Then configure in config.php, ldap + DN patterns, ldap host etc. Next add a certificate with openssl.</para> + + <para>Example config.php:</para> + + <programlisting> 'auth.ldap.dnpattern' => 'uid=%username%,dc=feide,dc=no,ou=feide,dc=uninett,dc=no', + 'auth.ldap.hostname' => 'ldap.uninett.no', + 'auth.ldap.attributes' => 'objectclass=*'</programlisting> + + <para>Example IdP Metadata saml20-idp-hosted:</para> + + <programlisting> 'dev2.andreas.feide.no' => array( + 'host' => 'dev2.andreas.feide.no', + 'SingleSignOnUrl' => "http://dev2.andreas.feide.no/saml2/idp/SSOService.php", + 'SingleLogOutUrl' => "http://dev2.andreas.feide.no/saml2/idp/LogoutService.php", + 'privatekey' => 'server.pem', + 'certificate' => 'server.crt', + 'base64attributes' => true, + 'auth' => 'auth/login.php' + )</programlisting> + + <para>The server.pem and server.crt is an example certificate shipped + with the package, and be used for demo purposes, but you must generate + your own to use in production services.</para> + + <para>You also need to configure metadata for trusted SPs, here is an + example:</para> + + <programlisting>_ "dev.andreas.feide.no" => array( + 'host' => 'dev.andreas.feide.no', + "assertionConsumerServiceURL" => "http://dev.andreas.feide.no/saml2/sp/AssertionConsumerService.php", + "issuer" => "dev.andreas.feide.no", + "spNameQualifier" => "dev.andreas.feide.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ),</programlisting> + </section> + + <section> + <title>Adding a SAML IdP signing certificate</title> + + <para>You should generate a new certificate for your IdP.</para> + + <warning> + <para>There is a certificate that follows this package that you can + use for test purposes, but off course NEVER use this in production as + the private key is also included in the package and can be downloaded + by anyone.</para> + </warning> + + <para>Here is an examples of openssl commands to generate a new key and + a selfsigned certificate to use for signing SAML messages:</para> + + <screen>openssl genrsa -des3 -out server2.key 1024 +openssl rsa -in server2.key -out server2.pem +openssl req -new -key server.key -out server2.csr +openssl x509 -req -days 60 -in server2.csr -signkey server2.key -out server2.crt</screen> + + <para>The certificate above will be valid for 60 days.</para> + + <note> + <para>simpleSAMLphp will only work with RSA and not DSA + certificates.</para> + </note> + </section> + + <section> + <title>Test SAML 2.0 IdP</title> + + <para>To test the SAML 2.0 IdP, it is best to configure two hosts with + simpleSAMLphp, and use the SAML 2.0 SP demo example to test the + IdP.</para> + </section> + </section> + + <section> + <title>Using the built-in SP WAYF functionality</title> + + <para>The WAYF is not yet a part of the simpleSAMLphp release. This + functionality will be added soon.</para> + </section> + + <section> + <title>Setting up WebSSO bridges</title> + + <para>simpleSAMLphp can be used to bridge between two WebSSO protocols. + Here is some short descriptions of how to setup the different bridge + configurations.</para> + + <section> + <title>Bridging SAML 2.0 <-> SAML 2.0</title> + + <para>In this setup you can bridge between two federations using SAML + 2.0.</para> + + <para>To approach this, you must configure both saml 2.0 IdP and SP + hosted metadata, and in the IdP hosted metadata configure the auth + parameter to be the SP initialization endpoint, like this:</para> + + <screen> 'auth' => 'saml2/sp/initSSO.php?idpentityid=sam.feide.no'</screen> + + <para>As you can see you specify the IdP in the remote federation as a + parameter to the initalization endpoint.</para> + + <note> + <para>This section of the documentation is only a placeholder. There + will be more detailed information added later. For now, ask the author + if you want more details of such a setup.</para> + + <para>Briding SAML 2.0 SLO is not implemented. Will be improved + soon.</para> + </note> + </section> + + <section> + <title>Bridging Shibboleth 1.3 <-> Shibboleth 1.3</title> + + <para>Will be supported soon.</para> + </section> + + <section> + <title>Bridging Shibboleth 1.3 <-> SAML 2.0</title> + + <para>Will be supported soon.</para> + </section> + + <section> + <title>Bridging SAML 2.0 <-> Shibboleth 1.3</title> + + <para>Will be supported soon.</para> + </section> + + <section> + <title>Bridging SAML 2.0 <-> OpenID</title> + + <para>Will be supported soon.</para> + </section> + + <section> + <title>Bridging Shibboelth 1.3 <-> OpenID</title> + + <para>Will be supported soon.</para> + </section> + </section> + + <section> + <title>Authentication API</title> + + <para>The authentication plugin should be placed in the auth directory. + </para> + + <para>The following parameters must be accepted in the incomming + URL:</para> + + <itemizedlist> + <listitem> + <para><literal>RelayState</literal>: This is the URL that the user + should be sent back to after authentication within the plugin.</para> + </listitem> + + <listitem> + <para><literal>RequestID</literal>: This is the ID of an incomming + request.</para> + </listitem> + </itemizedlist> + + <para>The initSSO.php takes in addition the following parameters:</para> + + <itemizedlist> + <listitem> + <para><literal>idpentityid</literal>: This is the entityid of the IdP + to authenticate with. This parameter is optional, if not set the + default for this host will be used.</para> + </listitem> + + <listitem> + <para><literal>spentityid</literal>: This is which SP config to use. + This parameter is optional, if not set the default for this host will + be used.</para> + </listitem> + </itemizedlist> + + <para>In hosted IdP metadata there is a config parameter auth that will + tell simpleSAML which authentication plugin that can be used.</para> + + <tip> + <para>The authentication API is pretty basic. The easiest way to + understand how it works is to look at one of the existing plugins that + is located in the auth directory of your installation.</para> + </tip> + </section> +</article> \ No newline at end of file diff --git a/lib/SimpleSAML/Bindings/SAML20/HTTPPost.php b/lib/SimpleSAML/Bindings/SAML20/HTTPPost.php new file mode 100644 index 000000000..8d68c99f7 --- /dev/null +++ b/lib/SimpleSAML/Bindings/SAML20/HTTPPost.php @@ -0,0 +1,213 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnResponse.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_Bindings_SAML20_HTTPPost { + + private $configuration = null; + private $metadata = null; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + + public function sendResponseUnsigned($response, $idpentityid, $spentityid, $relayState = null, $endpoint = 'assertionConsumerServiceURL') { + + $idpmd = $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted'); + $spmd = $this->metadata->getMetaData($spentityid, 'saml20-sp-remote'); + + $destination = $spmd[$endpoint]; + + echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Send SAML 2.0 Authentication Response</title> + </head> + <body> + <h1>Send SAML 2.0 Authentication Response</h1> + + <form style="border: 1px solid #777; margin: 2em; padding: 2em" method="post" action="' . $destination . '"> + <input type="hidden" name="SAMLResponse" value="' . base64_encode($response) . '" /> + <input type="hidden" name="RelayState" value="' . $relayState. '"> + <input type="submit" value="Submit the SAML 1.1 Response" /> + </form> + + <ul> + <li>From IdP: <tt>' . $idpentityid . '</tt></li> + <li>To SP: <tt>' . $spentityid . '</tt></li> + <li>SP Assertion Consumer Service URL: <tt>' . $destination . '</tt></li> + <li>RelayState: <tt>' . $relayState . '</tt></li> + </ul> + + <p>SAML Message: <pre>' . htmlentities($response) . '</pre> + + + </body> + </html>'; + } + + public function sendResponse($response, $idpentityid, $spentityid, $relayState = null) { + + $idpmd = $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted'); + $spmd = $this->metadata->getMetaData($spentityid, 'saml20-sp-remote'); + + $destination = $spmd['assertionConsumerServiceURL']; + + /* + $privatekey = "/home/as/erlang/feide2/cert/edugain/server1Key.pem"; + $publiccert = "/home/as/erlang/feide2/cert/edugain/server2chain.pem"; + */ + + $privatekey = "/home/as/erlang/feide2/cert/server.pem"; + $publiccert = "/home/as/erlang/feide2/cert/server.crt"; + + $privatekey = $this->configuration->getValue('basedir') . '/cert/' . $idpmd['privatekey']; + $publiccert = $this->configuration->getValue('basedir') . '/cert/' . $idpmd['certificate']; + + if (!file_exists($privatekey)) + throw new Exception('Could not find private key file [' . $privatekey . '] which is needed to sign the authentication response'); + + if (!file_exists($publiccert)) + throw new Exception('Could not find certificate [' . $publiccert . '] to attach to the authentication resposne'); + + + /* + * XMLDSig. Sign the complete request with the key stored in cert/server.pem + */ + $objXMLSecDSig = new XMLSecurityDSig(); + //$objXMLSecDSig->idKeys[] = 'ResponseID'; + #$objXMLSecDSig->idKeys = array('ResponseID'); + + $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); + + + try { + $responsedom = new DOMDocument(); + $responsedom->loadXML(str_replace("\n", "", str_replace ("\r", "", $response))); + } catch (Exception $e) { + throw new Exception("foo"); + } + $responseroot = $responsedom->getElementsByTagName('Response')->item(0); + + //$assertionroot = $responsedom->getElementsByTagName('Assertion')->item(1); + $firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0); + + //$objXMLSecDSig->addReferenceList(array($responseroot), XMLSecurityDSig::SHA1, //array('http://www.w3.org/2000/09/xmldsig#enveloped-signature')); + + $objXMLSecDSig->addReferenceList(array($firstassertionroot), XMLSecurityDSig::SHA1, array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#')); + + #$objXMLSecDSig->addRefInternal($responseroot, $responseroot, XMLSecurityDSig::SHA1); + + /* create new XMLSecKey using RSA-SHA-1 and type is private key */ + $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'private')); + + /* load the private key from file - last arg is bool if key in file (TRUE) or is string (FALSE) */ + $objKey->loadKey($privatekey,TRUE); + + + + + + $objXMLSecDSig->sign($objKey); + + $public_cert = file_get_contents($publiccert); + $objXMLSecDSig->add509Cert($public_cert, true); + /* + $public_cert = file_get_contents("cert/edugain/public2.pem"); + $objXMLSecDSig->add509Cert($public_cert, true); + + $public_cert = file_get_contents("cert/edugain/public3.pem"); + $objXMLSecDSig->add509Cert($public_cert, true); + */ + + + $objXMLSecDSig->appendSignature($firstassertionroot, true, true); + //$objXMLSecDSig->appendSignature($responseroot, true, false); + + $response = $responsedom->saveXML(); + + + # openssl genrsa -des3 -out server.key 1024 + # openssl rsa -in server.key -out server.pem + # openssl req -new -key server.key -out server.csr + # openssl x509 -req -days 60 -in server.csr -signkey server.key -out server.crt + + if ($this->configuration->getValue('debug')) { + + $p = new SimpleSAML_XHTML_Template($this->configuration, 'post-debug.php'); + + $p->data['header'] = 'SAML Response Debug-mode'; + $p->data['RelayStateName'] = 'RelayState'; + $p->data['RelayState'] = $relayState; + $p->data['destination'] = $destination; + $p->data['response'] = str_replace("\n", "", base64_encode($response)); + $p->data['responseHTML'] = htmlentities($responsedom->saveHTML()); + + $p->show(); + + + } else { + + $p = new SimpleSAML_XHTML_Template($this->configuration, 'post.php'); + + $p->data['RelayStateName'] = 'RelayState'; + $p->data['RelayState'] = $relayState; + $p->data['destination'] = $destination; + $p->data['response'] = base64_encode($response); + + $p->show(); + + + } + + + } + + public function decodeResponse($post) { + $rawResponse = $post["SAMLResponse"]; + $relaystate = $post["RelayState"]; + + $samlResponseXML = base64_decode( $rawResponse ); + + //error_log("Response is: " . $samlResponseXML); + + $samlResponse = new SimpleSAML_XML_SAML20_AuthnResponse($this->configuration, $this->metadata); + + $samlResponse->setXML($samlResponseXML); + + if (isset($relaystate)) { + $samlResponse->setRelayState($relaystate); + } + + #echo("Authn response = " . $samlResponse ); + + return $samlResponse; + + } + + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php b/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php new file mode 100644 index 000000000..cf12376a5 --- /dev/null +++ b/lib/SimpleSAML/Bindings/SAML20/HTTPRedirect.php @@ -0,0 +1,125 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_Bindings_SAML20_HTTPRedirect { + + private $configuration = null; + private $metadata = null; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + public function sendMessage($request, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnUrl', $direction = 'SAMLRequest', $mode = 'SP') { + if (!in_array($mode, array('SP', 'IdP'))) { + throw new Exception('mode parameter of sendMessage() must be either SP or IdP'); + } + $metadataset = 'saml20-idp-remote'; + if ($mode == 'IdP') { + $metadataset = 'saml20-sp-remote'; + } + + $md = $this->metadata->getMetaData($remoteentityid, $metadataset); + $idpTargetUrl = $md[$endpoint]; + + $encodedRequest = urlencode( base64_encode( gzdeflate( $request ) )); + + $redirectURL = $idpTargetUrl . "?" . $direction . "=" . $encodedRequest; + if (isset($relayState)) { + $redirectURL .= "&RelayState=" . urlencode($relayState); + } + + + header("Location: " . $redirectURL); + + } + + + + public function decodeRequest($get) { + if (!isset($get['SAMLRequest'])) { + throw new Exception('SAMLRequest parameter not set in paramter (on SAML 2.0 HTTP Redirect binding endpoint)'); + } + $rawRequest = $get["SAMLRequest"]; + $relaystate = isset($get["RelayState"]) ? $get["RelayState"] : null; + + $samlRequestXML = gzinflate(base64_decode( $rawRequest )); + + $samlRequest = new SimpleSAML_XML_SAML20_AuthnRequest($this->configuration, $this->metadata); + + $samlRequest->setXML($samlRequestXML); + + if (isset($relaystate)) { + $samlRequest->setRelayState($relaystate); + } + + #echo("Authn response = " . $samlResponse ); + + return $samlRequest; + + } + + public function decodeLogoutRequest($get) { + if (!isset($get['SAMLRequest'])) { + throw new Exception('SAMLRequest parameter not set in paramter (on SAML 2.0 HTTP Redirect binding endpoint)'); + } + $rawRequest = $get["SAMLRequest"]; + $relaystate = isset($get["RelayState"]) ? $get["RelayState"] : null; + + $samlRequestXML = gzinflate(base64_decode( $rawRequest )); + + $samlRequest = new SimpleSAML_XML_SAML20_LogoutRequest($this->configuration, $this->metadata); + + $samlRequest->setXML($samlRequestXML); + + if (isset($relaystate)) { + $samlRequest->setRelayState($relaystate); + } + + #echo("Authn response = " . $samlResponse ); + + return $samlRequest; + } + + public function decodeLogoutResponse($get) { + if (!isset($get['SAMLResponse'])) { + throw new Exception('SAMLResponse parameter not set in paramter (on SAML 2.0 HTTP Redirect binding endpoint)'); + } + $rawRequest = $get["SAMLResponse"]; + $relaystate = isset($get["RelayState"]) ? $get["RelayState"] : null; + + $samlRequestXML = gzinflate(base64_decode( $rawRequest )); + + $samlRequest = new SimpleSAML_XML_SAML20_LogoutResponse($this->configuration, $this->metadata); + + $samlRequest->setXML($samlRequestXML); + + if (isset($relaystate)) { + $samlRequest->setRelayState($relaystate); + } + + #echo("Authn response = " . $samlResponse ); + + return $samlRequest; + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Bindings/Shib13/HTTPPost.php b/lib/SimpleSAML/Bindings/Shib13/HTTPPost.php new file mode 100644 index 000000000..cbe2a19a3 --- /dev/null +++ b/lib/SimpleSAML/Bindings/Shib13/HTTPPost.php @@ -0,0 +1,198 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/Shib13/AuthnResponse.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_Bindings_Shib13_HTTPPost { + + private $configuration = null; + private $metadata = null; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + + public function sendResponseUnsigned($response, $idpentityid, $spentityid, $relayState = null, $endpoint = 'assertionConsumerServiceURL') { + + $idpmd = $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted'); + $spmd = $this->metadata->getMetaData($spentityid, 'saml20-sp-remote'); + + $destination = $spmd[$endpoint]; + + echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Send SAML 2.0 Authentication Response</title> + </head> + <body> + <h1>Send SAML 2.0 Authentication Response</h1> + + <form style="border: 1px solid #777; margin: 2em; padding: 2em" method="post" action="' . $destination . '"> + <input type="hidden" name="SAMLResponse" value="' . base64_encode($response) . '" /> + <input type="hidden" name="TARGET" value="' . $relayState. '"> + <input type="submit" value="Submit the SAML 1.1 Response" /> + </form> + + <ul> + <li>From IdP: <tt>' . $idpentityid . '</tt></li> + <li>To SP: <tt>' . $spentityid . '</tt></li> + <li>SP Assertion Consumer Service URL: <tt>' . $destination . '</tt></li> + <li>RelayState: <tt>' . $relayState . '</tt></li> + </ul> + + <p>SAML Message: <pre>' . htmlentities($response) . '</pre> + + + </body> + </html>'; + } + + public function sendResponse($response, $idpentityid, $spentityid, $relayState = null) { + + $idpmd = $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted'); + $spmd = $this->metadata->getMetaData($spentityid, 'saml20-sp-remote'); + + $destination = $spmd['assertionConsumerServiceURL']; + + /* + $privatekey = "/home/as/erlang/feide2/cert/edugain/server1Key.pem"; + $publiccert = "/home/as/erlang/feide2/cert/edugain/server2chain.pem"; + */ + + $privatekey = "/home/as/erlang/feide2/cert/server.pem"; + $publiccert = "/home/as/erlang/feide2/cert/server.crt"; + + + /* + * XMLDSig. Sign the complete request with the key stored in cert/server.pem + */ + $objXMLSecDSig = new XMLSecurityDSig(); + //$objXMLSecDSig->idKeys[] = 'ResponseID'; + #$objXMLSecDSig->idKeys = array('ResponseID'); + + $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); + + $responsedom = new DOMDocument(); + $responsedom->loadXML(str_replace ("\r", "", $response)); + + $responseroot = $responsedom->getElementsByTagName('Response')->item(0); + + //$assertionroot = $responsedom->getElementsByTagName('Assertion')->item(1); + $firstassertionroot = $responsedom->getElementsByTagName('Assertion')->item(0); + + #$objXMLSecDSig->addReferenceList(array($responseroot), XMLSecurityDSig::SHA1, array('http://www.w3.org/2000/09/xmldsig#enveloped-signature')); + $objXMLSecDSig->addReferenceList(array($firstassertionroot), XMLSecurityDSig::SHA1, array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', + 'http://www.w3.org/2001/10/xml-exc-c14n#')); + + #$objXMLSecDSig->addRefInternal($responseroot, $responseroot, XMLSecurityDSig::SHA1); + + /* create new XMLSecKey using RSA-SHA-1 and type is private key */ + $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA1, array('type'=>'private')); + + /* load the private key from file - last arg is bool if key in file (TRUE) or is string (FALSE) */ + $objKey->loadKey($privatekey,TRUE); + + + + + + $objXMLSecDSig->sign($objKey); + + $public_cert = file_get_contents($publiccert); + $objXMLSecDSig->add509Cert($public_cert, true); + /* + $public_cert = file_get_contents("cert/edugain/public2.pem"); + $objXMLSecDSig->add509Cert($public_cert, true); + + $public_cert = file_get_contents("cert/edugain/public3.pem"); + $objXMLSecDSig->add509Cert($public_cert, true); + */ + + + $objXMLSecDSig->appendSignature($firstassertionroot, true); + + $response = $responsedom->saveXML(); + + + # openssl genrsa -des3 -out server.key 1024 + # openssl rsa -in server.key -out server.pem + # openssl req -new -key server.key -out server.csr + # openssl x509 -req -days 60 -in server.csr -signkey server.key -out server.crt + + + + echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> + <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>Send SAML 2.0 Authentication Response</title> + </head> + <body> + <h1>Send SAML 2.0 Authentication Response</h1> + + <form style="border: 1px solid #777; margin: 2em; padding: 2em" method="post" action="' . $destination . '"> + <input type="hidden" name="SAMLResponse" value="' . base64_encode($response) . '" /> + <input type="hidden" name="TARGET" value="' . $relayState. '"> + <input type="submit" value="Submit the SAML 1.1 Response" /> + </form> + + <ul> + <li>From IdP: <tt>' . $idpentityid . '</tt></li> + <li>To SP: <tt>' . $spentityid . '</tt></li> + <li>SP Assertion Consumer Service URL: <tt>' . $destination . '</tt></li> + <li>RelayState: <tt>' . $relayState . '</tt></li> + </ul> + + <p>SAML Message: <pre>' . htmlentities($responsedom->saveHTML()) . '</pre> + + + </body> + </html>'; + + } + + public function decodeResponse($post) { + $rawResponse = $post["SAMLResponse"]; + $relaystate = $post["TARGET"]; + + $samlResponseXML = base64_decode( $rawResponse ); + + $samlResponse = new SimpleSAML_XML_Shib13_AuthnResponse($this->configuration, $this->metadata); + + $samlResponse->setXML($samlResponseXML); + + if (isset($relaystate)) { + $samlResponse->setRelayState($relaystate); + } + + #echo("Authn response = " . $samlResponse ); + + return $samlResponse; + + } + + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Configuration.php b/lib/SimpleSAML/Configuration.php new file mode 100644 index 000000000..116c018a8 --- /dev/null +++ b/lib/SimpleSAML/Configuration.php @@ -0,0 +1,53 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_Configuration { + + private static $instance = null; + + private $configpath = null; + private $configuration = null; + + // private constructor restricts instantiaton to getInstance() + private function __construct($configpath) { + + $this->configpath = $configpath; + + } + + public function getInstance() { + return self::$instance; + } + + public static function init($path) { + self::$instance = new SimpleSAML_Configuration($path); + } + + private function loadConfig() { + require_once($this->configpath . '/config.php'); + $this->configuration = $config; + } + + public function getValue($name) { + if (!isset($this->configuration)) { + $this->loadConfig(); + } + return $this->configuration[$name]; + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Session.php b/lib/SimpleSAML/Session.php new file mode 100644 index 000000000..03befb7c6 --- /dev/null +++ b/lib/SimpleSAML/Session.php @@ -0,0 +1,223 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/XML/AuthnResponse.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); + +/** + * A class representing a session. + */ +class SimpleSAML_Session { + + const STATE_ONLINE = 1; + const STATE_LOGOUTINPROGRESS = 2; + const STATE_LOGGEDOUT = 3; + + private static $instance = null; + + private $configuration = null; + + private $authnrequests = array(); + private $authnresponse = null; + + private $logoutrequest = null; + + private $authenticated = null; + private $protocol = null; + private $attributes = null; + + + private $sessionindex = null; + private $nameid = null; + private $nameidformat = null; + + private $sp_at_idpsessions = array(); + + // Session duration parameters + private $sessionstarted = null; + private $sessionduration = null; + + // private constructor restricts instantiaton to getInstance() + private function __construct($protocol, SimpleSAML_XML_AuthnResponse $message = null, $authenticated = true) { + + $this->configuration = SimpleSAML_Configuration::getInstance(); + + $this->protocol = $protocol; + $this->authnresponse = $message; + + $this->authenticated = $authenticated; + if ($authenticated) { + $this->sessionstarted = time(); + } + + $this->sessionduration = $this->configuration->getValue('session.duration'); + } + + public function add_sp_session($entityid) { + $this->sp_at_idpsessions[$entityid] = self::STATE_ONLINE; + } + + public function get_next_sp_logout() { + + if (!$this->sp_at_idpsessions) return null; + + foreach ($this->sp_at_idpsessions AS $entityid => $sp) { + if ($sp == self::STATE_ONLINE) { + $this->sp_at_idpsessions[$entityid] = self::STATE_LOGOUTINPROGRESS; + return $entityid; + } + } + return null; + } + + public function set_sp_logout_completed($entityid) { + $this->sp_at_idpsessions[$entityid] = self::STATE_LOGGEDOUT; + } + + + public function dump_sp_sessions() { + foreach ($this->sp_at_idpsessions AS $entityid => $sp) { + error_log('Dump sp sessions: ' . $entityid . ' status: ' . $sp); + } + } + + public function getInstance() { + if (isset(self::$instance)) { + return self::$instance; + } elseif(isset($_SESSION['SimpleSAMLphp_SESSION'])) { + self::$instance = $_SESSION['SimpleSAMLphp_SESSION']; + return self::$instance; + } + return null; + } + + public static function init($protocol, $message = null, $authenticated = true) { + + $preinstance = self::getInstance(); + + if (isset($preinstance)) { + if (isset($message)) $preinstance->authnresponse = $message; + if (isset($authenticated)) $preinstance->setAuthenticated($authenticated); + } else { + self::$instance = new SimpleSAML_Session($protocol, $message, $authenticated); + $_SESSION['SimpleSAMLphp_SESSION'] = self::$instance; + } + } + + public function setAuthnRequest($requestid, SimpleSAML_XML_SAML20_AuthnRequest $xml) { + $this->authnrequests[$requestid] = $xml; + } + + public function getAuthnRequest($requestid) { + return $this->authnrequests[$requestid]; + } + + public function setAuthnResponse(SimpleSAML_XML_AuthnResponse $xml) { + $this->authnresponse = $xml; + } + + public function getAuthnResposne() { + return $this->authnresponse; + } + + public function setLogoutRequest(SimpleSAML_XML_SAML20_LogoutRequest $lr) { + $this->logoutrequest = $lr; + } + + public function getLogoutRequest() { + return $this->logoutrequest; + } + + public function setSessionIndex($sessionindex) { + $this->sessionindex = $sessionindex; + } + public function getSessionIndex() { + return $this->sessionindex; + } + public function setNameID($nameid) { + $this->nameid = $nameid; + } + public function getNameID() { + return $this->nameid; + } + public function setNameIDformat($nameidformat) { + $this->nameidformat = $nameidformat; + } + public function getNameIDformat() { + return $this->nameidformat; + } + + public function setAuthenticated($auth) { + $this->authenticated = $auth; + if ($auth) { + $this->sessionstarted = time(); + } + } + + public function setSessionDuration($duration) { + $this->sessionduration = $duration; + } + + + /* + * Is the session representing an authenticated user, and is the session still alive. + * This function will return false after the user has timed out. + */ + + public function isValid() { + if (!$this->isAuthenticated()) return false; + return $this->remainingTime() > 0; + } + + /* + * If the user is authenticated, how much time is left of the session. + */ + public function remainingTime() { + return $this->sessionduration - (time() - $this->sessionstarted); + } + + /* + * Is the user authenticated. This function does not check the session duration. + */ + public function isAuthenticated() { + return $this->authenticated; + } + + + + + public function getProtocol() { + return $this->protocol; + } + + public function getAttributes() { + return $this->attributes; + } + + public function getAttribute($name) { + return $this->attributes[$name]; + } + + public function setAttributes($attributes) { + $this->attributes = $attributes; + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/Utilities.php b/lib/SimpleSAML/Utilities.php new file mode 100644 index 000000000..6b6af783e --- /dev/null +++ b/lib/SimpleSAML/Utilities.php @@ -0,0 +1,92 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_Utilities { + + + public static function selfURLNoQuery() { + + $s = empty($_SERVER["HTTPS"]) ? '' + : ($_SERVER["HTTPS"] == "on") ? "s" + : ""; + $protocol = self::strleft(strtolower($_SERVER["SERVER_PROTOCOL"]), "/").$s; + $port = ($_SERVER["SERVER_PORT"] == "80") ? "" + : (":".$_SERVER["SERVER_PORT"]); + $querystring = ''; + return $protocol."://".$_SERVER['HTTP_HOST'].$port . $_SERVER['SCRIPT_NAME']; + + } + + public static function selfURL() { + + $s = empty($_SERVER["HTTPS"]) ? '' + : ($_SERVER["HTTPS"] == "on") ? "s" + : ""; + $protocol = self::strleft(strtolower($_SERVER["SERVER_PROTOCOL"]), "/").$s; + $port = ($_SERVER["SERVER_PORT"] == "80") ? "" + : (":".$_SERVER["SERVER_PORT"]); + $querystring = ''; + return $protocol."://".$_SERVER['HTTP_HOST'].$port.$_SERVER['REQUEST_URI']; + + } + + public static function addURLparameter($url, $parameter) { + if (strstr($url, '?')) { + return $url . '&' . $parameter; + } else { + return $url . '?' . $parameter; + } + } + + public static function strleft($s1, $s2) { + return substr($s1, 0, strpos($s1, $s2)); + } + + public static function checkDateConditions($start=NULL, $end=NULL) { + $currentTime = time(); + + if (! empty($start)) { + $startTime = strtotime($start); + /* Allow for a 10 minute difference in Time */ + if (($startTime < 0) || (($startTime - 600) > $currentTime)) { + return FALSE; + } + } + if (! empty($end)) { + $endTime = strtotime($end); + if (($endTime < 0) || ($endTime <= $currentTime)) { + return FALSE; + } + } + return TRUE; + } + + public static function generateID() { + + $length = 42; + $key = "_"; + for ( $i=0; $i < $length; $i++ ) + { + $key .= dechex( rand(0,15) ); + } + return $key; + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XHTML/Template.php b/lib/SimpleSAML/XHTML/Template.php new file mode 100644 index 000000000..11e845ee4 --- /dev/null +++ b/lib/SimpleSAML/XHTML/Template.php @@ -0,0 +1,45 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XHTML_Template { + + private $configuration = null; + private $template = 'default.php'; + + public $data = null; + + function __construct(SimpleSAML_Configuration $configuration, $template) { + $this->configuration = $configuration; + $this->template = $template; + + $this->data['baseurlpath'] = $this->configuration->getValue('baseurlpath'); + } + + public function show() { + $data = $this->data; + $filename = $this->configuration->getValue('templatedir') . '/' . $this->template; + if (!file_exists($filename)) { + throw new Exception('Could not find template file [' . $this->template . '] at [' . $filename . ']'); + } + require_once($filename); + } + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/AuthnResponse.php b/lib/SimpleSAML/XML/AuthnResponse.php new file mode 100644 index 000000000..c2cd28d46 --- /dev/null +++ b/lib/SimpleSAML/XML/AuthnResponse.php @@ -0,0 +1,115 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); + +require_once('xmlseclibs.php'); + +/** + * Configuration of SimpleSAMLphp + */ +abstract class SimpleSAML_XML_AuthnResponse { + + private $configuration = null; + private $metadata = 'default.php'; + + private $message = null; + private $dom; + private $relayState = null; + + private $validIDs = null; + + const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0'; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + + abstract public function validate(); + + abstract public function createSession(); + + + + abstract public function getAttributes(); + + + abstract public function getIssuer(); + + abstract public function getNameID(); + + + public function setXML($xml) { + $this->message = $xml; + } + + public function getXML() { + return $this->message; + } + + public function setRelayState($relayState) { + $this->relayState = $relayState; + } + + public function getRelayState() { + return $this->relayState; + } + + public function getDOM() { + if (isset($this->message) ) { + + /* + if (isset($this->dom)) { + return $this->dom; + } + */ + + $token = new DOMDocument(); + $token->loadXML(str_replace ("\r", "", $this->message)); + if (empty($token)) { + throw new Exception("Unable to load token"); + } + $this->dom = $token; + return $this->dom; + + } + + return null; + } + + + + + public static function generateID() { + + $length = 42; + $key = "_"; + for ( $i=0; $i < $length; $i++ ) + { + $key .= dechex( rand(0,15) ); + } + return $key; + } + + public static function generateIssueInstant($offset = 0) { + return gmdate("Y-m-d\TH:i:s\Z", time() + $offset); + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/MetaDataStore.php b/lib/SimpleSAML/XML/MetaDataStore.php new file mode 100644 index 000000000..8d15a3ed8 --- /dev/null +++ b/lib/SimpleSAML/XML/MetaDataStore.php @@ -0,0 +1,105 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_MetaDataStore { + + private $configuration = null; + private $metadata = null; + private $hostmap = null; + + function __construct(SimpleSAML_Configuration $configuration) { + $this->configuration = $configuration; + } + + public function load($set) { + $metadata = null; + if (!in_array($set, array( + 'saml20-sp-hosted', 'saml20-sp-remote','saml20-idp-hosted', 'saml20-idp-remote', + 'shib13-sp-hosted', 'shib13-sp-remote', 'shib13-idp-hosted', 'shib13-idp-remote'))) { + throw new Exception('Trying to load illegal set of Meta data [' . $set . ']'); + } + + $metadatasetfile = $this->configuration->getValue('metadatadir') . '/' . $set . '.php'; + + if (!file_exists($metadatasetfile)) { + throw new Exception('Could not open file: ' . $metadatasetfile); + } + include($metadatasetfile); + + if (!is_array($metadata)) { + throw new Exception('Could not load metadata set [' . $set . '] from file: ' . $metadatasetfile); + } + foreach ($metadata AS $key => $entry) { + $this->metadata[$set][$key] = $entry; + $this->metadata[$set][$key]['entityid'] = $key; + + if (isset($entry['host'])) { + $this->hostmap[$set][$entry['host']] = $key; + } + + } + /* + echo '<pre>'; + print_r(); + echo '</pre>'; + */ + } + + public function getMetaDataCurrentEntityID($set = 'saml20-sp-hosted') { + + if (!isset($this->metadata[$set])) { + $this->load($set); + } + $currenthost = $_SERVER['HTTP_HOST']; + if (!isset($this->hostmap[$set])) { + throw new Exception('No default entities defined for metadata set [' . $set . ']'); + } + if (!isset($currenthost)) { + throw new Exception('Could not get HTTP_HOST, in order to resolve default entity ID'); + } + if (!isset($this->hostmap[$set][$currenthost])) { + throw new Exception('Could not find any default metadata entities in set [' . $set . '] for host [' . $currenthost . ']'); + } + if (!$this->hostmap[$set][$currenthost]) throw new Exception('Could not find default metadata for current host'); + return $this->hostmap[$set][$currenthost]; + } + + public function getMetaDataCurrent($set = 'saml20-sp-hosted') { + return $this->getMetaData($this->getMetaDataCurrentEntityID($set), $set); + } + + public function getMetaData($entityid = null, $set = 'saml20-sp-hosted') { + if (!isset($entityid)) { + return $this->getMetaDataCurrent($set); + } + + //echo 'find metadata for entityid [' . $entityid . '] in metadata set [' . $set . ']'; + + if (!isset($this->metadata[$set])) { + $this->load($set); + } + if (!isset($this->metadata[$set][$entityid]) ) { + throw new Exception('Could not find metadata for entityid [' . $entityid . '] in metadata set [' . $set . ']'); + } + return $this->metadata[$set][$entityid]; + } + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/SAML20/AuthnRequest.php b/lib/SimpleSAML/XML/SAML20/AuthnRequest.php new file mode 100644 index 000000000..83986d75b --- /dev/null +++ b/lib/SimpleSAML/XML/SAML20/AuthnRequest.php @@ -0,0 +1,195 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_SAML20_AuthnRequest { + + private $configuration = null; + private $metadata = 'default.php'; + + private $message = null; + private $dom; + private $relayState = null; + + + const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0'; + + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + public function setXML($xml) { + $this->message = $xml; + } + + public function getXML() { + return $this->message; + } + + public function setRelayState($relayState) { + $this->relayState = $relayState; + } + + public function getRelayState() { + return $this->relayState; + } + + public function getDOM() { + if (isset($this->message) ) { + + /* if (isset($this->dom) && $this->dom != null ) { + return $this->dom; + } */ + + $token = new DOMDocument(); + $token->loadXML(str_replace ("\r", "", $this->message)); + if (empty($token)) { + throw new Exception("Unable to load token"); + } + $this->dom = $token; + return $this->dom; + + } + + return null; + } + + + public function getIssuer() { + $dom = $this->getDOM(); + $issuer = null; + + if (!$dom instanceof DOMDocument) { + throw new Exception("Could not get message DOM in AuthnRequest object"); + } + + //print_r($dom->saveXML()); + + if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { + if ($issuerNodes->length > 0) { + $issuer = $issuerNodes->item(0)->textContent; + } + } + return $issuer; + } + + public function getRequestID() { + $dom = $this->getDOM(); + $requestid = null; + + if (empty($dom)) { + throw new Exception("Could not get message DOM in AuthnRequest object"); + } + + $requestelement = $dom->getElementsByTagName('AuthnRequest')->item(0); + $requestid = $requestelement->getAttribute('ID'); + return $requestid; + /* + if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { + if ($issuerNodes->length > 0) { + $requestid = $issuerNodes->item(0)->textContent; + } + } + return $requestid; + */ + } + + public function createSession() { + + + $session = SimpleSAML_Session::getInstance(); + + if (!isset($session)) { + SimpleSAML_Session::init(self::PROTOCOL, null, false); + $session = SimpleSAML_Session::getInstance(); + } + + $session->setAuthnRequest($this->getRequestID(), $this); + + /* + if (isset($this->relayState)) { + $session->setRelayState($this->relayState); + } + */ + return $session; + } + + + public function generate($spentityid) { + $md = $this->metadata->getMetaData($spentityid); + + $id = self::generateID(); + $issueInstant = self::generateIssueInstant(); + + $assertionConsumerServiceURL = $md['assertionConsumerServiceURL']; + $spNameQualifier = $md['spNameQualifier']; + $nameidformat = isset($md['NameIDformat']) ? + $md['NameIDformat'] : + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'; + + $authnRequest = "<samlp:AuthnRequest " . + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"\n" . + "ID=\"" . $id . "\" " . + "Version=\"2.0\" " . + "IssueInstant=\"" . $issueInstant . "\" " . + "ForceAuthn=\"false\" " . + "IsPassive=\"false\" " . + "ProtocolBinding=\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\" " . + "AssertionConsumerServiceURL=\"" . $assertionConsumerServiceURL . "\">\n" . + "<saml:Issuer " . + "xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" . + $spentityid . + "</saml:Issuer>\n" . + "<samlp:NameIDPolicy " . + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " . + "Format=\"" . $nameidformat. "\" " . + "SPNameQualifier=\"" . $spNameQualifier . "\" " . + "AllowCreate=\"true\" />\n" . + "<samlp:RequestedAuthnContext " . + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " . + "Comparison=\"exact\">" . + "<saml:AuthnContextClassRef " . + "xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" . + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" . + "</saml:AuthnContextClassRef>" . + "</samlp:RequestedAuthnContext>\n" . + "</samlp:AuthnRequest>"; + + return $authnRequest; + } + + public static function generateID() { + + $length = 42; + $key = "_"; + for ( $i=0; $i < $length; $i++ ) + { + $key .= dechex( rand(0,15) ); + } + return $key; + } + + public static function generateIssueInstant() { + return gmdate("Y-m-d\TH:i:s\Z"); + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/SAML20/AuthnResponse.php b/lib/SimpleSAML/XML/SAML20/AuthnResponse.php new file mode 100644 index 000000000..1e7b18920 --- /dev/null +++ b/lib/SimpleSAML/XML/SAML20/AuthnResponse.php @@ -0,0 +1,528 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ă…kre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/AuthnResponse.php'); + +require_once('xmlseclibs.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_SAML20_AuthnResponse extends SimpleSAML_XML_AuthnResponse { + + private $configuration = null; + private $metadata = 'default.php'; + + private $message = null; + private $dom; + private $relayState = null; + + private $validIDs = null; + + const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0'; + + const TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'; + const EMAIL = 'urn:oasis:names:tc:SAML:2.0:nameid-format:email'; + + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + + public function validate() { + + $dom = $this->getDOM(); + + /* Create an XML security object, and register ID as the id attribute for sig references. */ + $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig->idKeys[] = 'ID'; + + /* Locate the signature element to be used. */ + $objDSig = $objXMLSecDSig->locateSignature($dom); + + + /* If no signature element was found, throw an error */ + if (!$objDSig) { + throw new Exception("Could not locate XML Signature element in Authentication Response"); + } + + + /* Must check certificate fingerprint now - validateReference removes it */ + // TODO FIX"!!! + if ( ! $this->validateCertFingerprint($objDSig) ) { + throw new Exception("Fingerprint Validation Failed"); + } + + /* Get information about canoncalization in to the xmlsec library. Read from the siginfo part. */ + $objXMLSecDSig->canonicalizeSignedInfo(); + + $refids = $objXMLSecDSig->getRefIDs(); + + + + /* Validate refrences */ + $retVal = $objXMLSecDSig->validateReference(); + if (! $retVal) { + throw new Exception("XMLsec: digest validation failed"); + } + + $key = NULL; + $objKey = $objXMLSecDSig->locateKey(); + + if ($objKey) { + if ($objKeyInfo = XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig)) { + /* Handle any additional key processing such as encrypted keys here */ + } + } + + if (empty($objKey)) { + throw new Exception("Error loading key to handle Signature"); + } + + if (! $objXMLSecDSig->verify($objKey)) { + throw new Exception("Unable to validate Signature"); + } + + $this->validIDs = $refids; + return true; + } + + + + + function validateCertFingerprint($dom) { +// $dom = $this->getDOM(); + $fingerprint = ""; + + + // Find the certificate in the document. + if ($x509certNodes = $dom->getElementsByTagName('X509Certificate')) { + if ($x509certNodes->length > 0) { + $x509cert = $x509certNodes->item(0)->textContent; + $x509data = base64_decode( $x509cert ); + $fingerprint = strtolower( sha1( $x509data ) ); + } + } + + // Get the issuer of the assertion. + $issuer = $this->getIssuer(); + $md = $this->metadata->getMetaData($issuer, 'saml20-idp-remote'); + + /* + * Get fingerprint from saml20-idp-remote metadata... + * + * Accept fingerprints with or without colons, case insensitive + */ + $issuerFingerprint = strtolower( str_replace(":", "", $md['certFingerprint']) ); + + + + if (empty($issuerFingerprint)) { + throw new Exception("Certificate finger print for entity ID [" . $issuer . "] in metadata was empty."); + } + if (empty($fingerprint)) { + throw new Exception("Certificate finger print in message was empty."); + } + + if ($fingerprint != $issuerFingerprint) { + echo "Expecting fingerprint $issuerFingerprint but got fingerprint $fingerprint .st"; + } + + return ($fingerprint == $issuerFingerprint); + } + + + public function createSession() { + + //($protocol, $message = null, $authenticated = true) { + SimpleSAML_Session::init(self::PROTOCOL, $this, true); + $session = SimpleSAML_Session::getInstance(); + $session->setAttributes($this->getAttributes()); + + + $nameid = $this->getNameID(); + + $session->setNameID($nameid['NameID']); + $session->setNameIDFormat($nameid['Format']); + $session->setSessionIndex($this->getSessionIndex()); + /* + $nameID["NameID"] = $node->nodeValue; + + $nameID["NameQualifier"] = $node->getAttribute('NameQualifier'); + $nameID["SPNameQualifier"] = $node->getAttribute('SPNameQualifier'); + */ + return $session; + } + + //TODO + function getSessionIndex() { + $token = $this->getDOM(); + if ($token instanceof DOMDocument) { + $xPath = new DOMXpath($token); + $xPath->registerNamespace('mysaml', SAML2_ASSERT_NS); + $xPath->registerNamespace('mysamlp', SAML2_PROTOCOL_NS); + + $query = '/mysamlp:Response/mysaml:Assertion/mysaml:AuthnStatement'; + $nodelist = $xPath->query($query); + if ($node = $nodelist->item(0)) { + return $node->getAttribute('SessionIndex'); + } + } + return NULL; + } + + + public function getAttributes() { + + + $md = $this->metadata->getMetadata($this->getIssuer(), 'saml20-idp-remote'); + + $base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false; + + define('SAML2_ASSERT_NS', 'urn:oasis:names:tc:SAML:2.0:assertion'); + define('SAML2_PROTOCOL_NS', 'urn:oasis:names:tc:SAML:2.0:protocol'); + + define('SAML2_BINDINGS_POST', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'); + + define('SAML2_STATUS_SUCCESS', 'urn:oasis:names:tc:SAML:2.0:status:Success'); + + /* + echo 'Validids<pre>'; + print_r($this->validIDs); + echo '</pre>'; + */ + + $attributes = array(); + $token = $this->getDOM(); + + + //echo '<pre>' . $this->getXML() . '</pre>'; + + + if ($token instanceof DOMDocument) { + + /* + echo "<PRE>token:"; + echo htmlentities($token->saveXML()); + echo ":</PRE>"; + */ + + $xPath = new DOMXpath($token); + $xPath->registerNamespace("mysaml", SAML2_ASSERT_NS); + $xPath->registerNamespace("mysamlp", SAML2_PROTOCOL_NS); + $query = "/mysamlp:Response/mysaml:Assertion/mysaml:Conditions"; + $nodelist = $xPath->query($query); + + if ($node = $nodelist->item(0)) { + + $start = $node->getAttribute("NotBefore"); + $end = $node->getAttribute("NotOnOrAfter"); + + if (! SimpleSAML_Utilities::checkDateConditions($start, $end)) { + error_log( " Date check failed ... (from $start to $end)"); + + return $attributes; + } + } + + $valididqueries = array(); + foreach ($this->validIDs AS $vid) { + $valididqueries[] = "@ID='" . $vid . "'"; + } + $valididquery = join(' or ', $valididqueries); + + + foreach ( + array( + "/mysamlp:Response[" . $valididquery . "]/mysaml:Assertion/mysaml:AttributeStatement/mysaml:Attribute", + "/mysamlp:Response/mysaml:Assertion[" . $valididquery . "]/mysaml:AttributeStatement/mysaml:Attribute") AS $query) { + +// echo 'performing query : ' . $query; + +// $query = "/mysamlp:Response[" . $valididquery . "]/mysaml:Assertion/mysaml:AttributeStatement/mysaml:Attribute"; + $nodelist = $xPath->query($query); + + + +// if (is_array($nodelist)) { + + + foreach ($nodelist AS $node) { + + if ($name = $node->getAttribute("Name")) { +// echo "Name "; + $value = array(); + foreach ($node->childNodes AS $child) { + if ($child->localName == "AttributeValue") { + $newvalue = $child->textContent; + if ($base64) { + $values = explode('_', $newvalue); + foreach($values AS $v) { + $value[] = base64_decode($v); + } + } else { + + $value[] = $newvalue; + } + } + } + $attributes[$name] = $value; + } + } + +// } + + } + + + + } +/* + echo '<p>Attributes<pre>'; + print_r($attributes); + echo '</pre>'; +*/ + return $attributes; + } + + + public function getIssuer() { + $dom = $this->getDOM(); + $issuer = null; + if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { + if ($issuerNodes->length > 0) { + $issuer = $issuerNodes->item(0)->textContent; + } + } + return $issuer; + } + + public function getNameID() { + + $dom = $this->getDOM(); + $nameID = array(); + + if ($dom instanceof DOMDocument) { + $xPath = new DOMXpath($dom); + $xPath->registerNamespace('mysaml', SAML2_ASSERT_NS); + $xPath->registerNamespace('mysamlp', SAML2_PROTOCOL_NS); + + $query = '/mysamlp:Response/mysaml:Assertion/mysaml:Subject/mysaml:NameID'; + $nodelist = $xPath->query($query); + if ($node = $nodelist->item(0)) { + + $nameID["NameID"] = $node->nodeValue; + $nameID["NameQualifier"] = $node->getAttribute('NameQualifier'); + $nameID["SPNameQualifier"] = $node->getAttribute('SPNameQualifier'); + $nameID["Format"] = $node->getAttribute('Format'); + } + } + //echo '<pre>'; print_r($nameID); echo '</pre>'; + return $nameID; + } + + + // Not updated for response. from request. + public function generate($idpentityid, $spentityid, $inresponseto, $nameid, $attributes) { + + //echo 'idp:' . $idpentityid . ' sp:' . $spentityid .' inresponseto:' . $inresponseto . ' namid:' . $nameid; + + $idpmd = $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted'); + $spmd = $this->metadata->getMetaData($spentityid, 'saml20-sp-remote'); + + $id = self::generateID(); + $issueInstant = self::generateIssueInstant(); + $assertionExpire = self::generateIssueInstant(60 * 5); # 5 minutes + $notBefore = self::generateIssueInstant(-30); + + $assertionid = self::generateID(); + $sessionindex = self::generateID(); + + + $issuer = $idpentityid; + + $assertionConsumerServiceURL = $spmd['assertionConsumerServiceURL']; + $spNameQualifier = $spmd['spNameQualifier']; + + $destination = $spmd['assertionConsumerServiceURL']; + + $base64 = isset($idpmd['base64attributes']) ? $idpmd['base64attributes'] : false; + + $encodedattributes = ''; + foreach ($attributes AS $name => $value) { + $encodedattributes .= $this->enc_attribute($name, $value[0], $base64); + } + $attributestatement = '<saml:AttributeStatement>' . $encodedattributes . '</saml:AttributeStatement>'; + + if (!$spmd['simplesaml.attributes']) + $attributestatement = ''; + + $namid = null; + if ($spmd['NameIDFormat'] == self::EMAIL) { + $nameid = $this->generateNameID($spmd['NameIDFormat'], $attributes[$spmd['simplesaml.nameidattribute']][0]); + } else { + $nameid = $this->generateNameID($spmd['NameIDFormat'], self::generateID(), $issuer, $spNameQualifier); + } + + /* + $authnResponse = '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + ID="' . $id . '" + InResponseTo="' . $inresponseto. '" Version="2.0" + IssueInstant="' . $issueInstant . '" + Destination="' . $destination . '"> + <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' . $issuer . '</saml:Issuer> + <samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> + <samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + Value="urn:oasis:names:tc:SAML:2.0:status:Success"> </samlp:StatusCode> + </samlp:Status> + <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" + ID="' . $assertionid . '" IssueInstant="' . $issueInstant . '"> + <saml:Issuer>' . $issuer . '</saml:Issuer> + <saml:Subject> + <saml:NameID NameQualifier="' . $issuer . '" SPNameQualifier="'. $spentityid. '" + Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + >' . $nameid. '</saml:NameID> + <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> + <saml:SubjectConfirmationData NotOnOrAfter="' . $assertionExpire . '" + InResponseTo="' . $inresponseto. '" + Recipient="' . $destination . '"/> + </saml:SubjectConfirmation> + </saml:Subject> + <saml:Conditions NotBefore="' . $issueInstant. '" NotOnOrAfter="' . $assertionExpire. '"> + <saml:AudienceRestriction> + <saml:Audience>' . $spentityid . '</saml:Audience> + </saml:AudienceRestriction> + </saml:Conditions> + <saml:AuthnStatement AuthnInstant="' . $issueInstant . '" + SessionIndex="' . $sessionindex . '"> + <saml:AuthnContext> + <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef> + </saml:AuthnContext> + </saml:AuthnStatement> + <saml:AttributeStatement> + ' . $encodedattributes . ' + </saml:AttributeStatement> + </saml:Assertion> +</samlp:Response> +'; + + + $authnResponse = '<samlp:Response + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" + ID="' . $id . '" + IssueInstant="' . $issueInstant . '" +Version="2.0"> + + <samlp:Status> + <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> + </samlp:Status> + <saml:Assertion ID="' . $assertionid . '" + IssueInstant="' . $issueInstant . '" + Version="2.0"> + <saml:Issuer>' . $issuer . '</saml:Issuer> + <saml:Subject> + <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">test</saml:NameID> + <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer" /> + </saml:Subject> + <saml:Conditions NotBefore="' . $notBefore. '" NotOnOrAfter="' . $assertionExpire. '"></saml:Conditions> + <saml:AuthnStatement AuthnInstant="' . $issueInstant. '"> + <saml:AuthnContext> + <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef> + </saml:AuthnContext> + </saml:AuthnStatement> + </saml:Assertion> +</samlp:Response> +'; +*/ + $authnResponse = '<samlp:Response + xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + ID="' . $id . '" + InResponseTo="' . $inresponseto. '" Version="2.0" + IssueInstant="' . $issueInstant . '" + Destination="' . $destination . '"> + <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' . $issuer . '</saml:Issuer> + <samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> + <samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + Value="urn:oasis:names:tc:SAML:2.0:status:Success" /> + </samlp:Status> + <saml:Assertion Version="2.0" + ID="' . $assertionid . '" IssueInstant="' . $issueInstant . '"> + <saml:Issuer>' . $issuer . '</saml:Issuer> + <saml:Subject> + ' . $nameid . ' + <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> + <saml:SubjectConfirmationData NotOnOrAfter="' . $assertionExpire . '" + InResponseTo="' . $inresponseto. '" + Recipient="' . $destination . '"/> + </saml:SubjectConfirmation> + </saml:Subject> + <saml:Conditions NotBefore="' . $notBefore. '" NotOnOrAfter="' . $assertionExpire. '"> + <saml:AudienceRestriction> + <saml:Audience>' . $spentityid . '</saml:Audience> + </saml:AudienceRestriction> + </saml:Conditions> + <saml:AuthnStatement AuthnInstant="' . $issueInstant . '" + SessionIndex="' . $sessionindex . '"> + <saml:AuthnContext> + <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef> + </saml:AuthnContext> + </saml:AuthnStatement> + ' . $attributestatement. ' + </saml:Assertion> +</samlp:Response> +'; + + +//echo $authnResponse; + + + // echo $authnResponse; exit(0); + return $authnResponse; + } + + + private function generateNameID($type = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + $value = 'anonymous', $namequalifier = null, $spnamequalifier = null) { + + if ($type == self::EMAIL) { + return '<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">' . $value . '</saml:NameID>'; + + } else { + return '<saml:NameID NameQualifier="' . $namequalifier . '" SPNameQualifier="'. $spnamequalifier. '" + Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + >' . $value. '</saml:NameID>'; + } + + } + + + private function enc_attribute($name,$value, $base64 = false) { + return '<saml:Attribute Name="' . $name. '"> + <saml:AttributeValue xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" + >' . ($base64 ? base64_encode($value) : htmlspecialchars($value) ) . '</saml:AttributeValue> + </saml:Attribute>'; + } + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/SAML20/LogoutRequest.php b/lib/SimpleSAML/XML/SAML20/LogoutRequest.php new file mode 100644 index 000000000..8dfb35d6a --- /dev/null +++ b/lib/SimpleSAML/XML/SAML20/LogoutRequest.php @@ -0,0 +1,182 @@ +<?php + + +/** + * SimpleSAMLphp + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_SAML20_LogoutRequest { + + private $configuration = null; + private $metadata = null; + + private $message = null; + private $dom; + private $relayState = null; + + + const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0'; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + public function setXML($xml) { + $this->message = $xml; + } + + public function getXML() { + return $this->message; + } + + public function setRelayState($relayState) { + $this->relayState = $relayState; + } + + public function getRelayState() { + return $this->relayState; + } + + public function getDOM() { + if (isset($this->message) ) { + + /* if (isset($this->dom) && $this->dom != null ) { + return $this->dom; + } */ + + $token = new DOMDocument(); + $token->loadXML(str_replace ("\r", "", $this->message)); + if (empty($token)) { + throw new Exception("Unable to load token"); + } + $this->dom = $token; + return $this->dom; + + } + + return null; + } + + + public function getIssuer() { + $dom = $this->getDOM(); + $issuer = null; + + if (!$dom instanceof DOMDocument) { + throw new Exception("Could not get message DOM in AuthnRequest object"); + } + + //print_r($dom->saveXML()); + + if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { + if ($issuerNodes->length > 0) { + $issuer = $issuerNodes->item(0)->textContent; + } + } + return $issuer; + } + + public function getRequestID() { + $dom = $this->getDOM(); + $requestid = null; + + if (empty($dom)) { + throw new Exception("Could not get message DOM in AuthnRequest object"); + } + + $requestelement = $dom->getElementsByTagName('LogoutRequest')->item(0); + $requestid = $requestelement->getAttribute('ID'); + return $requestid; + /* + if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { + if ($issuerNodes->length > 0) { + $requestid = $issuerNodes->item(0)->textContent; + } + } + return $requestid; + */ + } + + + + public function generate($issuer, $receiver, $nameid, $nameidformat, $sessionindex, $mode) { + + if (!in_array($mode, array('SP', 'IdP'))) { + throw new Exception('mode parameter of generate() must be either SP or IdP'); + } + if ($mode == 'IdP') { + $issuerset = 'saml20-idp-hosted'; + $receiverset = 'saml20-sp-remote'; + } else { + $issuerset = 'saml20-sp-hosted'; + $receiverset = 'saml20-idp-remote'; + } + + $issuermd = $this->metadata->getMetaData($issuer, $issuerset); + $receivermd = $this->metadata->getMetaData($receiver, $receiverset); + + $id = self::generateID(); + $issueInstant = self::generateIssueInstant(); + + $destination = $receivermd['SingleLogOutUrl']; + +/* + $spNameQualifier = $md['spNameQualifier']; + $nameidformat = isset($md['NameIDformat']) ? + $md['NameIDformat'] : + 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'; + */ + $logoutRequest = "<samlp:LogoutRequest " . + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " . + "ID=\"" . $id . "\" " . + "Version=\"2.0\" " . + "IssueInstant=\"" . $issueInstant . "\"> " . + "<saml:Issuer " . + "xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">" . + $issuer . + "</saml:Issuer>" . + "<saml:NameID " . + "xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " . +// "NameQualifier=\"" . $nameId["NameQualifier"] . "\" " . +// "SPNameQualifier=\"" . $nameId["SPNameQualifier"] . "\" " . + "Format=\"" . $nameidformat. "\">" . + $nameid . + "</saml:NameID>" . + "<samlp:SessionIndex " . + "xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">" . + $sessionindex . + "</samlp:SessionIndex>" . + "</samlp:LogoutRequest>"; + + return $logoutRequest; + } + + public static function generateID() { + + $length = 42; + $key = "_"; + for ( $i=0; $i < $length; $i++ ) + { + $key .= dechex( rand(0,15) ); + } + return $key; + } + + public static function generateIssueInstant() { + return gmdate("Y-m-d\TH:i:s\Z"); + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/SAML20/LogoutResponse.php b/lib/SimpleSAML/XML/SAML20/LogoutResponse.php new file mode 100644 index 000000000..c83cde719 --- /dev/null +++ b/lib/SimpleSAML/XML/SAML20/LogoutResponse.php @@ -0,0 +1,152 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); + +require_once('xmlseclibs.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_SAML20_LogoutResponse { + + private $configuration = null; + private $metadata = null; + + private $message = null; + private $dom; + private $relayState = null; + + const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0'; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + public function setXML($xml) { + $this->message = $xml; + } + + public function getXML() { + return $this->message; + } + + public function setRelayState($relayState) { + $this->relayState = $relayState; + } + + public function getRelayState() { + return $this->relayState; + } + + public function getDOM() { + if (isset($this->message) ) { + + /* + if (isset($this->dom)) { + return $this->dom; + } + */ + + $token = new DOMDocument(); + $token->loadXML(str_replace ("\r", "", $this->message)); + if (empty($token)) { + throw new Exception("Unable to load token"); + } + $this->dom = $token; + return $this->dom; + + } + + return null; + } + + + + public function getIssuer() { + $dom = $this->getDOM(); + $issuer = null; + if ($issuerNodes = $dom->getElementsByTagName('Issuer')) { + if ($issuerNodes->length > 0) { + $issuer = $issuerNodes->item(0)->textContent; + } + } + return $issuer; + } + + + // Not updated for response. from request. + public function generate($issuer, $receiver, $inresponseto, $mode ) { + if (!in_array($mode, array('SP', 'IdP'))) { + throw new Exception('mode parameter of generate() must be either SP or IdP'); + } + if ($mode == 'IdP') { + $issuerset = 'saml20-idp-hosted'; + $receiverset = 'saml20-sp-remote'; + } else { + $issuerset = 'saml20-sp-hosted'; + $receiverset = 'saml20-idp-remote'; + } + + + //echo 'idp:' . $idpentityid . ' sp:' . $spentityid .' inresponseto:' . $inresponseto . ' namid:' . $nameid; + + $issuermd = $this->metadata->getMetaData($issuer, $issuerset); + $receivermd = $this->metadata->getMetaData($receiver, $receiverset); + + $id = self::generateID(); + $issueInstant = self::generateIssueInstant(); + + $destination = $receivermd['SingleLogOutUrl']; + + $samlResponse = '<samlp:LogoutResponse xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" +ID="_' . $id . '" Version="2.0" IssueInstant="' . $issueInstant . '" Destination="'. $destination. '" InResponseTo="' . $inresponseto . '"> +<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' . $issuer . '</saml:Issuer> +<samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> +<samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" +Value="urn:oasis:names:tc:SAML:2.0:status:Success"> +</samlp:StatusCode> +<samlp:StatusMessage xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> +Successfully logged out from service ' . $issuer . ' +</samlp:StatusMessage> +</samlp:Status> +</samlp:LogoutResponse>'; + + return $samlResponse; + } + + + + + public static function generateID() { + + $length = 42; + $key = "_"; + for ( $i=0; $i < $length; $i++ ) + { + $key .= dechex( rand(0,15) ); + } + return $key; + } + + public static function generateIssueInstant($offset = 0) { + return gmdate("Y-m-d\TH:i:s\Z", time() + $offset); + } + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/Shib13/AuthnRequest.php b/lib/SimpleSAML/XML/Shib13/AuthnRequest.php new file mode 100644 index 000000000..ef7c0c9aa --- /dev/null +++ b/lib/SimpleSAML/XML/Shib13/AuthnRequest.php @@ -0,0 +1,114 @@ +<?php + + +/** + * SimpleSAMLphp + * + * PHP versions 4 and 5 + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_Shib13_AuthnRequest { + + private $configuration = null; + private $metadata = null; + + private $issuer = null; + private $relayState = null; + + private $requestid = null; + + + const PROTOCOL = 'shibboleth'; + + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + public function setRelayState($relayState) { + $this->relayState = $relayState; + } + + public function getRelayState() { + return $this->relayState; + } + + public function setIssuer($issuer) { + $this->issuer = $issuer; + } + public function getIssuer() { + return $this->issuer; + } + + + + public function parseGet($get) { + return null; + } + + public function setNewRequestID() { + $this->requestid = $this->generateID(); + } + + public function getRequestID() { + return $this->requestid; + } + + public function createSession() { + + $session = SimpleSAML_Session::getInstance(); + + if (!isset($session)) { + SimpleSAML_Session::init(self::PROTOCOL); + $session = SimpleSAML_Session::getInstance(); + } + + $session->setAuthnRequest($this->getRequestID(), $this); + + /* + if (isset($this->relayState)) { + $session->setRelayState($this->relayState); + } + */ + return $session; + } + + public function createRedirect($destination) { + $idpmetadata = $this->metadata->getMetaData($destination, 'shib13-idp-remote'); + $spmetadata = $this->metadata->getMetaData($this->getIssuer(), 'shib13-sp-hosted'); + + $desturl = $idpmetadata['SingleSignOnUrl']; + $shire = $spmetadata['AssertionConsumerService']; + $target = $this->getRelayState(); + + $url = $desturl . '?' . + 'providerId=' . urlencode($this->getIssuer()) . + '&shire=' . urlencode($shire) . + (isset($target) ? '&target=' . urlencode($target) : ''); + return $url; + } + + public static function generateID() { + $length = 42; + $key = "_"; + for ( $i=0; $i < $length; $i++ ) { + $key .= dechex( rand(0,15) ); + } + return $key; + } + + +} + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/XML/Shib13/AuthnResponse.php b/lib/SimpleSAML/XML/Shib13/AuthnResponse.php new file mode 100644 index 000000000..22412500c --- /dev/null +++ b/lib/SimpleSAML/XML/Shib13/AuthnResponse.php @@ -0,0 +1,398 @@ +<?php + + +/** + * SimpleSAMLphp + * + * LICENSE: See the COPYING file included in this distribution. + * + * @author Andreas Ĺkre Solberg, UNINETT AS. <andreas.solberg@uninett.no> + */ + +require_once('SimpleSAML/Configuration.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/AuthnResponse.php'); + +require_once('xmlseclibs.php'); + +/** + * Configuration of SimpleSAMLphp + */ +class SimpleSAML_XML_Shib13_AuthnResponse extends SimpleSAML_XML_AuthnResponse { + + private $configuration = null; + private $metadata = 'default.php'; + + private $message = null; + private $dom; + private $relayState = null; + + private $validIDs = null; + + const PROTOCOL = 'urn:oasis:names:tc:SAML:2.0'; + const SHIB_PROTOCOL_NS = 'urn:oasis:names:tc:SAML:1.0:protocol'; + const SHIB_ASSERT_NS = 'urn:oasis:names:tc:SAML:1.0:assertion'; + + function __construct(SimpleSAML_Configuration $configuration, SimpleSAML_XML_MetaDataStore $metadatastore) { + $this->configuration = $configuration; + $this->metadata = $metadatastore; + } + + // Inhereted public function setXML($xml) { + // Inhereted public function getXML() { + // Inhereted public function setRelayState($relayState) { + // Inhereted public function getRelayState() { + + + public function validate() { + + $dom = $this->getDOM(); + + /* Create an XML security object, and register ID as the id attribute for sig references. */ + $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig->idKeys[] = 'ResponseID'; + + /* Locate the signature element to be used. */ + $objDSig = $objXMLSecDSig->locateSignature($dom); + + + /* If no signature element was found, throw an error */ + if (!$objDSig) { + throw new Exception("Could not locate XML Signature element in Authentication Response"); + } + + + /* Must check certificate fingerprint now - validateReference removes it */ + // TODO FIX"!!! + if ( ! $this->validateCertFingerprint($objDSig) ) { + throw new Exception("Fingerprint Validation Failed"); + } + + /* Get information about canoncalization in to the xmlsec library. Read from the siginfo part. */ + $objXMLSecDSig->canonicalizeSignedInfo(); + + $refids = $objXMLSecDSig->getRefIDs(); + + + + /* Validate refrences */ + $retVal = $objXMLSecDSig->validateReference(); + if (! $retVal) { + throw new Exception("XMLsec: digest validation failed"); + } + + $key = NULL; + $objKey = $objXMLSecDSig->locateKey(); + + if ($objKey) { + if ($objKeyInfo = XMLSecEnc::staticLocateKeyInfo($objKey, $objDSig)) { + /* Handle any additional key processing such as encrypted keys here */ + } + } + + if (empty($objKey)) { + throw new Exception("Error loading key to handle Signature"); + } + + if (! $objXMLSecDSig->verify($objKey)) { + throw new Exception("Unable to validate Signature"); + } + + $this->validIDs = $refids; + return true; + } + + + + + function validateCertFingerprint($dom) { +// $dom = $this->getDOM(); + $fingerprint = ""; + + + // Find the certificate in the document. + if ($x509certNodes = $dom->getElementsByTagName('X509Certificate')) { + if ($x509certNodes->length > 0) { + $x509cert = $x509certNodes->item(0)->textContent; + $x509data = base64_decode( $x509cert ); + $fingerprint = strtolower( sha1( $x509data ) ); + } + } + + // Get the issuer of the assertion. + $issuer = $this->getIssuer(); + + //echo 'found issuer: ' . $this->getIssuer(); + $md = $this->metadata->getMetaData($issuer, 'shib13-idp-remote'); + + /* + * Get fingerprint from saml20-idp-remote metadata... + * + * Accept fingerprints with or without colons, case insensitive + */ + $issuerFingerprint = strtolower( str_replace(":", "", $md['certFingerprint']) ); + + //echo 'issuer fingerprint: ' . $issuerFingerprint; + + if (empty($issuerFingerprint)) { + throw new Exception("Certificate finger print for entity ID [" . $issuer . "] in metadata was empty."); + } + if (empty($fingerprint)) { + throw new Exception("Certificate finger print in message was empty."); + } + + if ($fingerprint != $issuerFingerprint) { + throw new Exception("Expecting certificate fingerprint [$issuerFingerprint] but got [$fingerprint]"); + } + + return ($fingerprint == $issuerFingerprint); + } + + + public function createSession() { + + SimpleSAML_Session::init(self::PROTOCOL, $this, true); + $session = SimpleSAML_Session::getInstance(); + $session->setAttributes($this->getAttributes()); + + $nameid = $this->getNameID(); + + $session->setNameID($nameid['NameID']); + $session->setNameIDFormat($nameid['Format']); + $session->setSessionIndex($this->getSessionIndex()); + /* + $nameID["NameID"] = $node->nodeValue; + + $nameID["NameQualifier"] = $node->getAttribute('NameQualifier'); + $nameID["SPNameQualifier"] = $node->getAttribute('SPNameQualifier'); + */ + return $session; + } + + //TODO + function getSessionIndex() { + $token = $this->getDOM(); + if ($token instanceof DOMDocument) { + $xPath = new DOMXpath($token); + $xPath->registerNamespace('mysamlp', self::SHIB_PROTOCOL_NS); + $xPath->registerNamespace('mysaml', self::SHIB_ASSERT_NS); + + $query = '/mysamlp:Response/mysaml:Assertion/mysaml:AuthnStatement'; + $nodelist = $xPath->query($query); + if ($node = $nodelist->item(0)) { + return $node->getAttribute('SessionIndex'); + } + } + return NULL; + } + + + public function getAttributes() { + + + $md = $this->metadata->getMetadata($this->getIssuer(), 'shib13-idp-remote'); + + //$base64 = isset($md['base64attributes']) ? $md['base64attributes'] : false; + + /* + define('SAML2_BINDINGS_POST', 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'); + define('SAML2_STATUS_SUCCESS', 'urn:oasis:names:tc:SAML:2.0:status:Success'); + */ + + /* + echo 'Validids<pre>'; + print_r($this->validIDs); + echo '</pre>'; + */ + + $attributes = array(); + $token = $this->getDOM(); + + + //echo $this->getXML(); + + $attributes = array(); + + if ($token instanceof DOMDocument) { + + $sxml = simplexml_import_dom($token); + + $sxml->registerXPathNamespace('samlp', self::SHIB_PROTOCOL_NS); + $sxml->registerXPathNamespace('saml', self::SHIB_ASSERT_NS); + + + + $assertions = $sxml->xpath('/samlp:Response[@ResponseID="' . $this->validIDs[0] . '"]/saml:Assertion'); + + foreach ($assertions AS $assertion) { + + if ($assertion->Conditions) { + + if (($start = (string)$assertion->Conditions['NotBefore']) && + ($end = (string)$assertion->Conditions['NotOnOrAfter'])) { + + if (! SimpleSAML_Utilities::checkDateConditions($start, $end)) { + error_log( " Date check failed ... (from $start to $end)"); + next; + } + + } + + } + + if (isset($assertion->AttributeStatement->Attribute)) { + foreach ($assertion->AttributeStatement->Attribute AS $attribute) { + $values = array(); + foreach ($attribute->AttributeValue AS $val) { + $values[] = (string) $val; + } + + $attributes[(string)$attribute['AttributeName']] = $values; + } + } + + } + + + /* + echo "<PRE>token:"; + echo htmlentities($token->saveXML()); + echo ":</PRE>"; + */ + /* + echo '<pre>Attributes: '; + print_r($attributes); + echo '</pre>'; + */ + } + return $attributes; + + + } + + + public function getIssuer() { + + $token = $this->getDOM(); + $xPath = new DOMXpath($token); + $xPath->registerNamespace('mysamlp', self::SHIB_PROTOCOL_NS); + $xPath->registerNamespace('mysaml', self::SHIB_ASSERT_NS); + + $query = '/mysamlp:Response/mysaml:Assertion/@Issuer'; + $nodelist = $xPath->query($query); + + if ($attr = $nodelist->item(0)) { + return $attr->value; + } else { + throw Exception('Could not find Issuer field in Authentication response'); + } + + } + + public function getNameID() { + + + $token = $this->getDOM(); + $nameID = array(); + if ($token instanceof DOMDocument) { + $xPath = new DOMXpath($token); + $xPath->registerNamespace('mysamlp', self::SHIB_PROTOCOL_NS); + $xPath->registerNamespace('mysaml', self::SHIB_ASSERT_NS); + + $query = '/mysamlp:Response/mysaml:Assertion/mysaml:AuthenticationStatement/mysaml:Subject/mysaml:NameIdentifier'; + $nodelist = $xPath->query($query); + if ($node = $nodelist->item(0)) { + $nameID["NameID"] = $node->nodeValue; + $nameID["Format"] = $node->getAttribute('Format'); + $nameID["NameQualifier"] = $node->getAttribute('NameQualifier'); + } + } + return $nameID; + + } + + + // Not updated for response. from request. + public function generate($idpentityid, $spentityid, $inresponseto, $nameid, $attributes) { + + //echo 'idp:' . $idpentityid . ' sp:' . $spentityid .' inresponseto:' . $inresponseto . ' namid:' . $nameid; + + $idpmd = $this->metadata->getMetaData($idpentityid, 'saml20-idp-hosted'); + $spmd = $this->metadata->getMetaData($spentityid, 'saml20-sp-remote'); + + $id = self::generateID(); + $issueInstant = self::generateIssueInstant(); + $assertionExpire = self::generateIssueInstant(60 * 5); # 5 minutes + + $assertionid = self::generateID(); + $sessionindex = self::generateID(); + + if (is_null($nameid)) { + $nameid = self::generateID(); + } + + $issuer = $idpentityid; + + $assertionConsumerServiceURL = $spmd['assertionConsumerServiceURL']; + $spNameQualifier = $spmd['spNameQualifier']; + + $destination = $spmd['assertionConsumerServiceURL']; + + $encodedattributes = ''; + foreach ($attributes AS $name => $value) { + $encodedattributes .= $this->enc_attribute($name, $value[0], true); + } + + $authnResponse = '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + ID="' . $id . '" + InResponseTo="' . $inresponseto. '" Version="2.0" + IssueInstant="' . $issueInstant . '" + Destination="' . $destination . '"> + <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">' . $issuer . '</saml:Issuer> + <samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"> + <samlp:StatusCode xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" + Value="urn:oasis:names:tc:SAML:2.0:status:Success"> </samlp:StatusCode> + </samlp:Status> + <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" + ID="' . $assertionid . '" IssueInstant="' . $issueInstant . '"> + <saml:Issuer>' . $issuer . '</saml:Issuer> + <saml:Subject> + <saml:NameID NameQualifier="' . $issuer . '" SPNameQualifier="'. $spentityid. '" + Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + >' . $nameid. '</saml:NameID> + <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"> + <saml:SubjectConfirmationData NotOnOrAfter="' . $assertionExpire . '" + InResponseTo="' . $inresponseto. '" + Recipient="' . $destination . '"/> + </saml:SubjectConfirmation> + </saml:Subject> + <saml:Conditions NotBefore="' . $issueInstant. '" NotOnOrAfter="' . $assertionExpire. '"> + <saml:AudienceRestriction> + <saml:Audience>' . $spentityid . '</saml:Audience> + </saml:AudienceRestriction> + </saml:Conditions> + <saml:AuthnStatement AuthnInstant="' . $issueInstant . '" + SessionIndex="' . $sessionindex . '"> + <saml:AuthnContext> + <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef> + </saml:AuthnContext> + </saml:AuthnStatement> + <saml:AttributeStatement> + ' . $encodedattributes . ' + </saml:AttributeStatement> + </saml:Assertion> +</samlp:Response> +'; + + return $authnResponse; + } + + + + + +} + +?> \ No newline at end of file diff --git a/lib/xmlseclibs.php b/lib/xmlseclibs.php new file mode 100644 index 000000000..f441eafd9 --- /dev/null +++ b/lib/xmlseclibs.php @@ -0,0 +1,1387 @@ +<?php +/** + * xmlseclibs.php + * + * Copyright (c) 2007, Robert Richards <rrichards@ctindustries.net>. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Robert Richards nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @author Robert Richards <rrichards@ctindustries.net> + * @copyright 2007 Robert Richards <rrichards@ctindustries.net> + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 1.0.0 + */ + +/* +Functions to generate simple cases of Exclusive Canonical XML - Callable function is C14NGeneral() +i.e.: $canonical = C14NGeneral($domelement, TRUE); +*/ + +/* helper function */ +function sortAndAddAttrs($element, $arAtts) { + $newAtts = array(); + foreach ($arAtts AS $attnode) { + $newAtts[$attnode->nodeName] = $attnode; + } + ksort($newAtts); + foreach ($newAtts as $attnode) { + $element->setAttribute($attnode->nodeName, $attnode->nodeValue); + } +} + +/* helper function */ +function canonical($tree, $element, $withcomments) { + if ($tree->nodeType != XML_DOCUMENT_NODE) { + $dom = $tree->ownerDocument; + } else { + $dom = $tree; + } + if ($element->nodeType != XML_ELEMENT_NODE) { + if ($element->nodeType == XML_COMMENT_NODE && ! $withcomments) { + return; + } + $tree->appendChild($dom->importNode($element, TRUE)); + return; + } + $arNS = array(); + if ($element->namespaceURI != "") { + if ($element->prefix == "") { + $elCopy = $dom->createElementNS($element->namespaceURI, $element->nodeName); + } else { + $prefix = $tree->lookupPrefix($element->namespaceURI); + if ($prefix == $element->prefix) { + $elCopy = $dom->createElementNS($element->namespaceURI, $element->nodeName); + } else { + $elCopy = $dom->createElement($element->nodeName); + $arNS[$element->namespaceURI] = $element->prefix; + } + } + } else { + $elCopy = $dom->createElement($element->nodeName); + } + $tree->appendChild($elCopy); + + /* Create DOMXPath based on original document */ + $xPath = new DOMXPath($element->ownerDocument); + + /* Get namespaced attributes */ + $arAtts = $xPath->query('attribute::*[namespace-uri(.) != ""]', $element); + + /* Create an array with namespace URIs as keys, and sort them */ + foreach ($arAtts AS $attnode) { + if (array_key_exists($attnode->namespaceURI, $arNS) && + ($arNS[$attnode->namespaceURI] == $attnode->prefix)) { + continue; + } + $prefix = $tree->lookupPrefix($attnode->namespaceURI); + if ($prefix != $attnode->prefix) { + $arNS[$attnode->namespaceURI] = $attnode->prefix; + } else { + $arNS[$attnode->namespaceURI] = NULL; + } + } + if (count($arNS) > 0) { + asort($arNS); + } + + /* Add namespace nodes */ + foreach ($arNS AS $namespaceURI=>$prefix) { + if ($prefix != NULL) { + $elCopy->setAttributeNS("http://www.w3.org/2000/xmlns/", + "xmlns:".$prefix, $namespaceURI); + } + } + if (count($arNS) > 0) { + ksort($arNS); + } + + /* Get attributes not in a namespace, and then sort and add them */ + $arAtts = $xPath->query('attribute::*[namespace-uri(.) = ""]', $element); + sortAndAddAttrs($elCopy, $arAtts); + + /* Loop through the URIs, and then sort and add attributes within that namespace */ + foreach ($arNS as $nsURI=>$prefix) { + $arAtts = $xPath->query('attribute::*[namespace-uri(.) = "'.$nsURI.'"]', $element); + sortAndAddAttrs($elCopy, $arAtts); + } + + foreach ($element->childNodes AS $node) { + canonical($elCopy, $node, $withcomments); + } +} + +/* +$element - DOMElement for which to produce the canonical version of +$exclusive - boolean to indicate exclusive canonicalization (must pass TRUE) +$withcomments - boolean indicating wether or not to include comments in canonicalized form +*/ +function C14NGeneral($element, $exclusive=FALSE, $withcomments=FALSE) { + /* IF PHP 5.2+ then use built in canonical functionality */ + $php_version = explode('.', PHP_VERSION); + if (($php_version[0] > 5) || ($php_version[0] == 5 && $php_version[1] >= 2) ) { + return $element->C14N($exclusive, $withcomments); + } + + /* Must be element */ + if (! $element instanceof DOMElement) { + return NULL; + } + /* Currently only exclusive XML is supported */ + if ($exclusive == FALSE) { + throw new Exception("Only exclusive canonicalization is supported in this version of PHP"); + } + + $copyDoc = new DOMDocument(); + canonical($copyDoc, $element, $withcomments); + return $copyDoc->saveXML($copyDoc->documentElement, LIBXML_NOEMPTYTAG); +} + +class XMLSecurityKey { + const TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; + const AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; + const AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; + const AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; + const RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; + const RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; + const RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + const DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'; + + private $cryptParams = array(); + public $type = 0; + public $key = NULL; + public $passphrase = ""; + public $iv = NULL; + public $name = NULL; + public $keyChain = NULL; + public $isEncrypted = FALSE; + public $encryptedCtx = NULL; + public $guid = NULL; + + public function __construct($type, $params=NULL) { + switch ($type) { + case (XMLSecurityKey::TRIPLEDES_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_TRIPLEDES; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; + break; + case (XMLSecurityKey::AES128_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; + break; + case (XMLSecurityKey::AES192_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; + break; + case (XMLSecurityKey::AES256_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; + break; + case (XMLSecurityKey::RSA_1_5): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + return; + case (XMLSecurityKey::RSA_OAEP_MGF1P): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_OAEP_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; + $this->cryptParams['hash'] = NULL; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + return; + case (XMLSecurityKey::RSA_SHA1): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + if (is_array($params) && ! empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + break; + default: + throw new Exception('Invalid Key Type'); + return; + } + $this->type = $type; + } + + public function generateSessionKey() { + $key = ''; + if (! empty($this->cryptParams['cipher']) && ! empty($this->cryptParams['mode'])) { + $keysize = mcrypt_module_get_algo_key_size($this->cryptParams['cipher']); + /* Generating random key using iv generation routines */ + if (($keysize > 0) && ($td = mcrypt_module_open(MCRYPT_RIJNDAEL_256, '',$this->cryptParams['mode'], ''))) { + if ($this->cryptParams['cipher'] == MCRYPT_RIJNDAEL_128) { + $keysize = 16; + if ($this->type == XMLSecurityKey::AES256_CBC) { + $keysize = 32; + } elseif ($this->type == XMLSecurityKey::AES192_CBC) { + $keysize = 24; + } + } + while (strlen($key) < $keysize) { + $key .= mcrypt_create_iv(mcrypt_enc_get_iv_size ($td),MCRYPT_RAND); + } + mcrypt_module_close($td); + $key = substr($key, 0, $keysize); + $this->key = $key; + } + } + return $key; + } + + public function loadKey($key, $isFile=FALSE, $isCert = FALSE) { + if ($isFile) { + $this->key = file_get_contents($key); + } else { + $this->key = $key; + } + if ($isCert) { + $this->key = openssl_x509_read($this->key); + openssl_x509_export($this->key, $str_cert); + $this->key = $str_cert; + } + if ($this->cryptParams['library'] == 'openssl') { + if ($this->cryptParams['type'] == 'public') { + $this->key = openssl_get_publickey($this->key); + } else { + $this->key = openssl_get_privatekey($this->key, $this->passphrase); + } + } else if ($this->cryptParams['cipher'] == MCRYPT_RIJNDAEL_128) { + /* Check key length */ + switch ($this->type) { + case (XMLSecurityKey::AES256_CBC): + if (strlen($this->key) < 25) { + throw new Exception('Key must contain at least 25 characters for this cipher'); + } + break; + case (XMLSecurityKey::AES192_CBC): + if (strlen($this->key) < 17) { + throw new Exception('Key must contain at least 17 characters for this cipher'); + } + break; + } + } + } + + private function encryptMcrypt($data) { + $td = mcrypt_module_open($this->cryptParams['cipher'], '', $this->cryptParams['mode'], ''); + $this->iv = mcrypt_create_iv (mcrypt_enc_get_iv_size($td), MCRYPT_RAND); + mcrypt_generic_init($td, $this->key, $this->iv); + $encrypted_data = $this->iv.mcrypt_generic($td, $data); + mcrypt_generic_deinit($td); + mcrypt_module_close($td); + return $encrypted_data; + } + + private function decryptMcrypt($data) { + $td = mcrypt_module_open($this->cryptParams['cipher'], '', $this->cryptParams['mode'], ''); + $iv_length = mcrypt_enc_get_iv_size($td); + + $this->iv = substr($data, 0, $iv_length); + $data = substr($data, $iv_length); + + mcrypt_generic_init($td, $this->key, $this->iv); + $decrypted_data = mdecrypt_generic($td, $data); + mcrypt_generic_deinit($td); + mcrypt_module_close($td); + if ($this->cryptParams['mode'] == MCRYPT_MODE_CBC) { + $dataLen = strlen($decrypted_data); + $paddingLength = substr($decrypted_data, $dataLen - 1, 1); + $decrypted_data = substr($decrypted_data, 0, $dataLen - ord($paddingLength)); + } + return $decrypted_data; + } + + private function encryptOpenSSL($data) { + if ($this->cryptParams['type'] == 'public') { + if (! openssl_public_encrypt($data, $encrypted_data, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure encrypting Data'); + return; + } + } else { + if (! openssl_private_encrypt($data, $encrypted_data, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure encrypting Data'); + return; + } + } + return $encrypted_data; + } + + private function decryptOpenSSL($data) { + if ($this->cryptParams['type'] == 'public') { + if (! openssl_public_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure decrypting Data'); + return; + } + } else { + if (! openssl_private_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure decrypting Data'); + return; + } + } + return $decrypted; + } + + private function signOpenSSL($data) { + if (! openssl_sign ($data, $signature, $this->key)) { + throw new Exception('Failure Signing Data'); + return; + } + return $signature; + } + + private function verifyOpenSSL($data, $signature) { + return openssl_verify ($data, $signature, $this->key); + } + + public function encryptData($data) { + switch ($this->cryptParams['library']) { + case 'mcrypt': + return $this->encryptMcrypt($data); + break; + case 'openssl': + return $this->encryptOpenSSL($data); + break; + } + } + + public function decryptData($data) { + switch ($this->cryptParams['library']) { + case 'mcrypt': + return $this->decryptMcrypt($data); + break; + case 'openssl': + return $this->decryptOpenSSL($data); + break; + } + } + + public function signData($data) { + switch ($this->cryptParams['library']) { + case 'openssl': + return $this->signOpenSSL($data); + break; + } + } + + public function verifySignature($data, $signature) { + switch ($this->cryptParams['library']) { + case 'openssl': + return $this->verifyOpenSSL($data, $signature); + break; + } + } + + public function getAlgorith() { + return $this->cryptParams['method']; + } + + static function makeAsnSegment($type, $string) { + switch ($type){ + case 0x02: + if (ord($string) > 0x7f) + $string = chr(0).$string; + break; + case 0x03: + $string = chr(0).$string; + break; + } + + $length = strlen($string); + + if ($length < 128){ + $output = sprintf("%c%c%s", $type, $length, $string); + } else if ($length < 0x0100){ + $output = sprintf("%c%c%c%s", $type, 0x81, $length, $string); + } else if ($length < 0x010000) { + $output = sprintf("%c%c%c%c%s", $type, 0x82, $length/0x0100, $length%0x0100, $string); + } else { + $output = NULL; + } + return($output); + } + + /* Modulus and Exponent must already be base64 decoded */ + static function convertRSA($modulus, $exponent) { + /* make an ASN publicKeyInfo */ + $exponentEncoding = XMLSecurityKey::makeAsnSegment(0x02, $exponent); + $modulusEncoding = XMLSecurityKey::makeAsnSegment(0x02, $modulus); + $sequenceEncoding = XMLSecurityKey:: makeAsnSegment(0x30, $modulusEncoding.$exponentEncoding); + $bitstringEncoding = XMLSecurityKey::makeAsnSegment(0x03, $sequenceEncoding); + $rsaAlgorithmIdentifier = pack("H*", "300D06092A864886F70D0101010500"); + $publicKeyInfo = XMLSecurityKey::makeAsnSegment (0x30, $rsaAlgorithmIdentifier.$bitstringEncoding); + + /* encode the publicKeyInfo in base64 and add PEM brackets */ + $publicKeyInfoBase64 = base64_encode($publicKeyInfo); + $encoding = "-----BEGIN PUBLIC KEY-----\n"; + $offset = 0; + while ($segment=substr($publicKeyInfoBase64, $offset, 64)){ + $encoding = $encoding.$segment."\n"; + $offset += 64; + } + return $encoding."-----END PUBLIC KEY-----\n"; + } + + public function serializeKey($parent) { + + } +} + +class XMLSecurityDSig { + const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#'; + const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; + const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'; + const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'; + const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160'; + + const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'; + const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'; + const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments'; + + const template = '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> + <ds:SignedInfo> + <ds:SignatureMethod /> + </ds:SignedInfo> +</ds:Signature>'; + + public $sigNode = NULL; + public $idKeys = array(); + public $idNS = array(); + private $signedInfo = NULL; + private $xPathCtx = NULL; + private $canonicalMethod = NULL; + private $prefix = 'ds'; + private $searchpfx = 'secdsig'; + + public function __construct() { + $sigdoc = new DOMDocument(); + $sigdoc->loadXML(XMLSecurityDSig::template); + $this->sigNode = $sigdoc->documentElement; + } + + private function getXPathObj() { + if (empty($this->xPathCtx) && ! empty($this->sigNode)) { + $xpath = new DOMXPath($this->sigNode->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + + $this->xPathCtx = $xpath; + } + return $this->xPathCtx; + } + + static function generate_GUID($prefix='_pfx') { + $uuid = md5(uniqid(rand(), true)); + $guid = $prefix.substr($uuid,0,8)."-". + substr($uuid,8,4)."-". + substr($uuid,12,4)."-". + substr($uuid,16,4)."-". + substr($uuid,20,12); + return $guid; + } + + public function locateSignature($objDoc) { + if ($objDoc instanceof DOMDocument) { + $doc = $objDoc; + } else { + $doc = $objDoc->ownerDocument; + } + if ($doc) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = ".//secdsig:Signature"; + $nodeset = $xpath->query($query, $objDoc); + + + $this->sigNode = $nodeset->item(0); + return $this->sigNode; + } + return NULL; + } + + public function createNewSignNode($name, $value=NULL) { + $doc = $this->sigNode->ownerDocument; + if (! is_null($value)) { + $node = $doc->createElementNS(XMLSecurityDSig::XMLDSIGNS, $this->prefix.':'.$name, $value); + } else { + $node = $doc->createElementNS(XMLSecurityDSig::XMLDSIGNS, $this->prefix.':'.$name); + } + return $node; + } + + public function setCanonicalMethod($method) { + switch ($method) { + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $this->canonicalMethod = $method; + break; + default: + throw new Exception('Invalid Canonical Method'); + } + if ($xpath = $this->getXPathObj()) { + $query = './'.$this->searchpfx.':SignedInfo'; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sinfo = $nodeset->item(0)) { + $query = './'.$this->searchpfx.'CanonicalizationMethod'; + $nodeset = $xpath->query($query, $sinfo); + if (! ($canonNode = $nodeset->item(0))) { + $canonNode = $this->createNewSignNode('CanonicalizationMethod'); + $sinfo->insertBefore($canonNode, $sinfo->firstChild); + } + $canonNode->setAttribute('Algorithm', $this->canonicalMethod); + } + } + } + + private function canonicalizeData($node, $canonicalmethod, $arXPath=NULL, $prefixList=NULL) { + $exclusive = FALSE; + $withComments = FALSE; + switch ($canonicalmethod) { + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + $exclusive = FALSE; + $withComments = FALSE; + break; + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + $withComments = TRUE; + break; + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + $exclusive = TRUE; + break; + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $exclusive = TRUE; + $withComments = TRUE; + break; + } +/* Support PHP versions < 5.2 not containing C14N methods in DOM extension */ + $php_version = explode('.', PHP_VERSION); + if (($php_version[0] < 5) || ($php_version[0] == 5 && $php_version[1] < 2) ) { + if (! is_null($arXPath)) { + throw new Exception("PHP 5.2.0 or higher is required to perform XPath Transformations"); + } + return C14NGeneral($node, $exclusive, $withComments); + } + return $node->C14N($exclusive, $withComments, $arXPath, $prefixList); + } + + public function canonicalizeSignedInfo() { + + $doc = $this->sigNode->ownerDocument; + $canonicalmethod = NULL; + if ($doc) { + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($signInfoNode = $nodeset->item(0)) { + $query = "./secdsig:CanonicalizationMethod"; + $nodeset = $xpath->query($query, $signInfoNode); + if ($canonNode = $nodeset->item(0)) { + $canonicalmethod = $canonNode->getAttribute('Algorithm'); + } + $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod); + return $this->signedInfo; + } + } + return NULL; + } + + public function calculateDigest ($digestAlgorithm, $data) { + switch ($digestAlgorithm) { + case XMLSecurityDSig::SHA1: + $alg = 'sha1'; + break; + case XMLSecurityDSig::SHA256: + $alg = 'sha256'; + break; + case XMLSecurityDSig::SHA512: + $alg = 'sha512'; + break; + case XMLSecurityDSig::RIPEMD160: + $alg = 'ripemd160'; + break; + default: + throw new Exception("Cannot validate digest: Unsupported Algorith <$digestAlgorithm>"); + } + return base64_encode(hash($alg, $data, TRUE)); + } + + public function validateDigest($refNode, $data) { + $xpath = new DOMXPath($refNode->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = 'string(./secdsig:DigestMethod/@Algorithm)'; + $digestAlgorithm = $xpath->evaluate($query, $refNode); + $digValue = $this->calculateDigest($digestAlgorithm, $data); + $query = 'string(./secdsig:DigestValue)'; + $digestValue = $xpath->evaluate($query, $refNode); + return ($digValue == $digestValue); + } + + public function processTransforms($refNode, $objData) { + $data = $objData; + $xpath = new DOMXPath($refNode->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = './secdsig:Transforms/secdsig:Transform'; + $nodelist = $xpath->query($query, $refNode); + $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + $arXPath = NULL; + $prefixList = NULL; + foreach ($nodelist AS $transform) { + $algorithm = $transform->getAttribute("Algorithm"); + switch ($algorithm) { + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $node = $transform->firstChild; + while ($node) { + if ($node->localName == 'InclusiveNamespaces') { + if ($pfx = $node->getAttribute('PrefixList')) { + $arpfx = array(); + $pfxlist = split(" ", $pfx); + foreach ($pfxlist AS $pfx) { + $val = trim($pfx); + if (! empty($val)) { + $arpfx[] = $val; + } + } + if (count($arpfx) > 0) { + $prefixList = $arpfx; + } + } + break; + } + $node = $node->nextSibling; + } + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + $canonicalMethod = $algorithm; + break; + case 'http://www.w3.org/TR/1999/REC-xpath-19991116': + $node = $transform->firstChild; + while ($node) { + if ($node->localName == 'XPath') { + $arXPath = array(); + $arXPath['query'] = '(.//. | .//@* | .//namespace::*)['.$node->nodeValue.']'; + $arXpath['namespaces'] = array(); + $nslist = $xpath->query('./namespace::*', $node); + foreach ($nslist AS $nsnode) { + if ($nsnode->localName != "xml") { + $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue; + } + } + break; + } + $node = $node->nextSibling; + } + break; + } + } + if ($data instanceof DOMNode) { + $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList); + } + return $data; + } + + public function processRefNode($refNode) { + $dataObject = NULL; + if ($uri = $refNode->getAttribute("URI")) { + $arUrl = parse_url($uri); + if (empty($arUrl['path'])) { + if ($identifier = $arUrl['fragment']) { + $xPath = new DOMXPath($refNode->ownerDocument); + if ($this->idNS && is_array($this->idNS)) { + foreach ($this->idNS AS $nspf=>$ns) { + $xPath->registerNamespace($nspf, $ns); + } + } + $iDlist = '@Id="'.$identifier.'"'; + if (is_array($this->idKeys)) { + foreach ($this->idKeys AS $idKey) { + $iDlist .= " or @$idKey='$identifier'"; + } + } + $query = '//*['.$iDlist.']'; + $dataObject = $xPath->query($query)->item(0); + } else { + $dataObject = $refNode->ownerDocument; + } + } else { + $dataObject = file_get_contents($arUrl); + } + } else { + $dataObject = $refNode->ownerDocument; + } + $data = $this->processTransforms($refNode, $dataObject); + return $this->validateDigest($refNode, $data); + } + + public function getRefNodeID($refNode) { + if ($uri = $refNode->getAttribute("URI")) { + $arUrl = parse_url($uri); + if (empty($arUrl['path'])) { + if ($identifier = $arUrl['fragment']) { + return $identifier; + } + } + } + return null; + } + + public function getRefIDs() { + $refids = array(); + $doc = $this->sigNode->ownerDocument; + + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo/secdsig:Reference"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length == 0) { + throw new Exception("Reference nodes not found"); + } + foreach ($nodeset AS $refNode) { + $refids[] = $this->getRefNodeID($refNode); + } + return $refids; + } + + public function validateReference() { + $doc = $this->sigNode->ownerDocument; + if (! $doc->isSameNode($this->sigNode)) { + $this->sigNode->parentNode->removeChild($this->sigNode); + } + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo/secdsig:Reference"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length == 0) { + throw new Exception("Reference nodes not found"); + } + foreach ($nodeset AS $refNode) { + if (! $this->processRefNode($refNode)) { + throw new Exception("Reference validation failed"); + } + } + return TRUE; + } + + private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms=NULL, $options=NULL) { + $prefix = NULL; + $prefix_ns = NULL; + $id_name = 'ID'; + + if (is_array($options)) { + $prefix = empty($options['prefix'])?NULL:$options['prefix']; + $prefix_ns = empty($options['prefix_ns'])?NULL:$options['prefix_ns']; + $id_name = empty($options['id_name'])?'Id':$options['id_name']; + } + + $refNode = $this->createNewSignNode('Reference'); + $sinfoNode->appendChild($refNode); + + if ($node instanceof DOMDocument) { + $uri = NULL; + } else { +/* Do wer really need to set a prefix? */ + $uri = XMLSecurityDSig::generate_GUID(); + $refNode->setAttribute("URI", '#'.$uri); + //$refNode->setAttribute("URI", ''); + } + + $transNodes = $this->createNewSignNode('Transforms'); + $refNode->appendChild($transNodes); + + if (is_array($arTransforms)) { + foreach ($arTransforms AS $transform) { + $transNode = $this->createNewSignNode('Transform'); + $transNodes->appendChild($transNode); + $transNode->setAttribute('Algorithm', $transform); + } + } elseif (! empty($this->canonicalMethod)) { + $transNode = $this->createNewSignNode('Transform'); + $transNodes->appendChild($transNode); + $transNode->setAttribute('Algorithm', $this->canonicalMethod); + } + + if (! empty($uri)) { + $attname = $id_name; + if (! empty($prefix)) { + $attname = $prefix.':'.$attname; + } + $node->setAttributeNS($prefix_ns, $attname, $uri); + } + + $canonicalData = $this->processTransforms($refNode, $node); + $digValue = $this->calculateDigest($algorithm, $canonicalData); + + $digestMethod = $this->createNewSignNode('DigestMethod'); + $refNode->appendChild($digestMethod); + $digestMethod->setAttribute('Algorithm', $algorithm); + + $digestValue = $this->createNewSignNode('DigestValue', $digValue); + $refNode->appendChild($digestValue); + } + + public function addReference($node, $algorithm, $arTransforms=NULL, $options=NULL) { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); + } + } + } + + public function addReferenceList($arNodes, $algorithm, $arTransforms=NULL, $options=NULL) { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + foreach ($arNodes AS $node) { + $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); + } + } + } + } + + public function addObject($data, $mimetype=NULL, $encoding=NULL) { + $objNode = $this->createNewSignNode('Object'); + $this->sigNode->appendChild($objNode); + if (! empty($mimetype)) { + $objNode->setAtribute('MimeType', $mimetype); + } + if (! empty($encoding)) { + $objNode->setAttribute('Encoding', $encoding); + } + + if ($data instanceof DOMElement) { + $newData = $this->sigNode->ownerDocument->importNode($data, TRUE); + } else { + $newData = $this->sigNode->ownerDocument->createTextNode($data); + } + $objNode->appendChild($newData); + + return $objNode; + } + + public function locateKey($node=NULL) { + if (empty($node)) { + $node = $this->sigNode; + } + if (! $node instanceof DOMNode) { + return NULL; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)"; + $algorithm = $xpath->evaluate($query, $node); + if ($algorithm) { + try { + $objKey = new XMLSecurityKey($algorithm, array('type'=>'public')); + } catch (Exception $e) { + return NULL; + } + return $objKey; + } + } + return NULL; + } + + public function verify($objKey) { + $doc = $this->sigNode->ownerDocument; + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "string(./secdsig:SignatureValue)"; + $sigValue = $xpath->evaluate($query, $this->sigNode); + if (empty($sigValue)) { + throw new Exception("Unable to locate SignatureValue"); + } + return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue)); + } + + public function signData($objKey, $data) { + return $objKey->signData($data); + } + + public function sign($objKey) { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + $query = "./secdsig:SignatureMethod"; + $nodeset = $xpath->query($query, $sInfo); + $sMethod = $nodeset->item(0); + $sMethod->setAttribute('Algorithm', $objKey->type); + $data = $this->canonicalizeData($sInfo, $this->canonicalMethod); + $sigValue = base64_encode($this->signData($objKey, $data)); + $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue); + if ($infoSibling = $sInfo->nextSibling) { + $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling); + } else { + $this->sigNode->appendChild($sigValueNode); + } + } + } + } + + public function appendCert() { + + } + + public function appendKey($objKey, $parent=NULL) { + $objKey->serializeKey($parent); + } + + public function appendSignature($parentNode, $insertBefore = FALSE, $assertion = false) { + $baseDoc = ($parentNode instanceof DOMDocument)?$parentNode:$parentNode->ownerDocument; + $newSig = $baseDoc->importNode($this->sigNode, TRUE); + + + + $xnode = null; + + $xpath = new DOMXPath($baseDoc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $xpath->registerNamespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol'); + $xpath->registerNamespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion'); + + + if ($insertBefore && !$assertion) { + + $query = "//samlp:Status"; + $nodeset = $xpath->query($query, $parentNode); + + $xnode = $nodeset->item(0); + if (!$xnode) + throw new Exception("Could not find node to sign before (Root signing mode)"); + + $parentNode->insertBefore($newSig, $xnode); + + } elseif ($insertBefore) { + + $query = "//saml:Assertion/saml:Subject"; + $nodeset = $xpath->query($query, $parentNode); + + $xnode = $nodeset->item(0); + if (!$xnode) + throw new Exception("Could not find node to sign before (Assertion signing mode)"); + + $parentNode->insertBefore($newSig, $xnode); + } else { + $parentNode->appendChild($newSig); + } + } + + static function get509XCert($cert, $isPEMFormat=TRUE) { + if ($isPEMFormat) { + $data = ''; + $arCert = explode("\n", $cert); + $inData = FALSE; + foreach ($arCert AS $curData) { + if (! $inData) { + if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { + $inData = TRUE; + } + } else { + if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { + break; + } + $data .= trim($curData); + } + } + } else { + $data = $cert; + } + return $data; + } + + static function staticAdd509Cert($parentRef, $cert, $isPEMFormat=TRUE, $isURL=False, $xpath=NULL) { + if ($isURL) { + $cert = file_get_contents($cert); + } + if (! $parentRef instanceof DOMElement) { + throw new Exception('Invalid parent Node parameter'); + } + $baseDoc = $parentRef->ownerDocument; + + if (empty($xpath)) { + $xpath = new DOMXPath($parentRef->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + } + $data = XMLSecurityDSig::get509XCert($cert, $isPEMFormat); + + $query = "./secdsig:KeyInfo"; + $nodeset = $xpath->query($query, $parentRef); + $keyInfo = $nodeset->item(0); + if (! $keyInfo) { + $inserted = FALSE; + $keyInfo = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:KeyInfo'); + + $query = "./secdsig:Object"; + $nodeset = $xpath->query($query, $parentRef); + if ($sObject = $nodeset->item(0)) { + $sObject->parentNode->insertBefore($keyInfo, $sObject); + $inserted = TRUE; + } + + if (! $inserted) { + $parentRef->appendChild($keyInfo); + } + } + $x509DataNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509Data'); + $keyInfo->appendChild($x509DataNode); + $x509CertNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509Certificate', $data); + $x509DataNode->appendChild($x509CertNode); + + } + + public function add509Cert($cert, $isPEMFormat=TRUE, $isURL=False) { + if ($xpath = $this->getXPathObj()) { + self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath); + } + } +} + +class XMLSecEnc { + const template = "<xenc:EncryptedData xmlns:xenc='http://www.w3.org/2001/04/xmlenc#'> + <xenc:CipherData> + <xenc:CipherValue></xenc:CipherValue> + </xenc:CipherData> +</xenc:EncryptedData>"; + + const Element = 'http://www.w3.org/2001/04/xmlenc#Element'; + const Content = 'http://www.w3.org/2001/04/xmlenc#Content'; + const URI = 3; + const XMLENCNS = 'http://www.w3.org/2001/04/xmlenc#'; + + private $encdoc = NULL; + private $rawNode = NULL; + public $type = NULL; + public $encKey = NULL; + + public function __construct() { + $this->encdoc = new DOMDocument(); + $this->encdoc->loadXML(XMLSecEnc::template); + } + + public function setNode($node) { + $this->rawNode = $node; + } + + public function encryptNode($objKey, $replace=TRUE) { + $data = ''; + if (empty($this->rawNode)) { + throw new Exception('Node to encrypt has not been set'); + } + if (! $objKey instanceof XMLSecurityKey) { + throw new Exception('Invalid Key'); + } + $doc = $this->rawNode->ownerDocument; + $xPath = new DOMXPath($this->encdoc); + $objList = $xPath->query('/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue'); + $cipherValue = $objList->item(0); + if ($cipherValue == NULL) { + throw new Exception('Error locating CipherValue element within template'); + } + switch ($this->type) { + case (XMLSecEnc::Element): + $data = $doc->saveXML($this->rawNode); + $this->encdoc->documentElement->setAttribute('Type', XMLSecEnc::Element); + break; + case (XMLSecEnc::Content): + $children = $this->rawNode->childNodes; + foreach ($children AS $child) { + $data .= $doc->saveXML($child); + } + $this->encdoc->documentElement->setAttribute('Type', XMLSecEnc::Content); + break; + default: + throw new Exception('Type is currently not supported'); + return; + } + + $encMethod = $this->encdoc->documentElement->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptionMethod')); + $encMethod->setAttribute('Algorithm', $objKey->getAlgorith()); + $cipherValue->parentNode->parentNode->insertBefore($encMethod, $cipherValue->parentNode); + + $strEncrypt = base64_encode($objKey->encryptData($data)); + $value = $this->encdoc->createTextNode($strEncrypt); + $cipherValue->appendChild($value); + + if ($replace) { + switch ($this->type) { + case (XMLSecEnc::Element): + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + return $this->encdoc; + } + $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, TRUE); + $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); + return $importEnc; + break; + case (XMLSecEnc::Content): + $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, TRUE); + while($this->rawNode->firstChild) { + $this->rawNode->removeChild($this->rawNode->firstChild); + } + $this->rawNode->appendChild($importEnc); + return $importEnc; + break; + } + } + } + + public function decryptNode($objKey, $replace=TRUE) { + $data = ''; + if (empty($this->rawNode)) { + throw new Exception('Node to decrypt has not been set'); + } + if (! $objKey instanceof XMLSecurityKey) { + throw new Exception('Invalid Key'); + } + $doc = $this->rawNode->ownerDocument; + $xPath = new DOMXPath($doc); + $xPath->registerNamespace('xmlencr', XMLSecEnc::XMLENCNS); + /* Only handles embedded content right now and not a reference */ + $query = "./xmlencr:CipherData/xmlencr:CipherValue"; + $nodeset = $xPath->query($query, $this->rawNode); + + if ($node = $nodeset->item(0)) { + $encryptedData = base64_decode($node->nodeValue); + $decrypted = $objKey->decryptData($encryptedData); + if ($replace) { + switch ($this->type) { + case (XMLSecEnc::Element): + $newdoc = new DOMDocument(); + $newdoc->loadXML($decrypted); + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + return $newdoc; + } + $importEnc = $this->rawNode->ownerDocument->importNode($newdoc->documentElement, TRUE); + $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); + return $importEnc; + break; + case (XMLSecEnc::Content): + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + $doc = $this->rawNode; + } else { + $doc = $this->rawNode->ownerDocument; + } + $newFrag = $doc->createDOMDocumentFragment(); + $newFrag->appendXML($decrypted); + $this->rawNode->parentNode->replaceChild($newFrag, $this->rawNode); + return $this->rawNode->parentNode; + break; + default: + return $decrypted; + } + } else { + return $decrypted; + } + } else { + throw new Exception("Cannot locate encrypted data"); + } + } + + public function encryptKey($srcKey, $rawKey, $append=TRUE) { + if ((! $srcKey instanceof XMLSecurityKey) || (! $rawKey instanceof XMLSecurityKey)) { + throw new Exception('Invalid Key'); + } + $strEncKey = base64_encode($srcKey->encryptData($rawKey->key)); + $root = $this->encdoc->documentElement; + $encKey = $this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptedKey'); + if ($append) { + $keyInfo = $root->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo')); + $keyInfo->appendChild($encKey); + } else { + $this->encKey = $encKey; + } + $encMethod = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptionMethod')); + $encMethod->setAttribute('Algorithm', $srcKey->getAlgorith()); + if (! empty($srcKey->name)) { + $keyInfo = $encKey->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo')); + $keyInfo->appendChild($this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyName', $srcKey->name)); + } + $cipherData = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:CipherData')); + $cipherData->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:CipherValue', $strEncKey)); + return; + } + + public function decryptKey($encKey) { + if (! $encKey->isEncrypted) { + throw new Exception("Key is not Encrypted"); + } + if (empty($encKey->key)) { + throw new Exception("Key is missing data to perform the decryption"); + } + return $this->decryptNode($encKey, FALSE); + } + + public function locateEncryptedData($element) { + if ($element instanceof DOMDocument) { + $doc = $element; + } else { + $doc = $element->ownerDocument; + } + if ($doc) { + $xpath = new DOMXPath($doc); + $query = "//*[local-name()='EncryptedData' and namespace-uri()='".XMLSecEnc::XMLENCNS."']"; + $nodeset = $xpath->query($query); + return $nodeset->item(0); + } + return NULL; + } + + public function locateKey($node=NULL) { + if (empty($node)) { + $node = $this->rawNode; + } + if (! $node instanceof DOMNode) { + return NULL; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('xmlsecenc', XMLSecEnc::XMLENCNS); + $query = ".//xmlsecenc:EncryptionMethod"; + $nodeset = $xpath->query($query, $node); + if ($encmeth = $nodeset->item(0)) { + $attrAlgorithm = $encmeth->getAttribute("Algorithm"); + try { + $objKey = new XMLSecurityKey($attrAlgorithm, array('type'=>'private')); + } catch (Exception $e) { + return NULL; + } + return $objKey; + } + } + return NULL; + } + + static function staticLocateKeyInfo($objBaseKey=NULL, $node=NULL) { + if (empty($node) || (! $node instanceof DOMNode)) { + return NULL; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('xmlsecenc', XMLSecEnc::XMLENCNS); + $xpath->registerNamespace('xmlsecdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "./xmlsecdsig:KeyInfo"; + $nodeset = $xpath->query($query, $node); + if ($encmeth = $nodeset->item(0)) { + foreach ($encmeth->childNodes AS $child) { + switch ($child->localName) { + case 'KeyName': + if (! empty($objBaseKey)) { + $objBaseKey->name = $child->nodeValue; + } + break; + case 'KeyValue': + foreach ($child->childNodes AS $keyval) { + switch ($keyval->localName) { + case 'DSAKeyValue': + throw new Exception("DSAKeyValue currently not supported"); + break; + case 'RSAKeyValue': + $modulus = NULL; + $exponent = NULL; + if ($modulusNode = $keyval->getElementsByTagName('Modulus')->item(0)) { + $modulus = base64_decode($modulusNode->nodeValue); + } + if ($exponentNode = $keyval->getElementsByTagName('Exponent')->item(0)) { + $exponent = base64_decode($exponentNode->nodeValue); + } + if (empty($modulus) || empty($exponent)) { + throw new Exception("Missing Modulus or Exponent"); + } + $publicKey = XMLSecurityKey::convertRSA($modulus, $exponent); + $objBaseKey->loadKey($publicKey); + break; + } + } + break; + case 'RetrievalMethod': + /* Not currently supported */ + break; + case 'EncryptedKey': + $objenc = new XMLSecEnc(); + $objenc->setNode($child); + if (! $objKey = $objenc->locateKey()) { + throw new Exception("Unable to locate algorithm for this Encrypted Key"); + } + $objKey->isEncrypted = TRUE; + $objKey->encryptedCtx = $objenc; + XMLSecEnc::staticLocateKeyInfo($objKey, $child); + return $objKey; + break; + case 'X509Data': + if ($x509certNodes = $child->getElementsByTagName('X509Certificate')) { + if ($x509certNodes->length > 0) { + $x509cert = $x509certNodes->item(0)->textContent; + $x509cert = str_replace(array("\r", "\n"), "", $x509cert); + $x509cert = "-----BEGIN CERTIFICATE-----\n".chunk_split($x509cert, 64, "\n")."-----END CERTIFICATE-----\n"; + $objBaseKey->loadKey($x509cert); + } + } + break; + } + } + } + return $objBaseKey; + } + return NULL; + } + + public function locateKeyInfo($objBaseKey=NULL, $node=NULL) { + if (empty($node)) { + $node = $this->rawNode; + } + return XMLSecEnc::staticLocateKeyInfo($objBaseKey, $node); + } +} +?> \ No newline at end of file diff --git a/metadata-templates/saml20-idp-hosted.php b/metadata-templates/saml20-idp-hosted.php new file mode 100644 index 000000000..b1eab5941 --- /dev/null +++ b/metadata-templates/saml20-idp-hosted.php @@ -0,0 +1,37 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + * The SAML 2.0 IdP Hosted config is used by the SAML 2.0 IdP to identify itself. + * + */ + + +$metadata = array( + + // The SAML entity ID is the index of this config. + 'dev2.andreas.feide.no' => array( + + // The hostname of the server (VHOST) that this SAML entity will use. + 'host' => 'dev2.andreas.feide.no', + + // SAML endpoints. + 'SingleSignOnUrl' => "http://dev2.andreas.feide.no/saml2/idp/SSOService.php", + 'SingleLogOutUrl' => "http://dev2.andreas.feide.no/saml2/idp/LogoutService.php", + + // X.509 key and certificate. Relative to the cert directory. + 'privatekey' => 'server.pem', + 'certificate' => 'server.crt', + + /* If base64attributes is set to true, then all attributes will be base64 encoded. Make sure + * that you set the SP to have the same value for this. + */ + 'base64attributes' => false, + + // Authentication plugin to use. login.php is the default one that uses LDAP. + 'auth' => 'auth/login.php' + ) + +); + +?> diff --git a/metadata-templates/saml20-idp-remote.php b/metadata-templates/saml20-idp-remote.php new file mode 100644 index 000000000..3f0afacb3 --- /dev/null +++ b/metadata-templates/saml20-idp-remote.php @@ -0,0 +1,41 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + * The SAML 2.0 IdP Remote config is used by the SAML 2.0 SP to identify trusted SAML 2.0 IdPs. + * + */ + + + +$metadata = array( + "feide2.erlang.no-saml2" => + array( + "SingleSignOnUrl" => "https://feide2.erlang.no/saml2/idp/SSOService.php", + "SingleLogOutUrl" => "https://feide2.erlang.no/saml2/idp/LogoutService.php", + "certFingerprint" => "afe71c28ef740bc87425be13a2263d37971da1f9", + "base64attributes" => true), + + 'dev2.andreas.feide.no' => + array( + "SingleSignOnUrl" => "http://dev2.andreas.feide.no/saml2/idp/SSOService.php", + "SingleLogOutUrl" => "http://dev2.andreas.feide.no/saml2/idp/LogoutService.php", + "certFingerprint" => "afe71c28ef740bc87425be13a2263d37971da1f9", + "base64attributes" => false), + + "sam.feide.no" => + array( + "SingleSignOnUrl" => "https://sam.feide.no/amserver/SSORedirect/metaAlias/idp", + "SingleLogOutUrl" => "https://sam.feide.no/amserver/IDPSloRedirect/metaAlias/idp", + "certFingerprint" => "3a:e7:d3:d3:06:ba:57:fd:7f:62:6a:4b:a8:64:b3:4a:53:d9:5d:d0", + "base64attributes" => true), + + "max.feide.no" => + array( + "SingleSignOnUrl" => "https://max.feide.no/amserver/SSORedirect/metaAlias/idp", + "SingleLogOutUrl" => "https://max.feide.no/amserver/IDPSloRedirect/metaAlias/idp", + "certFingerprint" => "d79b0e23c0833d2f5b8d94abd54ae693708b1eef", + "base64attributes" => false ) + + ); +?> diff --git a/metadata-templates/saml20-sp-hosted.php b/metadata-templates/saml20-sp-hosted.php new file mode 100644 index 000000000..d6357b0aa --- /dev/null +++ b/metadata-templates/saml20-sp-hosted.php @@ -0,0 +1,37 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + * The SAML 2.0 IdP Remote config is used by the SAML 2.0 SP to identify itself. + * + */ + +$metadata = array( + "dev.andreas.feide.no" => array( + 'host' => 'dev.andreas.feide.no', + "assertionConsumerServiceURL" => "http://dev.andreas.feide.no/saml2/sp/AssertionConsumerService.php", + "issuer" => "dev.andreas.feide.no", + "spNameQualifier" => "dev.andreas.feide.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ), + "feide2.erlang.no" => array( + 'host' => 'feide2.erlang.no', + "assertionConsumerServiceURL" => "https://feide2.erlang.no/saml2/sp/AssertionConsumerService.php", + "issuer" => "feide2.erlang.no", + "spNameQualifier" => "feide2.erlang.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ), + "feide3.erlang.no" => array( + 'host' => 'feide3.erlang.no', + "assertionConsumerServiceURL" => "https://feide3.erlang.no/saml2/sp/AssertionConsumerService.php", // + "issuer" => "feide3.erlang.no", + "spNameQualifier" => "feide3.erlang.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + ) +); + + +?> diff --git a/metadata-templates/saml20-sp-remote.php b/metadata-templates/saml20-sp-remote.php new file mode 100644 index 000000000..78fc98b30 --- /dev/null +++ b/metadata-templates/saml20-sp-remote.php @@ -0,0 +1,84 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + * The SAML 2.0 SP Remote config is used by the SAML 2.0 IdP to identify trusted SAML 2.0 SPs. + * + * Required parameters: + * + * assertionConsumerServiceURL + * spNameQualifier + * NameIDFormat + * simplesaml.attributes (Will you send an attributestatement [true/false]) + * + * Optional parameters: + * + * ForceAuthn (default: "false") + * simplesaml.nameidattribute (only needed when you are using NameID format email. + * + */ + +$metadata = array( + + 'dev.andreas.feide.no' => array( + 'assertionConsumerServiceURL' => 'http://dev.andreas.feide.no/saml2/sp/AssertionConsumerService.php', + 'spNameQualifier' => 'dev.andreas.feide.no', + 'ForceAuthn' => 'false', + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', + 'simplesaml.attributes' => true + ), + + /* + * This example shows an example config that works with Google Apps for education. + * What is important is that you have an attribute in your IdP that maps to the local part of the email address + * at Google Apps. In example, if your google account is foo.com, and you have a user that has an email john@foo.com, then you + * must set the simplesaml.nameidattribute to be the name of an attribute that for this user has the value of 'john'. + */ + 'google.com' => array( + 'assertionConsumerServiceURL' => 'https://www.google.com/a/foo.com/acs', + 'spNameQualifier' => 'google.com', + 'ForceAuthn' => 'false', + 'NameIDFormat' => 'urn:oasis:names:tc:SAML:2.0:nameid-format:email', + 'simplesaml.nameidattribute' => 'uid', + 'simplesaml.attributes' => false + ), + + "feide2.erlang.no" => array( + "assertionConsumerServiceURL" => "https://feide2.erlang.no/saml2/sp/AssertionConsumerService.php", + "spNameQualifier" => "feide2.erlang.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + 'simplesaml.nameidattribute' => 'uid', + 'simplesaml.attributes' => true + ), + + "feide3.erlang.no" => array( + "assertionConsumerServiceURL" => "https://feide3.erlang.no/saml2/sp/AssertionConsumerService.php", // + "spNameQualifier" => "feide3.erlang.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + 'simplesaml.attributes' => true + ), + + "skjak.uninett.no" => array( + "assertionConsumerServiceURL" => "https://skjak.uninett.no/Shibboleth.sso/SAML2/POST", // + "spNameQualifier" => "skjak.uninett.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + 'simplesaml.attributes' => true + ), + "skjak.uninett.no" => array( +// "assertionConsumerServiceURL" => "https://skjak2.uninett.no:443/fam/Consumer/metaAlias/sp_meta_alias", // + "assertionConsumerServiceURL" => "https://skjak.uninett.no/Shibboleth.sso/SAML2/POST", // + "spNameQualifier" => "skjak.uninett.no", + "ForceAuthn" => "false", + "NameIDFormat" => "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", + 'simplesaml.attributes' => true + ) + + + +); + + +?> diff --git a/metadata-templates/shib13-idp-hosted.php b/metadata-templates/shib13-idp-hosted.php new file mode 100644 index 000000000..ce175ddb8 --- /dev/null +++ b/metadata-templates/shib13-idp-hosted.php @@ -0,0 +1,16 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + */ + + +$metadata = array( + 'feide.erlang.no-shib13' => array( + 'issuer' => 'feide.erlang.no', + 'assertionDurationMinutes' => 10, + 'audience' => 'urn:mace:feide:shiblab' + ) +); + +?> \ No newline at end of file diff --git a/metadata-templates/shib13-idp-remote.php b/metadata-templates/shib13-idp-remote.php new file mode 100644 index 000000000..aba114a16 --- /dev/null +++ b/metadata-templates/shib13-idp-remote.php @@ -0,0 +1,29 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + */ + + +$metadata = array( + + 'urn:mace:switch.ch:aaitest:dukono.switch.ch' => array( + 'SingleSignOnUrl' => 'https://dukono.switch.ch/shibboleth-idp/SSO', + 'certFingerprint' => 'c7279a9f28f11380509e075441e3dc55fb9ab864' +// 'certFingerprint' => '4e730f327ce8d9fe6269298d8f777a4bd0937ba5' +// c7279a9f28f11380509e075441e3dc55fb9ab864 + // "SingleLogOutUrl" => "https://mars.feide.no/amserver/IDPSloRedirect/metaAlias/idp", + ), + + 'feide.erlang.no-shib13' => array( + 'issuer' => 'feide.erlang.no', + 'assertionDurationMinutes' => 10, + 'audience' => 'urn:mace:feide:shiblab' + ), + + 'urn:mace:dfnwayf' => array( + 'SingleSignOnUrl' => 'https://dfn.wayf.com/WAYF' + ) +); + +?> \ No newline at end of file diff --git a/metadata-templates/shib13-sp-hosted.php b/metadata-templates/shib13-sp-hosted.php new file mode 100644 index 000000000..70c78d235 --- /dev/null +++ b/metadata-templates/shib13-sp-hosted.php @@ -0,0 +1,35 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + */ + +$metadata = array( + 'http://dev.andreas.feide.no' => array( + 'AssertionConsumerService' => 'http://dev.andreas.feide.no/shib13/sp/AssertionConsumerService.php', + 'host' => 'dev.andreas.feide.no' + ), + 'https://sp.shiblab.feide.no' => array( + 'shire' => 'http://sp.shiblab.feide.no/Shibboleth.sso/SAML/POST', + 'spnamequalifier' => 'urn:feide.no', + 'audience' => 'urn:mace:feide:shiblab' + ), + 'urn:geant:edugain:component:be:switchaai-test:central' => array( + 'shire' => 'https://edugain-login.switch.ch/ShiBE-R/WebSSOResponseListener', + 'spnamequalifier' => 'urn:geant:edugain:component:be:rediris:rediris.es', + 'audience' => 'urn:geant:edugain:component:be:switchaai-test:central' + ), + 'urn:geant:edugain:component:be:rediris:rediris.es' => array( + 'shire' => 'http://serrano.rediris.es:8080/PAPIWebSSOResponseListener/request', + 'spnamequalifier' => 'urn:geant:edugain:component:be:rediris:rediris.es', + 'audience' => 'urn:geant:edugain:component:be:rediris:rediris.es' + ), + 'https://skjak.uninett.no/shibboleth/target' => array( + 'shire' => 'https://skjak.uninett.no/Shibboleth.shire', + 'spnamequalifier' => 'https://skjak.uninett.no/shibboleth/target', + 'audience' => 'https://skjak.uninett.no/shibboleth/target' + ) + +); + +?> \ No newline at end of file diff --git a/metadata-templates/shib13-sp-remote.php b/metadata-templates/shib13-sp-remote.php new file mode 100644 index 000000000..f79d904bf --- /dev/null +++ b/metadata-templates/shib13-sp-remote.php @@ -0,0 +1,32 @@ +<?php +/* + * SAML 2.0 Meta data for simpleSAMLphp + * + */ + + +$metadata = array( + 'https://sp.shiblab.feide.no' => array( + 'shire' => 'http://sp.shiblab.feide.no/Shibboleth.sso/SAML/POST', + 'spnamequalifier' => 'urn:feide.no', + 'audience' => 'urn:mace:feide:shiblab' + ), + 'urn:geant:edugain:component:be:switchaai-test:central' => array( + 'shire' => 'https://edugain-login.switch.ch/ShiBE-R/WebSSOResponseListener', + 'spnamequalifier' => 'urn:geant:edugain:component:be:rediris:rediris.es', + 'audience' => 'urn:geant:edugain:component:be:switchaai-test:central' + ), + 'urn:geant:edugain:component:be:rediris:rediris.es' => array( + 'shire' => 'http://serrano.rediris.es:8080/PAPIWebSSOResponseListener/request', + 'spnamequalifier' => 'urn:geant:edugain:component:be:rediris:rediris.es', + 'audience' => 'urn:geant:edugain:component:be:rediris:rediris.es' + ), + 'https://skjak.uninett.no/shibboleth/target' => array( + 'shire' => 'https://skjak.uninett.no/Shibboleth.shire', + 'spnamequalifier' => 'https://skjak.uninett.no/shibboleth/target', + 'audience' => 'https://skjak.uninett.no/shibboleth/target' + ) + +); + +?> \ No newline at end of file diff --git a/templates/error.php b/templates/error.php new file mode 100644 index 000000000..8d4600144 --- /dev/null +++ b/templates/error.php @@ -0,0 +1,97 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<title><?php echo $data['header']; ?></title> + +<style type="text/css"> + +/* these styles are in the head of this page because this is a unique page */ + +/* THE BIG GUYS */ +* {margin:0;padding:0} +body {text-align:center;padding: 20px 0;background: #222;color:#333;font:83%/1.5 arial,tahoma,verdana,sans-serif} +img {border:none;display:block} +hr {margin: 1em 0;background:#eee;height:1px;color:#eee;border:none;clear:both} + +/* LINKS */ +a,a:link,a:link,a:link,a:hover {font-weight:bold;background:transparent;text-decoration:underline;cursor:pointer} +a:link {color:#c00} +a:visited {color:#999} +a:hover,a:active {color:#069} + +/* LISTS */ +ul {margin: .3em 0 1.5em 2em} + ul.related {margin-top:-1em} +li {margin-left:2em} +dt {font-weight:bold} +#wrap {border: 1px solid #fff;position:relative;background:#fff;width:600px;margin: 0 auto;text-align:left} +#header {background: #666 url("/<?php echo $data['baseurlpath']; ?>resources/sprites.gif") repeat-x 0 100%;margin: 0 0 25px;padding: 0 0 8px} +#header h1 {color:#fff;font-size: 145%;padding:20px 20px 12px} +#poweredby {width:96px;height:63px;position:absolute;top:0;right:0} +#content {padding: 0 20px} + +/* TYPOGRAPHY */ +p, ul, ol {margin: 0 0 1.5em} +h1, h2, h3, h4, h5, h6 {letter-spacing: -1px;font-family: arial,verdana,sans-serif;margin: 1.2em 0 .3em;color:#000;border-bottom: 1px solid #eee;padding-bottom: .1em} +h1 {font-size: 196%;margin-top:0;border:none} +h2 {font-size: 136%} +h3 {font-size: 126%} +h4 {font-size: 116%} +h5 {font-size: 106%} +h6 {font-size: 96%} + +.old {text-decoration:line-through} +</style> +</head> +<body> + +<div id="wrap"> + + <div id="header"> + <h1>simpleSAMLphp error page</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bomb_l.png" alt="Login screen" /></div> + </div> + + <div id="content"> + + + + + <h2><?php if (isset($data['header'])) { echo $data['header']; } else { echo "Some error occured"; } ?></h2> + + <p> + + <?php echo $data['message']; ?> + + </p> + + + <p>The debug information below may be interesting for the administrator / help desk:</p> + + <div style="border: 1px solid #eee; padding: 1em; font-size: x-small"> + <p style="margin: 1px"><?php echo htmlentities($data['e']->getMessage()); ?></p> + <div style=" padding: 1em; font-family: monospace; "> + <?php echo htmlentities($data['e']->getTraceAsString()); ?> + </div> + </div> + + <h2 style="clear: both">How to get help</h2> + + + <p>This error probably is due to some unexpected behaviour or to misconfiguration of simpleSAMLphp. Contact the administrator of this login service, and send them the error message above.</p> + + + + <hr /> + + Copyright © 2007 <a href="http://rnd.feide.no/">Feide RnD</a> + + <hr /> + + </div> + +</div> + +</body> +</html> diff --git a/templates/login.php b/templates/login.php new file mode 100644 index 000000000..756e37f62 --- /dev/null +++ b/templates/login.php @@ -0,0 +1,124 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<title><?php echo $data['header']; ?></title> +<script> +<!-- +function sf(){document.f.username.focus();} +// --> +</script> +<style type="text/css"> + +/* these styles are in the head of this page because this is a unique page */ + +/* THE BIG GUYS */ +* {margin:0;padding:0} +body {text-align:center;padding: 20px 0;background: #222;color:#333;font:83%/1.5 arial,tahoma,verdana,sans-serif} +img {border:none;display:block} +hr {margin: 1em 0;background:#eee;height:1px;color:#eee;border:none;clear:both} + +/* LINKS */ +a,a:link,a:link,a:link,a:hover {font-weight:bold;background:transparent;text-decoration:underline;cursor:pointer} +a:link {color:#c00} +a:visited {color:#999} +a:hover,a:active {color:#069} + +/* LISTS */ +ul {margin: .3em 0 1.5em 2em} + ul.related {margin-top:-1em} +li {margin-left:2em} +dt {font-weight:bold} +#wrap {border: 1px solid #fff;position:relative;background:#fff;width:600px;margin: 0 auto;text-align:left} +#header {background: #666 url("/<?php echo $data['baseurlpath']; ?>resources/sprites.gif") repeat-x 0 100%;margin: 0 0 25px;padding: 0 0 8px} +#header h1 {color:#fff;font-size: 145%;padding:20px 20px 12px} +#poweredby {width:96px;height:63px;position:absolute;top:0;right:0} +#content {padding: 0 20px} + +/* TYPOGRAPHY */ +p, ul, ol {margin: 0 0 1.5em} +h1, h2, h3, h4, h5, h6 {letter-spacing: -1px;font-family: arial,verdana,sans-serif;margin: 1.2em 0 .3em;color:#000;border-bottom: 1px solid #eee;padding-bottom: .1em} +h1 {font-size: 196%;margin-top:0;border:none} +h2 {font-size: 136%} +h3 {font-size: 126%} +h4 {font-size: 116%} +h5 {font-size: 106%} +h6 {font-size: 96%} + +.old {text-decoration:line-through} +</style> +</head> +<body onload="sf();"> + +<div id="wrap"> + + <div id="header"> + <h1>simpleSAMLphp authentication</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/lock.png" alt="Login screen" /></div> + </div> + + <div id="content"> + + <?php if (isset($data['error'])) { ?> + <div style="border-left: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8; background: #f5f5f5" + <img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bomb.png" style="float: left; margin: 15px " /> + <h2>What you entered was not accepted!</h2> + + <p><?php echo $data['error']; ?> </p> + </div> + <?php } ?> + + <h2 style="break: both">Enter your username and password</h2> + + <p> + A service has requested you to authenticate your self. That means you need to enter your username and password in the form below. + </p> + + <form action="?" method="post" name="f"> + + <table> + <tr> + <td rowspan="2"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/pencil.png" /></td> + <td style="padding: .3em;">Username</td> + <td><input type="text" tabindex="1" name="username" + <?php if (isset($data['username'])) { + echo 'value="' . $data['username'] . '"'; + } ?> /></td> + <td style="padding: .4em; rowspan="2"> + <input type="submit" tabindex="3" value="Login" /> + <input type="hidden" name="RelayState" value="<?php echo $data['relaystate']; ?>" /> + </td> + </tr> + <tr> + <td style="padding: .3em;">Password</td> + <td><input type="password" tabindex="2" name="password" /></td> + </tr> + </table> + + + </form> + + + <h2>Help! I don't remember my password.</h2> + + + <p>Too bad! - Without your username and password you cannot authenticate your self and access the service. + There may be someone that can help you. Contact the help desk at your university!</p> + + <h2>About simpleSAMLphp</h2> + <p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it? + You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p> + + + <hr /> + + Copyright © 2007 <a href="http://rnd.feide.no/">Feide RnD</a> + + <hr /> + + </div> + +</div> + +</body> +</html> diff --git a/templates/post-debug.php b/templates/post-debug.php new file mode 100644 index 000000000..b3dfebf11 --- /dev/null +++ b/templates/post-debug.php @@ -0,0 +1,91 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<title><?php echo $data['header']; ?></title> +<script> +<!-- +function sf(){document.f.username.focus();} +// --> +</script> +<style type="text/css"> + +/* these styles are in the head of this page because this is a unique page */ + +/* THE BIG GUYS */ +* {margin:0;padding:0} +body {text-align:center;padding: 20px 0;background: #222;color:#333;font:83%/1.5 arial,tahoma,verdana,sans-serif} +img {border:none;display:block} +hr {margin: 1em 0;background:#eee;height:1px;color:#eee;border:none;clear:both} + +/* LINKS */ +a,a:link,a:link,a:link,a:hover {font-weight:bold;background:transparent;text-decoration:underline;cursor:pointer} +a:link {color:#c00} +a:visited {color:#999} +a:hover,a:active {color:#069} + +/* LISTS */ +ul {margin: .3em 0 1.5em 2em} + ul.related {margin-top:-1em} +li {margin-left:2em} +dt {font-weight:bold} +#wrap {border: 1px solid #fff;position:relative;background:#fff;width:600px;margin: 0 auto;text-align:left} +#header {background: #666 url("/<?php echo $data['baseurlpath']; ?>resources/sprites.gif") repeat-x 0 100%;margin: 0 0 25px;padding: 0 0 8px} +#header h1 {color:#fff;font-size: 145%;padding:20px 20px 12px} +#poweredby {width:96px;height:63px;position:absolute;top:0;right:0} +#content {padding: 0 20px} + +/* TYPOGRAPHY */ +p, ul, ol {margin: 0 0 1.5em} +h1, h2, h3, h4, h5, h6 {letter-spacing: -1px;font-family: arial,verdana,sans-serif;margin: 1.2em 0 .3em;color:#000;border-bottom: 1px solid #eee;padding-bottom: .1em} +h1 {font-size: 196%;margin-top:0;border:none} +h2 {font-size: 136%} +h3 {font-size: 126%} +h4 {font-size: 116%} +h5 {font-size: 106%} +h6 {font-size: 96%} + +.old {text-decoration:line-through} +</style> +</head> +<body onload="sf();"> + +<div id="wrap"> + + <div id="header"> + <h1>simpleSAMLphp authentication</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/debug.png" alt="Debug" /></div> + </div> + + <div id="content"> + + + + <h2>Sending a SAML response to the service</h2> + + <p>You are about to send a SAML response back to the service. Hit the send response button to continue.</p> + + <form method="post" action="<?php echo $data['destination']; ?>"> + <input type="hidden" name="SAMLResponse" value="<?php echo $data['response']; ?>" /> + <input type="hidden" name="<?php echo $data['RelayStateName']; ?>" value="<?php echo $data['RelayState']; ?>"> + <input type="submit" value="Submit the response to the service" /> + </form> + + <h2>Debug mode</h2> + + <p>As you are in debug mode you are lucky to see the content of the response you are sending:</p> + + <pre style="overflow: scroll; border: 1px solid #eee"><?php echo $data['responseHTML']; ?></pre> + + <hr /> + + Copyright © 2007 <a href="http://rnd.feide.no/">Feide RnD</a> + + <hr /> + + </div> + +</div> + +</body> +</html> diff --git a/templates/post.php b/templates/post.php new file mode 100644 index 000000000..2329cdbca --- /dev/null +++ b/templates/post.php @@ -0,0 +1,24 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8" /> + <title>SAML 2.0 POST</title> +</head> +<body onload="document.forms[0].submit()"> + + <noscript> + <p><strong>Note:</strong> Since your browser does not support JavaScript, you must press the button below once to proceed.</p> + </noscript> + + <form method="post" action="<?php echo $data['destination']; ?>"> + <input type="hidden" name="SAMLResponse" value="<?php echo $data['response']; ?>" /> + <input type="hidden" name="<?php echo $data['RelayStateName']; ?>" value="<?php echo $data['RelayState']; ?>"> + + <noscript> + <input type="submit" value="Submit the response to the service" /> + </noscript> + </form> + +</body> +</html> \ No newline at end of file diff --git a/templates/status.php b/templates/status.php new file mode 100644 index 000000000..609c04361 --- /dev/null +++ b/templates/status.php @@ -0,0 +1,111 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<title><?php echo $data['header']; ?></title> + +<style type="text/css"> + +/* these styles are in the head of this page because this is a unique page */ + +/* THE BIG GUYS */ +* {margin:0;padding:0} +body {text-align:center;padding: 20px 0;background: #222;color:#333;font:83%/1.5 arial,tahoma,verdana,sans-serif} +img {border:none;display:block} +hr {margin: 1em 0;background:#eee;height:1px;color:#eee;border:none;clear:both} + +/* LINKS */ +a,a:link,a:link,a:link,a:hover {font-weight:bold;background:transparent;text-decoration:underline;cursor:pointer} +a:link {color:#c00} +a:visited {color:#999} +a:hover,a:active {color:#069} + +/* LISTS */ +ul {margin: .3em 0 1.5em 2em} + ul.related {margin-top:-1em} +li {margin-left:2em} +dt {font-weight:bold} +#wrap {border: 1px solid #fff;position:relative;background:#fff;width:600px;margin: 0 auto;text-align:left} +#header {background: #666 url("/<?php echo $data['baseurlpath']; ?>resources/sprites.gif") repeat-x 0 100%;margin: 0 0 25px;padding: 0 0 8px} +#header h1 {color:#fff;font-size: 145%;padding:20px 20px 12px} +#poweredby {width:96px;height:63px;position:absolute;top:0;right:0} +#content {padding: 0 20px} + +/* TYPOGRAPHY */ +p, ul, ol {margin: 0 0 1.5em} +h1, h2, h3, h4, h5, h6 {letter-spacing: -1px;font-family: arial,verdana,sans-serif;margin: 1.2em 0 .3em;color:#000;border-bottom: 1px solid #eee;padding-bottom: .1em} +h1 {font-size: 196%;margin-top:0;border:none} +h2 {font-size: 136%} +h3 {font-size: 126%} +h4 {font-size: 116%} +h5 {font-size: 106%} +h6 {font-size: 96%} + +.old {text-decoration:line-through} +</style> +</head> +<body> + +<div id="wrap"> + + <div id="header"> + <h1>simpleSAMLphp status page</h1> + <div id="poweredby"><img src="/<?php echo $data['baseurlpath']; ?>resources/icons/bino.png" alt="Bino" /></div> + </div> + + <div id="content"> + + <h2><?php if (isset($data['header'])) { echo $data['header']; } else { echo "Some error occured"; } ?></h2> + + <p>Hi, this is the status page of simpleSAMLphp. Here you can see if your session is timed out, how long it lasts until it times out and all the attributes that is attached to your session.</p> + + <p><?php echo $data['valid']; ?>. Your session is valid for <?php echo $data['remaining']; ?> seconds from now.</p> + + <h2>Your attributes</h2> + + + <table> + <?php + + $attributes = $data['attributes']; + foreach ($attributes AS $name => $value) { + if (sizeof($value) > 1) { + echo '<tr><td>' . $name . '</td><td><ul>'; + foreach ($value AS $v) { + echo '<li>' . $v . '</li>'; + } + echo '</ul></td></tr>'; + } else { + echo '<tr><td>' . $name . '</td><td>' . $value[0] . '</td></tr>'; + } + } + + #echo "\n\n\n" . $session->getMessage() . "\n\n\n"; + + ?> + </table> + + <h2>Logout</h2> + + <p><?php echo $data['logout']; ?></p> + + <h2>About simpleSAMLphp</h2> + <p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it? + You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p> + + + + + + <hr /> + + Copyright © 2007 <a href="http://rnd.feide.no/">Feide RnD</a> + + <hr /> + + </div> + +</div> + +</body> +</html> diff --git a/www/_include.php b/www/_include.php new file mode 100644 index 000000000..ab5e7dcab --- /dev/null +++ b/www/_include.php @@ -0,0 +1,18 @@ +<?php + + + +$path_extra = dirname(dirname(__FILE__)) . '/lib'; + +$path = ini_get('include_path'); +$path = $path_extra . PATH_SEPARATOR . $path; +ini_set('include_path', $path); + +require_once('SimpleSAML/Configuration.php'); + +SimpleSAML_Configuration::init(dirname(dirname(__FILE__)) . '/config'); + + + + +?> \ No newline at end of file diff --git a/www/auth/login-auto.php b/www/auth/login-auto.php new file mode 100644 index 000000000..f06c29baf --- /dev/null +++ b/www/auth/login-auto.php @@ -0,0 +1,95 @@ +<?php + + +require_once('../../www/_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + + +$session = SimpleSAML_Session::getInstance(); + +$error = null; +$attributes = array(); + +if (isset($_POST['username'])) { + + + $dn = str_replace('%username%', $_POST['username'], $config->getValue('auth.ldap.dnpattern')); + $pwd = $_POST['password']; + + $ds = ldap_connect($config->getValue('auth.ldap.hostname')); + + if ($ds) { + + if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { + echo "Failed to set LDAP Protocol version to 3"; + exit; + } + /* + if (!ldap_start_tls($ds)) { + echo "Failed to start TLS"; + exit; + } + */ + if (!ldap_bind($ds, $dn, $pwd)) { + $error = "Bind failed, wrong username or password. Tried with DN=[" . $dn . "] DNPattern=[" . $config->getValue('auth.ldap.dnpattern') . "]"; + + + } else { + $sr = ldap_read($ds, $dn, $config->getValue('auth.ldap.attributes')); + $ldapentries = ldap_get_entries($ds, $sr); + + + for ($i = 0; $i < $ldapentries[0]['count']; $i++) { + $values = array(); + if ($ldapentries[0][$i] == 'jpegphoto') continue; + for ($j = 0; $j < $ldapentries[0][$ldapentries[0][$i]]['count']; $j++) { + $values[] = $ldapentries[0][$ldapentries[0][$i]][$j]; + } + + $attributes[$ldapentries[0][$i]] = $values; + } + + // generelt ldap_next_entry for flere, men bare ett her + //print_r($ldapentries); + //print_r($attributes); + + $session->setAuthenticated(true); + $session->setAttributes($attributes); + $returnto = $_SESSION['webssourl']. '?RequestID=' . $_REQUEST['RequestID']; + header("Location: " . $returnto); + + } + // ldap_close() om du vil, men frigjoeres naar skriptet slutter + } + + + + +} + + +$t = new SimpleSAML_XHTML_Template($config, 'login.php'); + +$t->data['header'] = 'simpleSAMLphp: Enter username and password'; +$t->data['requestid'] = $_REQUEST['RequestID']; +$t->data['error'] = $error; +if (isset($error)) { + $t->data['username'] = $_POST['username']; +} + +$t->show(); + + +?> diff --git a/www/auth/login-radius.php b/www/auth/login-radius.php new file mode 100644 index 000000000..49ceee2e9 --- /dev/null +++ b/www/auth/login-radius.php @@ -0,0 +1,95 @@ +<?php + + +require_once('../../www/_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + + +$session = SimpleSAML_Session::getInstance(); + +$error = null; +$attributes = array(); + +if (isset($_POST['username'])) { + + + $dn = str_replace('%username%', $_POST['username'], $config->getValue('auth.ldap.dnpattern')); + $pwd = $_POST['password']; + + $ds = ldap_connect($config->getValue('auth.ldap.hostname')); + + if ($ds) { + + if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { + echo "Failed to set LDAP Protocol version to 3"; + exit; + } + /* + if (!ldap_start_tls($ds)) { + echo "Failed to start TLS"; + exit; + } + */ + if (!ldap_bind($ds, $dn, $pwd)) { + $error = "Bind failed, wrong username or password. Tried with DN=[" . $dn . "] DNPattern=[" . $config->getValue('auth.ldap.dnpattern') . "]"; + + + } else { + $sr = ldap_read($ds, $dn, $config->getValue('auth.ldap.attributes')); + $ldapentries = ldap_get_entries($ds, $sr); + + + for ($i = 0; $i < $ldapentries[0]['count']; $i++) { + $values = array(); + if ($ldapentries[0][$i] == 'jpegphoto') continue; + for ($j = 0; $j < $ldapentries[0][$ldapentries[0][$i]]['count']; $j++) { + $values[] = $ldapentries[0][$ldapentries[0][$i]][$j]; + } + + $attributes[$ldapentries[0][$i]] = $values; + } + + // generelt ldap_next_entry for flere, men bare ett her + //print_r($ldapentries); + //print_r($attributes); + + $session->setAuthenticated(true); + $session->setAttributes($attributes); + $returnto = $_REQUEST['RelayState']; + header("Location: " . $returnto); + + } + // ldap_close() om du vil, men frigjoeres naar skriptet slutter + } + + + + +} + + +$t = new SimpleSAML_XHTML_Template($config, 'login.php'); + +$t->data['header'] = 'simpleSAMLphp: Enter username and password'; +$t->data['requestid'] = $_REQUEST['RequestID']; +$t->data['error'] = $error; +if (isset($error)) { + $t->data['username'] = $_POST['username']; +} + +$t->show(); + + +?> diff --git a/www/auth/login.php b/www/auth/login.php new file mode 100644 index 000000000..31f05dfbd --- /dev/null +++ b/www/auth/login.php @@ -0,0 +1,98 @@ +<?php + + +require_once('../../www/_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + + +$session = SimpleSAML_Session::getInstance(); + +$error = null; +$attributes = array(); + +if (isset($_POST['username'])) { + + + $dn = str_replace('%username%', $_POST['username'], $config->getValue('auth.ldap.dnpattern')); + $pwd = $_POST['password']; + + $ds = ldap_connect($config->getValue('auth.ldap.hostname')); + + if ($ds) { + + if (!ldap_set_option($ds, LDAP_OPT_PROTOCOL_VERSION, 3)) { + echo "Failed to set LDAP Protocol version to 3"; + exit; + } + /* + if (!ldap_start_tls($ds)) { + echo "Failed to start TLS"; + exit; + } + */ + if (!ldap_bind($ds, $dn, $pwd)) { + $error = "Bind failed, wrong username or password. Tried with DN=[" . $dn . "] DNPattern=[" . $config->getValue('auth.ldap.dnpattern') . "]"; + + + } else { + $sr = ldap_read($ds, $dn, $config->getValue('auth.ldap.attributes')); + $ldapentries = ldap_get_entries($ds, $sr); + + + for ($i = 0; $i < $ldapentries[0]['count']; $i++) { + $values = array(); + if ($ldapentries[0][$i] == 'jpegphoto') continue; + for ($j = 0; $j < $ldapentries[0][$ldapentries[0][$i]]['count']; $j++) { + $values[] = $ldapentries[0][$ldapentries[0][$i]][$j]; + } + + $attributes[$ldapentries[0][$i]] = $values; + } + + // generelt ldap_next_entry for flere, men bare ett her + //print_r($ldapentries); + //print_r($attributes); + + $session->setAuthenticated(true); + $session->setAttributes($attributes); + + $session->setNameID(SimpleSAML_Utilities::generateID()); + $session->setNameIDFormat('urn:oasis:names:tc:SAML:2.0:nameid-format:transient'); + + $returnto = $_REQUEST['RelayState']; + header("Location: " . $returnto); + exit(0); + + } + // ldap_close() om du vil, men frigjoeres naar skriptet slutter + } + + +} + + +$t = new SimpleSAML_XHTML_Template($config, 'login.php'); + +$t->data['header'] = 'simpleSAMLphp: Enter username and password'; +$t->data['relaystate'] = $_REQUEST['RelayState']; +$t->data['error'] = $error; +if (isset($error)) { + $t->data['username'] = $_POST['username']; +} + +$t->show(); + + +?> diff --git a/www/example-simple/saml2-example.php b/www/example-simple/saml2-example.php new file mode 100644 index 000000000..7da667c98 --- /dev/null +++ b/www/example-simple/saml2-example.php @@ -0,0 +1,52 @@ +<?php + +require_once('../_include.php'); + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/XML/SAML20/AuthnResponse.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +session_start(); + +/* Load simpleSAMLphp, configuration and metadata */ +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); +$session = SimpleSAML_Session::getInstance(); + +/* Check if valid local session exists.. */ +if (!isset($session) || !$session->isValid() ) { + header('Location: /' . $config->getValue('baseurlpath') . 'saml2/sp/initSSO.php?RelayState=' . urlencode(SimpleSAML_Utilities::selfURL())); + exit(0); +} + +$attributes = $session->getAttributes(); + +/* + * The attributes variable now contains all the attributes. So this variable is basicly all you need to perform integration in + * your PHP application. + * + * To debug the content of the attributes variable, do something like: + * + * print_r($attributes); + * + */ + + + +$et = new SimpleSAML_XHTML_Template($config, 'status.php'); + +$et->data['header'] = 'SAML 2.0 SP Demo Example'; +$et->data['remaining'] = $session->remainingTime(); +$et->data['attributes'] = $attributes; +$et->data['valid'] = $session->isValid() ? 'Session is valid' : 'Session is invalid'; +$et->data['logout'] = '[ <a href="https://sam.feide.no/amserver/saml2/jsp/idpSingleLogoutInit.jsp?binding=urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect">IdP intiated logout from Feide (only if you are connected to the Feide IdP)</a> ]</p> + <p>[ <a href="/' . $config->getValue('baseurlpath') . 'saml2/sp/initSLO.php?RelayState=' . urlencode(SimpleSAML_Utilities::selfURL()) . '">SP initated logout</a> ]'; +$et->show(); + + +?> \ No newline at end of file diff --git a/www/example-simple/shib13-example.php b/www/example-simple/shib13-example.php new file mode 100644 index 000000000..377816a7f --- /dev/null +++ b/www/example-simple/shib13-example.php @@ -0,0 +1,35 @@ +<?php + +require_once('../_include.php'); + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XHTML/Template.php'); +session_start(); + + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + +$session = SimpleSAML_Session::getInstance(); + +if (!isset($session) || !$session->isValid() ) { + + header('Location: ' . $config->getValue('baseurlpath') . '/shib13/sp/initSSO.php?RelayState=' . urlencode(SimpleSAML_Utilities::selfURL())); + // . '&idpentityid=' . $idpentityid ); + exit(0); +} + +$et = new SimpleSAML_XHTML_Template($config, 'status.php'); + +$et->data['header'] = 'Shibboleth demo'; +$et->data['remaining'] = $session->remainingTime(); +$et->data['attributes'] = $session->getAttributes(); +$et->data['valid'] = $session->isValid() ? 'Session is valid' : 'Session is invalid'; +$et->data['logout'] = 'Shibboleth logout not implemented yet.'; + +$et->show(); + + +?> diff --git a/www/index.html b/www/index.html new file mode 100644 index 000000000..9940b30d5 --- /dev/null +++ b/www/index.html @@ -0,0 +1,88 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> +<title>simpleSAMLphp</title> + +<style type="text/css"> + +/* these styles are in the head of this page because this is a unique page */ + +/* THE BIG GUYS */ +* {margin:0;padding:0} +body {text-align:center;padding: 20px 0;background: #222;color:#333;font:83%/1.5 arial,tahoma,verdana,sans-serif} +img {border:none;display:block} +hr {margin: 1em 0;background:#eee;height:1px;color:#eee;border:none;clear:both} + +/* LINKS */ +a,a:link,a:link,a:link,a:hover {font-weight:bold;background:transparent;text-decoration:underline;cursor:pointer} +a:link {color:#c00} +a:visited {color:#999} +a:hover,a:active {color:#069} + +/* LISTS */ +ul {margin: .3em 0 1.5em 2em} + ul.related {margin-top:-1em} +li {margin-left:2em} +dt {font-weight:bold} +#wrap {border: 1px solid #fff;position:relative;background:#fff;width:600px;margin: 0 auto;text-align:left} +#header {background: #666 url("resources/sprites.gif") repeat-x 0 100%;margin: 0 0 25px;padding: 0 0 8px} +#header h1 {color:#fff;font-size: 145%;padding:20px 20px 12px} +#poweredby {width:96px;height:63px;position:absolute;top:0;right:0} +#content {padding: 0 20px} + +/* TYPOGRAPHY */ +p, ul, ol {margin: 0 0 1.5em} +h1, h2, h3, h4, h5, h6 {letter-spacing: -1px;font-family: arial,verdana,sans-serif;margin: 1.2em 0 .3em;color:#000;border-bottom: 1px solid #eee;padding-bottom: .1em} +h1 {font-size: 196%;margin-top:0;border:none} +h2 {font-size: 136%} +h3 {font-size: 126%} +h4 {font-size: 116%} +h5 {font-size: 106%} +h6 {font-size: 96%} + +.old {text-decoration:line-through} +</style> +</head> +<body> + +<div id="wrap"> + + <div id="header"> + <h1>simpleSAMLphp is installed</h1> + <div id="poweredby"><img src="resources/icons/compass_l.png" alt="Bino" /></div> + </div> + + <div id="content"> + + <h2>Welcome to simpleSAMlphp</h2> + + <p>You have installed simpleSAMLphp on this web host.</p> + + <p>After you have configured it properly as described in the documentation you may want to test one of the two examples: + <ul> + <li><a href="example-simple/saml2-example.php">SAML 2.0 SP example</a></li> + <li><a href="example-simple/shib13-example.php">Shibboleth 1.3 SP example</a></li> + </ul> + </p> + + <h2>About simpleSAMLphp</h2> + <p>Hey! This simpleSAMLphp thing is pretty cool, where can I read more about it? + You can find more information about simpleSAMLphp at <a href="http://rnd.feide.no">the Feide RnD blog</a> over at <a href="http://uninett.no">UNINETT</a>.</p> + + + + + + <hr /> + + Copyright © 2007 <a href="http://rnd.feide.no/">Feide RnD</a> + + <hr /> + + </div> + +</div> + +</body> +</html> diff --git a/www/resources/icons/bino.png b/www/resources/icons/bino.png new file mode 100644 index 0000000000000000000000000000000000000000..8708a439c91d39ac5a195d1af6d87178663775c8 GIT binary patch literal 15118 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 literal 0 HcmV?d00001 diff --git a/www/resources/icons/bomb.png b/www/resources/icons/bomb.png new file mode 100644 index 0000000000000000000000000000000000000000..35811006b5cabde251612955aa1f6385c71829da GIT binary patch literal 3439 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 Ro~{4@002ovPDHLkV1nmltq%YI literal 0 HcmV?d00001 diff --git a/www/resources/icons/bomb_l.png b/www/resources/icons/bomb_l.png new file mode 100644 index 0000000000000000000000000000000000000000..901409504180b295662068d599aa85e7f5482003 GIT binary patch literal 8960 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 a$NvM{Nk$_xYo8AQ0000<MNUMnLSTYIlwcJA literal 0 HcmV?d00001 diff --git a/www/resources/icons/compass_l.png b/www/resources/icons/compass_l.png new file mode 100644 index 0000000000000000000000000000000000000000..2ad574a7b38e661b462e0ad8d72207234cd8e827 GIT binary patch literal 17694 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 WUo6VC!v|;p0000<MNUMnLSTYR@0UUV literal 0 HcmV?d00001 diff --git a/www/resources/icons/debug.png b/www/resources/icons/debug.png new file mode 100644 index 0000000000000000000000000000000000000000..5b78608a21cb1649da11ad6599a4f3c0821b9be1 GIT binary patch literal 10724 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 literal 0 HcmV?d00001 diff --git a/www/resources/icons/lock.png b/www/resources/icons/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..11073e2969f6ada809e80a322549f19197cd288c GIT binary patch literal 14639 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 literal 0 HcmV?d00001 diff --git a/www/resources/icons/pencil.png b/www/resources/icons/pencil.png new file mode 100644 index 0000000000000000000000000000000000000000..e79a164dc3d1699bafa050caa2d4a644dffa6f6d GIT binary patch literal 3004 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 literal 0 HcmV?d00001 diff --git a/www/resources/sprites.gif b/www/resources/sprites.gif new file mode 100644 index 0000000000000000000000000000000000000000..ae69d83412dc91ea11333eeca56f19d5aef833db GIT binary patch literal 12536 zcmbW-cTf}9+b{44h=_`;`cni1R$WCvU@eIBRTmIxYeA&11rQLiA-zj)0@4zS)KEhW zA(c)D0YYy{=q*49EhHi3hJD|A?|WzN{O&vV%sl^{Ip>e(%$a9C^EG&=uYUJ$r9*@x ziyr_n0KnYb9GlH<`TOs{z(70z5D*aH=jZ3^>+A3D@9pjF>FN3D)2HCzU>_f!pr9Zx zFE4j@_YWUFpzZA)9UblM?ccwD|M~OhFJHbWD=Xi*bH~li&DGWQ)vH%7E-qicezml; zeEat8yLa#Y`s=TcA3xr{ecQ>&$;!&=_3PJv|NXbAsi~o%p|!QOv9a-k2M@Hgw4ObC zcJ11=yLazuYHAu780hNiIygA!=;-|M#~;ql&K4FHHa0eOR#plM3aYBA%gf6*Zrs@0 z+q-w~-lIp4o<DzXW@h&I@#9;!Zh3fk+`M`7^5x6s=H^eIKGoOPfAZvsl9JM!H*X$3 ze5k0XXk=t$XJ>c+{(S%dU~6luqM~yB`t{#`|NZLKt3)DETU*=2#N_46mwI}7SFT*y z+1Zhom;dw6Kh@RMFJ8Q;rlzK$q4DCy3&Rv1t*ID^L~d+s;2j+^00173H!?E9Kl8u2 zeq{<B0suY$ln(YE^aOB(7xW<{H0)b=L}XNSOl(|yLgM$N<dh$&Y3Ui6Ku}h8PVUdV z{DQ)wVsJ@mS$Rbzq^i26wyqx90BeLJz&FttWOGYv)lIifx0`L<_@3Mj509?iq2ZB4 z!T@fRNFsG$C&_d3ZJ4Ho70T+`I(37#xwXBsOW$KKS^I1bmj@6Ol{T!(Z4MPVtzd(# z`q>&Ge#r=ASe@4%BXQG(h^@}=Opv=DE&Z^jpetGaNvTa&O<{MM>MI=RVQo<lQ2i5y z*i~EHm!lUddcmj;+@EiltYC|)D;X>{&o|04sxKWbv#fO?;p)pqAr7t47al>&$7`Jj zOKrQM6_X7fyg6LfqlQWn!fTsC>TZBcVS)sY$r!_`W?CapE85{<)pMPRmmXysH`Xk4 zr{8oX;~Q(2`f~5b$e6(ERtCXO%ItdJ^{b;*ue!5M5YY8W*r!!;529gXswwoCtSJ(< zIfqSFwC_bWZZ8q?ALW>$;Jd41wXRdWD8$~zbZd;P85+slULGv7??a>Z_h@t7Ic69% zXP>dXI@O24@OS`W`RO2`v(D4OB3HoEA;;C0r$fc{<!8dqSUAsolW+vjgiCua&qT<D z%g;t$PIaD*k}m?!Mk_&=XJb@5<mX~<jX2N6sV{=(;<f0@a|wFFcS(}%zdQtekCgKU z@{+>Kn=ijJJ2m?{rP}1}!jC7SX@#jzw?+yR9m20IroT>oyO`lzRI-@q23=VMdURY{ z0(~BNyOia%ShAGuM_*aW2@+OV&J8>3vivjhO6hW5tQuuGKT%&{r69$^Wu-9PvGi?- z<Y&q?56K6YN`*1sDA%O3SZ7PY;36EbB&)`V;$BuG*;5X0rj(RGCNx|ih>mDi_xcG9 zC3kqX&)U0o9LuE&;jXY=H)P?uUO(nowhkqFuC6yshbvNH^Qo@X#^s_iDtryPN=48* z6gQANBd!}L#$wq9noVEbzyOXY(V863I?$S%{^j6$2y@L(Ra$tzNVQC4d$Eewe8zgs z_15tX&&_5rSLBCIxIbI9tI=dl5eLhN{k^-P1X<NCH)>4lQC}+G?$z2`+wRjla(##J z;M}{Ne#5I3I|Igd)^`TY4X*DFJ$?Reci7UYVt2&m>-z4fL&SCZ*z5g*j&Wzf;_wO= zHdJlG1GGLlQC`$DIq5X&zt{0$BTkL@ZbQET61*C|H|2k<_v=jK1u+I$>Sp}pRLUbe z`a-%>C36w<mC9VojZkJS7o@qfR=~xTEJ}F;m9<*csl2~dH|oB>4qK|+ry}>L`x{M1 zRFE-p$DaFnbY8XDq~Y&uu(t^YDx966=kGbYV@?nbo%nTwvo{@~!sRjM)82EL%f%2b zYpr2}yHD#>;jwo{-(#FEw7ONx{i2wI+-mEe;s_fy2}sg{M2vF<&S9H`HE6-&vAIH5 z4Vy)+Xdx2#T;V&|W-%XHsNC_NA_j&nr!r|_^2R?!pJQ9j!fD@BV}BlZGHjKcpoOdB ze~Nv@w#u+*5qih-#3KybE=q1j8XD)FO2f8Y(b$YKkIg$%Y}l?~wHa-R&pX?IZCCc$ zjBz-gFVSh(aU*jx*4a2;aunO42H%YHh|QN?GVIit*o^nW=gaJ2J9Su_2|>pT<c>VV z-jm!)j5IE|c&-bpud($#F}C3H)rVb1R$EEw_<}2Uy1Gn#ws^_8#|z~R9^xKnZl!>Y z3l*Ms;Vj@=KdNF2m7E@STTN`G!tjO4U%R^PSX*gL#}^X-wD3FPgX!&BMK_YWj9+Vf z%II^ua;x}SkBk0x=1^PF?b@!McRo*m(`%0Em}|WsleR%~#>IDsqk6x<YqMyti?yci z_IV9#XVdXc+IvxE0hHPt0H;`w^N<j9_H(YV$+F%_ocTAkGe5=s!4EDN^+#HK&O6&9 zuYbp<Ki*|1Uz!6pyl?b4G4pf5)i=%$|H2LYfZr-qefQSbUSTkEV5jJ2Pl?Hw=)o*j zU9p~tyjcY6NuF>$*igIlaU5=_K<!J3X`I5-V%F3A$InWi@!Cr*Ah_Xj&o5<mCloC^ zS;IBaKMEXwTYWKFIsz@)t#JERW<6&#+Bh*>{^duR-8OC%LH_~?IHBZlWPh}Un^F~W zTj}MwvayaU^y=7e<*zQ4J;j;QYm$1(-`tKF@9}}w=4QD%>voS1B+={gO)B0#i<ubC ztF5nkQ{iSSWj)agg~Cwpj6S(elDFs$@ZEPF!HUG$@J}%8ZTF8+-NXfr=Z%B~mrwco z#1*Rs_*h}3Cq&AO>T?G%MN;<aR3vZZr6OlcRD4EV?dYWqsE7U#Khh(6M(-$k7X{JU z1x|5i_b}|kRe?*I(*nmBO@~aYg8$`aC9LVyEUxdt3zJovsd&t2Io<2=?RVtgC+-Zj z%2BH#?;6jFr@=C=npQ_W*Lr!jYz(OqUmfE_oR#cOhu=b1$A7}VlG?L`>x$JRL?B;X zJmy{fr|H)BG`IOHiZ6<c18P$8jo(}~@Wxs6-bih*d82GS*zG{ANw3#(<fbz5uT4Lt z5Bt5j1!4BM$KM3T9b39P%Iy8vanorVzw~~~LEtpwQ|_Vb%lA&R2%)BRKmQfC{HG?o zKhd$SKvsLj@Cj=mwfA%3ow&EAuJFNJZXH<LgkqlNGgM@HyY#s~#iESgu7LXTS0=8O zlRhI2mA5OuR;=1Bu||=?P)MkWoBgr<v37lWRT{5i&GG8~I9{w?N_aZ_?Q`J7NUs{S z!GGP|b#ju(g~D1*+&zH4#3kiB@X-pYXYoF1ExZ9SXR_fvX-#HSenoFoYy@oYPwk7r zFo(rx!BWU+k*lzllVb0~HMM3=+N-x+Ro;v?V9%cEgtg!7-Hh|KnUmY_!s;q-B_*@x zFPSxVJ&WJ^fj};(I5py5nQo^}`Yqmp-0l5Zxt+blUeY=O?+Z2E`6*?)Y`F2YKRIBh zP!qLc?gSso(b)w*;ZQ94e1;q1zf@YIRvkut#!#m8YM||!vx@ITUnLz{%UO55`jtr1 z`3f84P(Ay6Nqg~M(W|J9Al`oa;f+0B%MmgyOcyzOy3eakGhj2p$#3pgGiH~y&Q?0a zZ&3xp?EREY1D-@J{mJtl00!)URs1Pt5Z0H5?A<DR)SA8S{@5Uw4(&s=f3V*tZ*%wH zN6;IgX6zYRG^52#XDjW!-@=_7W^Y0OtdPg1JmIkjedyi#1Wx-`-ahdpkKTDQU@Hd0 zKB<dg_4EboK&CkBfjs7nSs=5M$KCD3aJF;4ayNK9*0~_=)gXX+kizpI5yK!+=b)q5 zpkre}l*OP^BEcsugHJmLpONrC2Mazw7A(OCmKF(#Ukd)!GUT#X$i?)KD<Yp2$3o<n zLlhVxHzY!t!l7#Fp?3^<p?6`S;}M}c;80y`sGfS5lzf=JM3|vr*h8<d?9MP#SeQ9D z>@g$E|J=8yUf(RzzdZqeyAkotn(@s}BHTtj9AOatQY73lJ=_@<E>Rrr#0Ynnh;WMx z^-z!Sw2bhQkN7P;;wv~JU@RiYD>M)rA=Y_VmnIk<85xlt84HVy2S+B3MSf>QCRs+M zcts_rN2P+J(qU2AV^O)-s60kgzC?6^S9DQ&bRjGnjEycGi>_is*DOcZNyOBv$3W9# zV6d2mu^6~~^dW-qJMq|7`PeqE*pBqrc33PH8;cu@B`{(KmScw`;)d1ZM$+TPU~!{k zaTCjNL`K}K1TTJGBz{pne$gv_Au@gq7EcAo)5hXA8Sz_|2|Hd1+vy2(Z~_CCz#U5f zbR`Hd69msE3VA0AXCxkOOcd!#6dg|#XC|I%OcZ<Z{g=1jPkVot%=j)H^<Ad%yX^RP zxw}ahUnKqNopd=W=}JbDQe%>GSK{@Oq#NgxZ!wc@-%VD_NLFu5zB8Vzv68ICOulzM z<-yUE5zCbOZ&MyNrWlT=7_FojGgBU)|MB$b4~x4$p1=6fmj1&g>W5v)4~Ov|e=~o) ze3AOvJM~pYs^j?|&W)+=<Eb7csUMiBA77+>_D=hhk>>d@(W@~nU_33TBrTYk7V;uJ z%sV|aBYhd19{HA+9?MLRKc5kIHzV<3M$(InO|Oj1w;7<Q4B$#e7BeI7d}hJJlw9x3 z{HV;rjLh=J%*v9?s+II=W@g1*AoK+g_7Dj71|qx@8@qr_Okm46u<bmk{Vpi|EeIC{ z!h3^y8$o^JAi}le!E0F~=d;FMWQ}`gO^jz9YRZ~!%$i-vnmeC8@10%qHk<Mydo3z^ zy)m15KIivqIU6r>cD!?F89DTl95ObC+n580&N;-&5zxpwY?Ldakt^zxdn_|Ip(|IM zl6z_*_l)Gve@!HxcKIop`BU2Grwsh(p`&>hCw^XH{k-gxcSRxgH+Y^rF7Fy8Pc<5N zT_az`DF1gWUcS0Zt{Oc5E-wELD_=(;U2h`)fkuJ8WWk><DTY1;4<`zYSp|<K63r<E ze;E~8XcStq;$HX^K7$w9O%!U17TIVN*&7wTbSZd~S>yySa-1kSTw3IgEAr4N{-^-@ zB3XRpn(!auLO!@+--+TNR&fZWI7|}!O#>XB366w=BPPJn6mTpHoFrM2B3hEFQIcj= zk`6D)oG8g)m4HM`vn5LlG)jvUO2Jm8B|fF46Q$*>(lW`iN|zD=@e}o;M;bEApztyn zt_(g=*2F4np_H{rmbYt^cVw1h;pLqZ<v2<?o>e{|SurG9F``j1YE>}?ub3>YAZJ!g zPw*;cSQWEYmGeH8bD5QkrIpL@O6o)<4Oh9vs@#@@?D#<FnUGyLgn@&wCLn-)h`=gD zNUDnMQYE5Ub=0~_bh7IBe$_FlYB9xXan0(p*3}Y^swI7^rGVAa`_(d1HHVJXT(GXW z>|1j&rsfK;MhQ`)TvnqxS@ZjT%?<0?TfVh7W6ITCYwvc~>L6<MCTsuLuf1no_rSOA zKCn)|tj^H4%y_@<kyO2jX1)2N`p4GwmcI2?uJty+`Ui-5J1MAxCe-#3^rbb_*%#{K z3UvcQ>2RpKRD*|R!@EZfAFUg_d>edR8~n;D{1FY_`wbydu&`sWa7|c*H7wGVR~bDC zi`s|9tirw@YfQ3kOz~~JquH40+6atk1noCwOTn`=;lYpKdB@;IKsXoyFP?;#uELo( zI7AasV~wctMbyP0pa@vwBm%LIfJ-6KipVBS<ZrIXb|A71fy8zrag#`bBA`zb)oYFF zcSQ~Pq9&~2<CCbdebnSJG+7Eg|EOj`5xwk-rf8zqfarD2iVZ0YRTH!N2(x94>4?GX zAu#kw3~Lp`R)q01n-1AD0sNYTVw+T5n~oBjj<cIYrJKdHAg67b#od}u`87*|n)7O! zCD_d}(k;@*TY42+F8j4y0kvEzZ^?{lQDnC$OSdX%wSH9OwW=Dos)1V7k*zY_tr~u< zdhFJFYppzuwm*#93_xxA$hLW0n^A0=sdT%!bjuTVn}t&QbDQ>ivF$eH?RKDchggiQ zbcelGhoenLSxm=kP<6{thdaB&L%Q>$R_AA%PEWs1FHolsveS>)8NluglE#K;VZ&^& z;eOai5H=c#jU{5^+1Nzst|YCl6q_!IGl$YaU75%(5V0$p-IXhi%hSRY*x-u%a9|Lw z6p1S*;wp^+CkTKlrS592Zm3N+%(xrw*Np&mBiY?(X*@~`-(-w$vB6{g@HjU-9)#~f z;(Mig2wFXTHa!D2J;LJM<3zwDv1fwK>meTRB}@0tYxOQF^)B1=uK4v*h`np<-c{*7 zs!|_KtB-Ee$1v_=`StCC`q=C~-ddjksZZzv;jlJAIEx^HA{-$RMAr$&IfPRe`p=x` zKdaq;&bD6y)h|ivKhNoxJ~1G3Vc@d%z?JI*^0ouA9Kgv$fYSQFb<V(z3xl^#4611l z-mx82M-6I_2JdnPwN4D_To`(wJ*0nq$k2A^q5qH(X~>u}^ytE{>GffA?O_YsVM~)? zEB|5ZtYI6@u-%0bTkR1ClaZIUBhLOKF7HO%vPRyaM%*uqx@G}{^Z=jVjehYT^~oCb ziyI9<jRulNgS5v&Y{!EA$HL-xW8qn2v8b{5im^n}*murYlI?hk|9EoNcxuJ?N6tt# zX*~DDXdY)g|H4E8YUqghXo(4+j5JZonJ7OoS$ScyPJ0r1eG+Cn+2}tBCru(blZXpM z^mSsBHnGE&h&3VN{E6LJL_CLh(gUD$oz$;Q8nGpfnUE&@Nt0P5B8No2KqhIEr%lMS zw&Z1hGUXk4EsMO4B2zC+(X^*FY^S!~P4%r4StKGmYl?%K;$D~r=uGo$rv==ng#xBU zv!{>8Pm7_aPm-s_b!JZ4&HSQFJQFZ;9zAomcSe#tBgdV&NS(PXGy9v)?3L_UdGzd6 z@~i@NR*5@%LxwkZOKeU$9`Ge@PAGRyD|=1{J*P{a)6<#1XE*;x!2E;wdHw8pWAwaf z<-9q0{x9zQW4nc?0SiyE7c43lEYS;g<OPS`g_qogS2ByQ0~Q^#7vG>4oqHEu$cxW( z=G2szKHJTG(OL3D6Fs<#e$>SP^im*sDM)8I#BMn_U^y&)IXrtg7QGx_xtvH|PIL!| zIRH}JSAGPnWM;2`;#acKD>>wqTpdcD9pz^Lr68VCluao|Qz|PdRb)yvmr`T5S{JZd zo4pFHTt$#qVboPPceO=ktxasLLuakiZViiG!`ZEgIIQ%!1NwE=33lrP?(0JV>l4}Q z#CYC18NEJ5UZ2*X&e~CD0;u!x)WvM-8k$P2q|(ULO)hoIZUdja#t7J8k=K~L8~fx9 z0B=J8LJ^duaRX?=IkdwVnn)i_6tXJnu=2}$z-jx<Q-Paj5;o7~Y)WG`Wgwe!Q=7l? zHZR(5T@Kv3l(TgOvL%n%Ql8pU?c2J++qx;ceJgNVG!bw#aa$9zt;O5ck=@bO-O)4K zxo5v)7`S8fe#bax#{{!uD!Xg0yK82@`}qCt)4*M;oL!rQT|3OKUBdQlUHVIVx>_Lp zbpqWnhwg@<yF=(6Q*?RWuI)*>r~RIn*`80}o^Q^cA8#)}mf^3<_!<Zh_22=*6BrRW zj93gK9>PeRVtnT@lI)o&fz0F_W-5f4j$vj`F?0Kvc|2ymEUO@pRg}Xj#IV2^md43L z5ZV2sfc;vP{kp(?XwH5;W*^qK51-m^;_bI=?6=9X+jZF;IczM3-8sd^ZLsk?Hva<{ zvS&x0JT#iaITFN~gm8#F4q28<(&bK@acAwh%Yj_Vd+u5ecOAo}%JOKsybXKa)_dMg zAdi*9W1kc`B7P(!Ec9DsM09v;R9sB__r&CcA4#bxX_*<I^lV^GR&IV?;m_g%aB@*e zMLDFjy0WIK7FrLhgE!>JhM|u%wP4zs+gm%ju-%<KJX|lnZ=ipOFfuqgJT^H&8lNIg zlV|2-MMM^tmsZwRsTA7!=Em0U&fYeY&SLCyIRNHS_8nEZ=1}3&XKf-qehoRiB&QQ= z_#yAlC<#SJB)IxgXM)sypEE~47IY=aKP=U1yzyIin(`|s=xA~gK2!ZYrLOCyQ(u-& zsK5nQE!aH&L4tw_?o7#Gp&9UAmcnqb;Bkv;yB^$^vgZ{Jh;WVqRB62S)nMkc7|)7H zsK+?YU-OPK3E{atLW)U)kkLUrQTiiwkD2E1)0gc0VAU@hMbFqoYQ4Cz(4BhoIXO0? zcCj~CH->Jk@p+}c=t;hvKfK;$wA{Ws-b4z@TOWsBxEX-=ZFo;=@;$=4j`Si=)_+hm z>y>WYUg!hf&rwGC?5qw|Tle+)BfhRrBO-XpXts;Uak<>*IRU7BI&Hi=P*+!nvrpe1 zol3~TaJc~P)!aZKVaw?t;VY5TAxG4}xuM5(<o^ykbI*Au?3_j9Ot_?H`b>oMS;G*Y zOZUuYqb?UY=S9m`F29UXt&yLLx!GYj7pJypITx?K1%8#F$CWQgcp&^@K2cwi*?i=( z()i_MQ~je0DP|Ve3V%GcD|wS@^KxY&)!y^yV*1O}^NSg;^WHjUx>c?eWqLr*F9AP} zJY359wD`6-+l#v5loPadZ8^tDxhhB~{36fCIWJb30?tcRlU&J9vM^dHOm=iBDN1*z zye-cCs6Z*s4>zKe6ct%fO2L)5($b0!(baOuh(cLK9l6xC5;;p*t;Enp*Q#31O08A5 zU37J;!G}bL{p?c~U#}xrC|1-D*_FM65?`*aL#I5CQDL*GQq;!zJXd%4TIFgboCcNJ zKx~b~1Rs_l0#(o~diMrqUw94D1UTvD(JXShoYs8oDxTIVW<aF1i9b>L&@N$LUfnM3 zgx@@T)&@|6{WZbuW0!n#`Bs-=F@CFCxs$ksR~=OP)T2ICUfZL!MBMJxIdWo$pnLB6 z=YEL?iI9PZD(jyI&2L`c9eiTIGua)saI)PUvHVo=Wz-??I&}1Pge`r{u{e%C;nYw8 zoxEruzDGQVwY@{~T#DNxf8DESnDXOY_nHn9bZ1OQoQr47M&HS1%*EcPdd;Vpxie>u ztBU`z2z2dbE@elMnajD!%04ULoJ#mgc`=zqsq7TnUxSP)Bi8GtDt)QQIqE(Yb5ri# zV(T$8_GZUbd-fLgCd6->psRw~9x|}!?2I@iaOk67A*elKph^H^KJ-18v6y48!&-xE zpjosAS>FEEs2Pv5y)?xY&e-k?;sMrj0K7X*fK#*}VPl6w=OXol6{>>8{c?pa7&ePo zc!ZqA%L(7{Y8G?h5kjTexgvM;1WqLq!hX~GDf$F^?+o1In~K}7$DQO`B|B*0x7vQ5 z_=Ih}z%q}}Tl-r)P`>Trsm;jy#(BRa%O4Um%#AYpeDO@Ne7l1FX7tmxynoeV+pqil z72~k>QUW92aU*Fn_LXt|`QgZp+wkf*kJtIqQ|g`S1Do-l_*WP9A`NsX)d@lDd^wIG zR<E>Q(Ck5h(3vjm1C5X06WlIezIv_8NPjCSrLEw%n_XQdK97@g*Ivu(Uc)_3+Tt5q z;k9Q`xM!88jvCScN^j10znG{=g%uPkzi;fe+1g4&A9qxVIBNV-v^E{f2iv$V{A;yO z8NIQ5uw~+1jA}D^XRGu?Y8p-6ecT3;k2~GzJZkzex)!voRjkp|)%zLxDT^9=Rcnda z>w_E2-a!`YY<HRYu|DPS)~@OuQ6Pi}@8k;o3I5|Wj_^(6=}+-@&f1?6O`@%u@+2z2 z`YQ1L*pEB;GAG^|7>GVjimoe=*Df(K!40H9KNl*;T{CuK4P@emif*AwOh1VSi_nIO zwK*l`p+--0CBJ|Tt}8rFD;+AjvRh*Ot@KI0(Qt{?vr@|n7Yj(~aOKC{GV2p%&s$wW zvQuvhIGL2a7>%}omeyBz_?Ov`j7DK2Un)I&6z%qWM$wdEh##lSLGY1flcZ-=#C1N{ zDhh~3=2j<s;VHeosyL3*r`M#km%q8$%@<pX+T3+FXI;gK|0=d{=s#j>uQ@q3K!-wm z-o5)8V@;+&8=5%$8y^v;i#!F-i~&CW0P@AQp^@;W@^g+RY5DOUe5A+yOGAt;)ujP3 z&8hT4JtEUmzake8#MYO5Ahy!efWxzz_RN7jG`;7&|6Ytem(qX%a3MjQN7I0FUQNQL zsv+n6riB&9nvVxmg<UfKTg2L{<*bGBnOpdo6K-Rz7r0gY=lD__=*7pz-=m+G&z>!2 zv@3;I$GkG0J3ndJp<ekR{(bqJ%r>J_HC{zD%=op~@p0^*IyFhWWc=&Pn%-Sz?l)43 zl@=5Xn7F4MH9u<c3)g*L@Wu9HI!0;nMlus`New!pmFuVmY3%U`s0ERXm()kSdp&z^ zX7Bke>8y_T`EhG=1x=jwq<jbw%D3{)RV?dYW%b8~*X3U_S$Sw}HIQEUxk#mA#q<+v z5GYn(e1h{$JS=ypSdA~X6%>nN)^K@vec3CM)fbaiBe2RZmGAjtyUiL!h(RIWPOSYc zg&4zLfz~8cti8Dk947=o>#|JNouB*uFR(SxJaP?t2-bdq`T>X_E?;SY_o1ktYxhai z_^*hi6YqVx5o89s0lnU{;kV~I#T8R;I>O~`gd9UoU#SW{dJ66F?IwFhEWxYws@R9< zN61+T46NgN?`G^*zd4zccd-WC&7>IO{AJxn+>_pqDG0v>m54^Xwa#|hAbU}*&%5Vq z{HN?y<dPn*vFDmzu&6j``A=Q=KyvTr0!{xFv-cW9#mc*-1{})MPWW(b?{0;!?W)6u z&lpCTUX#pOdu4_gAC9NjBT(xeP6*<Z&fb3!+o~srM9mq#(}z^}QqQG@njvTYmB47% zL~kZKA?Iavnb;@Xt<*j~wI+D?x}vvpdB|mLGZrD4D=M#di1I9lHH<*-Ryq9_u=QX6 zn&3<3t)lmucqp2`8GHI%Afw|zYy;Vgic?H{AbKa?%zxQBkTnX?p*KJ{R42aJUJW?n zNeJFd%V95?>9R%xx%9Fr&RQUbJ!clkm<;6ZL3H`W<1X=d>?1+kb3sDtLBjGuB9=i% zy@Eu?f{rtSj!6WI$p?$82cNYJmM{#K^a|#s2`D8B$cTi<N`zci54j>AB5xUT%_~G< zEJTSBq9_rnEFY?>9;#*;s%{vn;T5Wx9;(F%)sYC(Ru9uN47=tbAQu^CkRE0X3o`|W znU972#Rz+Bc>uOg(!W`Nzgfb**^Pa3z<ztl`1VR7{I%Bs*uH`B&6XFg<{<c%7XCpb z;-h86XRnA)=@Fjb2rt+_VvCIkVMK&VM22}qhNnk<gGEMSBcsP66Zu@b9GM~!m1245 zUjTu0a8$<shuDtAps_I+_1G56*k-TTw#Wmt#lvEI!LbCx*atyTgO-Pez2b(_<3_*- zXiFZ8o5se?GUDbW;^)2M7t`YxVDZb?c*<Bjjjy%K@jDU;yXpz_^aKsP!%wRcIM@WP zL?YmBBF{2W;O&9gie@Apk4hA4Ogzaq+q>US@xj)c_x()N_p^MlZTv1%@?DNEwvB=s zRf3n_CjI7}q`(*3s3hgaB$e?b)w{_zUL^nSoqQ|m0ByAzlXXgx^~RI`;G^w}ln34^ z_cK!TGg6cQf=YxGvy~L{yFVVk`0<zbkEc;TEHZxBH2&Yjwj}jG#kMQ;BQy0=myp@p zG^L<4AMZ53j5L2f+MZ7jx|<&OB0c2o0oq1qq{l|3$2X=YjHf5wJ<!_k-We%T8L1f= z8Qua{r-ibQX6Eq0_HJg;wM_7f%o6X+((%l4W@g!WVC6Mn)qe%s3?PyTM4#t@P<KI1 z4?$NQgtDVRof#l}BdE6oL>LG4GeH9{vWC2~1~aloO0vcpv&iFF(_LA!%>NYI#f<EQ z#_Z+BY&n3yC0h38(VVS+#CH21v1K&oaL021xEujij-X_&kWa2~X6|8lt_UtybRt)r zm3xYEAhu_$e*UM}Mjwc+?8Hwwjl7FidB6JPNx_94=m{y{@)RfXR9Sg9D0#Oe^KWb9 zt7Yb^!}IS<<ZDp!wOIN0Bnuvh7U*jf7+4h;!V8Qh3Ldf!%=XV*K@HWyXMD5GEVP6d zKA$MGq7>S&3SUYVy%sHU)F^VYDsqPNid-g&-m;3^M2p=ei$7}=dny!rSrvPU3Mv2u z{V2u$e6f`Thlzs2HNX*82VxsN0ghsUV?|5i`C_Y4lB!UWZdH=ub0D@LRtZqDG+Uw6 zZo0ToRIoU+v<O}b<`2yirB$rb8cJ!MWLdpN88ovD1}|%vD1%eVkbJR~EN>Gn@6agk zv?|9&mm5zXHYSw!QOXG#6$4fk{XP{#(G??^6-0Oixs)%q{|;=oC_<(L0eUoKFB8Is zL%5|7z$D}l-)ya`gnX+6fmOm~RU(KgvB@g&?y6J!Ri~wR)n|OG&jPFeg{YQ5RO>qk z-KSOmdaUN+KVo|cSaYSUMxHOWlQpW{H8=KaZb}`9ts1cQHlkL&yH;beR*%oMtF;fL z>K<qcTv8P>EUSBns570cGw-f@ykGZ3s{W~Oy#=uT8KT~@yWVQD-eJG~<!b$FDd-zb zs3Q>SjDR{#LR}`IPlD<`XbOC?Zusci@HwWz6WHK~Xb3242%2mN=A*4OEX)@c3WSB1 z!6Ff`_(@n|H!Nu%mMnDuwtrO#JxXi@mi=$A&E<owH5}{<FLi~N1K|}2c%>AgN)rLG zM%1_>>U<G!AOabKKqGkk68O_xsO1r|)f$QQMdDnMcp$O|f$Wt+5j0VK)~JF15L+OM z7=t1sP*anrX-)L3HG0MuJ@1QF&=XXsLa%qDsZto4CT7DLv*n7}@x`!!7<LSXi@@+E zF#xS50h^{n|A?(HsOdPeNvym{oY?dWyXllo^BKS9)1c<F<;@bvW*K6$9KQJ?yZH!F zz=R-hCAQ@%s6`3cqFmmhN^JR^-EzaG^_E}jO;D>^`GMK$5L@-|t@qfi_odq&__gVS z+Wthg8RFZnPYcK&Z#R=}f2!5a2U|;<_7{HbR>XE2cDuE7hn-S~!#`l_Y|QI$@#}B} zb-1xR+@(9;X?1#Nb>3GMQugTdj_vdzcKWe9eWkGhO4uMRY`6_J(ij`<hm8SYW7*hv z>8?1fu0-RmB%7{uzphNTE)b{-q;>eov_P&B?xz;6$OZ>C#+CZv%0ResHm*{-yF#nG z%DB6RFSdT&aJOzGs2hbm5Zfj#JjUigY@dlA{wopRU5>}I55%@dtA}8GAhu(EJriy{ zL{JY2*~8b`X|3KVo8DQs-g&;(f_m3td#T7?YOJ7QmB5y9-!>m?{rXsLeQZ!42ieD! zCIGYvJR5?*JA#luK{ShSJdPlSBAg@<#I<?-r)>Lw@$Wwq*MByvUmDdfQ_(L+>i?C~ zf6;c}vj4!Ptbr@*gj?o<w+I6&>jSFVgEwpkfA=4}6*s7sHK>Ie)TtQMBMtt+8N6pZ z^uT}Ue%6qF#gHLt$doi>-ZS)=GxV4wbekY#5jXst&$XywyNY24((vD$VLsQs_8)nb zHR4z?;>_n-(uhaT2wz-3T^RlBKkAt^`UN%Wg&KYBA^4g$8hqkFY{UG=LbJxgE5;)E zU`rZH>={eqj3r+<0Nd28@gJ!1^q%odKG<@`^VY`;_+Yy(pzI+Cu9zr6O;nO5s(L1B zI1{yev-O{ZW=+<kCSg6i1GH`8Ot!2~wp}2$^U*eoh(!@QNkklp_{{vUia2TTI%&wC zG?GOcMv=yPNE0N|G>0_1PMW_!UeG2lW|5as<RucBvQA#(khd;O?VOmRYfsUy3*A!{ zWc5t#lcoUNDFNz~kj(U9ooV6hX%Y1F5%RPsb^17W`jpJf8L^qOIy2|&W+c!DTzj57 zBh8(Wqt5(lH+wl?_EPrjmC6IORVL4>_RijLpLGrrR5zcy<36X3p3@-D-Q~_{iOuWC z%s<eX*H@l5w3~kzFmFVjH|EYil36fSUNF~Lu&`UOG+nR?Sg=wSbN~q3i7ndmwN_`* zQJJ^sY`6F};DBr0xQnhbOYX``9(=B~Tk<kp@(Eb-&0g~3E(ORe`|B+8(e}2v@NJLf zsQBe*@^UP9IYwqBUU?-^XC>8cCEau-GhhYCM_cYnwhSdphmvc0fVSWON~t@goR78v zf|4GBRm!W?I;&8-Rha22JYW^UpP0F;Xqh#X&RUb{S__|R1J-cvYkYC-L9g}7tP^zB z`}ksOw{Dwz$To3(vT~isT_?*>NjlVN(*v_z4xm!pscYHPb-vlkY|wNzHtaUG+&6ak zV4J<cj^E&-H@I8@6%SznGn$}1O(c*e`kr<?hjs!(6O-j_itBEk<csb5%`<_Ul6<jE z*p$I+%1&*{>2C4G_Se9z%L!X5<^lqJTZ(+K<!#;A*t#Xl_ttH-oNaZ?_MNG1jg14e zy(hc#;N*_J?v4Q;Z81AWQ#%iNJI1`7o2r5as=Lq3b}e#tEit>#r*^G2c5QgOFJ<Yk zPtqNA=}z`^XAIqCivE^IcRRV~F1z<xch6I0&x?<?IYKw&1pPMl{CRsJvW&2kjBs5> zggqk?!-$?@MDZB0Cz<iG%oJT_stPmRo|zHI%$#C^cub%yD_fSON7yS=5iHJO74gBg zk5xLws^YO~HduAC`}Mj9U<<?W_8X@5;T!u%zSzpL+fK4OblIKu2V$$2dq|SN?%QA! zbU6d|oc=)0Py%NpheO10$Pmu-6laFV;dAYLAa^c@y9nVf^SO44OY7rq@wi)%Lpsy! zz4tr@hQ~T7BB>+!-x1ri-*kQ}EibF6g49&jRoB--8)1kB6daAjv^2Lhb+mT2W4m!Z zU43}M`SQWvhDV3SM<&K5$)suG?9|-M{L<pe!s_xGWu3OMMcvul-KNtG49@R!*jxbt zX$B-$FI4EHf-zS7XY03<Qujde)i2v(&hUP>!ec+=8^p<Jh8QPZ7JQien^~!G*Qvtp zA1c-@plh{`c;KxM6Zo!A#b#N0zQUYqb@KiB_md?}M1`=)MP^xiv8{I*DtlUO_3y-1 z@p<&w>z<qBp6;&@dsNWAFjo<#I@20@TFN5Ut9q^@@%*Czt-JqIY`s67l-1FR5WWt- zwKACh1ZWZGQ@=V=^`bjKTT^X)68dSB90zQmlA8jL?U`tKeNeLWk$ax)*SNh%$aoZ} zqph*KI#T<5Dn1KAr%q#H_DpsBn461(`SyWm6l?qZ%h!>*_WudCy#J2a#{ZrZe%@nw zCS1-_Vm9Jps^M(ZrGG27+K#{LpS^eL?(6S{G9?AyjjxQ)C;dOfHk;XY`tS1bD?s;- zqf4OoBiD+vK9ftFvi)XPma+rsN0)O$&Pp!-47=#^zra?s<jmu+KS~OJ*p<F50==ZH z6y<n|@+mh}l2THT=i*XYUP&n}t%6Ffmeq_Htya`7y0}(q@8mv!U|9bXY<d47w#uvJ zH3T)O_1Xc8N9*;2j;<BZG56JX4bvYLsSUFVzuZ5*nCJU{itVEf)b^q)1kIqXzQ+Kz z6=@j3BgV95;j1>Z7Ll9f9<AcKO4Y4r3~V;r&N{_zcAWcKUftyAR{baTQjGCd*Og+M zEu4I9`NwWmj8bj)txlV5yxLOicJH0N^4dNvp3-N+1HpGY1byqD|1M&CPL3b3O?yWl zcP_4=Pq;O#(<ePTukR5*kG|U@c`a4!k^T18_ojl5C^M$R&bc#YBCl35W@GPA8FPsS z%FOv^DbL-R|B<l`Xr1|Y8QXJ!h;`UMV!J{0-Du(|`_bA?zW3Y2%0m3M@G2Ysh^_z5 MkQu~J4xsrz0I+fH?f?J) literal 0 HcmV?d00001 diff --git a/www/saml2/idp/SSOService.php b/www/saml2/idp/SSOService.php new file mode 100644 index 000000000..4d744c4fa --- /dev/null +++ b/www/saml2/idp/SSOService.php @@ -0,0 +1,140 @@ +<?php + + +require_once('../../../www/_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/XML/SAML20/AuthnResponse.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); +require_once('SimpleSAML/XHTML/Template.php'); + + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + +$idpentityid = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted'); +$idpmeta = $metadata->getMetaDataCurrent('saml20-idp-hosted'); + +$requestid = null; +$session = null; + + +if (isset($_GET['SAMLRequest'])) { + + + try { + $binding = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + $authnrequest = $binding->decodeRequest($_GET); + + $session = $authnrequest->createSession(); + + $requestid = $authnrequest->getRequestID(); + + + + $session->setAuthnRequest($requestid, $authnrequest); + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['header'] = 'Error getting incomming request'; + $et->data['message'] = 'Something bad happened when simpleSAML got the incomming authentication request'; + $et->data['e'] = $exception; + + $et->show(); + + } + +} elseif(isset($_GET['RequestID'])) { + + try { + + $requestid = $_GET['RequestID']; + $session = SimpleSAML_Session::getInstance(); + $authnrequest = $session->getAuthnRequest($requestid); + + if (!$authnrequest) throw new Exception('Could not retrieve cached RequestID = ' . $requestid); + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['header'] = 'Error retrieving authnrequest cache'; + $et->data['message'] = 'simpleSAML cannot find the authnrequest that it earlier stored.'; + $et->data['e'] = $exception; + + $et->show(); + + } + + + /* + $authnrequest = new SimpleSAML_XML_SAML20_AuthnRequest($config, $metadata); + $authnrequest->setXML($authnrequestXML); + */ + + + +} else { + + echo 'You must either provide a SAML Request message or a RequestID on this interface.'; + exit(0); + +} + + + + +if (!$session->isAuthenticated() ) { + + $relaystate = SimpleSAML_Utilities::selfURLNoQuery() . '?RelayState=' . urlencode($_GET['RelayState']) . + '&RequestID=' . urlencode($requestid); + $authurl = SimpleSAML_Utilities::addURLparameter('/' . $config->getValue('baseurlpath') . $idpmeta['auth'], + 'RelayState=' . urlencode($relaystate)); + header('Location: ' . $authurl); + exit(0); +} else { + + try { + + $session->add_sp_session($authnrequest->getIssuer()); + + $ar = new SimpleSAML_XML_SAML20_AuthnResponse($config, $metadata); + $authnResponseXML = $ar->generate($idpentityid, $authnrequest->getIssuer(), + $requestid, null, $session->getAttributes()); + + #echo $authnResponseXML; + #print_r($session); + + //sendResponse($response, $idpentityid, $spentityid, $relayState = null) { + $httppost = new SimpleSAML_Bindings_SAML20_HTTPPost($config, $metadata); + + //echo 'Relaystate[' . $authnrequest->getRelayState() . ']'; + + $httppost->sendResponse($authnResponseXML, + $idpentityid, $authnrequest->getIssuer(), $authnrequest->getRelayState()); + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['header'] = 'Error sending response to service'; + $et->data['message'] = 'Some error occured when trying to issue the authentication response, and send it back to the SP.'; + $et->data['e'] = $exception; + + $et->show(); + + } + +} + + +?> \ No newline at end of file diff --git a/www/saml2/idp/SingleLogoutService.php b/www/saml2/idp/SingleLogoutService.php new file mode 100644 index 000000000..39ba55d17 --- /dev/null +++ b/www/saml2/idp/SingleLogoutService.php @@ -0,0 +1,146 @@ +<?php + + +require_once('../../../www/_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/LogoutRequest.php'); +require_once('SimpleSAML/XML/SAML20/LogoutResponse.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +//require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); +require_once('SimpleSAML/XHTML/Template.php'); + + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + +$idpentityid = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted'); + +$session = SimpleSAML_Session::getInstance(); + +$session->dump_sp_sessions(); + +/* + * If we get an LogoutRequest then we initiate the logout process. + */ +if (isset($_GET['SAMLRequest'])) { + + $binding = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + $logoutrequest = $binding->decodeLogoutRequest($_GET); + + $session->setAuthenticated(false); + + //$requestid = $authnrequest->getRequestID(); + //$session->setAuthnRequest($requestid, $authnrequest); + + //echo '<pre>' . htmlentities($logoutrequest->getXML()) . '</pre>'; + + error_log('IdP LogoutService: got Logoutrequest from ' . $logoutrequest->getIssuer() . ' '); + + $session->set_sp_logout_completed($logoutrequest->getIssuer() ); + $session->setLogoutRequest($logoutrequest); + +/* + * We receive a Logout Response to a Logout Request that we have issued earlier. + */ +} elseif (isset($_GET['SAMLResponse'])) { + + $binding = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + $loginresponse = $binding->decodeLogoutResponse($_GET); + + $session->set_sp_logout_completed($loginresponse->getIssuer()); + + error_log('IdP LogoutService: got LogoutResponse from ' . $loginresponse->getIssuer() . ' '); +} + +/* + * We proceed to send logout requests to all remaining SPs. + */ +$spentityid = $session->get_next_sp_logout(); +if ($spentityid) { + + error_log('IdP LogoutService: next SP ' . $spentityid); + + try { + $lr = new SimpleSAML_XML_SAML20_LogoutRequest($config, $metadata); + + // ($issuer, $receiver, $nameid, $nameidformat, $sessionindex, $mode) { + $req = $lr->generate($idpentityid, $spentityid, $session->getNameID(), $session->getNameIDFormat(), $session->getSessionIndex(), 'IdP'); + + $httpredirect = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + + $relayState = SimpleSAML_Utilities::selfURL(); + if (isset($_GET['RelayState'])) { + $relayState = $_GET['RelayState']; + } + + //$request, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnUrl', $direction = 'SAMLRequest', $mode = 'SP' + $httpredirect->sendMessage($req, $spentityid, $relayState, 'SingleLogOutUrl', 'SAMLRequest', 'IdP'); + + exit(); + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['header'] = 'Error sending logout request to service'; + $et->data['message'] = 'Some error occured when trying to issue the logout response, and send it to the SP.'; + $et->data['e'] = $exception; + + $et->show(); + exit(0); + } + + +} + +/* + * Logout procedure is done and we send a Logout Response back to the SP + */ +error_log('IdP LogoutService: SPs done '); +try { + + $logoutrequest = $session->getLogoutRequest(); + if (!$logoutrequest) { + throw new Exception('Could not get reference to the logout request.'); + } + + $rg = new SimpleSAML_XML_SAML20_LogoutResponse($config, $metadata); + + // generate($issuer, $receiver, $inresponseto, $mode ) + + $logoutResponseXML = $rg->generate($idpentityid, $logoutrequest->getIssuer(), $logoutrequest->getRequestID(), 'IdP'); + + // echo '<pre>' . htmlentities($logoutResponseXML) . '</pre>'; + // exit(); + + $httpredirect = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + + $relayState = SimpleSAML_Utilities::selfURL(); + if (isset($_GET['RelayState'])) { + $relayState = $_GET['RelayState']; + } + + //$request, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnUrl', $direction = 'SAMLRequest', $mode = 'SP' + $httpredirect->sendMessage($logoutResponseXML, $logoutrequest->getIssuer(), $relayState, 'SingleLogOutUrl', 'SAMLResponse', 'IdP'); + +} catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['header'] = 'Error sending response to service'; + $et->data['message'] = 'Some error occured when trying to issue the logout response, and send it to the SP.'; + $et->data['e'] = $exception; + + $et->show(); + +} + + + +?> \ No newline at end of file diff --git a/www/saml2/sp/AssertionConsumerService.php b/www/saml2/sp/AssertionConsumerService.php new file mode 100644 index 000000000..2c2d99066 --- /dev/null +++ b/www/saml2/sp/AssertionConsumerService.php @@ -0,0 +1,52 @@ +<?php + +require_once('../../_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +session_start(); + +try { + + $config = SimpleSAML_Configuration::getInstance(); + $metadata = new SimpleSAML_XML_MetaDataStore($config); + + $binding = new SimpleSAML_Bindings_SAML20_HTTPPost($config, $metadata); + $authnResponse = $binding->decodeResponse($_POST); + + $authnResponse->validate(); + + $session = $authnResponse->createSession(); + if (isset($session)) { + + $relayState = $authnResponse->getRelayState(); + if (isset($relayState)) { + header("Location: " . $relayState); + exit(0); + } else { + echo 'Could not find RelayState parameter, you are stucked here.'; + } + } else { + throw new Exception('Unkown error. Could not get session.'); + } + +} catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['header'] = 'Error receiving response from IdP'; + $et->data['message'] = 'Some error occured when trying to issue the authentication request to the IdP.'; + $et->data['e'] = $exception; + + $et->show(); + +} + + +?> \ No newline at end of file diff --git a/www/saml2/sp/SingleLogoutService.php b/www/saml2/sp/SingleLogoutService.php new file mode 100644 index 000000000..c31ebd546 --- /dev/null +++ b/www/saml2/sp/SingleLogoutService.php @@ -0,0 +1,78 @@ +<?php + +require_once('../../_include.php'); + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/LogoutRequest.php'); +require_once('SimpleSAML/XML/SAML20/LogoutResponse.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + + + + + +// Get the local session +$session = SimpleSAML_Session::getInstance(); + + +// Destroy local session if exists. +if (isset($session) && $session->isAuthenticated() ) { + $session->setAuthenticated(false); +} + + + + +if (isset($_GET['SAMLRequest'])) { + + // Create a HTTPRedirect binding + $binding = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + + // Decode the LogoutRequest using the HTTP Redirect binding. + $logoutrequest = $binding->decodeLogoutRequest($_GET); + + // Extract some parameters from the logout request + $requestid = $logoutrequest->getRequestID(); + $requester = $logoutrequest->getIssuer(); + $relayState = $logoutrequest->getRelayState(); + + + //$responder = $config->getValue('saml2-hosted-sp'); + $responder = $metadata->getMetaDataCurrentEntityID(); + + + + // Create a logout response + $lr = new SimpleSAML_XML_SAML20_LogoutResponse($config, $metadata); + $logoutResponseXML = $lr->generate($responder, $requester, $requestid, 'SP'); + + + // Create a HTTP Redirect binding. + $httpredirect = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + + // Send the Logout response using HTTP POST binding. + $httpredirect->sendMessage($logoutResponseXML, $requester, $logoutrequest->getRelayState(), 'SingleLogOutUrl', 'SAMLResponse'); + +} elseif(isset($_GET['SAMLResponse'])) { + + + if (isset($_GET['RelayState'])) { + header('Location: ' . $_GET['RelayState']); + } else { + + echo 'You are now successfully logged out.'; + + } + +} + + + +?> \ No newline at end of file diff --git a/www/saml2/sp/initSLO.php b/www/saml2/sp/initSLO.php new file mode 100644 index 000000000..e3aae4e17 --- /dev/null +++ b/www/saml2/sp/initSLO.php @@ -0,0 +1,66 @@ +<?php + +require_once('../../_include.php'); + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/LogoutRequest.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +//require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); + + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + +$session = SimpleSAML_Session::getInstance(); + +$idpentityid = isset($_GET['idpentityid']) ? $_GET['idpentityid'] : $config->getValue('default-saml20-idp') ; +$spentityid = isset($_GET['spentityid']) ? $_GET['spentityid'] : $metadata->getMetaDataCurrentEntityID(); + + +if (isset($session) ) { + + try { + $lr = new SimpleSAML_XML_SAML20_LogoutRequest($config, $metadata); + + // ($issuer, $receiver, $nameid, $nameidformat, $sessionindex, $mode) { + $req = $lr->generate($spentityid, $idpentityid, $session->getNameID(), $session->getNameIDFormat(), $session->getSessionIndex(), 'SP'); + + $httpredirect = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + + $relayState = SimpleSAML_Utilities::selfURL(); + if (isset($_GET['RelayState'])) { + $relayState = $_GET['RelayState']; + } + + //$request, $remoteentityid, $relayState = null, $endpoint = 'SingleSignOnUrl', $direction = 'SAMLRequest', $mode = 'SP' + $httpredirect->sendMessage($req, $idpentityid, $relayState, 'SingleLogOutUrl', 'SAMLRequest', 'SP'); + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->$data['message'] = 'Some error occured when trying to issue the logout request to the IdP.'; + $et->$data['e'] = $exception; + + $et->show(); + + } + +} else { + + + $relaystate = $session->getRelayState(); + + header('Location: ' . $relaystate ); + + #print_r($metadata->getMetaData('sam.feide.no')); + #print_r($req); + +} + + +?> \ No newline at end of file diff --git a/www/saml2/sp/initSSO.php b/www/saml2/sp/initSSO.php new file mode 100644 index 000000000..a5d5a403d --- /dev/null +++ b/www/saml2/sp/initSSO.php @@ -0,0 +1,97 @@ +<?php + +require_once('../../_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XHTML/Template.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/SAML20/AuthnRequest.php'); +//require_once('SimpleSAML/XML/SAML20/AuthnResponse.php'); +require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +//require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + + +$session = SimpleSAML_Session::getInstance(); + +try { + + $idpentityid = isset($_GET['idpentityid']) ? $_GET['idpentityid'] : $config->getValue('default-saml20-idp') ; + $spentityid = isset($_GET['spentityid']) ? $_GET['spentityid'] : $metadata->getMetaDataCurrentEntityID(); + +} catch (Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + $et->data['message'] = 'Error loading SAML 2.0 metadata'; + $et->data['e'] = $exception; + $et->show(); + exit(0); +} + +if (!isset($session) || !$session->isValid() ) { + + try { + $sr = new SimpleSAML_XML_SAML20_AuthnRequest($config, $metadata); + + $req = $sr->generate($spentityid); + + + + $httpredirect = new SimpleSAML_Bindings_SAML20_HTTPRedirect($config, $metadata); + + $relayState = SimpleSAML_Utilities::selfURL(); + if (isset($_GET['RelayState'])) { + $relayState = $_GET['RelayState']; + } + + $httpredirect->sendMessage($req, $idpentityid, $relayState); + + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['message'] = 'Some error occured when trying to issue the authentication request to the IdP.'; + $et->data['e'] = $exception; + + $et->show(); + + } + +} else { + + + + $relaystate = $session->getRelayState(); + + if (isset($relaystate) && !empty($relaystate)) { + header('Location: ' . $relaystate ); + } else { + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['message'] = 'Could not get relay state, do not know where to send the user.'; + $et->data['e'] = new Exception(); + + $et->show(); + + + } + +} + + + + +#print_r($metadata->getMetaData('sam.feide.no')); +#print_r($req); + +//echo 'Location: ' . $relaystate; + + +?> diff --git a/www/shib13/sp/AssertionConsumerService.php b/www/shib13/sp/AssertionConsumerService.php new file mode 100644 index 000000000..20daca2af --- /dev/null +++ b/www/shib13/sp/AssertionConsumerService.php @@ -0,0 +1,69 @@ +<?php + +require_once('../../_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/Shib13/AuthnRequest.php'); +require_once('SimpleSAML/Bindings/Shib13/HTTPPost.php'); +require_once('SimpleSAML/XHTML/Template.php'); + +session_start(); + +try { + + /* + echo '<pre>'; + print_r($_POST); + echo '</pre>'; + */ + $config = SimpleSAML_Configuration::getInstance(); + $metadata = new SimpleSAML_XML_MetaDataStore($config); + + #print_r($metadata->getMetaData('sam.feide.no')); +# $sr = new SimpleSAML_XML_Shib13_AuthnResponse($config, $metadata); + + $binding = new SimpleSAML_Bindings_Shib13_HTTPPost($config, $metadata); + $authnResponse = $binding->decodeResponse($_POST); + + $xml = $authnResponse->getXML(); + /* + echo '<pre>'; + echo $xml; + echo '</pre>'; +*/ + + $authnResponse->validate(); + $session = $authnResponse->createSession(); + + + + if (isset($session)) { + $relayState = $authnResponse->getRelayState(); + if (isset($relayState)) { + header("Location: " . $relayState); + exit(0); + } else { + echo 'Could not find RelayState parameter, you are stucked here.'; + } + } else { + throw new Exception('Unkown error. Could not get session.'); + } + + +} catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['message'] = 'Some error occured when trying to issue the authentication request to the IdP.'; + $et->data['e'] = $exception; + + $et->show(); + + +} + + +?> \ No newline at end of file diff --git a/www/shib13/sp/initSSO.php b/www/shib13/sp/initSSO.php new file mode 100644 index 000000000..ccb3a799d --- /dev/null +++ b/www/shib13/sp/initSSO.php @@ -0,0 +1,99 @@ +<?php + +require_once('../../_include.php'); + + +require_once('SimpleSAML/Utilities.php'); +require_once('SimpleSAML/Session.php'); +require_once('SimpleSAML/XHTML/Template.php'); +require_once('SimpleSAML/XML/MetaDataStore.php'); +require_once('SimpleSAML/XML/Shib13/AuthnRequest.php'); +//require_once('SimpleSAML/XML/SAML20/AuthnResponse.php'); +//require_once('SimpleSAML/Bindings/SAML20/HTTPRedirect.php'); +//require_once('SimpleSAML/Bindings/SAML20/HTTPPost.php'); + +session_start(); + +$config = SimpleSAML_Configuration::getInstance(); +$metadata = new SimpleSAML_XML_MetaDataStore($config); + + +$session = SimpleSAML_Session::getInstance(); + + +/* + * Incomming URL parameters + * + * idpentityid The entityid of the wanted IdP to authenticate with. If not provided will use default. + * spentityid The entityid of the SP config to use. If not provided will use default to host. + * + */ + +try { + + $idpentityid = isset($_GET['idpentityid']) ? $_GET['idpentityid'] : $config->getValue('default-shib13-idp') ; + $spentityid = isset($_GET['spentityid']) ? $_GET['spentityid'] : $metadata->getMetaDataCurrentEntityID('shib13-sp-hosted'); + +} catch (Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + $et->data['message'] = 'Error loading SAML 2.0 metadata'; + $et->data['e'] = $exception; + $et->show(); + exit(0); +} + +if (!isset($session) || !$session->isValid() ) { + + try { + $ar = new SimpleSAML_XML_Shib13_AuthnRequest($config, $metadata); + $ar->setIssuer($spentityid); + if(isset($_GET['RelayState'])) + $ar->setRelayState($_GET['RelayState']); + + $url = $ar->createRedirect($idpentityid); + header('Location: ' . $url); +// echo 'IdP: ' . $idpentityid . ' SP: ' . $spentityid; + + exit(0); + + } catch(Exception $exception) { + + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['message'] = 'Some error occured when trying to issue the authentication request to the IdP.'; + $et->data['e'] = $exception; + + $et->show(); + + } + +} else { + + + + $relaystate = $session->getRelayState(); + + if (isset($relaystate) && !empty($relaystate)) { + header('Location: ' . $relaystate ); + } else { + $et = new SimpleSAML_XHTML_Template($config, 'error.php'); + + $et->data['message'] = 'Could not get relay state, do not know where to send the user.'; + $et->data['e'] = new Exception(); + + $et->show(); + + + } + +} + + +#print_r($metadata->getMetaData('sam.feide.no')); +#print_r($req); + +//echo 'Location: ' . $relaystate; + + +?> -- GitLab