simplesamlphp-1.15.3/0000755000000000000000000000000013245225041013153 5ustar rootrootsimplesamlphp-1.15.3/metadata-templates/0000755000000000000000000000000013245225037016734 5ustar rootrootsimplesamlphp-1.15.3/metadata-templates/adfs-idp-hosted.php0000644000000000000000000000045013245225037022417 0ustar rootroot '__DEFAULT__', 'privatekey' => 'server.pem', 'certificate' => 'server.crt', 'auth' => 'example-userpass', 'authproc' => array( // Convert LDAP names to WS-Fed Claims. 100 => array('class' => 'core:AttributeMap', 'name2claim'), ), ); simplesamlphp-1.15.3/metadata-templates/saml20-idp-hosted.php0000644000000000000000000000450613245225037022606 0ustar rootroot '__DEFAULT__', // X.509 key and certificate. Relative to the cert directory. 'privatekey' => 'server.pem', 'certificate' => 'server.crt', /* * Authentication source to use. Must be one that is configured in * 'config/authsources.php'. */ 'auth' => 'example-userpass', /* * WARNING: SHA-1 is disallowed starting January the 1st, 2014. * * Uncomment the following option to start using SHA-256 for your signatures. * Currently, SimpleSAMLphp defaults to SHA-1, which has been deprecated since * 2011, and will be disallowed by NIST as of 2014. Please refer to the following * document for more information: * * http://csrc.nist.gov/publications/nistpubs/800-131A/sp800-131A.pdf * * If you are uncertain about service providers supporting SHA-256 or other * algorithms of the SHA-2 family, you can configure it individually in the * SP-remote metadata set for those that support it. Once you are certain that * all your configured SPs support SHA-2, you can safely remove the configuration * options in the SP-remote metadata set and uncomment the following option. * * Please refer to the IdP hosted reference for more information. */ //'signature.algorithm' => 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256', /* Uncomment the following to use the uri NameFormat on attributes. */ /* 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'authproc' => array( // Convert LDAP names to oids. 100 => array('class' => 'core:AttributeMap', 'name2oid'), ), */ /* * Uncomment the following to specify the registration information in the * exported metadata. Refer to: * http://docs.oasis-open.org/security/saml/Post2.0/saml-metadata-rpi/v1.0/cs01/saml-metadata-rpi-v1.0-cs01.html * for more information. */ /* 'RegistrationInfo' => array( 'authority' => 'urn:mace:example.org', 'instant' => '2008-01-17T11:28:03Z', 'policies' => array( 'en' => 'http://example.org/policy', 'es' => 'http://example.org/politica', ), ), */ ); simplesamlphp-1.15.3/metadata-templates/shib13-idp-hosted.php0000644000000000000000000000115413245225037022575 0ustar rootroot '__DEFAULT__', // X.509 key and certificate. Relative to the cert directory. 'privatekey' => 'server.pem', 'certificate' => 'server.crt', /* * Authentication source to use. Must be one that is configured in * 'config/authsources.php'. */ 'auth' => 'example-userpass', ); simplesamlphp-1.15.3/metadata-templates/wsfed-idp-remote.php0000644000000000000000000000034113245225037022616 0ustar rootroot 'https://localhost:9031/idp/prp.wsf', 'certificate' => 'pingfed-localhost.pem', ); simplesamlphp-1.15.3/metadata-templates/shib13-sp-remote.php0000644000000000000000000000064213245225037022451 0ustar rootroot 'http://sp.shiblab.feide.no/Shibboleth.sso/SAML/POST', 'audience' => 'urn:mace:feide:shiblab', 'base64attributes' => FALSE, ); simplesamlphp-1.15.3/metadata-templates/adfs-sp-remote.php0000644000000000000000000000041413245225037022272 0ustar rootroot 'https://localhost/adfs/ls/', 'simplesaml.nameidattribute' => 'uid', 'authproc' => array( 50 => array( 'class' => 'core:AttributeLimit', 'cn', 'mail', 'uid', 'eduPersonAffiliation', ), ), ); simplesamlphp-1.15.3/metadata-templates/shib13-idp-remote.php0000644000000000000000000000060113245225037022576 0ustar rootroot 'https://idp.example.org/shibboleth-idp/SSO', 'certificate' => 'example.pem', ); */ simplesamlphp-1.15.3/metadata-templates/saml20-sp-remote.php0000644000000000000000000000225113245225037022454 0ustar rootroot 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', 'SingleLogoutService' => 'https://saml2sp.example.org/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', ); /* * This example shows an example config that works with G Suite (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 G Suite. 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'. */ $metadata['google.com'] = array( 'AssertionConsumerService' => 'https://www.google.com/a/g.feide.no/acs', 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', 'simplesaml.nameidattribute' => 'uid', 'simplesaml.attributes' => FALSE, ); simplesamlphp-1.15.3/metadata-templates/shib13-sp-hosted.php0000644000000000000000000000035013245225037022440 0ustar rootroot '__DEFAULT__', ); simplesamlphp-1.15.3/metadata-templates/saml20-idp-remote.php0000644000000000000000000000033013245225037022602 0ustar rootroot '__DEFAULT__', ); simplesamlphp-1.15.3/LICENSE0000644000000000000000000005764713245225037014210 0ustar rootroot GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS simplesamlphp-1.15.3/tests/0000755000000000000000000000000013245225037014322 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/0000755000000000000000000000000013245225037015772 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/core/0000755000000000000000000000000013245225037016722 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/core/lib/0000755000000000000000000000000013245225037017470 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/core/lib/Auth/0000755000000000000000000000000013245225037020371 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/0000755000000000000000000000000013245225037022007 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/ScopeFromAttributeTest.php0000644000000000000000000000736613245225037027155 0ustar rootrootprocess($request); return $request; } /* * Test the most basic functionality. */ public function testBasic() { $config = array( 'sourceAttribute' => 'eduPersonPrincipalName', 'targetAttribute' => 'scope', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('scope', $attributes); $this->assertEquals($attributes['scope'], array('example.com')); } /* * If scope already set, module must not overwrite. */ public function testNoOverwrite() { $config = array( 'sourceAttribute' => 'eduPersonPrincipalName', 'targetAttribute' => 'scope', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), 'scope' => array('example.edu') ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['scope'], array('example.edu')); } /* * If source attribute not set, nothing happens */ public function testNoSourceAttribute() { $config = array( 'sourceAttribute' => 'eduPersonPrincipalName', 'targetAttribute' => 'scope', ); $request = array( 'Attributes' => array( 'mail' => array('j.doe@example.edu', 'john@example.org'), 'scope' => array('example.edu') ) ); $result = self::processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); } /* * When multiple @ signs in attribute, should use last one. */ public function testMultiAt() { $config = array( 'sourceAttribute' => 'eduPersonPrincipalName', 'targetAttribute' => 'scope', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('john@doe@example.com'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['scope'], array('example.com')); } /* * When the source attribute doesn't have a scope, a warning is emitted * NOTE: currently disabled: this triggers a warning and a warning * wants to start a session which we cannot do in phpunit. How to fix? */ public function testNoAt() { $config = array( 'sourceAttribute' => 'eduPersonPrincipalName', 'targetAttribute' => 'scope', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('johndoe'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('scope', $attributes); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/TargetedIDTest.php0000644000000000000000000001602113245225037025334 0ustar rootrootprocess($request); return $request; } // /** // * Test the most basic functionality // */ // public function testBasic() // { // $config = array( // ); // $request = array( // 'Attributes' => array(), // 'UserID' => 'user2@example.org', // ); // $result = self::processFilter($config, $request); // $attributes = $result['Attributes']; // $this->assertArrayHasKey('eduPersonTargetedID', $attributes); // $this->assertRegExp('/^[0-9a-f]{40}$/', $attributes['eduPersonTargetedID'][0]); // } // // /** // * Test with src and dst entityIds. // * Make sure to overwrite any present eduPersonTargetedId // */ // public function testWithSrcDst() // { // $config = array( // ); // $request = array( // 'Attributes' => array( // 'eduPersonTargetedID' => 'dummy', // ), // 'UserID' => 'user2@example.org', // 'Source' => array( // 'metadata-set' => 'saml20-idp-hosted', // 'entityid' => 'urn:example:src:id', // ), // 'Destination' => array( // 'metadata-set' => 'saml20-sp-remote', // 'entityid' => 'joe', // ), // ); // $result = self::processFilter($config, $request); // $attributes = $result['Attributes']; // $this->assertArrayHasKey('eduPersonTargetedID', $attributes); // $this->assertRegExp('/^[0-9a-f]{40}$/', $attributes['eduPersonTargetedID'][0]); // } // // /** // * Test with nameId config option set. // */ // public function testNameIdGeneration() // { // $config = array( // 'nameId' => true, // ); // $request = array( // 'Attributes' => array( // ), // 'UserID' => 'user2@example.org', // 'Source' => array( // 'metadata-set' => 'saml20-idp-hosted', // 'entityid' => 'urn:example:src:id', // ), // 'Destination' => array( // 'metadata-set' => 'saml20-sp-remote', // 'entityid' => 'joe', // ), // ); // $result = self::processFilter($config, $request); // $attributes = $result['Attributes']; // $this->assertArrayHasKey('eduPersonTargetedID', $attributes); // $this->assertRegExp('#^[0-9a-f]{40}$#', $attributes['eduPersonTargetedID'][0]); // } // // /** // * Test that Id is the same for subsequent invocations with same input. // */ // public function testIdIsPersistent() // { // $config = array( // ); // $request = array( // 'Attributes' => array( // 'eduPersonTargetedID' => 'dummy', // ), // 'UserID' => 'user2@example.org', // 'Source' => array( // 'metadata-set' => 'saml20-idp-hosted', // 'entityid' => 'urn:example:src:id', // ), // 'Destination' => array( // 'metadata-set' => 'saml20-sp-remote', // 'entityid' => 'joe', // ), // ); // for ($i = 0; $i < 10; ++$i) { // $result = self::processFilter($config, $request); // $attributes = $result['Attributes']; // $tid = $attributes['eduPersonTargetedID'][0]; // if (isset($prevtid)) { // $this->assertEquals($prevtid, $tid); // $prevtid = $tid; // } // } // } // // /** // * Test that Id is different for two different usernames and two different sp's // */ // public function testIdIsUnique() // { // $config = array( // ); // $request = array( // 'Attributes' => array( // ), // 'UserID' => 'user2@example.org', // 'Source' => array( // 'metadata-set' => 'saml20-idp-hosted', // 'entityid' => 'urn:example:src:id', // ), // 'Destination' => array( // 'metadata-set' => 'saml20-sp-remote', // 'entityid' => 'joe', // ), // ); // $result = self::processFilter($config, $request); // $tid1 = $result['Attributes']['eduPersonTargetedID'][0]; // // $request['UserID'] = 'user3@example.org'; // $result = self::processFilter($config, $request); // $tid2 = $result['Attributes']['eduPersonTargetedID'][0]; // // $this->assertNotEquals($tid1, $tid2); // // $request['Destination']['entityid'] = 'urn:example.org:another-sp'; // $result = self::processFilter($config, $request); // $tid3 = $result['Attributes']['eduPersonTargetedID'][0]; // // $this->assertNotEquals($tid2, $tid3); // } /** * Test no userid set * * @expectedException Exception */ public function testNoUserID() { $config = array( ); $request = array( 'Attributes' => array(), ); $result = self::processFilter($config, $request); } /** * Test with specified attribute not set * * @expectedException Exception */ public function testAttributeNotExists() { $config = array( 'attributename' => 'uid', ); $request = array( 'Attributes' => array( 'displayName' => 'Jack Student', ), ); $result = self::processFilter($config, $request); } /** * Test with configuration error 1 * * @expectedException Exception */ public function testConfigInvalidAttributeName() { $config = array( 'attributename' => 5, ); $request = array( 'Attributes' => array( 'displayName' => 'Jack Student', ), ); $result = self::processFilter($config, $request); } /** * Test with configuration error 2 * * @expectedException Exception */ public function testConfigInvalidNameId() { $config = array( 'nameId' => 'persistent', ); $request = array( 'Attributes' => array( 'displayName' => 'Jack Student', ), ); $result = self::processFilter($config, $request); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/AttributeValueMapTest.php0000644000000000000000000001654513245225037026771 0ustar rootrootprocess($request); return $request; } /** * Test the most basic functionality. * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process */ public function testBasic() { $config = array( 'sourceattribute' => 'memberOf', 'targetattribute' => 'eduPersonAffiliation', 'values' => array( 'member' => array( 'theGroup', 'otherGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('memberOf', $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertEquals($attributes['eduPersonAffiliation'], array('member')); } /** * Test basic functionality, remove duplicates * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process */ public function testNoDuplicates() { $config = array( 'sourceattribute' => 'memberOf', 'targetattribute' => 'eduPersonAffiliation', 'values' => array( 'member' => array( 'theGroup', 'otherGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup', 'otherGroup'), 'eduPersonAffiliation' => array('member', 'someValue'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('memberOf', $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertEquals($attributes['eduPersonAffiliation'], array('member', 'someValue')); } /** * Test the %replace functionality. * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process */ public function testReplace() { $config = array( 'sourceattribute' => 'memberOf', 'targetattribute' => 'eduPersonAffiliation', '%replace', 'values' => array( 'member' => array( 'theGroup', 'otherGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup'), 'eduPersonAffiliation' => array('someValue'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('memberOf', $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertEquals($attributes['eduPersonAffiliation'], array('member')); } /** * Test the %keep functionality. * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process */ public function testKeep() { $config = array( 'sourceattribute' => 'memberOf', 'targetattribute' => 'eduPersonAffiliation', '%keep', 'values' => array( 'member' => array( 'theGroup', 'otherGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup'), 'eduPersonAffiliation' => array('someValue'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('memberOf', $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertEquals($attributes['eduPersonAffiliation'], array('someValue','member')); } /** * Test unknown flag Exception * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process */ public function testUnknownFlag() { $config = array( '%test', 'targetattribute' => 'affiliation', 'sourceattribute' => 'memberOf', 'values' => array( 'member' => array( 'theGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup'), ), ); $result = self::processFilter($config, $request); $this->assertArrayHasKey('affiliation', $result['Attributes']); $this->assertArrayNotHasKey('memberOf', $result['Attributes']); $this->assertContains('member', $result['Attributes']['affiliation']); } /** * Test missing Source attribute * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process * * @expectedException \Exception */ public function testMissingSourceAttribute() { $config = array( 'targetattribute' => 'affiliation', 'values' => array( 'member' => array( 'theGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup'), ), ); self::processFilter($config, $request); } /** * Test missing Target attribute * * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::__construct * @covers SimpleSAML\Module\core\Auth\Process\AttributeValueMap::process * * @expectedException \Exception */ public function testMissingTargetAttribute() { $config = array( 'sourceattribute' => 'memberOf', 'values' => array( 'member' => array( 'theGroup', ), ), ); $request = array( 'Attributes' => array( 'memberOf' => array('theGroup'), ), ); self::processFilter($config, $request); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/ScopeAttributeTest.php0000644000000000000000000001750113245225037026321 0ustar rootrootprocess($request); return $request; } /* * Test the most basic functionality. */ public function testBasic() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), 'eduPersonAffiliation' => array('member'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('eduPersonScopedAffiliation', $attributes); $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@example.com')); } /* * If target attribute already set, module must add, not overwrite. */ public function testNoOverwrite() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), 'eduPersonAffiliation' => array('member'), 'eduPersonScopedAffiliation' => array('library-walk-in@example.edu'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('library-walk-in@example.edu', 'member@example.com')); } /* * If same scope already set, module must do nothing, not duplicate value. */ public function testNoDuplication() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), 'eduPersonAffiliation' => array('member'), 'eduPersonScopedAffiliation' => array('member@example.com'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@example.com')); } /* * If source attribute not set, nothing happens */ public function testNoSourceAttribute() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'mail' => array('j.doe@example.edu', 'john@example.org'), 'eduPersonAffiliation' => array('member'), 'eduPersonScopedAffiliation' => array('library-walk-in@example.edu'), ) ); $result = self::processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); } /* * If scope attribute not set, nothing happens */ public function testNoScopeAttribute() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'mail' => array('j.doe@example.edu', 'john@example.org'), 'eduPersonScopedAffiliation' => array('library-walk-in@example.edu'), 'eduPersonPrincipalName' => array('jdoe@example.com'), ) ); $result = self::processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); } /* * When multiple @ signs in attribute, will use the first one. */ public function testMultiAt() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('john@doe@example.com'), 'eduPersonAffiliation' => array('member'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@doe@example.com')); } /* * When multiple values in source attribute, should render multiple targets. */ public function testMultivaluedSource() { $config = array( 'scopeAttribute' => 'eduPersonPrincipalName', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), 'eduPersonAffiliation' => array('member','staff','faculty'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('member@example.com','staff@example.com','faculty@example.com')); } /* * When the source attribute doesn't have a scope, the entire value is used. */ public function testNoAt() { $config = array( 'scopeAttribute' => 'schacHomeOrganization', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', ); $request = array( 'Attributes' => array( 'schacHomeOrganization' => array('example.org'), 'eduPersonAffiliation' => array('student'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('student@example.org')); } /* * When the target attribute exists and onlyIfEmpty is set */ public function testOnlyIfEmpty() { $config = array( 'scopeAttribute' => 'schacHomeOrganization', 'sourceAttribute' => 'eduPersonAffiliation', 'targetAttribute' => 'eduPersonScopedAffiliation', 'onlyIfEmpty' => true, ); $request = array( 'Attributes' => array( 'schacHomeOrganization' => array('example.org'), 'eduPersonAffiliation' => array('student'), 'eduPersonScopedAffiliation' => array('staff@example.org', 'member@example.org'), ) ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['eduPersonScopedAffiliation'], array('staff@example.org', 'member@example.org')); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/AttributeCopyTest.php0000644000000000000000000001136313245225037026162 0ustar rootrootprocess($request); return $request; } /** * Test the most basic functionality. */ public function testBasic() { $config = array( 'test' => 'testnew', ); $request = array( 'Attributes' => array('test' => array('AAP')), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test', $attributes); $this->assertArrayHasKey('testnew', $attributes); $this->assertEquals($attributes['testnew'], array('AAP')); } /** * Test the most basic functionality. */ public function testArray() { $config = array( 'test' => array('new1','new2'), ); $request = array( 'Attributes' => array('test' => array('AAP')), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test', $attributes); $this->assertArrayHasKey('new1', $attributes); $this->assertArrayHasKey('new2', $attributes); $this->assertEquals($attributes['new1'], array('AAP')); $this->assertEquals($attributes['new2'], array('AAP')); } /** * Test that existing attributes are left unmodified. */ public function testExistingNotModified() { $config = array( 'test' => 'testnew', ); $request = array( 'Attributes' => array( 'test' => array('AAP'), 'original1' => array('original_value1'), 'original2' => array('original_value2'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('testnew', $attributes); $this->assertEquals($attributes['test'], array('AAP')); $this->assertArrayHasKey('original1', $attributes); $this->assertEquals($attributes['original1'], array('original_value1')); $this->assertArrayHasKey('original2', $attributes); $this->assertEquals($attributes['original2'], array('original_value2')); } /** * Test copying multiple attributes */ public function testCopyMultiple() { $config = array( 'test1' => 'new1', 'test2' => 'new2', ); $request = array( 'Attributes' => array('test1' => array('val1'), 'test2' => array('val2.1','val2.2')), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('new1', $attributes); $this->assertEquals($attributes['new1'], array('val1')); $this->assertArrayHasKey('new2', $attributes); $this->assertEquals($attributes['new2'], array('val2.1','val2.2')); } /** * Test behaviour when target attribute exists (should be replaced). */ public function testCopyClash() { $config = array( 'test' => 'new1', ); $request = array( 'Attributes' => array( 'test' => array('testvalue1'), 'new1' => array('newvalue1'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['new1'], array('testvalue1')); } /** * Test wrong attribute name * * @expectedException Exception */ public function testWrongAttributeName() { $config = array( array('value2'), ); $request = array( 'Attributes' => array( 'test' => array('value1'), ), ); $result = self::processFilter($config, $request); } /** * Test wrong attribute value * * @expectedException Exception */ public function testWrongAttributeValue() { $config = array( 'test' => 100, ); $request = array( 'Attributes' => array( 'test' => array('value1'), ), ); $result = self::processFilter($config, $request); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/AttributeAddTest.php0000644000000000000000000001171113245225037025735 0ustar rootrootprocess($request); return $request; } /** * Test the most basic functionality. */ public function testBasic() { $config = array( 'test' => array('value1', 'value2'), ); $request = array( 'Attributes' => array(), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test', $attributes); $this->assertEquals($attributes['test'], array('value1', 'value2')); } /** * Test that existing attributes are left unmodified. */ public function testExistingNotModified() { $config = array( 'test' => array('value1', 'value2'), ); $request = array( 'Attributes' => array( 'original1' => array('original_value1'), 'original2' => array('original_value2'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test', $attributes); $this->assertEquals($attributes['test'], array('value1', 'value2')); $this->assertArrayHasKey('original1', $attributes); $this->assertEquals($attributes['original1'], array('original_value1')); $this->assertArrayHasKey('original2', $attributes); $this->assertEquals($attributes['original2'], array('original_value2')); } /** * Test single string as attribute value. */ public function testStringValue() { $config = array( 'test' => 'value', ); $request = array( 'Attributes' => array(), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test', $attributes); $this->assertEquals($attributes['test'], array('value')); } /** * Test adding multiple attributes in one config. */ public function testAddMultiple() { $config = array( 'test1' => array('value1'), 'test2' => array('value2'), ); $request = array( 'Attributes' => array(), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test1', $attributes); $this->assertEquals($attributes['test1'], array('value1')); $this->assertArrayHasKey('test2', $attributes); $this->assertEquals($attributes['test2'], array('value2')); } /** * Test behavior when appending attribute values. */ public function testAppend() { $config = array( 'test' => array('value2'), ); $request = array( 'Attributes' => array( 'test' => array('value1'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['test'], array('value1', 'value2')); } /** * Test replacing attribute values. */ public function testReplace() { $config = array( '%replace', 'test' => array('value2'), ); $request = array( 'Attributes' => array( 'test' => array('value1'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['test'], array('value2')); } /** * Test wrong usage generates exceptions * * @expectedException Exception */ public function testWrongFlag() { $config = array( '%nonsense', 'test' => array('value2'), ); $request = array( 'Attributes' => array( 'test' => array('value1'), ), ); $result = self::processFilter($config, $request); } /** * Test wrong attribute value * * @expectedException Exception */ public function testWrongAttributeValue() { $config = array( '%replace', 'test' => array(true), ); $request = array( 'Attributes' => array( 'test' => array('value1'), ), ); $result = self::processFilter($config, $request); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/PHPTest.php0000644000000000000000000000251513245225037024012 0ustar rootrootprocess($request); return $request; } /** * Test the configuration of the filter. * * @expectedException SimpleSAML_Error_Exception */ public function testInvalidConfiguration() { $config = array(); new sspmod_core_Auth_Process_PHP($config, null); } /** * Check that defining the code works as expected. */ public function testCodeDefined() { $config = array( 'code' => ' $attributes["key"] = "value"; ', ); $request = array('Attributes' => array()); $expected = array( 'Attributes' => array( 'key' => 'value', ), ); $this->assertEquals($expected, $this->processFilter($config, $request)); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/AttributeRealmTest.php0000644000000000000000000000747713245225037026323 0ustar rootrootprocess($request); return $request; } /** * Test the most basic functionality. */ public function testBasic() { $config = array( ); $request = array( 'Attributes' => array(), 'UserID' => 'user2@example.org', ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('realm', $attributes); $this->assertEquals($attributes['realm'], array('example.org')); } /** * Test no userid set * * @expectedException Exception */ public function testNoUserID() { $config = array( ); $request = array( 'Attributes' => array(), ); $result = self::processFilter($config, $request); } /** * Test with configuration. */ public function testAttributeNameConfig() { $config = array( 'attributename' => 'schacHomeOrganization', ); $request = array( 'Attributes' => array( 'displayName' => 'Joe User', 'schacGender' => 9, ), 'UserID' => 'user2@example.org', ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('schacHomeOrganization', $attributes); $this->assertArrayHasKey('displayName', $attributes); $this->assertEquals($attributes['schacHomeOrganization'], array('example.org')); } /** * When target attribute exists it will be overwritten */ public function testTargetAttributeOverwritten() { $config = array( 'attributename' => 'schacHomeOrganization', ); $request = array( 'Attributes' => array( 'displayName' => 'Joe User', 'schacGender' => 9, 'schacHomeOrganization' => 'example.com', ), 'UserID' => 'user2@example.org', ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('schacHomeOrganization', $attributes); $this->assertEquals($attributes['schacHomeOrganization'], array('example.org')); } /** * When source attribute has no "@" no realm is added */ public function testNoAtisNoOp() { $config = array(); $request = array( 'Attributes' => array( 'displayName' => 'Joe User', ), 'UserID' => 'user2', ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('realm', $attributes); } /** * When source attribute has more than one "@" no realm is added */ public function testMultiAtisNoOp() { $config = array(); $request = array( 'Attributes' => array( 'displayName' => 'Joe User', ), 'UserID' => 'user2@home@example.org', ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('realm', $attributes); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/AttributeAlterTest.php0000644000000000000000000002316113245225037026316 0ustar rootrootprocess($request); return $request; } /** * Test the most basic functionality. */ public function testBasic() { $config = array( 'subject' => 'test', 'pattern' => '/wrong/', 'replacement' => 'right', ); $request = array( 'Attributes' => array( 'test' => array('somethingiswrong'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test', $attributes); $this->assertEquals($attributes['test'], array('somethingisright')); } /** * Test the most basic functionality. */ public function testWithTarget() { $config = array( 'subject' => 'test', 'target' => 'test2', 'pattern' => '/wrong/', 'replacement' => 'right', ); $request = array( 'Attributes' => array( 'something' => array('somethingelse'), 'test' => array('wrong'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('test2', $attributes); $this->assertEquals($attributes['test'], array('wrong')); $this->assertEquals($attributes['test2'], array('right')); } /** * Module is a no op if subject attribute is not present. */ public function testNomatch() { $config = array( 'subject' => 'test', 'pattern' => '/wrong/', 'replacement' => 'right', ); $request = array( 'Attributes' => array( 'something' => array('somevalue'), 'somethingelse' => array('someothervalue'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes, array('something' => array('somevalue'), 'somethingelse' => array('someothervalue'))); } /** * Test replacing attribute value. */ public function testReplaceMatch() { $config = array( 'subject' => 'source', 'pattern' => '/wrong/', 'replacement' => 'right', '%replace', ); $request = array( 'Attributes' => array( 'source' => array('wrongthing'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['source'], array('right')); } /** * Test replacing attribute value. */ public function testReplaceMatchWithTarget() { $config = array( 'subject' => 'source', 'pattern' => '/wrong/', 'replacement' => 'right', 'target' => 'test', '%replace', ); $request = array( 'Attributes' => array( 'source' => array('wrong'), 'test' => array('wrong'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['test'], array('right')); } /** * Test replacing attribute values. */ public function testReplaceNoMatch() { $config = array( 'subject' => 'test', 'pattern' => '/doink/', 'replacement' => 'wrong', 'target' => 'test', '%replace', ); $request = array( 'Attributes' => array( 'source' => array('wrong'), 'test' => array('right'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['test'], array('right')); } /** * Test removing attribute values. * Note that removing a value does not renumber the attributes array. * Also ensure unrelated attributes are not touched. */ public function testRemoveMatch() { $config = array( 'subject' => 'eduPersonAffiliation', 'pattern' => '/^emper/', '%remove', ); $request = array( 'Attributes' => array( 'displayName' => array('emperor kuzco'), 'eduPersonAffiliation' => array('member', 'emperor', 'staff'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertEquals($attributes['displayName'], array('emperor kuzco')); $this->assertEquals($attributes['eduPersonAffiliation'], array(0 => 'member', 2 => 'staff')); } /** * Test removing attribute values, resulting in an empty attribute. */ public function testRemoveMatchAll() { $config = array( 'subject' => 'eduPersonAffiliation', 'pattern' => '/^emper/', '%remove', ); $request = array( 'Attributes' => array( 'displayName' => array('emperor kuzco'), 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testWrongConfig() { $config = array( 'subject' => 'eduPersonAffiliation', 'pattern' => '/^emper/', '%dwiw', ); $request = array( 'Attributes' => array( 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testIncompleteConfig() { $config = array( 'subject' => 'eduPersonAffiliation', ); $request = array( 'Attributes' => array( 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testIncompleteConfig2() { $config = array( 'subject' => 'test', 'pattern' => '/wrong/', ); $request = array( 'Attributes' => array( 'test' => array('somethingiswrong'), ), ); $request = array( 'Attributes' => array( 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testIncompleteConfig3() { $config = array( 'subject' => 'test', 'pattern' => '/wrong/', '%replace', '%remove', ); $request = array( 'Attributes' => array( 'test' => array('somethingiswrong'), ), ); $request = array( 'Attributes' => array( 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testIncompleteConfig4() { $config = array( 'subject' => 'test', 'pattern' => '/wrong/', 'target' => 'test2', '%remove', ); $request = array( 'Attributes' => array( 'test' => array('somethingiswrong'), ), ); $request = array( 'Attributes' => array( 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testIncompleteConfig5() { $config = array( 'subject' => 'test', 'pattern' => '/wrong/', 'replacement' => null, ); $request = array( 'Attributes' => array( 'test' => array('somethingiswrong'), ), ); $request = array( 'Attributes' => array( 'eduPersonAffiliation' => array('emperess', 'emperor'), ), ); $result = self::processFilter($config, $request); } } simplesamlphp-1.15.3/tests/modules/core/lib/Auth/Process/AttributeLimitTest.php0000644000000000000000000002365213245225037026332 0ustar rootrootprocess($request); return $request; } /** * Test reading IdP Attributes. */ public function testIdPAttrs() { $config = array( 'cn', 'mail' ); $request = array( 'Attributes' => array( 'eduPersonTargetedID' => array('eptid@example.org'), 'eduPersonAffiliation' => array('member'), 'cn' => array('user name'), 'mail' => array('user@example.org'), ), 'Destination' => array( ), 'Source' => array( 'attributes' => array('cn','mail'), ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); $this->assertArrayNotHasKey('eduPersonTargetedID', $attributes); $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); $this->assertCount(2, $attributes); $config = array( 'cn', 'default' => TRUE, ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); $this->assertArrayNotHasKey('eduPersonTargetedID', $attributes); $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); $this->assertCount(2, $attributes); } /** * Tests when no attributes are in metadata. */ public function testNULLMetadataAttrs() { $config = array( 'cn', 'mail' ); $request = array( 'Attributes' => array( 'eduPersonTargetedID' => array('eptid@example.org'), 'eduPersonAffiliation' => array('member'), 'cn' => array('user name'), 'mail' => array('user@example.org'), ), 'Destination' => array( ), 'Source' => array( ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); $this->assertArrayNotHasKey('eduPersonTargetedID', $attributes); $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); $this->assertCount(2, $attributes); $config = array( 'cn', 'default' => TRUE, ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertArrayHasKey('cn', $attributes); $this->assertArrayNotHasKey('mail', $attributes); $this->assertArrayNotHasKey('eduPersonTargetedID', $attributes); $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); $this->assertCount(1, $attributes); $config = array( ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertCount(4, $attributes); $this->assertArrayHasKey('eduPersonTargetedID', $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); } /** * setUpBeforeClass a request that will be used for the following tests. * note the above tests don't use self::$request for processFilter input. */ protected static $request; public static function setUpBeforeClass() { self::$request = array( 'Attributes' => array( 'eduPersonTargetedID' => array('eptid@example.org'), 'eduPersonAffiliation' => array('member'), 'cn' => array('common name'), 'mail' => array('user@example.org'), ), 'Destination' => array( 'attributes' => array('cn','mail'), ), 'Source' => array( ), ); } /** * Test the most basic functionality. */ public function testBasic() { $config = array( 'cn', 'mail' ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); $this->assertCount(2, $attributes); } /** * Test defaults with metadata available. */ public function testDefaultWithMetadata() { $config = array( 'default' => TRUE, ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); $this->assertCount(2, $attributes); } /** * Test defaults with attributes and metadata */ public function testDefaultWithAttrs() { $config = array( 'default' => TRUE, 'eduPersonTargetedID', 'eduPersonAffiliation', ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertCount(2, $attributes); $this->assertArrayHasKey('cn', $attributes); $this->assertArrayHasKey('mail', $attributes); $this->assertArrayNotHasKey('eduPersonTargetedID', $attributes); $this->assertArrayNotHasKey('eduPersonAffiliation', $attributes); } /** * Test for exception with illegal config. * * @expectedException Exception */ public function testInvalidConfig() { $config = array( 'invalidArg' => TRUE, ); $result = self::processFilter($config, self::$request); } /** * Test for invalid attribute name * * @expectedException Exception */ public function testInvalidAttributeName() { $config = array( null ); $result = self::processFilter($config, self::$request); } /** * Test for attribute value matching */ public function testMatchAttributeValues() { $config = array( 'eduPersonAffiliation' => array('member') ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertCount(1, $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertEquals($attributes['eduPersonAffiliation'], array('member')); $config = array( 'eduPersonAffiliation' => array('member','staff') ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertCount(1, $attributes); $this->assertArrayHasKey('eduPersonAffiliation', $attributes); $this->assertEquals($attributes['eduPersonAffiliation'], array('member')); $config = array( 'eduPersonAffiliation' => array('student') ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertCount(0, $attributes); $config = array( 'eduPersonAffiliation' => array('student','staff') ); $result = self::processFilter($config, self::$request); $attributes = $result['Attributes']; $this->assertCount(0, $attributes); } /** * Test for allowed attributes not an array. * * This test is very unlikely and would require malformed metadata processing. * Cannot be generated via config options. * * @expectedException Exception */ public function testMatchAttributeValuesNotArray() { $config = array( ); $request = array( 'Attributes' => array( 'eduPersonTargetedID' => array('eptid@example.org'), 'eduPersonAffiliation' => array('member'), 'cn' => array('user name'), 'mail' => array('user@example.org'), 'discardme' => array('somethingiswrong'), ), 'Destination' => array( 'attributes' => array('eduPersonAffiliation' => 'student'), ), 'Source' => array( ), ); $result = self::processFilter($config, $request); } /** * Test attributes not intersecting */ public function testNoIntersection() { $config = array( 'default' => TRUE, ); $request = array( 'Attributes' => array( 'eduPersonTargetedID' => array('eptid@example.org'), 'eduPersonAffiliation' => array('member'), 'cn' => array('user name'), 'mail' => array('user@example.org'), 'discardme' => array('somethingiswrong'), ), 'Destination' => array( 'attributes' => array('urn:oid:1.2.840.113549.1.9.1'), ), 'Source' => array( ), ); $result = self::processFilter($config, $request); $attributes = $result['Attributes']; $this->assertCount(0, $attributes); $this->assertEmpty($attributes); } } simplesamlphp-1.15.3/tests/modules/consent/0000755000000000000000000000000013245225037017443 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/consent/lib/0000755000000000000000000000000013245225037020211 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/consent/lib/Auth/0000755000000000000000000000000013245225037021112 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/consent/lib/Auth/Process/0000755000000000000000000000000013245225037022530 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/consent/lib/Auth/Process/ConsentTest.php0000644000000000000000000001155613245225037025522 0ustar rootroot * @package SimpleSAMLphp */ namespace SimpleSAML\Test\Module\consent\Auth\Process; use \SimpleSAML_Configuration as Configuration; class ConsentTest extends \PHPUnit_Framework_TestCase { public function setUp() { $this->config = Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); } /** * Helper function to run the filter with a given configuration. * * @param array $config The filter configuration. * @param array $request The request state. * @return array The state array after processing. */ private function processFilter(array $config, array $request) { $filter = new \sspmod_consent_Auth_Process_Consent($config, null); $filter->process($request); return $request; } /** * Test for the private checkDisable() method. */ public function testCheckDisable() { // test consent disable regex with match $config = array(); // test consent disable with match on specific SP entityid $request = array( 'Source' => array( 'entityid' => 'https://idp.example.org', 'metadata-set' => 'saml20-idp-local', 'consent.disable' => array( 'https://valid.flatstring.example.that.does.not.match', ), 'SingleSignOnService' => array( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Location' => 'https://idp.example.org/saml2/idp/SSOService.php', ), ), ), 'Destination' => array( // valid entityid equal to the last one in the consent.disable array 'entityid' => 'https://sp.example.org/my-sp', 'metadata-set' => 'saml20-sp-remote', 'consent.disable' => array( array('type' => 'regex', 'pattern' => '/invalid/i'), 'https://sp.example.org/my-sp', // accept the SP that has this specific entityid 'https://idp.example.org', ), ), 'UserID' => 'jdoe', 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), ), ); $result = $this->processFilter($config, $request); // the state should NOT have changed because NO consent should be necessary (match) $this->assertEquals($request, $result); // test consent disable with match on SP through regular expression $request = array( 'Source' => array( 'entityid' => 'https://idp.example.org', 'metadata-set' => 'saml20-idp-local', 'consent.disable' => array( array(), // invalid consent option array should be ignored 1234, // bad option array(''), // no type array('type'=>'invalid'), // invalid consent option type should be ignored array('type'=>'regex'), // regex consent option without pattern should be ignored array('type'=>'regex', 'pattern'=>'/.*\.valid.regex\.that\.does\.not\.match.*/i'), // accept any SP that has an entityid that contains the string ".example.org" array('type'=>'regex', 'pattern'=>'/.*\.example\.org\/.*/i'), ), 'SingleSignOnService' => array( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Location' => 'https://idp.example.org/saml2/idp/SSOService.php', ), ), ), 'Destination' => array( 'entityid' => 'https://sp.example.org/my-sp', // sp contains the string ".example.org" 'metadata-set' => 'saml20-sp-remote', ), 'UserID' => 'jdoe', 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), ), ); $result = $this->processFilter($config, $request); // the state should NOT have changed because NO consent should be necessary (match) $this->assertEquals($request, $result); // test corner cases $request['Source']['consent.disable'] = array( 'https://valid.flatstring.example.that.does.not.match', array('foo' => 'bar'), ); $request['Destination']['consent.disable'] = 1; $result = $this->processFilter($config, $request); // the state should NOT have changed because NO consent should be necessary (match) $this->assertEquals($request, $result); } } simplesamlphp-1.15.3/tests/modules/saml/0000755000000000000000000000000013245225037016726 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/saml/lib/0000755000000000000000000000000013245225037017474 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/saml/lib/Auth/0000755000000000000000000000000013245225037020375 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/saml/lib/Auth/Process/0000755000000000000000000000000013245225037022013 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/saml/lib/Auth/Process/NameIDAttributeTest.php0000644000000000000000000000774513245225037026362 0ustar rootroot * @package SimpleSAMLphp */ use PHPUnit\Framework\TestCase; class NameIDAttributeTest extends TestCase { /* * Helper function to run the filter with a given configuration. * * @param array $config The filter configuration. * @param array $request The request state. * @return array The state array after processing. */ private function processFilter(array $config, array $request) { $filter = new sspmod_saml_Auth_Process_NameIDAttribute($config, null); $filter->process($request); return $request; } /** * Test minimal configuration. */ public function testMinimalConfig() { $config = array(); $nameId = new \SAML2\XML\saml\NameID(); $nameId->value = 'eugene@oombaas'; $nameId->Format = \SAML2\Constants::NAMEID_PERSISTENT; $spId = 'eugeneSP'; $idpId = 'eugeneIdP'; $request = array( 'Source' => array( 'entityid' => $spId, ), 'Destination' => array( 'entityid' => $idpId, ), 'saml:sp:NameID' => $nameId, ); $result = $this->processFilter($config, $request); $this->assertEquals("{$spId}!{$idpId}!{$nameId->value}", $result['Attributes']['nameid'][0]); } /** * Test custom attribute name. */ public function testCustomAttributeName() { $attributeName = 'eugeneNameIDAttribute'; $config = array('attribute' => $attributeName); $nameId = new \SAML2\XML\saml\NameID(); $nameId->value = 'eugene@oombaas'; $nameId->Format = \SAML2\Constants::NAMEID_PERSISTENT; $spId = 'eugeneSP'; $idpId = 'eugeneIdP'; $request = array( 'Source' => array( 'entityid' => $spId, ), 'Destination' => array( 'entityid' => $idpId, ), 'saml:sp:NameID' => $nameId, ); $result = $this->processFilter($config, $request); $this->assertTrue(isset($result['Attributes'][$attributeName])); $this->assertEquals("{$spId}!{$idpId}!{$nameId->value}", $result['Attributes'][$attributeName][0]); } /** * Test custom format. */ public function testFormat() { $config = array('format' => '%V'); $nameId = new \SAML2\XML\saml\NameID(); $nameId->value = 'eugene@oombaas'; $nameId->Format = \SAML2\Constants::NAMEID_PERSISTENT; $spId = 'eugeneSP'; $idpId = 'eugeneIdP'; $request = array( 'Source' => array( 'entityid' => $spId, ), 'Destination' => array( 'entityid' => $idpId, ), 'saml:sp:NameID' => $nameId, ); $result = $this->processFilter($config, $request); $this->assertEquals("{$nameId->value}", $result['Attributes']['nameid'][0]); } /** * Test custom attribute name with format. */ public function testCustomAttributeNameAndFormat() { $attributeName = 'eugeneNameIDAttribute'; $config = array('attribute' => $attributeName, 'format' => '%V'); $nameId = new \SAML2\XML\saml\NameID(); $nameId->value = 'eugene@oombaas'; $nameId->Format = \SAML2\Constants::NAMEID_PERSISTENT; $spId = 'eugeneSP'; $idpId = 'eugeneIdP'; $request = array( 'Source' => array( 'entityid' => $spId, ), 'Destination' => array( 'entityid' => $idpId, ), 'saml:sp:NameID' => $nameId, ); $result = $this->processFilter($config, $request); $this->assertTrue(isset($result['Attributes'][$attributeName])); $this->assertEquals("{$nameId->value}", $result['Attributes'][$attributeName][0]); } } simplesamlphp-1.15.3/tests/modules/saml/lib/Auth/Process/FilterScopesTest.php0000644000000000000000000001064013245225037025767 0ustar rootroot * @package SimpleSAMLphp */ namespace SimpleSAML\Test\Module\saml\Auth\Process; class FilterScopesTest extends \PHPUnit_Framework_TestCase { /* * Helper function to run the filter with a given configuration. * * @param array $config The filter configuration. * @param array $request The request state. * @return array The state array after processing. */ private function processFilter(array $config, array $request) { $filter = new \SimpleSAML\Module\saml\Auth\Process\FilterScopes($config, null); $filter->process($request); return $request; } /** * Test valid scopes. */ public function testValidScopes() { // test declared scopes $config = array(); $request = array( 'Source' => array( 'SingleSignOnService' => array( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Location' => 'https://example.org/saml2/idp/SSOService.php', ), ), 'scope' => array( 'example.com', 'example.net', ), ), 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.com'), ), ); $result = $this->processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); // test multiple values $request['Attributes'] = array( 'eduPersonPrincipalName' => array( 'jdoe@example.com', 'jdoe@example.net', ), ); $result = $this->processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); // test implicit scope $request['Attributes'] = array( 'eduPersonPrincipalName' => array('jdoe@example.org'), ); $result = $this->processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); // test alternative attributes $config['attributes'] = array( 'mail', ); $request['Attributes'] = array( 'mail' => array('john.doe@example.org'), ); $result = $this->processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); // test non-scoped attributes $request['Attributes']['givenName'] = 'John Doe'; $result = $this->processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); } /** * Test invalid scopes. */ public function testInvalidScopes() { // test scope not matching anything, empty attribute $config = array(); $request = array( 'Source' => array( 'SingleSignOnService' => array( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Location' => 'https://example.org/saml2/idp/SSOService.php', ), ), 'scope' => array( 'example.com', 'example.net', ), ), 'Attributes' => array( 'eduPersonPrincipalName' => array('jdoe@example.edu'), ), ); $result = $this->processFilter($config, $request); $this->assertEquals(array(), $result['Attributes']); // test some scopes allowed and some others not $request['Attributes']['eduPersonPrincipalName'][] = 'jdoe@example.com'; $result = $this->processFilter($config, $request); $this->assertEquals( array( 'eduPersonPrincipalName' => array( 'jdoe@example.com', ), ), $result['Attributes'] ); // test attribute missing scope $request['Attributes'] = array( 'eduPersonPrincipalName' => array('jdoe'), ); $result = $this->processFilter($config, $request); $this->assertEquals($request['Attributes'], $result['Attributes']); } } simplesamlphp-1.15.3/tests/modules/saml/lib/Auth/Source/0000755000000000000000000000000013245225037021635 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/saml/lib/Auth/Source/Auth_Source_SP_Test.php0000644000000000000000000002137113245225037026174 0ustar rootroottestResult = $testResult; } public function getTestResult() { return $this->testResult; } } /** * Wrap the SSP sspmod_saml_Auth_Source_SP class * - Use introspection to make startSSO2Test available * - Override sendSAML2AuthnRequest() to catch the AuthnRequest being sent */ class SP_Tester extends \sspmod_saml_Auth_Source_SP { public function __construct($info, $config) { parent::__construct($info, $config); } public function startSSO2Test(\SimpleSAML_Configuration $idpMetadata, array $state) { $reflector = new \ReflectionObject($this); $method = $reflector->getMethod('startSSO2'); $method->setAccessible(true); $method->invoke($this, $idpMetadata, $state); } // override the method that sends the request to avoid sending anything public function sendSAML2AuthnRequest(array &$state, \SAML2\Binding $binding, \SAML2\AuthnRequest $ar) { // Exit test. Continuing would mean running into a assert(FALSE) throw new ExitTestException( array( 'state' => $state, 'binding' => $binding, 'ar' => $ar, ) ); } } /** * Set of test cases for sspmod_saml_Auth_Source_SP. */ class SP_Test extends \PHPUnit_Framework_TestCase { private $idpMetadata = null; private $idpConfigArray; private function getIdpMetadata() { if (!$this->idpMetadata) { $this->idpMetadata = new \SimpleSAML_Configuration( $this->idpConfigArray, 'Auth_Source_SP_Test::getIdpMetadata()' ); } return $this->idpMetadata; } protected function setUp() { $this->idpConfigArray = array( 'metadata-set' => 'saml20-idp-remote', 'entityid' => 'https://engine.surfconext.nl/authentication/idp/metadata', 'SingleSignOnService' => array( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Location' => 'https://engine.surfconext.nl/authentication/idp/single-sign-on', ), ), 'keys' => array( array( 'encryption' => false, 'signing' => true, 'type' => 'X509Certificate', 'X509Certificate' => 'MIID3zCCAsegAwIBAgIJAMVC9xn1ZfsuMA0GCSqGSIb3DQEBCwUAMIGFMQswCQYDVQQGEwJOTDEQMA4GA1UECAwHVXRyZ'. 'WNodDEQMA4GA1UEBwwHVXRyZWNodDEVMBMGA1UECgwMU1VSRm5ldCBCLlYuMRMwEQYDVQQLDApTVVJGY29uZXh0MSYwJA'. 'YDVQQDDB1lbmdpbmUuc3VyZmNvbmV4dC5ubCAyMDE0MDUwNTAeFw0xNDA1MDUxNDIyMzVaFw0xOTA1MDUxNDIyMzVaMIG'. 'FMQswCQYDVQQGEwJOTDEQMA4GA1UECAwHVXRyZWNodDEQMA4GA1UEBwwHVXRyZWNodDEVMBMGA1UECgwMU1VSRm5ldCBC'. 'LlYuMRMwEQYDVQQLDApTVVJGY29uZXh0MSYwJAYDVQQDDB1lbmdpbmUuc3VyZmNvbmV4dC5ubCAyMDE0MDUwNTCCASIwD'. 'QYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKthMDbB0jKHefPzmRu9t2h7iLP4wAXr42bHpjzTEk6gttHFb4l/hFiz1Y'. 'BI88TjiH6hVjnozo/YHA2c51us+Y7g0XoS7653lbUN/EHzvDMuyis4Xi2Ijf1A/OUQfH1iFUWttIgtWK9+fatXoGUS6ti'. 'rQvrzVh6ZstEp1xbpo1SF6UoVl+fh7tM81qz+Crr/Kroan0UjpZOFTwxPoK6fdLgMAieKSCRmBGpbJHbQ2xxbdykBBrBb'. 'dfzIX4CDepfjE9h/40ldw5jRn3e392jrS6htk23N9BWWrpBT5QCk0kH3h/6F1Dm6TkyG9CDtt73/anuRkvXbeygI4wml9'. 'bL3rE8CAwEAAaNQME4wHQYDVR0OBBYEFD+Ac7akFxaMhBQAjVfvgGfY8hNKMB8GA1UdIwQYMBaAFD+Ac7akFxaMhBQAjV'. 'fvgGfY8hNKMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAC8L9D67CxIhGo5aGVu63WqRHBNOdo/FAGI7LUR'. 'DFeRmG5nRw/VXzJLGJksh4FSkx7aPrxNWF1uFiDZ80EuYQuIv7bDLblK31ZEbdg1R9LgiZCdYSr464I7yXQY9o6FiNtSK'. 'ZkQO8EsscJPPy/Zp4uHAnADWACkOUHiCbcKiUUFu66dX0Wr/v53Gekz487GgVRs8HEeT9MU1reBKRgdENR8PNg4rbQfLc'. '3YQKLWK7yWnn/RenjDpuCiePj8N8/80tGgrNgK/6fzM3zI18sSywnXLswxqDb/J+jgVxnQ6MrsTf1urM8MnfcxG/82oHI'. 'wfMh/sXPCZpo+DTLkhQxctJ3M=', ), ), ); $this->config = Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); } /** * Create a SAML AuthnRequest using sspmod_saml_Auth_Source_SP * * @param array $state The state array to use in the test. This is an array of the parameters described in section * 2 of https://simplesamlphp.org/docs/development/saml:sp * * @return \SAML2\AuthnRequest The AuthnRequest generated. */ private function createAuthnRequest($state = array()) { $info = array('AuthId' => 'default-sp'); $config = array(); $as = new SP_Tester($info, $config); /** @var \SAML2\AuthnRequest $ar */ $ar = null; try { $as->startSSO2Test($this->getIdpMetadata(), $state); $this->assertTrue(false, 'Expected ExitTestException'); } catch (ExitTestException $e) { $r = $e->getTestResult(); $ar = $r['ar']; } return $ar; } /** * Test generating an AuthnRequest * @test */ public function testAuthnRequest() { /** @var \SAML2\AuthnRequest $ar */ $ar = $this->createAuthnRequest(); // Assert values in the generated AuthnRequest /** @var $xml \DOMElement */ $xml = $ar->toSignedXML(); $q = \SAML2\Utils::xpQuery($xml, '/samlp:AuthnRequest/@Destination'); $this->assertEquals( $this->idpConfigArray['SingleSignOnService'][0]['Location'], $q[0]->value ); $q = \SAML2\Utils::xpQuery($xml, '/samlp:AuthnRequest/saml:Issuer'); $this->assertEquals( 'http://localhost/simplesaml/module.php/saml/sp/metadata.php/default-sp', $q[0]->textContent ); } /** * Test setting a Subject * @test * */ public function testNameID() { $state = array( 'saml:NameID' => array('Value' => 'user@example.org', 'Format' => \SAML2\Constants::NAMEID_UNSPECIFIED) ); /** @var \SAML2\AuthnRequest $ar */ $ar = $this->createAuthnRequest($state); $nameID = $ar->getNameId(); $this->assertEquals($state['saml:NameID']['Value'], $nameID->value); $this->assertEquals($state['saml:NameID']['Format'], $nameID->Format); /** @var $xml \DOMElement */ $xml = $ar->toSignedXML(); $q = \SAML2\Utils::xpQuery($xml, '/samlp:AuthnRequest/saml:Subject/saml:NameID/@Format'); $this->assertEquals( $state['saml:NameID']['Format'], $q[0]->value ); $q = \SAML2\Utils::xpQuery($xml, '/samlp:AuthnRequest/saml:Subject/saml:NameID'); $this->assertEquals( $state['saml:NameID']['Value'], $q[0]->textContent ); } /** * Test setting an AuthnConextClassRef * @test * */ public function testAuthnContextClassRef() { $state = array( 'saml:AuthnContextClassRef' => 'http://example.com/myAuthnContextClassRef' ); /** @var \SAML2\AuthnRequest $ar */ $ar = $this->createAuthnRequest($state); $a = $ar->getRequestedAuthnContext(); $this->assertEquals( $state['saml:AuthnContextClassRef'], $a['AuthnContextClassRef'][0] ); /** @var $xml \DOMElement */ $xml = $ar->toSignedXML(); $q = \SAML2\Utils::xpQuery($xml, '/samlp:AuthnRequest/samlp:RequestedAuthnContext/saml:AuthnContextClassRef'); $this->assertEquals( $state['saml:AuthnContextClassRef'], $q[0]->textContent ); } /** * Test setting ForcedAuthn * @test * */ public function testForcedAuthn() { $state = array( 'ForceAuthn' => true ); /** @var \SAML2\AuthnRequest $ar */ $ar = $this->createAuthnRequest($state); $this->assertEquals( $state['ForceAuthn'], $ar->getForceAuthn() ); /** @var $xml \DOMElement */ $xml = $ar->toSignedXML(); $q = \SAML2\Utils::xpQuery($xml, '/samlp:AuthnRequest/@ForceAuthn'); $this->assertEquals( $state['ForceAuthn'] ? 'true' : 'false', $q[0]->value ); } } simplesamlphp-1.15.3/tests/modules/ldap/0000755000000000000000000000000013245225037016712 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/ldap/lib/0000755000000000000000000000000013245225037017460 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/ldap/lib/Auth/0000755000000000000000000000000013245225037020361 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/ldap/lib/Auth/Process/0000755000000000000000000000000013245225037021777 5ustar rootrootsimplesamlphp-1.15.3/tests/modules/ldap/lib/Auth/Process/BaseFilterTest.php0000644000000000000000000000147013245225037025372 0ustar rootrootgetMockBuilder('sspmod_ldap_Auth_Process_BaseFilter') ->disableOriginalConstructor() ->getMockForAbstractClass(); $class = new \ReflectionClass($stub); $method = $class->getMethod('var_export'); $method->setAccessible(true); $this->assertEquals( "array ( 'ldap.hostname' => 'ldap://172.17.101.32', 'ldap.port' => 389, 'ldap.password' => '********', )", $method->invokeArgs($stub, array(array( 'ldap.hostname' => 'ldap://172.17.101.32', 'ldap.port' => 389, 'ldap.password' => 'password', ))) ); } } simplesamlphp-1.15.3/tests/BuiltInServer.php0000644000000000000000000001277013245225037017577 0ustar rootroot * @package SimpleSAMLphp */ namespace SimpleSAML\Test; class BuiltInServer { /** * The PID of the running server. * * @var int */ protected $pid = 0; /** * The address (host:port) where the server is listening for connections after being started. * * @var string */ protected $address; /** * The name of a "router" file to run for every request performed to this server. * * @var string */ protected $router = ''; /** * The document root of the server. * * @var string */ protected $docroot; /** * BuiltInServer constructor. * * @param string|null $router The name of a "router" file to run first for every request performed to this server. * @param string|null $docroot The document root to use when starting the server. * * @see http://php.net/manual/en/features.commandline.webserver.php */ public function __construct($router = null, $docroot = null) { if (!is_null($router)) { $this->setRouter($router); } if (!is_null($docroot)) { $this->docroot = $docroot; } else { $this->docroot = dirname(dirname(__FILE__)).'/www/'; } } /** * Start the built-in server in a random port. * * This method will wait up to 5 seconds for the server to start. When it returns an address, it is guaranteed that * the server has started and is listening for connections. If it returns false on the other hand, there will be no * guarantee that the server started properly. * * @return string The address where the server is listening for connections, or false if the server failed to start * for some reason. * * @todo This method should be resilient to clashes in the randomly-picked port number. */ public function start() { $port = mt_rand(1025, 65535); $this->address = 'localhost:'.$port; $command = sprintf( 'php -S %s -t %s %s >> /dev/null 2>&1 & echo $!', $this->address, $this->docroot, $this->router ); // execute the command and store the process ID $output = array(); exec($command, $output); $this->pid = (int) $output[0]; // wait until it's listening for connections to avoid race conditions $start = microtime(true); while (($sock = @fsockopen('localhost', $port, $errno, $errstr, 10)) === false) { // set a 5 secs timeout waiting for the server to start if (microtime(true) > $start + 5) { $this->pid = false; // signal failure $this->address = false; break; } } if ($sock !== false) { fclose($sock); } return $this->address; } /** * Stop the built-in server. */ public function stop() { if ($this->pid === 0) { return; } exec('kill '.$this->pid); $this->pid = 0; } /** * Get the PID of the running server. * * @return int The PID of the server, or 0 if the server was not started. */ public function getPid() { return $this->pid; } /** * Get the name of the "router" file. * * @return string The name of the "router" file. */ public function getRouter() { return $this->router; } /** * Set the "router" file. * * @param string $router The name of a "router" file to use when starting the server. */ public function setRouter($router) { $file = dirname(dirname(__FILE__)).'/tests/routers/'.$router.'.php'; if (!file_exists($file)) { throw new \InvalidArgumentException('Unknown router "'.$router.'".'); } $this->router = $file; } /** * This function performs an HTTP GET request to the built-in server. * * @param string $query The query to perform. * @param array $parameters An array (can be empty) with parameters for the requested URI. * @param array $curlopts An array (can be empty) with options for cURL. * * @return array|string The response obtained from the built-in server. */ public function get($query, $parameters, $curlopts = array()) { $ch = curl_init(); $url = 'http://'.$this->address.$query; $url .= (!empty($parameters)) ? '?'.http_build_query($parameters) : ''; curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => 1, CURLOPT_HEADER => 1, )); curl_setopt_array($ch, $curlopts); $resp = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); list($header, $body) = explode("\r\n\r\n", $resp, 2); $raw_headers = explode("\r\n", $header); array_shift($raw_headers); $headers = array(); foreach ($raw_headers as $header) { list($name, $value) = explode(':', $header, 2); $headers[trim($name)] = trim($value); } curl_close($ch); return array( 'code' => $code, 'headers' => $headers, 'body' => $body, ); } } simplesamlphp-1.15.3/tests/www/0000755000000000000000000000000013245225037015146 5ustar rootrootsimplesamlphp-1.15.3/tests/www/IndexTest.php0000644000000000000000000000677013245225037017600 0ustar rootroot * @package SimpleSAMLphp */ namespace SimpleSAML\Test\Web; include(dirname(__FILE__).'/../BuiltInServer.php'); use \SimpleSAML\Test\BuiltInServer; class IndexTest extends \PHPUnit_Framework_TestCase { /** * @var \SimpleSAML\Test\BuiltInServer */ protected $server; /** * @var string */ protected $server_addr; /** * @var int */ protected $server_pid; /** * @var string */ protected $shared_file; /** * The setup method that is run before any tests in this class. */ protected function setup() { $this->server = new BuiltInServer('configLoader'); $this->server_addr = $this->server->start(); $this->server_pid = $this->server->getPid(); $this->shared_file = sys_get_temp_dir().'/'.$this->server_pid.'.lock'; @unlink($this->shared_file); // remove it if it exists } protected function updateConfig($config) { @unlink($this->shared_file); $config = "shared_file, $config); } /** * A simple test to make sure the index.php file redirects appropriately to the right URL. */ public function testRedirection() { if (defined('HHVM_VERSION')) { // can't test this in HHVM for the moment $this->markTestSkipped('The web-based tests cannot be run in HHVM for the moment.'); } if (version_compare(phpversion(), '5.4') === -1) { // no built-in server prior to 5.4 $this->markTestSkipped('The web-based tests cannot be run in PHP versions older than 5.4.'); } // test most basic redirection $this->updateConfig(array( 'baseurlpath' => 'http://example.org/simplesaml/' )); $resp = $this->server->get('/index.php', array(), array( CURLOPT_FOLLOWLOCATION => 0, )); $this->assertEquals('302', $resp['code']); $this->assertEquals( 'http://example.org/simplesaml/module.php/core/frontpage_welcome.php', $resp['headers']['Location'] ); // test non-default path and https $this->updateConfig(array( 'baseurlpath' => 'https://example.org/' )); $resp = $this->server->get('/index.php', array(), array( CURLOPT_FOLLOWLOCATION => 0, )); $this->assertEquals('302', $resp['code']); $this->assertEquals( 'https://example.org/module.php/core/frontpage_welcome.php', $resp['headers']['Location'] ); // test URL guessing $this->updateConfig(array( 'baseurlpath' => '/simplesaml/' )); $resp = $this->server->get('/index.php', array(), array( CURLOPT_FOLLOWLOCATION => 0, )); $this->assertEquals('302', $resp['code']); $this->assertEquals( 'http://'.$this->server_addr.'/simplesaml/module.php/core/frontpage_welcome.php', $resp['headers']['Location'] ); } /** * The tear down method that is executed after all tests in this class. */ protected function tearDown() { unlink($this->shared_file); $this->server->stop(); } } simplesamlphp-1.15.3/tests/bootstrap.php0000644000000000000000000000034413245225037017051 0ustar rootroot * @package simplesamlphp/simplesamlphp */ class StoreTest extends \PHPUnit_Framework_TestCase { /** * @covers \SimpleSAML\Store::getInstance * @test */ public function defaultStore() { Configuration::loadFromArray(array( ), '[ARRAY]', 'simplesaml'); $store = Store::getInstance(); $this->assertEquals(false, $store); } /** * @covers \SimpleSAML\Store::getInstance * @test */ public function phpSessionStore() { Configuration::loadFromArray(array( ), '[ARRAY]', 'simplesaml'); $store = Store::getInstance(); $this->assertEquals(false, $store); } /** * @covers \SimpleSAML\Store::getInstance * @test */ public function memcacheStore() { Configuration::loadFromArray(array( 'store.type' => 'memcache', ), '[ARRAY]', 'simplesaml'); $store = Store::getInstance(); $this->assertInstanceOf('\SimpleSAML\Store\Memcache', $store); } /** * @covers \SimpleSAML\Store::getInstance * @test */ public function sqlStore() { Configuration::loadFromArray(array( 'store.type' => 'sql', 'store.sql.dsn' => 'sqlite::memory:', 'store.sql.prefix' => 'phpunit_', ), '[ARRAY]', 'simplesaml'); $store = Store::getInstance(); $this->assertInstanceOf('SimpleSAML\Store\SQL', $store); } /** * @covers \SimpleSAML\Store::getInstance * @test */ public function pathStore() { Configuration::loadFromArray(array( 'store.type' => '\SimpleSAML\Store\SQL', 'store.sql.dsn' => 'sqlite::memory:', 'store.sql.prefix' => 'phpunit_', ), '[ARRAY]', 'simplesaml'); $store = Store::getInstance(); $this->assertInstanceOf('SimpleSAML\Store\SQL', $store); } /** * @covers \SimpleSAML\Store::getInstance * @expectedException \SimpleSAML\Error\CriticalConfigurationError * @test */ public function notFoundStoreException() { Configuration::loadFromArray(array( 'store.type' => '\Test\SimpleSAML\Store\Dummy', 'store.sql.dsn' => 'sqlite::memory:', 'store.sql.prefix' => 'phpunit_', ), '[ARRAY]', 'simplesaml'); Store::getInstance(); } protected function tearDown() { $config = Configuration::getInstance(); $store = Store::getInstance(); $this->clearInstance($config, '\SimpleSAML_Configuration'); $this->clearInstance($store, '\SimpleSAML\Store'); } protected function clearInstance($service, $className) { $reflectedClass = new \ReflectionClass($className); $reflectedInstance = $reflectedClass->getProperty('instance'); $reflectedInstance->setAccessible(true); $reflectedInstance->setValue($service, null); $reflectedInstance->setAccessible(false); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/ConfigurationTest.php0000644000000000000000000010477413245225037023173 0ustar rootrootassertTrue(is_string($c->getVersion())); } /** * Test that the default instance fails to load even if we previously loaded another instance. * @expectedException \SimpleSAML\Error\CriticalConfigurationError */ public function testLoadDefaultInstance() { SimpleSAML_Configuration::loadFromArray(array('key' => 'value'), '', 'dummy'); SimpleSAML_Configuration::getInstance(); } /** * Test that after a \SimpleSAML\Error\CriticalConfigurationError exception, a basic, self-survival configuration * is loaded. */ public function testCriticalConfigurationError() { try { SimpleSAML_Configuration::getInstance(); $this->fail('Exception expected'); } catch (\SimpleSAML\Error\CriticalConfigurationError $var) { // This exception is expected. } /* * After the above failure an emergency configuration is create to allow core SSP components to function and * possibly log/display the error. */ $c = SimpleSAML_Configuration::getInstance(); $this->assertNotEmpty($c->toArray()); } /** * Test SimpleSAML_Configuration::getValue() */ public function testGetValue() { $c = SimpleSAML_Configuration::loadFromArray(array( 'exists_true' => true, 'exists_null' => null, )); $this->assertEquals($c->getValue('missing'), null); $this->assertEquals($c->getValue('missing', true), true); $this->assertEquals($c->getValue('missing', true), true); $this->assertEquals($c->getValue('exists_true'), true); $this->assertEquals($c->getValue('exists_null'), null); $this->assertEquals($c->getValue('exists_null', false), null); } /** * Test SimpleSAML_Configuration::getValue(), REQUIRED_OPTION flag. * @expectedException Exception */ public function testGetValueRequired() { $c = SimpleSAML_Configuration::loadFromArray(array()); $c->getValue('missing', SimpleSAML_Configuration::REQUIRED_OPTION); } /** * Test SimpleSAML_Configuration::hasValue() */ public function testHasValue() { $c = SimpleSAML_Configuration::loadFromArray(array( 'exists_true' => true, 'exists_null' => null, )); $this->assertEquals($c->hasValue('missing'), false); $this->assertEquals($c->hasValue('exists_true'), true); $this->assertEquals($c->hasValue('exists_null'), true); } /** * Test SimpleSAML_Configuration::hasValue() */ public function testHasValueOneOf() { $c = SimpleSAML_Configuration::loadFromArray(array( 'exists_true' => true, 'exists_null' => null, )); $this->assertEquals($c->hasValueOneOf(array()), false); $this->assertEquals($c->hasValueOneOf(array('missing')), false); $this->assertEquals($c->hasValueOneOf(array('exists_true')), true); $this->assertEquals($c->hasValueOneOf(array('exists_null')), true); $this->assertEquals($c->hasValueOneOf(array('missing1', 'missing2')), false); $this->assertEquals($c->hasValueOneOf(array('exists_true', 'missing')), true); $this->assertEquals($c->hasValueOneOf(array('missing', 'exists_true')), true); } /** * Test SimpleSAML_Configuration::getBaseURL() */ public function testGetBaseURL() { // Need to set a default configuration because the SSP Logger attempts to use it. SimpleSAML_Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); $c = SimpleSAML_Configuration::loadFromArray(array()); $this->assertEquals($c->getBaseURL(), 'simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'simplesaml/')); $this->assertEquals($c->getBaseURL(), 'simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/simplesaml/')); $this->assertEquals($c->getBaseURL(), 'simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'path/to/simplesaml/')); $this->assertEquals($c->getBaseURL(), 'path/to/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/path/to/simplesaml/')); $this->assertEquals($c->getBaseURL(), 'path/to/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'https://example.org/ssp/')); $this->assertEquals($c->getBaseURL(), 'ssp/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'https://example.org/')); $this->assertEquals($c->getBaseURL(), ''); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'http://example.org/ssp/')); $this->assertEquals($c->getBaseURL(), 'ssp/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '')); $this->assertEquals($c->getBaseURL(), ''); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/')); $this->assertEquals($c->getBaseURL(), ''); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'simplesaml')); $this->assertEquals($c->getBaseURL(), 'simplesaml/'); } /** * Test SimpleSAML_Configuration::getBasePath() */ public function testGetBasePath() { $c = SimpleSAML_Configuration::loadFromArray(array()); $this->assertEquals($c->getBasePath(), '/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'simplesaml/')); $this->assertEquals($c->getBasePath(), '/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/simplesaml/')); $this->assertEquals($c->getBasePath(), '/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'simplesaml')); $this->assertEquals($c->getBasePath(), '/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/simplesaml')); $this->assertEquals($c->getBasePath(), '/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'path/to/simplesaml/')); $this->assertEquals($c->getBasePath(), '/path/to/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/path/to/simplesaml/')); $this->assertEquals($c->getBasePath(), '/path/to/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/path/to/simplesaml')); $this->assertEquals($c->getBasePath(), '/path/to/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'https://example.org/ssp/')); $this->assertEquals($c->getBasePath(), '/ssp/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'https://example.org/')); $this->assertEquals($c->getBasePath(), '/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'http://example.org/ssp/')); $this->assertEquals($c->getBasePath(), '/ssp/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'http://example.org/ssp/simplesaml')); $this->assertEquals($c->getBasePath(), '/ssp/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'http://example.org/ssp/simplesaml/')); $this->assertEquals($c->getBasePath(), '/ssp/simplesaml/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '')); $this->assertEquals($c->getBasePath(), '/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => '/')); $this->assertEquals($c->getBasePath(), '/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'https://example.org:8443')); $this->assertEquals($c->getBasePath(), '/'); $c = SimpleSAML_Configuration::loadFromArray(array('baseurlpath' => 'https://example.org:8443/')); $this->assertEquals($c->getBasePath(), '/'); } /** * Test SimpleSAML_Configuration::resolvePath() */ public function testResolvePath() { $c = SimpleSAML_Configuration::loadFromArray(array( 'basedir' => '/basedir/', )); $this->assertEquals($c->resolvePath(null), null); $this->assertEquals($c->resolvePath('/otherdir'), '/otherdir'); $this->assertEquals($c->resolvePath('relativedir'), '/basedir/relativedir'); $this->assertEquals($c->resolvePath('slash/'), '/basedir/slash'); $this->assertEquals($c->resolvePath('slash//'), '/basedir/slash'); } /** * Test SimpleSAML_Configuration::getPathValue() */ public function testGetPathValue() { $c = SimpleSAML_Configuration::loadFromArray(array( 'basedir' => '/basedir/', 'path_opt' => 'path', 'slashes_opt' => 'slashes//', )); $this->assertEquals($c->getPathValue('missing'), null); $this->assertEquals($c->getPathValue('path_opt'), '/basedir/path/'); $this->assertEquals($c->getPathValue('slashes_opt'), '/basedir/slashes/'); } /** * Test SimpleSAML_Configuration::getBaseDir() */ public function testGetBaseDir() { $c = SimpleSAML_Configuration::loadFromArray(array()); $this->assertEquals($c->getBaseDir(), dirname(dirname(dirname(dirname(__FILE__)))) . '/'); $c = SimpleSAML_Configuration::loadFromArray(array( 'basedir' => '/basedir', )); $this->assertEquals($c->getBaseDir(), '/basedir/'); $c = SimpleSAML_Configuration::loadFromArray(array( 'basedir' => '/basedir/', )); $this->assertEquals($c->getBaseDir(), '/basedir/'); } /** * Test SimpleSAML_Configuration::getBoolean() */ public function testGetBoolean() { $c = SimpleSAML_Configuration::loadFromArray(array( 'true_opt' => true, 'false_opt' => false, )); $this->assertEquals($c->getBoolean('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getBoolean('true_opt', '--missing--'), true); $this->assertEquals($c->getBoolean('false_opt', '--missing--'), false); } /** * Test SimpleSAML_Configuration::getBoolean() missing option * @expectedException Exception */ public function testGetBooleanMissing() { $c = SimpleSAML_Configuration::loadFromArray(array()); $c->getBoolean('missing_opt'); } /** * Test SimpleSAML_Configuration::getBoolean() wrong option * @expectedException Exception */ public function testGetBooleanWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'wrong' => 'true', )); $c->getBoolean('wrong'); } /** * Test SimpleSAML_Configuration::getString() */ public function testGetString() { $c = SimpleSAML_Configuration::loadFromArray(array( 'str_opt' => 'Hello World!', )); $this->assertEquals($c->getString('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getString('str_opt', '--missing--'), 'Hello World!'); } /** * Test SimpleSAML_Configuration::getString() missing option * @expectedException Exception */ public function testGetStringMissing() { $c = SimpleSAML_Configuration::loadFromArray(array()); $c->getString('missing_opt'); } /** * Test SimpleSAML_Configuration::getString() wrong option * @expectedException Exception */ public function testGetStringWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'wrong' => false, )); $c->getString('wrong'); } /** * Test SimpleSAML_Configuration::getInteger() */ public function testGetInteger() { $c = SimpleSAML_Configuration::loadFromArray(array( 'int_opt' => 42, )); $this->assertEquals($c->getInteger('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getInteger('int_opt', '--missing--'), 42); } /** * Test SimpleSAML_Configuration::getInteger() missing option * @expectedException Exception */ public function testGetIntegerMissing() { $c = SimpleSAML_Configuration::loadFromArray(array()); $c->getInteger('missing_opt'); } /** * Test SimpleSAML_Configuration::getInteger() wrong option * @expectedException Exception */ public function testGetIntegerWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'wrong' => '42', )); $c->getInteger('wrong'); } /** * Test SimpleSAML_Configuration::getIntegerRange() */ public function testGetIntegerRange() { $c = SimpleSAML_Configuration::loadFromArray(array( 'int_opt' => 42, )); $this->assertEquals($c->getIntegerRange('missing_opt', 0, 100, '--missing--'), '--missing--'); $this->assertEquals($c->getIntegerRange('int_opt', 0, 100), 42); } /** * Test SimpleSAML_Configuration::getIntegerRange() below limit * @expectedException Exception */ public function testGetIntegerRangeBelow() { $c = SimpleSAML_Configuration::loadFromArray(array( 'int_opt' => 9, )); $this->assertEquals($c->getIntegerRange('int_opt', 10, 100), 42); } /** * Test SimpleSAML_Configuration::getIntegerRange() above limit * @expectedException Exception */ public function testGetIntegerRangeAbove() { $c = SimpleSAML_Configuration::loadFromArray(array( 'int_opt' => 101, )); $this->assertEquals($c->getIntegerRange('int_opt', 10, 100), 42); } /** * Test SimpleSAML_Configuration::getValueValidate() */ public function testGetValueValidate() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => 'b', )); $this->assertEquals($c->getValueValidate('missing_opt', array('a', 'b', 'c'), '--missing--'), '--missing--'); $this->assertEquals($c->getValueValidate('opt', array('a', 'b', 'c')), 'b'); } /** * Test SimpleSAML_Configuration::getValueValidate() wrong option * @expectedException Exception */ public function testGetValueValidateWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => 'd', )); $c->getValueValidate('opt', array('a', 'b', 'c')); } /** * Test SimpleSAML_Configuration::getArray() */ public function testGetArray() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array('a', 'b', 'c'), )); $this->assertEquals($c->getArray('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getArray('opt'), array('a', 'b', 'c')); } /** * Test SimpleSAML_Configuration::getArray() wrong option * @expectedException Exception */ public function testGetArrayWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => 'not_an_array', )); $c->getArray('opt'); } /** * Test SimpleSAML_Configuration::getArrayize() */ public function testGetArrayize() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array('a', 'b', 'c'), 'opt_int' => 42, 'opt_str' => 'string', )); $this->assertEquals($c->getArrayize('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getArrayize('opt'), array('a', 'b', 'c')); $this->assertEquals($c->getArrayize('opt_int'), array(42)); $this->assertEquals($c->getArrayize('opt_str'), array('string')); } /** * Test SimpleSAML_Configuration::getArrayizeString() */ public function testGetArrayizeString() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array('a', 'b', 'c'), 'opt_str' => 'string', )); $this->assertEquals($c->getArrayizeString('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getArrayizeString('opt'), array('a', 'b', 'c')); $this->assertEquals($c->getArrayizeString('opt_str'), array('string')); } /** * Test SimpleSAML_Configuration::getArrayizeString() option with an array that contains something that isn't a string. * @expectedException Exception */ public function testGetArrayizeStringWrongValue() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array('a', 'b', 42), )); $c->getArrayizeString('opt'); } /** * Test SimpleSAML_Configuration::getConfigItem() */ public function testGetConfigItem() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array('a' => 42), )); $this->assertEquals($c->getConfigItem('missing_opt', '--missing--'), '--missing--'); $opt = $c->getConfigItem('opt'); $this->assertInstanceOf('SimpleSAML_Configuration', $opt); $this->assertEquals($opt->getValue('a'), 42); } /** * Test SimpleSAML_Configuration::getConfigItem() wrong option * @expectedException Exception */ public function testGetConfigItemWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => 'not_an_array', )); $c->getConfigItem('opt'); } /** * Test SimpleSAML_Configuration::getConfigList() */ public function testGetConfigList() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opts' => array( 'a' => array('opt1' => 'value1'), 'b' => array('opt2' => 'value2'), ), )); $this->assertEquals($c->getConfigList('missing_opt', '--missing--'), '--missing--'); $opts = $c->getConfigList('opts'); $this->assertInternalType('array', $opts); $this->assertEquals(array_keys($opts), array('a', 'b')); $this->assertInstanceOf('SimpleSAML_Configuration', $opts['a']); $this->assertEquals($opts['a']->getValue('opt1'), 'value1'); $this->assertInstanceOf('SimpleSAML_Configuration', $opts['b']); $this->assertEquals($opts['b']->getValue('opt2'), 'value2'); } /** * Test SimpleSAML_Configuration::getConfigList() wrong option * @expectedException Exception */ public function testGetConfigListWrong() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => 'not_an_array', )); $c->getConfigList('opt'); } /** * Test SimpleSAML_Configuration::getConfigList() with an array of wrong options. * @expectedException Exception */ public function testGetConfigListWrongArrayValues() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opts' => array( 'a', 'b', ), )); $c->getConfigList('opts'); } /** * Test SimpleSAML_Configuration::getOptions() */ public function testGetOptions() { $c = SimpleSAML_Configuration::loadFromArray(array( 'a' => true, 'b' => null, )); $this->assertEquals($c->getOptions(), array('a', 'b')); } /** * Test SimpleSAML_Configuration::toArray() */ public function testToArray() { $c = SimpleSAML_Configuration::loadFromArray(array( 'a' => true, 'b' => null, )); $this->assertEquals($c->toArray(), array('a' => true, 'b' => null)); } /** * Test SimpleSAML_Configuration::getDefaultEndpoint(). * * Iterate over all different valid definitions of endpoints and check if the expected output is produced. */ public function testGetDefaultEndpoint() { /* * First we run the full set of tests covering all possible configurations for indexed endpoint types, * basically AssertionConsumerService and ArtifactResolutionService. Since both are the same, we just run the * tests for AssertionConsumerService. */ $acs_eps = array( // just a string with the location 'https://example.com/endpoint.php', // an array of strings with location of different endpoints array( 'https://www1.example.com/endpoint.php', 'https://www2.example.com/endpoint.php', ), // define location and binding array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ), ), // define the ResponseLocation too array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, 'ResponseLocation' => 'https://example.com/endpoint.php', ), ), // make sure indexes are NOT taken into account (they just identify endpoints) array( array( 'index' => 1, 'Location' => 'https://www1.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, ), array( 'index' => 2, 'Location' => 'https://www2.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ), ), // make sure isDefault has priority over indexes array( array( 'index' => 1, 'Location' => 'https://www2.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ), array( 'index' => 2, 'isDefault' => true, 'Location' => 'https://www1.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, ), ), // make sure endpoints with invalid bindings are ignored and those marked as NOT default are still used array( array( 'index' => 1, 'Location' => 'https://www1.example.com/endpoint.php', 'Binding' => 'invalid_binding', ), array( 'index' => 2, 'isDefault' => false, 'Location' => 'https://www2.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ), ), ); $acs_expected_eps = array( // output should be completed with the default binding (HTTP-POST for ACS) array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ), // we should just get the first endpoint with the default binding array( 'Location' => 'https://www1.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ), // if we specify the binding, we should get it back array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST ), // if we specify ResponseLocation, we should get it back too array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, 'ResponseLocation' => 'https://example.com/endpoint.php', ), // indexes must NOT be taken into account, order is the only thing that matters here array( 'Location' => 'https://www1.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, 'index' => 1, ), // isDefault must have higher priority than indexes array( 'Location' => 'https://www1.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, 'isDefault' => true, 'index' => 2, ), // the first valid enpoint should be used even if it's marked as NOT default array( 'index' => 2, 'isDefault' => false, 'Location' => 'https://www2.example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_POST, ) ); $a = array( 'metadata-set' => 'saml20-sp-remote', 'ArtifactResolutionService' => 'https://example.com/ars', 'SingleSignOnService' => 'https://example.com/sso', 'SingleLogoutService' => array( 'Location' => 'https://example.com/slo', 'Binding' => 'valid_binding', // test unknown bindings if we don't specify a list of valid ones ), ); $valid_bindings = array( \SAML2\Constants::BINDING_HTTP_POST, \SAML2\Constants::BINDING_HTTP_REDIRECT, \SAML2\Constants::BINDING_HOK_SSO, \SAML2\Constants::BINDING_HTTP_ARTIFACT. \SAML2\Constants::BINDING_SOAP, ); // run all general tests with AssertionConsumerService endpoint type foreach ($acs_eps as $i => $ep) { $a['AssertionConsumerService'] = $ep; $c = SimpleSAML_Configuration::loadFromArray($a); $this->assertEquals($acs_expected_eps[$i], $c->getDefaultEndpoint( 'AssertionConsumerService', $valid_bindings )); } // now make sure SingleSignOnService, SingleLogoutService and ArtifactResolutionService works fine $a['metadata-set'] = 'shib13-idp-remote'; $c = SimpleSAML_Configuration::loadFromArray($a); $this->assertEquals( array( 'Location' => 'https://example.com/sso', 'Binding' => 'urn:mace:shibboleth:1.0:profiles:AuthnRequest', ), $c->getDefaultEndpoint('SingleSignOnService') ); $a['metadata-set'] = 'saml20-idp-remote'; $c = SimpleSAML_Configuration::loadFromArray($a); $this->assertEquals( array( 'Location' => 'https://example.com/ars', 'Binding' => \SAML2\Constants::BINDING_SOAP, ), $c->getDefaultEndpoint('ArtifactResolutionService') ); $this->assertEquals( array( 'Location' => 'https://example.com/slo', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, ), $c->getDefaultEndpoint('SingleLogoutService') ); // test for old shib1.3 AssertionConsumerService $a['metadata-set'] = 'shib13-sp-remote'; $a['AssertionConsumerService'] = 'https://example.com/endpoint.php'; $c = SimpleSAML_Configuration::loadFromArray($a); $this->assertEquals( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => 'urn:oasis:names:tc:SAML:1.0:profiles:browser-post', ), $c->getDefaultEndpoint('AssertionConsumerService') ); // test for no valid endpoints specified $a['SingleLogoutService'] = array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => 'invalid_binding', 'isDefault' => true, ), ); $c = SimpleSAML_Configuration::loadFromArray($a); try { $c->getDefaultEndpoint('SingleLogoutService', $valid_bindings); $this->fail('Failed to detect invalid endpoint binding.'); } catch (Exception $e) { $this->assertEquals( '[ARRAY][\'SingleLogoutService\']:Could not find a supported SingleLogoutService '.'endpoint.', $e->getMessage() ); } $a['metadata-set'] = 'foo'; $c = SimpleSAML_Configuration::loadFromArray($a); try { $c->getDefaultEndpoint('SingleSignOnService'); $this->fail('No valid metadata set specified.'); } catch (Exception $e) { $this->assertStringStartsWith('Missing default binding for', $e->getMessage()); } } /** * Test SimpleSAML_Configuration::getEndpoints(). */ public function testGetEndpoints() { // test response location for old-style configurations $c = SimpleSAML_Configuration::loadFromArray(array( 'metadata-set' => 'saml20-idp-remote', 'SingleSignOnService' => 'https://example.com/endpoint.php', 'SingleSignOnServiceResponse' => 'https://example.com/response.php', )); $e = array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, 'ResponseLocation' => 'https://example.com/response.php', ) ); $this->assertEquals($e, $c->getEndpoints('SingleSignOnService')); // test for input failures // define a basic configuration array $a = array( 'metadata-set' => 'saml20-idp-remote', 'SingleSignOnService' => null, ); // define a set of tests $tests = array( // invalid endpoint definition 10, // invalid definition of endpoint inside the endpoints array array( 1234 ), // missing location array( array( 'foo' => 'bar', ), ), // invalid location array( array( 'Location' => 1234, ) ), // missing binding array( array( 'Location' => 'https://example.com/endpoint.php', ), ), // invalid binding array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => 1234, ), ), // invalid response location array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, 'ResponseLocation' => 1234, ), ), // invalid index array( array( 'Location' => 'https://example.com/endpoint.php', 'Binding' => \SAML2\Constants::BINDING_HTTP_REDIRECT, 'index' => 'string', ), ), ); // define a set of exception messages to expect $msgs = array( 'Expected array or string.', 'Expected a string or an array.', 'Missing Location.', 'Location must be a string.', 'Missing Binding.', 'Binding must be a string.', 'ResponseLocation must be a string.', 'index must be an integer.', ); // now run all the tests expecting the correct exception message foreach ($tests as $i => $test) { $a['SingleSignOnService'] = $test; $c = SimpleSAML_Configuration::loadFromArray($a); try { $c->getEndpoints('SingleSignOnService'); } catch (Exception $e) { $this->assertStringEndsWith($msgs[$i], $e->getMessage()); } } } /** * Test SimpleSAML_Configuration::getLocalizedString() */ public function testGetLocalizedString() { $c = SimpleSAML_Configuration::loadFromArray(array( 'str_opt' => 'Hello World!', 'str_array' => array( 'en' => 'Hello World!', 'no' => 'Hei Verden!', ), )); $this->assertEquals($c->getLocalizedString('missing_opt', '--missing--'), '--missing--'); $this->assertEquals($c->getLocalizedString('str_opt'), array('en' => 'Hello World!')); $this->assertEquals($c->getLocalizedString('str_array'), array('en' => 'Hello World!', 'no' => 'Hei Verden!')); } /** * Test SimpleSAML_Configuration::getLocalizedString() not array nor simple string * @expectedException Exception */ public function testGetLocalizedStringNotArray() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => 42, )); $c->getLocalizedString('opt'); } /** * Test SimpleSAML_Configuration::getLocalizedString() not string key * @expectedException Exception */ public function testGetLocalizedStringNotStringKey() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array(42 => 'text'), )); $c->getLocalizedString('opt'); } /** * Test SimpleSAML_Configuration::getLocalizedString() not string value * @expectedException Exception */ public function testGetLocalizedStringNotStringValue() { $c = SimpleSAML_Configuration::loadFromArray(array( 'opt' => array('en' => 42), )); $c->getLocalizedString('opt'); } /** * Test that Configuration objects can be initialized from an array. * * ATTENTION: this test must be kept the last. */ public function testLoadInstanceFromArray() { $c = array( 'key' => 'value' ); // test loading a custom instance SimpleSAML_Configuration::loadFromArray($c, '', 'dummy'); $this->assertEquals('value', SimpleSAML_Configuration::getInstance('dummy')->getValue('key', null)); // test loading the default instance SimpleSAML_Configuration::loadFromArray($c, '', 'simplesaml'); $this->assertEquals('value', SimpleSAML_Configuration::getInstance()->getValue('key', null)); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Store/0000755000000000000000000000000013245225037020072 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/Store/SQLTest.php0000644000000000000000000001171313245225037022105 0ustar rootroot * @package simplesamlphp/simplesamlphp */ class SQLTest extends \PHPUnit_Framework_TestCase { protected function setUp() { \SimpleSAML_Configuration::loadFromArray(array( 'store.type' => 'sql', 'store.sql.dsn' => 'sqlite::memory:', 'store.sql.prefix' => 'phpunit_', ), '[ARRAY]', 'simplesaml'); } /** * @covers \SimpleSAML\Store::getInstance * @covers \SimpleSAML\Store\SQL::__construct * @test */ public function SQLInstance() { $store = Store::getInstance(); $this->assertInstanceOf('SimpleSAML\Store\SQL', $store); } /** * @covers \SimpleSAML\Store\SQL::initTableVersionTable * @covers \SimpleSAML\Store\SQL::initKVTable * @test */ public function kvstoreTableVersion() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $version = $store->getTableVersion('kvstore'); $this->assertEquals(1, $version); } /** * @covers \SimpleSAML\Store\SQL::getTableVersion * @test */ public function newTableVersion() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $version = $store->getTableVersion('test'); $this->assertEquals(0, $version); } /** * @covers \SimpleSAML\Store\SQL::setTableVersion * @covers \SimpleSAML\Store\SQL::insertOrUpdate * @test */ public function testSetTableVersion() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $store->setTableVersion('kvstore', 2); $version = $store->getTableVersion('kvstore'); $this->assertEquals(2, $version); } /** * @covers \SimpleSAML\Store\SQL::get * @test */ public function testGetEmptyData() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $value = $store->get('test', 'foo'); $this->assertEquals(null, $value); } /** * @covers \SimpleSAML\Store\SQL::get * @covers \SimpleSAML\Store\SQL::set * @covers \SimpleSAML\Store\SQL::insertOrUpdate * @test */ public function testInsertData() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $store->set('test', 'foo', 'bar'); $value = $store->get('test', 'foo'); $this->assertEquals('bar', $value); } /** * @covers \SimpleSAML\Store\SQL::get * @covers \SimpleSAML\Store\SQL::set * @covers \SimpleSAML\Store\SQL::insertOrUpdate * @test */ public function testOverwriteData() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $store->set('test', 'foo', 'bar'); $store->set('test', 'foo', 'baz'); $value = $store->get('test', 'foo'); $this->assertEquals('baz', $value); } /** * @covers \SimpleSAML\Store\SQL::get * @covers \SimpleSAML\Store\SQL::set * @covers \SimpleSAML\Store\SQL::insertOrUpdate * @covers \SimpleSAML\Store\SQL::delete * @test */ public function testDeleteData() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $store->set('test', 'foo', 'bar'); $store->delete('test', 'foo'); $value = $store->get('test', 'foo'); $this->assertEquals(null, $value); } /** * @covers \SimpleSAML\Store\SQL::get * @covers \SimpleSAML\Store\SQL::set * @covers \SimpleSAML\Store\SQL::insertOrUpdate * @covers \SimpleSAML\Store\SQL::delete * @test */ public function testVeryLongKey() { /** @var \SimpleSAML\Store\SQL $store */ $store = Store::getInstance(); $key = str_repeat('x', 100); $store->set('test', $key, 'bar'); $store->delete('test', $key); $value = $store->get('test', $key); $this->assertEquals(null, $value); } protected function tearDown() { $config = Configuration::getInstance(); $store = Store::getInstance(); $this->clearInstance($config, '\SimpleSAML_Configuration'); $this->clearInstance($store, '\SimpleSAML\Store'); } protected function clearInstance($service, $className) { $reflectedClass = new \ReflectionClass($className); $reflectedInstance = $reflectedClass->getProperty('instance'); $reflectedInstance->setAccessible(true); $reflectedInstance->setValue($service, null); $reflectedInstance->setAccessible(false); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Store/RedisTest.php0000644000000000000000000001122013245225037022505 0ustar rootrootconfig = array(); $this->mocked_redis = $this->getMockBuilder('Predis\Client') ->setMethods(array('get', 'set', 'setex', 'del', 'disconnect')) ->disableOriginalConstructor() ->getMock(); $this->mocked_redis->method('get') ->will($this->returnCallback(array($this, 'getMocked'))); $this->mocked_redis->method('set') ->will($this->returnCallback(array($this, 'setMocked'))); $this->mocked_redis->method('setex') ->will($this->returnCallback(array($this, 'setexMocked'))); $this->mocked_redis->method('del') ->will($this->returnCallback(array($this, 'delMocked'))); $nop = function () { return; }; $this->mocked_redis->method('disconnect') ->will($this->returnCallback($nop)); $this->redis = new Store\Redis($this->mocked_redis); } public function getMocked($key) { return array_key_exists($key, $this->config) ? $this->config[$key] : null; } public function setMocked($key, $value) { $this->config[$key] = $value; } public function setexMocked($key, $expire, $value) { // Testing expiring data is more trouble than it's worth for now $this->setMocked($key, $value); } public function delMocked($key) { unset($this->config[$key]); } /** * @covers \SimpleSAML\Store::getInstance * @covers \SimpleSAML\Store\Redis::__construct * @test */ public function testRedisInstance() { $config = Configuration::loadFromArray(array( 'store.type' => 'redis', 'store.redis.prefix' => 'phpunit_', ), '[ARRAY]', 'simplesaml'); $store = Store::getInstance(); $this->assertInstanceOf('SimpleSAML\Store\Redis', $store); $this->clearInstance($config, '\SimpleSAML_Configuration'); $this->clearInstance($store, '\SimpleSAML\Store'); } /** * @covers \SimpleSAML\Store\Redis::get * @covers \SimpleSAML\Store\Redis::set * @test */ public function testInsertData() { $value = 'TEST'; $this->redis->set('test', 'key', $value); $res = $this->redis->get('test', 'key'); $expected = $value; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Store\Redis::get * @covers \SimpleSAML\Store\Redis::set * @test */ public function testInsertExpiringData() { $value = 'TEST'; $this->redis->set('test', 'key', $value, $expire = 80808080); $res = $this->redis->get('test', 'key'); $expected = $value; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Store\Redis::get * @test */ public function testGetEmptyData() { $res = $this->redis->get('test', 'key'); $this->assertNull($res); } /** * @covers \SimpleSAML\Store\Redis::get * @covers \SimpleSAML\Store\Redis::set * @test */ public function testOverwriteData() { $value1 = 'TEST1'; $value2 = 'TEST2'; $this->redis->set('test', 'key', $value1); $this->redis->set('test', 'key', $value2); $res = $this->redis->get('test', 'key'); $expected = $value2; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Store\Redis::get * @covers \SimpleSAML\Store\Redis::set * @covers \SimpleSAML\Store\Redis::delete * @test */ public function testDeleteData() { $this->redis->set('test', 'key', 'TEST'); $this->redis->delete('test', 'key'); $res = $this->redis->get('test', 'key'); $this->assertNull($res); } protected function clearInstance($service, $className) { $reflectedClass = new \ReflectionClass($className); $reflectedInstance = $reflectedClass->getProperty('instance'); $reflectedInstance->setAccessible(true); $reflectedInstance->setValue($service, null); $reflectedInstance->setAccessible(false); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/XML/0000755000000000000000000000000013245225037017436 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/XML/Shib13/0000755000000000000000000000000013245225037020467 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/XML/Shib13/AuthnResponseTest.php0000644000000000000000000000623413245225037024643 0ustar rootroot * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SimpleSAML\Test\XML\Shib13; use SimpleSAML\XML\Shib13\AuthnResponse; class AuthnResponseTest extends \PHPUnit_Framework_TestCase { const XMLDOC = <<< XML NameIdentifier XML; const BADXMLDOC = <<< XML NameIdentifier XML; /** * @var AuthnResponse */ private $xml; protected function setUp() { $this->xml = new AuthnResponse(); $this->xml->setXML(static::XMLDOC); } /** * @covers \SimpleSAML\XML\Shib13\AuthnResponse::setXML * @test */ public function setXML() { $this->xml = new AuthnResponse(); $this->xml->setXML(static::XMLDOC); } /** * @covers \SimpleSAML\XML\Shib13\AuthnResponse::doXPathQuery * @covers \SimpleSAML\XML\Shib13\AuthnResponse::getIssuer * @covers \SimpleSAML\XML\Shib13\AuthnResponse::setXML * @test */ public function getIssuer() { $result = $this->xml->getIssuer(); $this->assertEquals( 'Issuer', $result ); } /** * @covers \SimpleSAML\XML\Shib13\AuthnResponse::getIssuer * @covers \SimpleSAML\XML\Shib13\AuthnResponse::setXML * @expectedException \Exception * @test */ public function getIssuerException() { $xml = new AuthnResponse(); $xml->setXML(static::BADXMLDOC); $xml->getIssuer(); } /** * @covers \SimpleSAML\XML\Shib13\AuthnResponse::getNameID * @covers \SimpleSAML\XML\Shib13\AuthnResponse::setXML * @test */ public function getNameID() { $result = $this->xml->getNameID(); $this->assertEquals( array( 'Value' => 'NameIdentifier', 'Format' => 'urn:mace:shibboleth:1.0:nameIdentifier', ), $result ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/XML/SignerTest.php0000644000000000000000000002262513245225037022245 0ustar rootrootroot = vfsStream::setup( self::ROOTDIRNAME, null, array( self::DEFAULTCERTDIR => array( self::PRIVATEKEY => $this->private_key, self::CERTIFICATE1 => $this->certificate1, self::CERTIFICATE2 => $this->certificate2, ), ) ); $this->root_directory = vfsStream::url(self::ROOTDIRNAME); $this->certdir = $this->root_directory.DIRECTORY_SEPARATOR.self::DEFAULTCERTDIR; $this->privatekey_file = $this->certdir.DIRECTORY_SEPARATOR.self::PRIVATEKEY; $this->certificate_file1 = $this->certdir.DIRECTORY_SEPARATOR.self::CERTIFICATE1; $this->certificate_file2 = $this->certdir.DIRECTORY_SEPARATOR.self::CERTIFICATE2; $this->config = Configuration::loadFromArray(array( 'certdir' => $this->certdir, ), '[ARRAY]', 'simplesaml'); } public function tearDown() { $this->clearInstance($this->config, '\SimpleSAML_Configuration', array()); } public function testSignerBasic() { $res = new Signer(array()); $this->assertNotNull($res); } public function testSignBasic() { $node = new \DOMDocument(); $node->loadXML('value'); $element = $node->getElementsByTagName("node")->item(0); $doc = new \DOMDocument(); $insertInto = $doc->appendChild(new \DOMElement('insert')); $signer = new Signer(array()); $signer->loadPrivateKey($this->privatekey_file, null, true); $signer->sign($element, $insertInto); $res = $doc->saveXML(); $this->assertContains('DigestValue', $res); $this->assertContains('SignatureValue', $res); } private static function getCertificateValue($certificate) { $replacements = array( "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----", "\n", ); return str_replace($replacements, "", $certificate); } public function testSignWithCertificate() { $node = new \DOMDocument(); $node->loadXML('value'); $element = $node->getElementsByTagName("node")->item(0); $doc = new \DOMDocument(); $insertInto = $doc->appendChild(new \DOMElement('insert')); $signer = new Signer(array()); $signer->loadPrivateKey($this->privatekey_file, null, true); $signer->loadCertificate($this->certificate_file1, true); $signer->sign($element, $insertInto); $res = $doc->saveXML(); $expected = self::getCertificateValue($this->certificate1); $this->assertContains('X509Certificate', $res); $this->assertContains($expected, $res); } public function testSignWithMultiCertificate() { $node = new \DOMDocument(); $node->loadXML('value'); $element = $node->getElementsByTagName("node")->item(0); $doc = new \DOMDocument(); $insertInto = $doc->appendChild(new \DOMElement('insert')); $signer = new Signer(array()); $signer->loadPrivateKey($this->privatekey_file, null, true); $signer->loadCertificate($this->certificate_file1, true); $signer->addCertificate($this->certificate_file2, true); $signer->sign($element, $insertInto); $res = $doc->saveXML(); $expected1 = self::getCertificateValue($this->certificate1); $expected2 = self::getCertificateValue($this->certificate2); $this->assertContains('X509Certificate', $res); $this->assertContains($expected1, $res); $this->assertContains($expected2, $res); } public function testSignMissingPrivateKey() { $node = new \DOMDocument(); $node->loadXML('value'); $element = $node->getElementsByTagName("node")->item(0); $doc = new \DOMDocument(); $insertInto = $doc->appendChild(new \DOMElement('insert')); $signer = new Signer(array()); $this->setExpectedException('\Exception'); $signer->sign($element, $insertInto); } protected function clearInstance($service, $className, $value = null) { $reflectedClass = new \ReflectionClass($className); $reflectedInstance = $reflectedClass->getProperty('instance'); $reflectedInstance->setAccessible(true); $reflectedInstance->setValue($service, $value); $reflectedInstance->setAccessible(false); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/XML/ParserTest.php0000644000000000000000000000613713245225037022252 0ustar rootroot * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace SimpleSAML\Test\XML; use SimpleSAML\XML\Parser; class ParserTest extends \PHPUnit_Framework_TestCase { const XMLDOC = <<< XML Hello, World! XML; /** @var Parser */ private $xml; protected function setUp() { $this->xml = new Parser(static::XMLDOC); } /** * @covers \SimpleSAML\XML\Parser::getValue * @covers \SimpleSAML\XML\Parser::__construct * @test */ public function getValue() { $result = $this->xml->getValue('/Root/Value', true); $this->assertEquals( 'Hello, World!', $result ); } /** * @covers \SimpleSAML\XML\Parser::getValue * @covers \SimpleSAML\XML\Parser::__construct * @test */ public function getEmptyValue() { $result = $this->xml->getValue('/Root/Foo', false); $this->assertEquals( null, $result ); } /** * @covers \SimpleSAML\XML\Parser::getValue * @covers \SimpleSAML\XML\Parser::__construct * @expectedException \Exception * @test */ public function getValueException() { $this->xml->getValue('/Root/Foo', true); } /** * @covers \SimpleSAML\XML\Parser::getValueDefault * @covers \SimpleSAML\XML\Parser::__construct * @test */ public function getDefaultValue() { $result = $this->xml->getValueDefault('/Root/Other', 'Hello'); $this->assertEquals( 'Hello', $result ); } /** * @covers \SimpleSAML\XML\Parser::getValueAlternatives * @covers \SimpleSAML\XML\Parser::__construct * @test */ public function getValueAlternatives() { $result = $this ->xml ->getValueAlternatives(array( '/Root/Other', '/Root/Value' ), true) ; $this->assertEquals( 'Hello, World!', $result ); } /** * @covers \SimpleSAML\XML\Parser::getValueAlternatives * @covers \SimpleSAML\XML\Parser::__construct * @test */ public function getEmptyValueAlternatives() { $result = $this ->xml ->getValueAlternatives(array( '/Root/Foo', '/Root/Bar' ), false) ; $this->assertEquals( null, $result ); } /** * @covers \SimpleSAML\XML\Parser::getValueAlternatives * @covers \SimpleSAML\XML\Parser::__construct * @expectedException \Exception * @test */ public function getValueAlternativesException() { $this ->xml ->getValueAlternatives(array( '/Root/Foo', '/Root/Bar' ), true) ; } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/XML/ErrorsTest.php0000644000000000000000000000302713245225037022265 0ustar rootroot * @package simplesamlphp/simplesamlphp */ namespace SimpleSAML\Test\XML; use SimpleSAML\XML\Errors; class ErrorsTest extends \PHPUnit_Framework_TestCase { /** * @covers \SimpleSAML\XML\Errors::begin * @covers \SimpleSAML\XML\Errors::addErrors * @covers \SimpleSAML\XML\Errors::end * @test */ public function loggingErrors() { Errors::begin(); $xmlstr = "Test"; simplexml_load_string($xmlstr); $errors = Errors::end(); $errors = Errors::formatErrors($errors); $this->assertEquals( "level=3,code=76,line=1,col=18,msg=Opening and ending tag mismatch: Test line 1 and test\n", $errors ); } /** * @covers \SimpleSAML\XML\Errors::formatError * @covers \SimpleSAML\XML\Errors::formatErrors * @test */ public function formatErrors() { $error = new \LibXMLError(); $error->level = 'level'; $error->code = 'code'; $error->line = 'line'; $error->column = 'col'; $error->message = ' msg '; $errors = Errors::formatErrors(array($error, $error)); $this->assertEquals( "level=level,code=code,line=line,col=col,msg=msg\nlevel=level,code=code,line=line,col=col,msg=msg\n", $errors ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Auth/0000755000000000000000000000000013245225037017677 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/Auth/TimeLimitedTokenTest.php0000644000000000000000000000454013245225037024462 0ustar rootroot 'random'), '[ARRAY]', 'simplesaml'); $token = new TimeLimitedToken(); $this->assertFalse($token->validate('malformed')); $this->assertFalse($token->validate('mal-for-med')); $this->assertFalse($token->validate('mal-formed')); } /** * Basic test to see if validation works for valid tokens. */ public function testValidToken() { \SimpleSAML_Configuration::loadFromArray(array('secretsalt' => 'random'), '[ARRAY]', 'simplesaml'); $token = new TimeLimitedToken(); $t = $token->generate(); $this->assertTrue($token->validate($t)); } /** * Test that token validation takes the verification data into account. */ public function testValidTokenWithData() { \SimpleSAML_Configuration::loadFromArray(array('secretsalt' => 'random'), '[ARRAY]', 'simplesaml'); $tokenWithData = new TimeLimitedToken(); $tokenWithData->addVerificationData('some more random data'); $t = $tokenWithData->generate(); $this->assertTrue($tokenWithData->validate($t)); $tokenWithoutData = new TimeLimitedToken(); $this->assertFalse($tokenWithoutData->validate($t)); } /** * Test that expired tokens are rejected. */ public function testExpiredToken() { \SimpleSAML_Configuration::loadFromArray(array('secretsalt' => 'random'), '[ARRAY]', 'simplesaml'); $token = new TimeLimitedToken(); $this->assertFalse($token->validate('7-c0803e76fff1df0ceb222dee80aa1d73f35d84dd')); } /** * Test that a token that has been manipulated to extend its validity is rejected. */ public function testManipulatedToken() { \SimpleSAML_Configuration::loadFromArray(array('secretsalt' => 'random'), '[ARRAY]', 'simplesaml'); $token = new TimeLimitedToken(1); $t = $token->generate(); list($offset, $hash) = explode('-', $t); sleep(1); $this->assertFalse($token->validate(dechex(hexdec($offset) + 1).'-'.$hash)); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Auth/SimpleTest.php0000644000000000000000000000642013245225037022503 0ustar rootrootgetMethod('getProcessedURL'); $method->setAccessible(true); // fool the routines to make them believe we are running in a web server $_SERVER['REQUEST_URI'] = '/'; // test merging configuration option with passed URL \SimpleSAML_Configuration::loadFromArray(array( 'application' => array( 'baseURL' => 'https://example.org' ) ), '[ARRAY]', 'simplesaml'); $s = new \SimpleSAML\Auth\Simple(''); $this->assertEquals('https://example.org/', $method->invokeArgs($s, array(null))); // test a full URL passed as parameter $this->assertEquals( 'https://example.org/foo/bar?a=b#fragment', $method->invokeArgs( $s, array('http://some.overridden.host/foo/bar?a=b#fragment') ) ); // test a full, current URL with no parameters $_SERVER['REQUEST_URI'] = '/foo/bar?a=b#fragment'; $this->assertEquals('https://example.org/foo/bar?a=b#fragment', $method->invokeArgs($s, array(null))); // test ports are overridden by configuration $_SERVER['SERVER_PORT'] = '1234'; $this->assertEquals('https://example.org/foo/bar?a=b#fragment', $method->invokeArgs($s, array(null))); // test config option with ending with / and port \SimpleSAML_Configuration::loadFromArray(array( 'application' => array( 'baseURL' => 'http://example.org:8080/' ) ), '[ARRAY]', 'simplesaml'); $s = new \SimpleSAML\Auth\Simple(''); $this->assertEquals('http://example.org:8080/foo/bar?a=b#fragment', $method->invokeArgs($s, array(null))); // test again with a relative URL as a parameter $this->assertEquals( 'http://example.org:8080/something?foo=bar#something', $method->invokeArgs($s, array('/something?foo=bar#something')) ); // now test with no configuration $_SERVER['SERVER_NAME'] = 'example.org'; \SimpleSAML_Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); $s = new \SimpleSAML\Auth\Simple(''); $this->assertEquals('http://example.org:1234/foo/bar?a=b#fragment', $method->invokeArgs($s, array(null))); // no configuration, https and port $_SERVER['HTTPS'] = 'on'; $this->assertEquals('https://example.org:1234/foo/bar?a=b#fragment', $method->invokeArgs($s, array(null))); // no configuration and a relative URL as a parameter $this->assertEquals( 'https://example.org:1234/something?foo=bar#something', $method->invokeArgs($s, array('/something?foo=bar#something')) ); // finally, no configuration and full URL as a parameter $this->assertEquals( 'https://example.org/one/two/three?foo=bar#fragment', $method->invokeArgs($s, array('https://example.org/one/two/three?foo=bar#fragment')) ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Auth/StateTest.php0000644000000000000000000000472513245225037022340 0ustar rootroot array(), 'Expire' => 1234, 'LogoutState' => 'logoutState', 'AuthInstant' => 123456, 'RememberMe' => true, 'saml:sp:NameID' => 'nameID', ); // check just mandatory parameters $state = $mandatory; $expected = $mandatory; $this->assertEquals( $expected, SimpleSAML_Auth_State::getPersistentAuthData($state), 'Mandatory state attributes did not survive as expected'.print_r($expected, true) ); // check missing mandatory parameters unset($state['LogoutState']); unset($state['RememberMe']); $expected = $state; $this->assertEquals( $expected, SimpleSAML_Auth_State::getPersistentAuthData($state), 'Some error occurred with missing mandatory parameters' ); // check additional non-persistent parameters $additional = array( 'additional1' => 1, 'additional2' => 2, ); $state = array_merge($mandatory, $additional); $expected = $mandatory; $this->assertEquals( $expected, SimpleSAML_Auth_State::getPersistentAuthData($state), 'Additional parameters survived' ); // check additional persistent parameters $additional['PersistentAuthData'] = array('additional1'); $state = array_merge($mandatory, $additional); $expected = $state; unset($expected['additional2']); unset($expected['PersistentAuthData']); $this->assertEquals( $expected, SimpleSAML_Auth_State::getPersistentAuthData($state), 'Some error occurred with additional, persistent parameters' ); // check only additional persistent parameters $state = $additional; $expected = $state; unset($expected['additional2']); unset($expected['PersistentAuthData']); $this->assertEquals( $expected, SimpleSAML_Auth_State::getPersistentAuthData($state), 'Some error occurred with additional, persistent parameters, and no mandatory ones' ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/DatabaseTest.php0000644000000000000000000002322713245225037022061 0ustar rootroot * @package SimpleSAMLphp */ class SimpleSAML_DatabaseTest extends PHPUnit_Framework_TestCase { /** * @var SimpleSAML_Configuration */ protected $config; /** * @var SimpleSAML\Database */ protected $db; /** * Make protected functions available for testing * * @param string $getMethod The method to get. * @requires PHP 5.3.2 * * @return mixed The method itself. */ protected static function getMethod($getMethod) { $class = new ReflectionClass('SimpleSAML\Database'); $method = $class->getMethod($getMethod); $method->setAccessible(true); return $method; } /** * @covers SimpleSAML\Database::getInstance * @covers SimpleSAML\Database::generateInstanceId * @covers SimpleSAML\Database::__construct * @covers SimpleSAML\Database::connect */ public function setUp() { $config = array( 'database.dsn' => 'sqlite::memory:', 'database.username' => null, 'database.password' => null, 'database.prefix' => 'phpunit_', 'database.persistent' => true, 'database.slaves' => array(), ); $this->config = new SimpleSAML_Configuration($config, "test/SimpleSAML/DatabaseTest.php"); // Ensure that we have a functional configuration class $this->assertInstanceOf('SimpleSAML_Configuration', $this->config); $this->assertEquals($config['database.dsn'], $this->config->getString('database.dsn')); $this->db = SimpleSAML\Database::getInstance($this->config); // Ensure that we have a functional database class. $this->assertInstanceOf('SimpleSAML\Database', $this->db); } /** * @covers SimpleSAML\Database::getInstance * @covers SimpleSAML\Database::generateInstanceId * @covers SimpleSAML\Database::__construct * @covers SimpleSAML\Database::connect * @expectedException Exception * @test */ public function connectionFailure() { $config = array( 'database.dsn' => 'mysql:host=localhost;dbname=saml', 'database.username' => 'notauser', 'database.password' => 'notausersinvalidpassword', 'database.prefix' => 'phpunit_', 'database.persistent' => true, 'database.slaves' => array(), ); $this->config = new SimpleSAML_Configuration($config, "test/SimpleSAML/DatabaseTest.php"); $db = SimpleSAML\Database::getInstance($this->config); } /** * @covers SimpleSAML\Database::getInstance * @covers SimpleSAML\Database::generateInstanceId * @covers SimpleSAML\Database::__construct * @covers SimpleSAML\Database::connect * @test */ public function instances() { $config = array( 'database.dsn' => 'sqlite::memory:', 'database.username' => null, 'database.password' => null, 'database.prefix' => 'phpunit_', 'database.persistent' => true, 'database.slaves' => array(), ); $config2 = array( 'database.dsn' => 'sqlite::memory:', 'database.username' => null, 'database.password' => null, 'database.prefix' => 'phpunit2_', 'database.persistent' => true, 'database.slaves' => array(), ); $config1 = new SimpleSAML_Configuration($config, "test/SimpleSAML/DatabaseTest.php"); $config2 = new SimpleSAML_Configuration($config2, "test/SimpleSAML/DatabaseTest.php"); $config3 = new SimpleSAML_Configuration($config, "test/SimpleSAML/DatabaseTest.php"); $db1 = SimpleSAML\Database::getInstance($config1); $db2 = SimpleSAML\Database::getInstance($config2); $db3 = SimpleSAML\Database::getInstance($config3); $generateInstanceId = self::getMethod('generateInstanceId'); $instance1 = $generateInstanceId->invokeArgs($db1, array($config1)); $instance2 = $generateInstanceId->invokeArgs($db2, array($config2)); $instance3 = $generateInstanceId->invokeArgs($db3, array($config3)); // Assert that $instance1 and $instance2 have different instance ids $this->assertNotEquals( $instance1, $instance2, "Database instances should be different, but returned the same id" ); // Assert that $instance1 and $instance3 have identical instance ids $this->assertEquals( $instance1, $instance3, "Database instances should have the same id, but returned different id" ); // Assert that $db1 and $db2 are different instances $this->assertNotEquals( spl_object_hash($db1), spl_object_hash($db2), "Database instances should be different, but returned the same spl_object_hash" ); // Assert that $db1 and $db3 are identical instances $this->assertEquals( spl_object_hash($db1), spl_object_hash($db3), "Database instances should be the same, but returned different spl_object_hash" ); } /** * @covers SimpleSAML\Database::getInstance * @covers SimpleSAML\Database::generateInstanceId * @covers SimpleSAML\Database::__construct * @covers SimpleSAML\Database::connect * @covers SimpleSAML\Database::getSlave * @test */ public function slaves() { $getSlave = self::getMethod('getSlave'); $master = spl_object_hash(PHPUnit_Framework_Assert::readAttribute($this->db, 'dbMaster')); $slave = spl_object_hash($getSlave->invokeArgs($this->db, array())); $this->assertTrue(($master == $slave), "getSlave should have returned the master database object"); $config = array( 'database.dsn' => 'sqlite::memory:', 'database.username' => null, 'database.password' => null, 'database.prefix' => 'phpunit_', 'database.persistent' => true, 'database.slaves' => array( array( 'dsn' => 'sqlite::memory:', 'username' => null, 'password' => null, ), ), ); $sspConfiguration = new SimpleSAML_Configuration($config, "test/SimpleSAML/DatabaseTest.php"); $msdb = SimpleSAML\Database::getInstance($sspConfiguration); $slaves = PHPUnit_Framework_Assert::readAttribute($msdb, 'dbSlaves'); $gotSlave = spl_object_hash($getSlave->invokeArgs($msdb, array())); $this->assertEquals( spl_object_hash($slaves[0]), $gotSlave, "getSlave should have returned a slave database object" ); } /** * @covers SimpleSAML\Database::applyPrefix * @test */ public function prefix() { $prefix = $this->config->getString('database.prefix'); $table = "saml20_idp_hosted"; $pftable = $this->db->applyPrefix($table); $this->assertEquals($prefix.$table, $pftable, "Did not properly apply the table prefix"); } /** * @covers SimpleSAML\Database::write * @covers SimpleSAML\Database::read * @covers SimpleSAML\Database::exec * @covers SimpleSAML\Database::query * @test */ public function querying() { $table = $this->db->applyPrefix("sspdbt"); $this->assertEquals($this->config->getString('database.prefix')."sspdbt", $table); $this->db->write( "CREATE TABLE IF NOT EXISTS $table (ssp_key INT(16) NOT NULL, ssp_value TEXT NOT NULL)", false ); $query1 = $this->db->read("SELECT * FROM $table"); $this->assertEquals(0, $query1->fetch(), "Table $table is not empty when it should be."); $ssp_key = time(); $ssp_value = md5(rand(0, 10000)); $stmt = $this->db->write( "INSERT INTO $table (ssp_key, ssp_value) VALUES (:ssp_key, :ssp_value)", array('ssp_key' => array($ssp_key, PDO::PARAM_INT), 'ssp_value' => $ssp_value) ); $this->assertEquals(1, $stmt, "Could not insert data into $table."); $query2 = $this->db->read("SELECT * FROM $table WHERE ssp_key = :ssp_key", array('ssp_key' => $ssp_key)); $data = $query2->fetch(); $this->assertEquals($data['ssp_value'], $ssp_value, "Inserted data doesn't match what is in the database"); } /** * @covers SimpleSAML\Database::read * @covers SimpleSAML\Database::query * @expectedException Exception * @test */ public function readFailure() { $table = $this->db->applyPrefix("sspdbt"); $this->assertEquals($this->config->getString('database.prefix')."sspdbt", $table); $this->db->read("SELECT * FROM $table"); } /** * @covers SimpleSAML\Database::write * @covers SimpleSAML\Database::exec * @expectedException Exception * @test */ public function noSuchTable() { $this->db->write("DROP TABLE phpunit_nonexistent", false); } public function tearDown() { $table = $this->db->applyPrefix("sspdbt"); $this->db->write("DROP TABLE IF EXISTS $table", false); unset($this->config); unset($this->db); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/ModuleTest.php0000644000000000000000000000651113245225037021577 0ustar rootrootassertTrue(Module::isModuleEnabled('core')); } /** * Test for SimpleSAML\Module::getModuleDir(). */ public function testGetModuleDir() { // test for the most basic functionality $this->assertEquals( dirname(dirname(dirname(dirname(__FILE__)))).'/modules/module', Module::getModuleDir('module') ); } /** * Test for SimpleSAML\Module::getModuleURL(). */ public function testGetModuleURL() { \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => 'https://example.com/simplesaml/' ), '', 'simplesaml'); $this->assertEquals( 'https://example.com/simplesaml/module.php/module/script.php', Module::getModuleURL('module/script.php') ); $this->assertEquals( 'https://example.com/simplesaml/module.php/module/script.php?param1=value1¶m2=value2', Module::getModuleURL('module/script.php', array( 'param1' => 'value1', 'param2' => 'value2', )) ); } /** * Test for SimpleSAML\Module::getModules(). */ public function testGetModules() { $this->assertGreaterThan(0, count(Module::getModules())); } /** * Test for SimpleSAML\Module::resolveClass(). It will make sure that an exception is thrown if we are not asking * for a class inside a module (that is, there is no colon separating the name of the module and the name of the * class). * * @expectedException \Exception */ public function testResolveClassNoModule() { Module::resolveClass('nomodule', ''); } /** * Test for SimpleSAML\Module::resolveClass(). It will make sure that an exception is thrown if the class we are * asking for cannot be found. * * @expectedException \Exception */ public function testResolveClassNotFound() { Module::resolveClass('core:Missing', ''); } /** * Test for SimpleSAML\Module::resolveClass(). It will make sure that an exception is thrown if the class we are * asking for can be resolved, but does not extend a given class. * * @expectedException \Exception */ public function testResolveClassNotSubclass() { Module::resolveClass('core:PHP', 'Auth_Process', '\Exception'); } /** * Test for SimpleSAML\Module::resolveClass(). It covers all the valid use cases. */ public function tesstResolveClass() { // most basic test $this->assertEquals('sspmod_core_ACL', Module::resolveClass('core:ACL', '')); // test for the $type parameter correctly translated into a path $this->assertEquals('sspmod_core_Auth_Process_PHP', Module::resolveClass('core:PHP', 'Auth_Process')); // test for valid subclasses $this->assertEquals('sspmod_core_Auth_Process_PHP', Module::resolveClass( 'core:PHP', 'Auth_Process', 'SimpleSAML_Auth_ProcessingFilter' )); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Locale/0000755000000000000000000000000013245225037020175 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/Locale/LanguageTest.php0000644000000000000000000001265213245225037023277 0ustar rootrootassertEquals('en', $l->getDefaultLanguage()); // test defaults coming from configuration $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.available' => array('en', 'es', 'nn'), 'language.default' => 'es', )); $l = new Language($c); $this->assertEquals('es', $l->getDefaultLanguage()); } /** * Test SimpleSAML\Locale\Language::getLanguageCookie(). */ public function testGetLanguageCookie() { // test it works when no cookie is set \SimpleSAML_Configuration::loadFromArray(array(), '', 'simplesaml'); $this->assertNull(Language::getLanguageCookie()); // test that it works fine with defaults \SimpleSAML_Configuration::loadFromArray(array(), '', 'simplesaml'); $_COOKIE['language'] = 'en'; $this->assertEquals('en', Language::getLanguageCookie()); // test that it works with non-defaults \SimpleSAML_Configuration::loadFromArray(array( 'language.available' => array('en', 'es', 'nn'), 'language.cookie.name' => 'xyz' ), '', 'simplesaml'); $_COOKIE['xyz'] = 'Es'; // test values are converted to lowercase too $this->assertEquals('es', Language::getLanguageCookie()); } /** * Test SimpleSAML\Locale\Language::getLanguageList(). */ public function testGetLanguageListNoConfig() { // test defaults $c = \SimpleSAML_Configuration::loadFromArray(array(), '', 'simplesaml'); $l = new Language($c); $l->setLanguage('en'); $this->assertEquals(array('en' => true), $l->getLanguageList()); } /** * Test SimpleSAML\Locale\Language::getLanguageList(). */ public function testGetLanguageListCorrectConfig() { // test langs from from language_names $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.available' => array('en', 'nn', 'es'), ), '', 'simplesaml'); $l = new Language($c); $l->setLanguage('es'); $this->assertEquals(array( 'en' => false, 'es' => true, 'nn' => false, ), $l->getLanguageList()); } /** * Test SimpleSAML\Locale\Language::getLanguageList(). */ public function testGetLanguageListIncorrectConfig() { // test non-existent langs $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.available' => array('foo', 'bar'), ), '', 'simplesaml'); $l = new Language($c); $l->setLanguage('foo'); $this->assertEquals(array('en' => true), $l->getLanguageList()); } /** * Test SimpleSAML\Locale\Language::getLanguageParameterName(). */ public function testGetLanguageParameterName() { // test for default configuration $c = \SimpleSAML_Configuration::loadFromArray(array(), '', 'simplesaml'); $l = new Language($c); $this->assertEquals('language', $l->getLanguageParameterName()); // test for valid configuration $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.parameter.name' => 'xyz' ), '', 'simplesaml'); $l = new Language($c); $this->assertEquals('xyz', $l->getLanguageParameterName()); } /** * Test SimpleSAML\Locale\Language::isLanguageRTL(). */ public function testIsLanguageRTL() { // test defaults $c = \SimpleSAML_Configuration::loadFromArray(array(), '', 'simplesaml'); $l = new Language($c); $l->setLanguage('en'); $this->assertFalse($l->isLanguageRTL()); // test non-defaults, non-RTL $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.rtl' => array('foo', 'bar'), ), '', 'simplesaml'); $l = new Language($c); $l->setLanguage('en'); $this->assertFalse($l->isLanguageRTL()); // test non-defaults, RTL $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.available' => array('en', 'nn', 'es'), 'language.rtl' => array('nn', 'es'), ), '', 'simplesaml'); $l = new Language($c); $l->setLanguage('es'); $this->assertTrue($l->isLanguageRTL()); } /** * Test SimpleSAML\Locale\Language::setLanguage(). */ public function testSetLanguage() { // test with valid configuration, no cookies set $c = \SimpleSAML_Configuration::loadFromArray(array( 'language.available' => array('en', 'nn', 'es'), 'language.parameter.name' => 'xyz', 'language.parameter.setcookie' => false, ), '', 'simplesaml'); $_GET['xyz'] = 'Es'; // test also that lang code is transformed to lower caps $l = new Language($c); $this->assertEquals('es', $l->getLanguage()); // test with valid configuration, no cookies, language set unavailable $_GET['xyz'] = 'unavailable'; $l = new Language($c); $this->assertEquals('en', $l->getLanguage()); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Locale/TranslateTest.php0000644000000000000000000000173413245225037023510 0ustar rootrootassertEquals($testString, $t->noop($testString)); } /** * Test SimpleSAML\Locale\Translate::t(). */ public function testTFallback() { $c = \SimpleSAML_Configuration::loadFromArray(array()); $t = new Translate($c); $testString = 'Blablabla'; // $fallbackdefault = true $result = 'not translated ('.$testString.')'; $this->assertEquals($result, $t->t($testString)); // $fallbackdefault = false, should be a noop $this->assertEquals($testString, $t->t($testString, array(), false)); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Locale/LocalizationTest.php0000644000000000000000000000275713245225037024211 0ustar rootroot 'SimpleSAMLphp') ); $l = new Localization($c); $this->assertTrue($l->isI18NBackendDefault()); $this->assertEquals(Localization::DEFAULT_DOMAIN, 'messages'); } /** * Test SimpleSAML\Locale\Localization::activateDomain(). */ public function testAddDomain() { $c = \SimpleSAML_Configuration::loadFromArray( array('language.i18n.backend' => 'gettext/gettext') ); $l = new Localization($c); $newDomain = 'test'; $newDomainLocaleDir = $l->getLocaleDir(); $l->addDomain($newDomainLocaleDir, $newDomain); $registeredDomains = $l->getRegisteredDomains(); $this->assertArrayHasKey($newDomain, $registeredDomains); $this->assertEquals($registeredDomains[$newDomain], $newDomainLocaleDir); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/0000755000000000000000000000000013245225037020076 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/TimeTest.php0000644000000000000000000001427113245225037022352 0ustar rootrootassertEquals('2016-03-03T14:48:05Z', Time::generateTimestamp(1457016485)); // test timestamp generation for current time $this->assertRegExp('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/', Time::generateTimestamp()); } /** * Test the SimpleSAML\Utils\Time::initTimezone() method. * * @covers SimpleSAML\Utils\Time::initTimezone */ public function testInitTimezone() { $tz = 'UTC'; $os = @date_default_timezone_get(); if ($os === 'UTC') { // avoid collisions $tz = 'Europe/Oslo'; } // test guessing timezone from the OS \SimpleSAML_Configuration::loadFromArray(array('timezone' => null), '[ARRAY]', 'simplesaml'); @Time::initTimezone(); $this->assertEquals($os, @date_default_timezone_get()); // clear initialization $c = new \ReflectionProperty('\SimpleSAML\Utils\Time', 'tz_initialized'); $c->setAccessible(true); $c->setValue(false); // test unknown timezone \SimpleSAML_Configuration::loadFromArray(array('timezone' => 'INVALID'), '[ARRAY]', 'simplesaml'); try { @Time::initTimezone(); $this->fail('Failed to recognize an invalid timezone.'); } catch (\SimpleSAML_Error_Exception $e) { $this->assertEquals('Invalid timezone set in the "timezone" option in config.php.', $e->getMessage()); } // test a valid timezone \SimpleSAML_Configuration::loadFromArray(array('timezone' => $tz), '[ARRAY]', 'simplesaml'); @Time::initTimezone(); $this->assertEquals($tz, @date_default_timezone_get()); // make sure initialization happens only once \SimpleSAML_Configuration::loadFromArray(array('timezone' => 'Europe/Madrid'), '[ARRAY]', 'simplesaml'); @Time::initTimezone(); $this->assertEquals($tz, @date_default_timezone_get()); } /** * Test the SimpleSAML\Utils\Time::parseDuration() method. * * @covers SimpleSAML\Utils\Time::parseDuration */ public function testParseDuration() { // set up base date and time, and fixed durations from there $base = gmmktime(0, 0, 0, 1, 1, 2000); $second = gmmktime(0, 0, 1, 1, 1, 2000); // +1 sec $minute = gmmktime(0, 1, 0, 1, 1, 2000); // +1 min $hour = gmmktime(1, 0, 0, 1, 1, 2000); // +1 hour $day = gmmktime(0, 0, 0, 1, 2, 2000); // +1 day $week = gmmktime(0, 0, 0, 1, 8, 2000); // +1 week $month = gmmktime(0, 0, 0, 2, 1, 2000); // +1 month $year = gmmktime(0, 0, 0, 1, 1, 2001); // +1 year // corner cases $manymonths = gmmktime(0, 0, 0, 3, 1, 2001); // +14 months = +1 year +2 months $negmonths = gmmktime(0, 0, 0, 10, 1, 1999); // -3 months = -1 year +9 months // test valid duration with timestamp and zeroes $this->assertEquals($base + (60 * 60) + 60 + 1, Time::parseDuration('P0Y0M0DT1H1M1S', $base)); // test seconds $this->assertEquals($second, Time::parseDuration('PT1S', $base), "Failure checking for 1 second duration."); // test minutes $this->assertEquals($minute, Time::parseDuration('PT1M', $base), "Failure checking for 1 minute duration."); // test hours $this->assertEquals($hour, Time::parseDuration('PT1H', $base), "Failure checking for 1 hour duration."); // test days $this->assertEquals($day, Time::parseDuration('P1D', $base), "Failure checking for 1 day duration."); // test weeks $this->assertEquals($week, Time::parseDuration('P1W', $base), "Failure checking for 1 week duration."); // test month $this->assertEquals($month, Time::parseDuration('P1M', $base), "Failure checking for 1 month duration."); // test year $this->assertEquals($year, Time::parseDuration('P1Y', $base), "Failure checking for 1 year duration."); // test months > 12 $this->assertEquals( $manymonths, Time::parseDuration('P14M', $base), "Failure checking for 14 months duration (1 year and 2 months)." ); // test negative months $this->assertEquals( $negmonths, Time::parseDuration('-P3M', $base), "Failure checking for -3 months duration (-1 year + 9 months)." ); // test from current time $now = time(); $this->assertGreaterThanOrEqual( $now + 60, Time::parseDuration('PT1M'), "Failure testing for 1 minute over current time." ); // test invalid input parameters try { // invalid duration Time::parseDuration(0); $this->fail("Did not fail with invalid duration parameter."); } catch (\InvalidArgumentException $e) { $this->assertEquals('Invalid input parameters', $e->getMessage()); } try { // invalid timestamp Time::parseDuration('', array()); $this->fail("Did not fail with invalid timestamp parameter."); } catch (\InvalidArgumentException $e) { $this->assertEquals('Invalid input parameters', $e->getMessage()); } // test invalid durations try { // invalid string Time::parseDuration('abcdefg'); $this->fail("Did not fail with invalid ISO 8601 duration."); } catch (\InvalidArgumentException $e) { $this->assertStringStartsWith('Invalid ISO 8601 duration: ', $e->getMessage()); } try { // missing T delimiter Time::parseDuration('P1S'); $this->fail("Did not fail with duration missing T delimiter."); } catch (\InvalidArgumentException $e) { $this->assertStringStartsWith('Invalid ISO 8601 duration: ', $e->getMessage()); } } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/RandomTest.php0000644000000000000000000000111013245225037022660 0ustar rootrootassertStringStartsWith('_', Random::generateID()); // check the length $this->assertEquals(Random::ID_LENGTH, strlen(Random::generateID())); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/NetTest.php0000644000000000000000000000723613245225037022205 0ustar rootrootassertFalse(Net::ipCIDRcheck('127.0.0.0', '127.0.0.1')); // check wrong CIDR w/ mask $this->assertFalse(Net::ipCIDRcheck('127.0.0.256/24', '127.0.0.1')); // check wrong IP $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/24', '127.0.0')); $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/24', '127.0.0.*')); // check limits for standard classes $this->assertTrue(Net::ipCIDRcheck('127.0.0.0/24', '127.0.0.0')); $this->assertTrue(Net::ipCIDRcheck('127.0.0.0/24', '127.0.0.255')); $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/24', '127.0.0.256')); $this->assertTrue(Net::ipCIDRcheck('127.0.0.0/16', '127.0.0.0')); $this->assertTrue(Net::ipCIDRcheck('127.0.0.0/16', '127.0.255.255')); $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/16', '127.0.255.256')); $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/16', '127.0.256.255')); // check limits for non-standard classes $this->assertTrue(Net::ipCIDRcheck('127.0.0.0/23', '127.0.0.0')); $this->assertTrue(Net::ipCIDRcheck('127.0.0.0/23', '127.0.1.255')); $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/23', '127.0.1.256')); $this->assertFalse(Net::ipCIDRcheck('127.0.0.0/23', '127.0.2.0')); } /** * Test IPv6 support in SimpleSAML\Utils\Net::ipCIDRcheck. * * @covers SimpleSAML\Utils\Net::ipCIDRcheck */ public function testIpv6CIDRcheck() { // check CIDR w/o mask $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::', '2001:0DB8::1')); // check wrong CIDR w/ mask $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/128', '2001:0DB8::1')); // check wrong IP $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/128', '2001:0DB8::Z')); // check limits for standard classes $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/128', '2001:0DB8:0000:0000:0000:0000:0000:0000')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/128', '2001:0DB8::0')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/128', '2001:0DB8::1')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/112', '2001:0DB8::1')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/112', '2001:0DB8::1:1')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/112', '2001:0DB8::FFFF')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/112', '2001:0DB8::1:FFFF')); // check limits for non-standard classes $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/108', '2001:0DB8::1:1')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/108', '2001:0DB8::F:1')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/108', '2001:0DB8::FF:1')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/108', '2001:0DB8::1FF:1')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/108', '2001:0DB8::FFFF:1')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/104', '2001:0DB8::1:1')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/104', '2001:0DB8::F:1')); $this->assertTrue(Net::ipCIDRcheck('2001:0DB8::/104', '2001:0DB8::FF:1')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/104', '2001:0DB8::1FF:1')); $this->assertFalse(Net::ipCIDRcheck('2001:0DB8::/104', '2001:0DB8::FFFF:1')); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/ArraysTest.php0000644000000000000000000000701513245225037022713 0ustar rootrootassertEquals($array, Arrays::arrayize($array)); // check non-empty array as input $array = array('key' => 'value'); $this->assertEquals($array, Arrays::arrayize($array)); // check indexes are ignored when input is an array $this->assertArrayNotHasKey('invalid', Arrays::arrayize($array, 'invalid')); // check default index $expected = array('string'); $this->assertEquals($expected, Arrays::arrayize($expected[0])); // check string index $index = 'key'; $expected = array($index => 'string'); $this->assertEquals($expected, Arrays::arrayize($expected[$index], $index)); } /** * Test the transpose() function. */ public function testTranspose() { // check not array $this->assertFalse(Arrays::transpose('string')); // check bad arrays $this->assertFalse( Arrays::transpose(array('1', '2', '3')), 'Invalid two-dimensional array was accepted' ); $this->assertFalse( Arrays::transpose(array('1' => 0, '2' => '0', '3' => array(0))), 'Invalid elements on a two-dimensional array were accepted' ); // check array with numerical keys $array = array( 'key1' => array( 'value1' ), 'key2' => array( 'value1', 'value2' ) ); $transposed = array( array( 'key1' => 'value1', 'key2' => 'value1' ), array( 'key2' => 'value2' ) ); $this->assertEquals( $transposed, Arrays::transpose($array), 'Unexpected result of transpose()' ); // check array with string keys $array = array( 'key1' => array( 'subkey1' => 'value1' ), 'key2' => array( 'subkey1' => 'value1', 'subkey2' => 'value2' ) ); $transposed = array( 'subkey1' => array( 'key1' => 'value1', 'key2' => 'value1' ), 'subkey2' => array( 'key2' => 'value2' ) ); $this->assertEquals( $transposed, Arrays::transpose($array), 'Unexpected result of transpose()' ); // check array with no keys in common between sub arrays $array = array( 'key1' => array( 'subkey1' => 'value1' ), 'key2' => array( 'subkey2' => 'value1', 'subkey3' => 'value2' ) ); $transposed = array( 'subkey1' => array( 'key1' => 'value1', ), 'subkey2' => array( 'key2' => 'value1' ), 'subkey3' => array( 'key2' => 'value2' ) ); $this->assertEquals( $transposed, Arrays::transpose($array), 'Unexpected result of transpose()' ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/HTTPTest.php0000644000000000000000000003523113245225037022232 0ustar rootroot 'bar', 'bar' => 'foo', ); $this->assertEquals($url.'?foo=bar&bar=foo', HTTP::addURLParameters($url, $params)); $url = 'http://example.com/?'; $params = array( 'foo' => 'bar', 'bar' => 'foo', ); $this->assertEquals($url.'foo=bar&bar=foo', HTTP::addURLParameters($url, $params)); $url = 'http://example.com/?foo=bar'; $params = array( 'bar' => 'foo', ); $this->assertEquals($url.'&bar=foo', HTTP::addURLParameters($url, $params)); } /** * Test SimpleSAML\Utils\HTTP::guessBasePath(). */ public function testGuessBasePath() { $original = $_SERVER; $_SERVER['REQUEST_URI'] = '/simplesaml/module.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/simplesaml/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/simplesaml/module.php/some/path/to/other/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/simplesaml/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/module.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/module.php/some/path/to/other/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/some/path/module.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/some/path/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/some/path/module.php/some/path/to/other/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/module.php'; $this->assertEquals('/some/path/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/some/dir/in/www/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/some/dir/in/www/script.php'; $this->assertEquals('/', HTTP::guessBasePath()); $_SERVER['REQUEST_URI'] = '/simplesaml/some/dir/in/www/script.php'; $_SERVER['SCRIPT_FILENAME'] = '/some/path/simplesamlphp/www/some/dir/in/www/script.php'; $this->assertEquals('/simplesaml/', HTTP::guessBasePath()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getSelfHost() with and without custom port. */ public function testGetSelfHost() { $original = $_SERVER; \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => '', ), '[ARRAY]', 'simplesaml'); $_SERVER['SERVER_PORT'] = '80'; $this->assertEquals('localhost', HTTP::getSelfHost()); $_SERVER['SERVER_PORT'] = '3030'; $this->assertEquals('localhost', HTTP::getSelfHost()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getSelfHostWithPort(), with and without custom port. */ public function testGetSelfHostWithPort() { $original = $_SERVER; \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => '', ), '[ARRAY]', 'simplesaml'); // standard port for HTTP $_SERVER['SERVER_PORT'] = '80'; $this->assertEquals('localhost', HTTP::getSelfHostWithNonStandardPort()); // non-standard port $_SERVER['SERVER_PORT'] = '3030'; $this->assertEquals('localhost:3030', HTTP::getSelfHostWithNonStandardPort()); // standard port for HTTPS $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = '443'; $this->assertEquals('localhost', HTTP::getSelfHostWithNonStandardPort()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::getSelfURL(). */ public function testGetSelfURLMethods() { $original = $_SERVER; /* * Test a URL pointing to a script that's not part of the public interface. This allows us to test calls to * getSelfURL() from scripts outside of SimpleSAMLphp */ \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => 'http://example.com/simplesaml/', ), '[ARRAY]', 'simplesaml'); $url = 'https://example.com/app/script.php/some/path?foo=bar'; $this->setupEnvFromURL($url); $_SERVER['SCRIPT_FILENAME'] = '/var/www/app/script.php'; $this->assertEquals($url, HTTP::getSelfURL()); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/app/script.php/some/path', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a request URI that doesn't match the current script $cfg = \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => 'https://example.org/simplesaml/', ), '[ARRAY]', 'simplesaml'); $baseDir = $cfg->getBaseDir(); $_SERVER['SCRIPT_FILENAME'] = $baseDir.'www/module.php'; $this->setupEnvFromURL('http://www.example.com/protected/resource.asp?foo=bar'); $this->assertEquals('http://www.example.com/protected/resource.asp?foo=bar', HTTP::getSelfURL()); $this->assertEquals('http://www.example.com', HTTP::getSelfURLHost()); $this->assertEquals('http://www.example.com/protected/resource.asp', HTTP::getSelfURLNoQuery()); $this->assertFalse(HTTP::isHTTPS()); $this->assertEquals('example.org', HTTP::getSelfHostWithNonStandardPort()); $this->assertEquals('http://www.example.com', HTTP::getSelfURLHost()); // test a valid, full URL, based on a full URL in the configuration \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => 'https://example.com/simplesaml/', ), '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('http://www.example.org/module.php/module/file.php?foo=bar'); $this->assertEquals( 'https://example.com/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a full URL *without* a trailing slash in the configuration \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => 'https://example.com/simplesaml', ), '[ARRAY]', 'simplesaml'); $this->assertEquals( 'https://example.com/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a full URL *without* a path in the configuration \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => 'https://example.com', ), '[ARRAY]', 'simplesaml'); $this->assertEquals( 'https://example.com/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.com', HTTP::getSelfURLHost()); $this->assertEquals('https://example.com/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a relative path in the configuration \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => '/simplesaml/', ), '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('http://www.example.org/simplesaml/module.php/module/file.php?foo=bar'); $this->assertEquals( 'http://www.example.org/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('http://www.example.org', HTTP::getSelfURLHost()); $this->assertEquals('http://www.example.org/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertFalse(HTTP::isHTTPS()); $this->assertEquals('http://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a relative path in the configuration and a non standard port \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => '/simplesaml/', ), '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('http://example.org:8080/simplesaml/module.php/module/file.php?foo=bar'); $this->assertEquals( 'http://example.org:8080/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('http://example.org:8080', HTTP::getSelfURLHost()); $this->assertEquals('http://example.org:8080/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery()); $this->assertFalse(HTTP::isHTTPS()); $this->assertEquals('http://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); // test a valid, full URL, based on a relative path in the configuration, a non standard port and HTTPS \SimpleSAML_Configuration::loadFromArray(array( 'baseurlpath' => '/simplesaml/', ), '[ARRAY]', 'simplesaml'); $this->setupEnvFromURL('https://example.org:8080/simplesaml/module.php/module/file.php?foo=bar'); $this->assertEquals( 'https://example.org:8080/simplesaml/module.php/module/file.php?foo=bar', HTTP::getSelfURL() ); $this->assertEquals('https://example.org:8080', HTTP::getSelfURLHost()); $this->assertEquals( 'https://example.org:8080/simplesaml/module.php/module/file.php', HTTP::getSelfURLNoQuery() ); $this->assertTrue(HTTP::isHTTPS()); $this->assertEquals('https://'.HTTP::getSelfHostWithNonStandardPort(), HTTP::getSelfURLHost()); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), without regex. */ public function testCheckURLAllowedWithoutRegex() { $original = $_SERVER; \SimpleSAML_Configuration::loadFromArray(array( 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), 'trusted.url.regex' => false, ), '[ARRAY]', 'simplesaml'); $_SERVER['REQUEST_URI'] = '/module.php'; $allowed = array( 'https://sp.example.com/', 'http://sp.example.com/', 'https://app.example.com/', 'http://app.example.com/', ); foreach ($allowed as $url) { $this->assertEquals(HTTP::checkURLAllowed($url), $url); } $this->setExpectedException('SimpleSAML_Error_Exception'); HTTP::checkURLAllowed('https://evil.com'); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), with regex. */ public function testCheckURLAllowedWithRegex() { $original = $_SERVER; \SimpleSAML_Configuration::loadFromArray(array( 'trusted.url.domains' => array('.*\.example\.com'), 'trusted.url.regex' => true, ), '[ARRAY]', 'simplesaml'); $_SERVER['REQUEST_URI'] = '/module.php'; $allowed = array( 'https://sp.example.com/', 'http://sp.example.com/', 'https://app1.example.com/', 'http://app1.example.com/', 'https://app2.example.com/', 'http://app2.example.com/', ); foreach ($allowed as $url) { $this->assertEquals(HTTP::checkURLAllowed($url), $url); } $this->setExpectedException('SimpleSAML_Error_Exception'); HTTP::checkURLAllowed('https://evil.com'); $_SERVER = $original; } /** * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), with the regex as a * subdomain of an evil domain. */ public function testCheckURLAllowedWithRegexWithoutDelimiters() { $original = $_SERVER; \SimpleSAML_Configuration::loadFromArray(array( 'trusted.url.domains' => array('app\.example\.com'), 'trusted.url.regex' => true, ), '[ARRAY]', 'simplesaml'); $_SERVER['REQUEST_URI'] = '/module.php'; $this->setExpectedException('SimpleSAML_Error_Exception'); HTTP::checkURLAllowed('https://app.example.com.evil.com'); $_SERVER = $original; } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/ConfigTest.php0000644000000000000000000000271013245225037022654 0ustar rootrootassertEquals($configDir, dirname(dirname(dirname(dirname(__DIR__)))) . '/config'); } /** * Test valid dir specified by env var overrides default config dir */ public function testEnvVariableConfigDir() { putenv('SIMPLESAMLPHP_CONFIG_DIR=' . __DIR__); $configDir = Config::getConfigDir(); $this->assertEquals($configDir, __DIR__); } /** * Test invalid dir specified by env var results in a thrown exception */ public function testInvalidEnvVariableConfigDirThrowsException() { // I used a random hash to ensure this test directory is always invalid $invalidDir = __DIR__ . '/e9826ad19cbc4f5bf20c0913ffcd2ce6'; putenv('SIMPLESAMLPHP_CONFIG_DIR=' . $invalidDir); $this->setExpectedException( 'InvalidArgumentException', 'Config directory specified by environment variable SIMPLESAMLPHP_CONFIG_DIR is not a directory. ' . 'Given: "' . $invalidDir . '"' ); Config::getConfigDir(); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/Config/0000755000000000000000000000000013245225037021303 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/Config/MetadataTest.php0000644000000000000000000002241013245225037024373 0ustar rootrootassertEquals('Invalid input parameters', $e->getMessage()); } // test missing type $contact = array( 'name' => 'John Doe' ); try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertStringStartsWith('"contactType" is mandatory and must be one of ', $e->getMessage()); } // test invalid type $contact = array( 'contactType' => 'invalid' ); try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertStringStartsWith('"contactType" is mandatory and must be one of ', $e->getMessage()); } // test all valid contact types foreach (Metadata::$VALID_CONTACT_TYPES as $type) { $contact = array( 'contactType' => $type ); $parsed = Metadata::getContact($contact); $this->assertArrayHasKey('contactType', $parsed); $this->assertArrayNotHasKey('givenName', $parsed); $this->assertArrayNotHasKey('surName', $parsed); } // test basic name parsing $contact = array( 'contactType' => 'technical', 'name' => 'John Doe' ); $parsed = Metadata::getContact($contact); $this->assertArrayNotHasKey('name', $parsed); $this->assertArrayHasKey('givenName', $parsed); $this->assertArrayHasKey('surName', $parsed); $this->assertEquals('John', $parsed['givenName']); $this->assertEquals('Doe', $parsed['surName']); // test comma-separated names $contact = array( 'contactType' => 'technical', 'name' => 'Doe, John' ); $parsed = Metadata::getContact($contact); $this->assertArrayHasKey('givenName', $parsed); $this->assertArrayHasKey('surName', $parsed); $this->assertEquals('John', $parsed['givenName']); $this->assertEquals('Doe', $parsed['surName']); // test long names $contact = array( 'contactType' => 'technical', 'name' => 'John Fitzgerald Doe Smith' ); $parsed = Metadata::getContact($contact); $this->assertArrayNotHasKey('name', $parsed); $this->assertArrayHasKey('givenName', $parsed); $this->assertArrayNotHasKey('surName', $parsed); $this->assertEquals('John Fitzgerald Doe Smith', $parsed['givenName']); // test comma-separated long names $contact = array( 'contactType' => 'technical', 'name' => 'Doe Smith, John Fitzgerald' ); $parsed = Metadata::getContact($contact); $this->assertArrayNotHasKey('name', $parsed); $this->assertArrayHasKey('givenName', $parsed); $this->assertArrayHasKey('surName', $parsed); $this->assertEquals('John Fitzgerald', $parsed['givenName']); $this->assertEquals('Doe Smith', $parsed['surName']); // test givenName $contact = array( 'contactType' => 'technical', ); $invalid_types = array(0, array(0), 0.1, true, false); foreach ($invalid_types as $type) { $contact['givenName'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals('"givenName" must be a string and cannot be empty.', $e->getMessage()); } } // test surName $contact = array( 'contactType' => 'technical', ); $invalid_types = array(0, array(0), 0.1, true, false); foreach ($invalid_types as $type) { $contact['surName'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals('"surName" must be a string and cannot be empty.', $e->getMessage()); } } // test company $contact = array( 'contactType' => 'technical', ); $invalid_types = array(0, array(0), 0.1, true, false); foreach ($invalid_types as $type) { $contact['company'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals('"company" must be a string and cannot be empty.', $e->getMessage()); } } // test emailAddress $contact = array( 'contactType' => 'technical', ); $invalid_types = array(0, 0.1, true, false, array()); foreach ($invalid_types as $type) { $contact['emailAddress'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals( '"emailAddress" must be a string or an array and cannot be empty.', $e->getMessage() ); } } $invalid_types = array(array("string", true), array("string", 0)); foreach ($invalid_types as $type) { $contact['emailAddress'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals( 'Email addresses must be a string and cannot be empty.', $e->getMessage() ); } } $valid_types = array('email@example.com', array('email1@example.com', 'email2@example.com')); foreach ($valid_types as $type) { $contact['emailAddress'] = $type; $parsed = Metadata::getContact($contact); $this->assertEquals($type, $parsed['emailAddress']); } // test telephoneNumber $contact = array( 'contactType' => 'technical', ); $invalid_types = array(0, 0.1, true, false, array()); foreach ($invalid_types as $type) { $contact['telephoneNumber'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals( '"telephoneNumber" must be a string or an array and cannot be empty.', $e->getMessage() ); } } $invalid_types = array(array("string", true), array("string", 0)); foreach ($invalid_types as $type) { $contact['telephoneNumber'] = $type; try { Metadata::getContact($contact); } catch (\InvalidArgumentException $e) { $this->assertEquals('Telephone numbers must be a string and cannot be empty.', $e->getMessage()); } } $valid_types = array('1234', array('1234', '5678')); foreach ($valid_types as $type) { $contact['telephoneNumber'] = $type; $parsed = Metadata::getContact($contact); $this->assertEquals($type, $parsed['telephoneNumber']); } // test completeness $contact = array(); foreach (Metadata::$VALID_CONTACT_OPTIONS as $option) { $contact[$option] = 'string'; } $contact['contactType'] = 'technical'; $contact['name'] = 'to_be_removed'; $contact['attributes'] = array('test' => 'testval'); $parsed = Metadata::getContact($contact); foreach (array_keys($parsed) as $key) { $this->assertEquals($parsed[$key], $contact[$key]); } $this->assertArrayNotHasKey('name', $parsed); } /** * Test \SimpleSAML\Utils\Config\Metadata::isHiddenFromDiscovery(). */ public function testIsHiddenFromDiscovery() { // test for success $metadata = array( 'EntityAttributes' => array( Metadata::$ENTITY_CATEGORY => array( Metadata::$HIDE_FROM_DISCOVERY, ), ), ); $this->assertTrue(Metadata::isHiddenFromDiscovery($metadata)); // test for failures $this->assertFalse(Metadata::isHiddenFromDiscovery(array('foo'))); $this->assertFalse(Metadata::isHiddenFromDiscovery(array( 'EntityAttributes' => 'bar', ))); $this->assertFalse(Metadata::isHiddenFromDiscovery(array( 'EntityAttributes' => array(), ))); $this->assertFalse(Metadata::isHiddenFromDiscovery(array( 'EntityAttributes' => array( Metadata::$ENTITY_CATEGORY => '', ), ))); $this->assertFalse(Metadata::isHiddenFromDiscovery(array( 'EntityAttributes' => array( Metadata::$ENTITY_CATEGORY => array(), ), ))); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/CryptoTest.php0000644000000000000000000003722113245225037022734 0ustar rootrootroot = vfsStream::setup( self::ROOTDIRNAME, null, array( self::DEFAULTCERTDIR => array(), ) ); $this->root_directory = vfsStream::url(self::ROOTDIRNAME); $this->certdir = $this->root_directory.DIRECTORY_SEPARATOR.self::DEFAULTCERTDIR; } /** * Test invalid input provided to the aesDecrypt() method. * * @expectedException \InvalidArgumentException * * @covers \SimpleSAML\Utils\Crypto::_aesDecrypt */ public function testAesDecryptBadInput() { $m = new \ReflectionMethod('\SimpleSAML\Utils\Crypto', '_aesDecrypt'); $m->setAccessible(true); $m->invokeArgs(null, array(array(), 'SECRET')); } /** * Test invalid input provided to the aesEncrypt() method. * * @expectedException \InvalidArgumentException * * @covers \SimpleSAML\Utils\Crypto::_aesEncrypt */ public function testAesEncryptBadInput() { $m = new \ReflectionMethod('\SimpleSAML\Utils\Crypto', '_aesEncrypt'); $m->setAccessible(true); $m->invokeArgs(null, array(array(), 'SECRET')); } /** * Test that aesDecrypt() works properly, being able to decrypt some previously known (and correct) * ciphertext. * * @covers \SimpleSAML\Utils\Crypto::_aesDecrypt */ public function testAesDecrypt() { if (!extension_loaded('openssl')) { $this->setExpectedException('\SimpleSAML_Error_Exception'); } $secret = 'SUPER_SECRET_SALT'; $m = new \ReflectionMethod('\SimpleSAML\Utils\Crypto', '_aesDecrypt'); $m->setAccessible(true); $plaintext = 'SUPER_SECRET_TEXT'; $ciphertext = 'uR2Yu0r4itInKx91D/l9y/08L5CIQyev9nAr27fh3Sshous4vbXRRcMcjqHDOrquD+2vqLyw7ygnbA9jA9TpB4hLZocvAWcTN8tyO82hiSY='; $this->assertEquals($plaintext, $m->invokeArgs(null, array(base64_decode($ciphertext), $secret))); } /** * Test that aesEncrypt() produces ciphertexts that aesDecrypt() can decrypt. * * @covers \SimpleSAML\Utils\Crypto::_aesDecrypt * @covers \SimpleSAML\Utils\Crypto::_aesEncrypt */ public function testAesEncrypt() { if (!extension_loaded('openssl')) { $this->setExpectedException('\SimpleSAML_Error_Exception'); } $secret = 'SUPER_SECRET_SALT'; $e = new \ReflectionMethod('\SimpleSAML\Utils\Crypto', '_aesEncrypt'); $d = new \ReflectionMethod('\SimpleSAML\Utils\Crypto', '_aesDecrypt'); $e->setAccessible(true); $d->setAccessible(true); $original_plaintext = 'SUPER_SECRET_TEXT'; $ciphertext = $e->invokeArgs(null, array($original_plaintext, $secret)); $decrypted_plaintext = $d->invokeArgs(null, array($ciphertext, $secret)); $this->assertEquals($original_plaintext, $decrypted_plaintext); } /** * Test that the pem2der() and der2pem() methods work correctly. * * @covers \SimpleSAML\Utils\Crypto::der2pem * @covers \SimpleSAML\Utils\Crypto::pem2der */ public function testFormatConversion() { $pem = <<assertEquals(trim($pem), trim(Crypto::der2pem(Crypto::pem2der($pem)))); } /** * @covers \SimpleSAML\Utils\Crypto::pwHash */ public function testGoodPwHash() { $pw = "password"; $algorithm = "SHA1"; $res = Crypto::pwHash($pw, $algorithm); /* * echo -n "password" | sha1sum | awk -F " " '{print $1}' | xxd -r -p | base64 * W6ph5Mm5Pz8GgiULbPgzG37mj9g= */ $expected = "{SHA}W6ph5Mm5Pz8GgiULbPgzG37mj9g="; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::pwHash */ public function testGoodSaltedPwHash() { $pw = "password"; $algorithm = "SSHA1"; $salt = "salt"; $res = Crypto::pwHash($pw, $algorithm, $salt); /* * echo -n "password""salt" | sha1sum | awk -v salt=$(echo -n "salt" | xxd -u -p) -F " " '{print $1 salt}' | xxd -r -p | base64 * yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0 */ $expected = "{SSHA}yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0"; $this->assertEquals($expected, $res); } /** * @expectedException \SimpleSAML_Error_Exception * * @covers \SimpleSAML\Utils\Crypto::pwHash */ public function testBadHashAlgorithm() { $pw = "password"; $algorithm = "wtf"; Crypto::pwHash($pw, $algorithm); } /** * @covers \SimpleSAML\Utils\Crypto::pwValid */ public function testGoodPwValid() { $pw = "password"; $algorithm = "SHA1"; $hash = Crypto::pwHash($pw, $algorithm); $res = Crypto::pwValid($hash, $pw); $this->assertTrue($res); } /** * @covers \SimpleSAML\Utils\Crypto::pwValid */ public function testGoodSaltedPwValid() { $pw = "password"; $algorithm = "SSHA1"; $salt = "salt"; $hash = Crypto::pwHash($pw, $algorithm, $salt); $res = Crypto::pwValid($hash, $pw); $this->assertTrue($res); } /** * @expectedException \SimpleSAML_Error_Exception * * @covers \SimpleSAML\Utils\Crypto::pwValid */ public function testBadHashAlgorithmValid() { $pw = "password"; $algorithm = "wtf"; $hash = "{".$algorithm."}B64STRING"; Crypto::pwValid($hash, $algorithm); } /** * @covers \SimpleSAML\Utils\Crypto::secureCompare */ public function testSecureCompareEqual() { $res = Crypto::secureCompare("string", "string"); $this->assertTrue($res); } /** * @covers \SimpleSAML\Utils\Crypto::secureCompare */ public function testSecureCompareNotEqual() { $res = Crypto::secureCompare("string1", "string2"); $this->assertFalse($res); } /** * @expectedException \SimpleSAML_Error_Exception * * @covers \SimpleSAML\Utils\Crypto::loadPrivateKey */ public function testLoadPrivateKeyRequiredMetadataMissing() { $config = new Configuration(array(), 'test'); $required = true; Crypto::loadPrivateKey($config, $required); } /** * @covers \SimpleSAML\Utils\Crypto::loadPrivateKey */ public function testLoadPrivateKeyNotRequiredMetadataMissing() { $config = new Configuration(array(), 'test'); $required = false; $res = Crypto::loadPrivateKey($config, $required); $this->assertNull($res); } /** * @expectedException \SimpleSAML_Error_Exception * * @covers \SimpleSAML\Utils\Crypto::loadPrivateKey */ public function testLoadPrivateKeyMissingFile() { $config = new Configuration(array('privatekey' => 'nonexistant'), 'test'); Crypto::loadPrivateKey($config, false, '', true); } /** * @covers \SimpleSAML\Utils\Crypto::loadPrivateKey */ public function testLoadPrivateKeyBasic() { $filename = $this->certdir.DIRECTORY_SEPARATOR.'key'; $data = 'data'; $config = new Configuration(array('privatekey' => $filename), 'test'); $full_path = true; file_put_contents($filename, $data); $res = Crypto::loadPrivateKey($config, false, '', $full_path); $expected = array('PEM' => $data); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPrivateKey */ public function testLoadPrivateKeyPassword() { $password = 'password'; $filename = $this->certdir.DIRECTORY_SEPARATOR.'key'; $data = 'data'; $config = new Configuration( array( 'privatekey' => $filename, 'privatekey_pass' => $password, ), 'test' ); $full_path = true; file_put_contents($filename, $data); $res = Crypto::loadPrivateKey($config, false, '', $full_path); $expected = array('PEM' => $data, 'password' => $password); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPrivateKey */ public function testLoadPrivateKeyPrefix() { $prefix = 'prefix'; $password = 'password'; $filename = $this->certdir.DIRECTORY_SEPARATOR.'key'; $data = 'data'; $config = new Configuration( array( $prefix.'privatekey' => $filename, $prefix.'privatekey_pass' => $password, ), 'test' ); $full_path = true; file_put_contents($filename, $data); $res = Crypto::loadPrivateKey($config, false, $prefix, $full_path); $expected = array('PEM' => $data, 'password' => $password); $this->assertEquals($expected, $res); } /** * @expectedException \SimpleSAML_Error_Exception * * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyRequiredMetadataMissing() { $config = new Configuration(array(), 'test'); $required = true; Crypto::loadPublicKey($config, $required); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyNotRequiredMetadataMissing() { $config = new Configuration(array(), 'test'); $required = false; $res = Crypto::loadPublicKey($config, $required); $this->assertNull($res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyFingerprintBasicString() { $fingerprint = 'fingerprint'; $config = new Configuration(array('certFingerprint' => $fingerprint), 'test'); $res = Crypto::loadPublicKey($config); $expected = array('certFingerprint' => array($fingerprint)); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyFingerprintBasicArray() { $fingerprint1 = 'fingerprint1'; $fingerprint2 = 'fingerprint2'; $config = new Configuration( array( 'certFingerprint' => array( $fingerprint1, $fingerprint2 ), ), 'test' ); $res = Crypto::loadPublicKey($config); $expected = array('certFingerprint' => array($fingerprint1, $fingerprint2)); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyFingerprintLowercase() { $fingerprint = 'FINGERPRINT'; $config = new Configuration(array('certFingerprint' => $fingerprint), 'test'); $res = Crypto::loadPublicKey($config); $expected = array('certFingerprint' => array(strtolower($fingerprint))); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyFingerprintRemoveColons() { $fingerprint = 'f:i:n:g:e:r:p:r:i:n:t'; $config = new Configuration(array('certFingerprint' => $fingerprint), 'test'); $res = Crypto::loadPublicKey($config); $expected = array('certFingerprint' => array(str_replace(':', '', $fingerprint))); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyNotX509Certificate() { $config = new Configuration( array( 'keys' => array( array( 'X509Certificate' => '', 'type' => 'NotX509Certificate', 'signing' => true ), ), ), 'test' ); $res = Crypto::loadPublicKey($config); $this->assertNull($res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyNotSigning() { $config = new Configuration( array( 'keys' => array( array( 'X509Certificate' => '', 'type' => 'X509Certificate', 'signing' => false ), ), ), 'test' ); $res = Crypto::loadPublicKey($config); $this->assertNull($res); } /** * @covers \SimpleSAML\Utils\Crypto::loadPublicKey */ public function testLoadPublicKeyBasic() { $x509certificate = 'x509certificate'; $config = new Configuration( array( 'keys' => array( array( 'X509Certificate' => $x509certificate, 'type' => 'X509Certificate', 'signing' => true ), ), ), 'test' ); $pubkey = Crypto::loadPublicKey($config); $res = $pubkey['certData']; $expected = $x509certificate; $this->assertEquals($expected, $res); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/SystemTest.php0000644000000000000000000001445713245225037022746 0ustar rootrootroot = vfsStream::setup( self::ROOTDIRNAME, null, array( self::DEFAULTTEMPDIR => array(), ) ); $this->root_directory = vfsStream::url(self::ROOTDIRNAME); } /** * @covers \SimpleSAML\Utils\System::getOS * @test */ public function testGetOSBasic() { $res = System::getOS(); $this->assertInternalType("int", $res); } /** * @covers \SimpleSAML\Utils\System::resolvePath * @test */ public function testResolvePathRemoveTrailingSlashes() { $base = "/base////"; $path = "test"; $res = System::resolvePath($path, $base); $expected = "/base/test"; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\System::resolvePath * @test */ public function testResolvePathPreferAbsolutePathToBase() { $base = "/base/"; $path = "/test"; $res = System::resolvePath($path, $base); $expected = "/test"; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\System::resolvePath * @test */ public function testResolvePathCurDirPath() { $base = "/base/"; $path = "/test/."; $res = System::resolvePath($path, $base); $expected = "/test"; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\System::resolvePath * @test */ public function testResolvePathParentPath() { $base = "/base/"; $path = "/test/child/.."; $res = System::resolvePath($path, $base); $expected = "/test"; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\System::writeFile * @test */ public function testWriteFileInvalidArguments() { $this->setExpectedException('\InvalidArgumentException'); System::writeFile(null, null, null); } /** * @requires PHP 5.4.0 * @covers \SimpleSAML\Utils\System::writeFile * @test */ public function testWriteFileBasic() { $tempdir = $this->root_directory . DIRECTORY_SEPARATOR . self::DEFAULTTEMPDIR; $config = $this->setConfigurationTempDir($tempdir); $filename = $this->root_directory . DIRECTORY_SEPARATOR . 'test'; System::writeFile($filename, ''); $this->assertFileExists($filename); $this->clearInstance($config, '\SimpleSAML_Configuration'); } /** * @requires PHP 5.4.0 * @covers \SimpleSAML\Utils\System::writeFile * @test */ public function testWriteFileContents() { $tempdir = $this->root_directory . DIRECTORY_SEPARATOR . self::DEFAULTTEMPDIR; $config = $this->setConfigurationTempDir($tempdir); $filename = $this->root_directory . DIRECTORY_SEPARATOR . 'test'; $contents = 'TEST'; System::writeFile($filename, $contents); $res = file_get_contents($filename); $expected = $contents; $this->assertEquals($expected, $res); $this->clearInstance($config, '\SimpleSAML_Configuration'); } /** * @requires PHP 5.4.0 * @covers \SimpleSAML\Utils\System::writeFile * @test */ public function testWriteFileMode() { $tempdir = $this->root_directory . DIRECTORY_SEPARATOR . self::DEFAULTTEMPDIR; $config = $this->setConfigurationTempDir($tempdir); $filename = $this->root_directory . DIRECTORY_SEPARATOR . 'test'; $mode = 0666; System::writeFile($filename, '', $mode); $res = $this->root->getChild('test')->getPermissions(); $expected = $mode; $this->assertEquals($expected, $res); $this->clearInstance($config, '\SimpleSAML_Configuration'); } /** * @covers \SimpleSAML\Utils\System::getTempDir * @test */ public function testGetTempDirBasic() { $tempdir = $this->root_directory . DIRECTORY_SEPARATOR . self::DEFAULTTEMPDIR; $config = $this->setConfigurationTempDir($tempdir); $res = System::getTempDir(); $expected = $tempdir; $this->assertEquals($expected, $res); $this->assertFileExists($res); $this->clearInstance($config, '\SimpleSAML_Configuration'); } /** * @covers \SimpleSAML\Utils\System::getTempDir * @test */ public function testGetTempDirNonExistant() { $tempdir = $this->root_directory . DIRECTORY_SEPARATOR . 'nonexistant'; $config = $this->setConfigurationTempDir($tempdir); $res = System::getTempDir(); $expected = $tempdir; $this->assertEquals($expected, $res); $this->assertFileExists($res); $this->clearInstance($config, '\SimpleSAML_Configuration'); } /** * @requires PHP 5.4.0 * @requires OS Linux * @covers \SimpleSAML\Utils\System::getTempDir * @test */ public function testGetTempDirBadOwner() { $bad_uid = posix_getuid() + 1; $tempdir = $this->root_directory . DIRECTORY_SEPARATOR . self::DEFAULTTEMPDIR; $config = $this->setConfigurationTempDir($tempdir); chown($tempdir, $bad_uid); $this->setExpectedException('\SimpleSAML_Error_Exception'); $res = System::getTempDir(); $this->clearInstance($config, '\SimpleSAML_Configuration'); } private function setConfigurationTempDir($directory) { $config = Configuration::loadFromArray(array( 'tempdir' => $directory, ), '[ARRAY]', 'simplesaml'); return $config; } protected function clearInstance($service, $className) { $reflectedClass = new \ReflectionClass($className); $reflectedInstance = $reflectedClass->getProperty('instance'); $reflectedInstance->setAccessible(true); $reflectedInstance->setValue($service, null); $reflectedInstance->setAccessible(false); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/XMLTest.php0000644000000000000000000002265113245225037022115 0ustar rootrootassertTrue($res); } /** * @expectedException \InvalidArgumentException * * @covers \SimpleSAML\Utils\XML::isDOMNodeOfType * @test */ public function testIsDomNodeOfTypeMissingNamespace() { $name = 'name'; $namespace_uri = '@missing'; $element = new \DOMElement($name, 'value', $namespace_uri); XML::isDOMNodeOfType($element, $name, $namespace_uri); } /** * @covers \SimpleSAML\Utils\XML::isDOMNodeOfType * @test */ public function testIsDomNodeOfTypeEmpty() { $name = 'name'; $namespace_uri = ''; $element = new \DOMElement($name); $res = XML::isDOMNodeOfType($element, $name, $namespace_uri); $this->assertFalse($res); } /** * @covers \SimpleSAML\Utils\XML::isDOMNodeOfType * @test */ public function testIsDomNodeOfTypeShortcut() { $name = 'name'; $namespace_uri = 'urn:oasis:names:tc:SAML:2.0:metadata'; $short_namespace_uri = '@md'; $element = new \DOMElement($name, 'value', $namespace_uri); $res = XML::isDOMNodeOfType($element, $name, $short_namespace_uri); $this->assertTrue($res); } /** * @covers \SimpleSAML\Utils\XML::isDOMNodeOfType * @test */ public function testIsDomNodeOfTypeIncorrectName() { $name = 'name'; $bad_name = 'bad name'; $namespace_uri = 'ns'; $element = new \DOMElement($name, 'value', $namespace_uri); $res = XML::isDOMNodeOfType($element, $bad_name, $namespace_uri); $this->assertFalse($res); } /** * @covers \SimpleSAML\Utils\XML::isDOMNodeOfType * @test */ public function testIsDomNodeOfTypeIncorrectNamespace() { $name = 'name'; $namespace_uri = 'ns'; $bad_namespace_uri = 'bad name'; $element = new \DOMElement($name, 'value', $namespace_uri); $res = XML::isDOMNodeOfType($element, $name, $bad_namespace_uri); $this->assertFalse($res); } /** * @covers \SimpleSAML\Utils\XML::getDOMText * @test */ public function testGetDomTextBasic() { $data = 'root value'; $dom = new \DOMDocument(); $element = $dom->appendChild(new \DOMElement('root')); $element->appendChild(new \DOMText($data)); $res = XML::getDOMText($element); $expected = $data; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::getDOMText * @test */ public function testGetDomTextMulti() { $data1 = 'root value 1'; $data2 = 'root value 2'; $dom = new \DOMDocument(); $element = $dom->appendChild(new \DOMElement('root')); $element->appendChild(new \DOMText($data1)); $element->appendChild(new \DOMText($data2)); $res = XML::getDOMText($element); $expected = $data1 . $data2 . $data1 . $data2; $this->assertEquals($expected, $res); } /** * @expectedException \SimpleSAML_Error_Exception * * @covers \SimpleSAML\Utils\XML::getDOMText * @test */ public function testGetDomTextIncorrectType() { $dom = new \DOMDocument(); $element = $dom->appendChild(new \DOMElement('root')); $comment = $element->appendChild(new \DOMComment('')); XML::getDOMText($element); } /** * @covers \SimpleSAML\Utils\XML::getDOMChildren * @test */ public function testGetDomChildrenBasic() { $name = 'name'; $namespace_uri = 'ns'; $dom = new \DOMDocument(); $element = new \DOMElement($name, 'value', $namespace_uri); $dom->appendChild($element); $res = XML::getDOMChildren($dom, $name, $namespace_uri); $expected = array($element); $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::getDOMChildren * @test */ public function testGetDomChildrenIncorrectType() { $dom = new \DOMDocument(); $text = new \DOMText('text'); $comment = new \DOMComment('comment'); $dom->appendChild($text); $dom->appendChild($comment); $res = XML::getDOMChildren($dom, 'name', 'ns'); $this->assertEmpty($res); } /** * @covers \SimpleSAML\Utils\XML::getDOMChildren * @test */ public function testGetDomChildrenIncorrectName() { $name = 'name'; $bad_name = 'bad name'; $namespace_uri = 'ns'; $dom = new \DOMDocument(); $element = new \DOMElement($name, 'value', $namespace_uri); $dom->appendChild($element); $res = XML::getDOMChildren($dom, $bad_name, $namespace_uri); $this->assertEmpty($res); } /** * @covers \SimpleSAML\Utils\XML::formatDOMElement * @test */ public function testFormatDomElementBasic() { $dom = new \DOMDocument(); $root = new \DOMElement('root'); $dom->appendChild($root); $root->appendChild(new \DOMText('text')); XML::formatDOMElement($root); $res = $dom->saveXML(); $expected = <<<'NOWDOC' text NOWDOC; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::formatDOMElement * @test */ public function testFormatDomElementNested() { $dom = new \DOMDocument(); $root = new \DOMElement('root'); $nested = new \DOMElement('nested'); $dom->appendChild($root); $root->appendChild($nested); $nested->appendChild(new \DOMText('text')); XML::formatDOMElement($root); $res = $dom->saveXML(); $expected = <<<'NOWDOC' text NOWDOC; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::formatDOMElement * @test */ public function testFormatDomElementIndentBase() { $indent_base = 'base'; $dom = new \DOMDocument(); $root = new \DOMElement('root'); $nested = new \DOMElement('nested'); $dom->appendChild($root); $root->appendChild($nested); $nested->appendChild(new \DOMText('text')); XML::formatDOMElement($root, $indent_base); $res = $dom->saveXML(); $expected = << $indent_base text $indent_base HEREDOC; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::formatDOMElement * @test */ public function testFormatDomElementTextAndChild() { $dom = new \DOMDocument(); $root = new \DOMElement('root'); $dom->appendChild($root); $root->appendChild(new \DOMText('text')); $root->appendChild(new \DOMElement('child')); XML::formatDOMElement($root); $res = $dom->saveXML(); $expected = << text HEREDOC; $this->assertEquals($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::formatXMLString * @test */ public function testFormatXmlStringBasic() { $xml = 'text'; $res = XML::formatXMLString($xml); $expected = <<<'NOWDOC' text NOWDOC; $this->assertEquals($expected, $res); } /** * @expectedException \DOMException * * @covers \SimpleSAML\Utils\XML::formatXMLString * @test */ public function testFormatXmlStringMalformedXml() { $xml = 'text'; XML::formatXMLString($xml); } /** * @covers \SimpleSAML\Utils\XML::isValid * @test */ public function testIsValidMalformedXml() { $xml = 'text'; $res = XML::isValid($xml, 'unused'); $expected = 'Failed to parse XML string for schema validation'; $this->assertContains($expected, $res); } /** * @covers \SimpleSAML\Utils\XML::isValid * @test */ public function testIsValidMetadata() { \SimpleSAML_Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); $schema = 'saml-schema-metadata-2.0.xsd'; $dom = $this->getMockBuilder('\DOMDocument') ->setMethods(array('schemaValidate')) ->disableOriginalConstructor() ->getMock(); /* * Unfortunately, we cannot actually test schemaValidate. To * effectively unit test this function we'd have to enable LIBXML_NONET * which disables network access when loading documents. PHP does not * currently support enabling this flag. */ $dom->method('schemaValidate') ->willReturn(true); $res = XML::isValid($dom, $schema); $this->assertTrue($res); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Utils/AttributesTest.php0000644000000000000000000001452713245225037023606 0ustar rootroot */ class AttributesTest extends \PHPUnit_Framework_TestCase { /** * Test the getExpectedAttribute() method with invalid attributes array. */ public function testGetExpectedAttributeInvalidAttributesArray() { // check with empty array as input $attributes = 'string'; $expected = 'string'; $this->setExpectedException( 'InvalidArgumentException', 'The attributes array is not an array, it is: '.print_r($attributes, true).'.' ); Attributes::getExpectedAttribute($attributes, $expected); } /** * Test the getExpectedAttributeMethod() method with invalid expected attribute parameter. */ public function testGetExpectedAttributeInvalidAttributeName() { // check with invalid attribute name $attributes = array(); $expected = false; $this->setExpectedException( 'InvalidArgumentException', 'The expected attribute is not a string, it is: '.print_r($expected, true).'.' ); Attributes::getExpectedAttribute($attributes, $expected); } /** * Test the getExpectedAttributeMethod() method with a non-normalized attributes array. */ public function testGetExpectedAttributeNonNormalizedArray() { // check with non-normalized attributes array $attributes = array( 'attribute' => 'value', ); $expected = 'attribute'; $this->setExpectedException( 'InvalidArgumentException', 'The attributes array is not normalized, values should be arrays.' ); Attributes::getExpectedAttribute($attributes, $expected); } /** * Test the getExpectedAttribute() method with valid input but missing expected attribute. */ public function testGetExpectedAttributeMissingAttribute() { // check missing attribute $attributes = array( 'attribute' => array('value'), ); $expected = 'missing'; $this->setExpectedException( 'SimpleSAML_Error_Exception', "No such attribute '".$expected."' found." ); Attributes::getExpectedAttribute($attributes, $expected); } /** * Test the getExpectedAttribute() method with an empty attribute. */ public function testGetExpectedAttributeEmptyAttribute() { // check empty attribute $attributes = array( 'attribute' => array(), ); $expected = 'attribute'; $this->setExpectedException( 'SimpleSAML_Error_Exception', "Empty attribute '".$expected."'.'" ); Attributes::getExpectedAttribute($attributes, $expected); } /** * Test the getExpectedAttributeMethod() method with multiple values (not being allowed). */ public function testGetExpectedAttributeMultipleValues() { // check attribute with more than value, that being not allowed $attributes = array( 'attribute' => array( 'value1', 'value2', ), ); $expected = 'attribute'; $this->setExpectedException( 'SimpleSAML_Error_Exception', 'More than one value found for the attribute, multiple values not allowed.' ); Attributes::getExpectedAttribute($attributes, $expected); } /** * Test that the getExpectedAttribute() method successfully obtains values from the attributes array. */ public function testGetExpectedAttribute() { // check one value $value = 'value'; $attributes = array( 'attribute' => array($value), ); $expected = 'attribute'; $this->assertEquals($value, Attributes::getExpectedAttribute($attributes, $expected)); // check multiple (allowed) values $value = 'value'; $attributes = array( 'attribute' => array($value, 'value2', 'value3'), ); $expected = 'attribute'; $this->assertEquals($value, Attributes::getExpectedAttribute($attributes, $expected, true)); } /** * Test the normalizeAttributesArray() function with input not being an array * * @expectedException \InvalidArgumentException */ public function testNormalizeAttributesArrayBadInput() { Attributes::normalizeAttributesArray('string'); } /** * Test the normalizeAttributesArray() function with an array with non-string attribute names. * * @expectedException \InvalidArgumentException */ public function testNormalizeAttributesArrayBadKeys() { Attributes::normalizeAttributesArray(array('attr1' => 'value1', 1 => 'value2')); } /** * Test the normalizeAttributesArray() function with an array with non-string attribute values. * * @expectedException \InvalidArgumentException */ public function testNormalizeAttributesArrayBadValues() { Attributes::normalizeAttributesArray(array('attr1' => 'value1', 'attr2' => 0)); } /** * Test the normalizeAttributesArray() function. */ public function testNormalizeAttributesArray() { $attributes = array( 'key1' => 'value1', 'key2' => array('value2', 'value3'), 'key3' => 'value1' ); $expected = array( 'key1' => array('value1'), 'key2' => array('value2', 'value3'), 'key3' => array('value1') ); $this->assertEquals( $expected, Attributes::normalizeAttributesArray($attributes), 'Attribute array normalization failed' ); } /** * Test the getAttributeNamespace() function. */ public function testNamespacedAttributes() { // test for only the name $this->assertEquals( array('default', 'name'), Attributes::getAttributeNamespace('name', 'default') ); // test for a given namespace and multiple '/' $this->assertEquals( array('some/namespace', 'name'), Attributes::getAttributeNamespace('some/namespace/name', 'default') ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Metadata/0000755000000000000000000000000013245225037020516 5ustar rootrootsimplesamlphp-1.15.3/tests/lib/SimpleSAML/Metadata/SAMLBuilderTest.php0000644000000000000000000001745113245225037024142 0ustar rootroot $entityId, 'name' => array('en' => 'Test SP'), 'metadata-set' => $set, 'attributes' => array( 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10', 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', 'urn:oid:0.9.2342.19200300.100.1.3', 'urn:oid:2.5.4.3', ), ); $samlBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId); $samlBuilder->addMetadata($set, $metadata); $spDesc = $samlBuilder->getEntityDescriptor(); $acs = $spDesc->getElementsByTagName("AttributeConsumingService"); $this->assertEquals(1, $acs->length); $attributes = $acs->item(0)->getElementsByTagName("RequestedAttribute"); $this->assertEquals(4, $attributes->length); for ($c = 0; $c < $attributes->length; $c++) { $curAttribute = $attributes->item($c); $this->assertTrue($curAttribute->hasAttribute("Name")); $this->assertFalse($curAttribute->hasAttribute("FriendlyName")); $this->assertEquals($metadata['attributes'][$c], $curAttribute->getAttribute("Name")); } // test SP20 array parsing, no friendly name $set = 'saml20-sp-remote'; $metadata = array( 'entityid' => $entityId, 'name' => array('en' => 'Test SP'), 'metadata-set' => $set, 'attributes' => array( 'eduPersonTargetedID' => 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10', 'eduPersonPrincipalName' => 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', 'eduPersonOrgDN' => 'urn:oid:0.9.2342.19200300.100.1.3', 'cn' => 'urn:oid:2.5.4.3', ), ); $samlBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId); $samlBuilder->addMetadata($set, $metadata); $spDesc = $samlBuilder->getEntityDescriptor(); $acs = $spDesc->getElementsByTagName("AttributeConsumingService"); $this->assertEquals(1, $acs->length); $attributes = $acs->item(0)->getElementsByTagName("RequestedAttribute"); $this->assertEquals(4, $attributes->length); $keys = array_keys($metadata['attributes']); for ($c = 0; $c < $attributes->length; $c++) { $curAttribute = $attributes->item($c); $this->assertTrue($curAttribute->hasAttribute("Name")); $this->assertTrue($curAttribute->hasAttribute("FriendlyName")); $this->assertEquals($metadata['attributes'][$keys[$c]], $curAttribute->getAttribute("Name")); $this->assertEquals($keys[$c], $curAttribute->getAttribute("FriendlyName")); } // test SP13 array parsing, no friendly name $set = 'shib13-sp-remote'; $metadata = array( 'entityid' => $entityId, 'name' => array('en' => 'Test SP'), 'metadata-set' => $set, 'attributes' => array( 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10', 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', 'urn:oid:0.9.2342.19200300.100.1.3', 'urn:oid:2.5.4.3', ), ); $samlBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId); $samlBuilder->addMetadata($set, $metadata); $spDesc = $samlBuilder->getEntityDescriptor(); $acs = $spDesc->getElementsByTagName("AttributeConsumingService"); $this->assertEquals(1, $acs->length); $attributes = $acs->item(0)->getElementsByTagName("RequestedAttribute"); $this->assertEquals(4, $attributes->length); for ($c = 0; $c < $attributes->length; $c++) { $curAttribute = $attributes->item($c); $this->assertTrue($curAttribute->hasAttribute("Name")); $this->assertFalse($curAttribute->hasAttribute("FriendlyName")); $this->assertEquals($metadata['attributes'][$c], $curAttribute->getAttribute("Name")); } // test SP20 array parsing, no friendly name $set = 'shib13-sp-remote'; $metadata = array( 'entityid' => $entityId, 'name' => array('en' => 'Test SP'), 'metadata-set' => $set, 'attributes' => array( 'eduPersonTargetedID' => 'urn:oid:1.3.6.1.4.1.5923.1.1.1.10', 'eduPersonPrincipalName' => 'urn:oid:1.3.6.1.4.1.5923.1.1.1.6', 'eduPersonOrgDN' => 'urn:oid:0.9.2342.19200300.100.1.3', 'cn' => 'urn:oid:2.5.4.3', ), ); $samlBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId); $samlBuilder->addMetadata($set, $metadata); $spDesc = $samlBuilder->getEntityDescriptor(); $acs = $spDesc->getElementsByTagName("AttributeConsumingService"); $this->assertEquals(1, $acs->length); $attributes = $acs->item(0)->getElementsByTagName("RequestedAttribute"); $this->assertEquals(4, $attributes->length); $keys = array_keys($metadata['attributes']); for ($c = 0; $c < $attributes->length; $c++) { $curAttribute = $attributes->item($c); $this->assertTrue($curAttribute->hasAttribute("Name")); $this->assertTrue($curAttribute->hasAttribute("FriendlyName")); $this->assertEquals($metadata['attributes'][$keys[$c]], $curAttribute->getAttribute("Name")); $this->assertEquals($keys[$c], $curAttribute->getAttribute("FriendlyName")); } } /** * Test the required protocolSupportEnumeration in AttributeAuthorityDescriptor */ public function testProtocolSupportEnumeration() { $entityId = 'https://entity.example.com/id'; $set = 'attributeauthority-remote'; // without protocolSupportEnumeration fallback to default: urn:oasis:names:tc:SAML:2.0:protocol $metadata = array( 'entityid' => $entityId, 'name' => array('en' => 'Test AA'), 'metadata-set' => $set, 'AttributeService' => array ( 0 => array ( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP', 'Location' => 'https://entity.example.com:8443/idp/profile/SAML2/SOAP/AttributeQuery', ), ), ); $samlBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId); $samlBuilder->addMetadata($set, $metadata); $entityDescriptorXml = $samlBuilder->getEntityDescriptorText(); $this->assertRegExp( '//', $entityDescriptorXml ); // explicit protocols $metadata['protocols'] = array( 0 => 'urn:oasis:names:tc:SAML:1.1:protocol', 1 => 'urn:oasis:names:tc:SAML:2.0:protocol', ); $samlBuilder = new SimpleSAML_Metadata_SAMLBuilder($entityId); $samlBuilder->addMetadata($set, $metadata); $entityDescriptorXml = $samlBuilder->getEntityDescriptorText(); $this->assertRegExp( '//', $entityDescriptorXml ); } } simplesamlphp-1.15.3/tests/lib/SimpleSAML/Metadata/SAMLParserTest.php0000644000000000000000000001262413245225037024005 0ustar rootroot 'https://incommon.org', ); $document = \SAML2\DOMDocumentFactory::fromString( << XML ); $entities = \SimpleSAML_Metadata_SAMLParser::parseDescriptorsElement($document->documentElement); $this->assertArrayHasKey('theEntityID', $entities); // RegistrationInfo is accessible in the SP or IDP metadata accessors $metadata = $entities['theEntityID']->getMetadata20SP(); $this->assertEquals($expected, $metadata['RegistrationInfo']); } /** * Test RegistrationInfo is inherited correctly from parent EntitiesDescriptor. * According to the spec overriding RegistrationInfo is not valid. We ignore attempts to override */ public function testRegistrationInfoInheritance() { $expected = array( 'registrationAuthority' => 'https://incommon.org', ); $document = \SAML2\DOMDocumentFactory::fromString( << XML ); $entities = \SimpleSAML_Metadata_SAMLParser::parseDescriptorsElement($document->documentElement); $this->assertArrayHasKey('theEntityID', $entities); $this->assertArrayHasKey('subEntityId', $entities); // RegistrationInfo is accessible in the SP or IDP metadata accessors $metadata = $entities['theEntityID']->getMetadata20SP(); $this->assertEquals($expected, $metadata['RegistrationInfo']); $metadata = $entities['subEntityId']->getMetadata20SP(); $this->assertEquals($expected, $metadata['RegistrationInfo']); $metadata = $entities['subEntityIdOverride']->getMetadata20SP(); $this->assertEquals($expected, $metadata['RegistrationInfo']); } /** * Test AttributeConsumingService is parsed */ public function testAttributeConsumingServiceParsing() { $document = \SAML2\DOMDocumentFactory::fromString( << Example service Dit is een voorbeeld voor de unittest. XML ); $entities = \SimpleSAML_Metadata_SAMLParser::parseDescriptorsElement($document->documentElement); $this->assertArrayHasKey('theEntityID', $entities); $metadata = $entities['theEntityID']->getMetadata20SP(); $this->assertEquals("Example service", $metadata['name']['en']); $this->assertEquals("Dit is een voorbeeld voor de unittest.", $metadata['description']['nl']); $expected_a = array("urn:mace:dir:attribute-def:eduPersonPrincipalName", "urn:mace:dir:attribute-def:mail", "urn:mace:dir:attribute-def:displayName"); $expected_r = array("urn:mace:dir:attribute-def:eduPersonPrincipalName"); $this->assertEquals($expected_a, $metadata['attributes']); $this->assertEquals($expected_r, $metadata['attributes.required']); } } simplesamlphp-1.15.3/tests/routers/0000755000000000000000000000000013245225037016025 5ustar rootrootsimplesamlphp-1.15.3/tests/routers/configLoader.php0000644000000000000000000000332613245225037021136 0ustar rootrootstart(); * 3. Get the PID of the server once it has started: * $pid = $server->getPid(); * 4. Build the path to the file that this script will use: * $file = sys_get_temp_dir().'/'.$pid.'.lock'; * 5. Dump the configuration array to the file: * file_put_contents("get($query, $parameters); * 7. Remove the temporary file when done: * unlink($file); */ include_once(sys_get_temp_dir().'/'.getmypid().'.lock'); // load SimpleSAMLphp's autoloader require_once(dirname(__FILE__).'/../../vendor/autoload.php'); // initialize configuration if (isset($config)) { SimpleSAML_Configuration::loadFromArray($config, '[ARRAY]', 'simplesaml'); } // let the script proceed // see: http://php.net/manual/en/features.commandline.webserver.php return false; simplesamlphp-1.15.3/tests/Utils/0000755000000000000000000000000013245225037015422 5ustar rootrootsimplesamlphp-1.15.3/tests/Utils/StateClearer.php0000644000000000000000000000425413245225037020516 0ustar rootrootbackups['$_COOKIE'] = $_COOKIE; $this->backups['$_ENV'] = $_ENV; $this->backups['$_FILES'] = $_FILES; $this->backups['$_GET'] = $_GET; $this->backups['$_POST'] = $_POST; $this->backups['$_SERVER'] = $_SERVER; $this->backups['$_SESSION'] = isset($_SESSION) ? $_SESSION : array(); $this->backups['$_REQUEST'] = $_REQUEST; } /** * Clear any global state. */ public function clearGlobals() { if (!empty($this->backups)) { $_COOKIE = $this->backups['$_COOKIE']; $_ENV = $this->backups['$_ENV']; $_FILES = $this->backups['$_FILES']; $_GET = $this->backups['$_GET']; $_POST = $this->backups['$_POST']; $_SERVER = $this->backups['$_SERVER']; $_SESSION = $this->backups['$_SESSION']; $_REQUEST = $this->backups['$_REQUEST']; } else { //TODO: what should this behavior be? } } /** * Clear any SSP specific state, such as SSP enviormental variables or cached internals. */ public function clearSSPState() { foreach ($this->clearableState as $var) { $var::clearInternalState(); } foreach ($this->vars_to_unset as $var) { putenv($var); } } } simplesamlphp-1.15.3/tests/Utils/ReduceSpillOverTest.php0000644000000000000000000000210313245225037022036 0ustar rootroot 'b'), '[ARRAY]', 'simplesaml'); $this->assertEquals('b', \SimpleSAML_Configuration::getInstance()->getString('a')); putenv('SIMPLESAMLPHP_CONFIG_DIR=' . __DIR__); } /** * Confirm global state removed prior to next test */ public function testStateRemoved() { $this->assertArrayNotHasKey('QUERY_STRING', $_SERVER); $this->assertFalse(getenv('SIMPLESAMLPHP_CONFIG_DIR')); try { \SimpleSAML_Configuration::getInstance(); $this->fail('Expected config configured in other tests to no longer be valid'); } catch (\SimpleSAML\Error\ConfigurationError $error) { // Expected error } } } simplesamlphp-1.15.3/tests/Utils/ClearStateTestCase.php0000644000000000000000000000171413245225037021621 0ustar rootrootbackupGlobals(); } } protected function setUp() { self::clearState(); } public static function tearDownAfterClass() { self::clearState(); } /** * Clear any SSP global state to reduce spill over between tests. */ public static function clearState() { self::$stateClearer->clearGlobals(); self::$stateClearer->clearSSPState(); } } simplesamlphp-1.15.3/docs/0000755000000000000000000000000013245225037014110 5ustar rootrootsimplesamlphp-1.15.3/docs/index.md0000644000000000000000000000631413245225037015545 0ustar rootrootSimpleSAMLphp Documentation =========================== * [Installing SimpleSAMLphp](simplesamlphp-install) * [Upgrade notes for version 1.15](simplesamlphp-upgrade-notes-1.15) * [Upgrade notes for version 1.14](simplesamlphp-upgrade-notes-1.14) * [Upgrade notes for version 1.13](simplesamlphp-upgrade-notes-1.13) * [Upgrade notes for version 1.12](simplesamlphp-upgrade-notes-1.12) * [Upgrade notes for version 1.11](simplesamlphp-upgrade-notes-1.11) * [Upgrade notes for version 1.10](simplesamlphp-upgrade-notes-1.10) * [Upgrade notes for version 1.9](simplesamlphp-upgrade-notes-1.9) * [Upgrade notes for version 1.8](simplesamlphp-upgrade-notes-1.8) * [Upgrade notes for version 1.7](simplesamlphp-upgrade-notes-1.7) * [Upgrade notes for version 1.6](simplesamlphp-upgrade-notes-1.6) * [Upgrade notes for version 1.5](simplesamlphp-upgrade-notes-1.5) * [Installation from the repository](simplesamlphp-install-repo) * [Changelog](simplesamlphp-changelog) * [Using SimpleSAMLphp as a SAML Service Provider](simplesamlphp-sp) * [Hosted SP Configuration Reference](./saml:sp) * [IdP remote reference](simplesamlphp-reference-idp-remote) * [Upgrading - migration to use the SAML authentication source](simplesamlphp-sp-migration) * [Configuring HTTP-Artifact](./simplesamlphp-artifact-sp) * [Using scoping](./simplesamlphp-scoping) * [Holder-of-Key profile](simplesamlphp-hok-sp) * [Identity Provider QuickStart](simplesamlphp-idp) * [IdP hosted reference](simplesamlphp-reference-idp-hosted) * [SP remote reference](simplesamlphp-reference-sp-remote) * [Use case: Setting up an IdP for G Suite (Google Apps)](simplesamlphp-googleapps) * [Configuring HTTP-Artifact](./simplesamlphp-artifact-idp) * [Identity Provider Advanced Topics](simplesamlphp-idp-more) * [Holder-of-Key profile](simplesamlphp-hok-idp) * [Automated Metadata Management](simplesamlphp-automated_metadata) * [Maintenance and configuration](simplesamlphp-maintenance) - covers session handling, php configuration etc. * [Authentication Processing Filters](simplesamlphp-authproc) - attribute filtering, attribute mapping, consent, group generation etc. * [Advanced features](simplesamlphp-advancedfeatures) - covers bridging protocols, attribute filtering, etc. * [State Information Lost](simplesamlphp-nostate) - more about this common error message * [SimpleSAMLphp Dictionaries and Translation](simplesamlphp-translation) * [Theming SimpleSAMLphp](simplesamlphp-theming) * [SimpleSAMLphp Modules](simplesamlphp-modules) - how to create own customized modules * [Key rollover](./saml:keyrollover) * [Creating authentication sources](./simplesamlphp-authsource) * [Implementing custom username/password authentication](./simplesamlphp-customauth) * [Storing sessions in Riak](./riak:simplesamlphp-riak) Documentation on specific SimpleSAMLphp modules: * [Consent module](./consent:consent) * [Installing and configuring the consentAdmin module](./consentAdmin:consentAdmin) * [Authorization](./authorize:authorize) * [autotest Module](./autotest:test) * [Statistics](./statistics:statistics) Documentation for SimpleSAMLphp developers: * [Error handling in SimpleSAMLphp](simplesamlphp-errorhandling) simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.11.md0000644000000000000000000000037313245225037022444 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.11 ==================================== * Support for the Holder-of-Key profile in the SAML 2.0 SP has been disabled by default. To enable it, set `saml20.hok.assertion` to `TRUE` in `config/authsources.php`. simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.12.md0000644000000000000000000000016013245225037022437 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.12 ==================================== * PHP version 5.3 is now required. simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.7.md0000644000000000000000000000153013245225037022365 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.7 =================================== * The attribute names generated by the twitter and facebook authentication sources have changed. * Several new options have been added to config.php, and some have been renamed. The old options should still work, but it is suggested that you look at the config.php file in config-templates, and compare it to your own. * There have been several changes to the internal API. Most of the changes will not be noticed by the application using SimpleSAMLphp. See the changelog for more details about the changes. * Relative redirects are no longer supported. If your application passes relative URL's to the `SimpleSAML_Utilities::redirect()`-function, it will no longer work. This also applies if you pass relative URL's to other functions that do redirects. simplesamlphp-1.15.3/docs/simplesamlphp-errorhandling.md0000644000000000000000000002715213245225037022153 0ustar rootrootException and error handling in SimpleSAMLphp ============================================= This document describes the way errors and exceptions are handled in authentication sources and authentication processing filters. The basic goal is to be able to throw an exception during authentication, and then have that exception transported back to the SP in a way that the SP understands. This means that internal SimpleSAMLphp exceptions must be mapped to transport specific error codes for the various transports that are supported by SimpleSAMLphp. E.g.: When a `SimpleSAML_Error_NoPassive` error is thrown by an authentication processing filter in a SAML 2.0 IdP, we want to map that exception to the `urn:oasis:names:tc:SAML:2.0:status:NoPassive` status code. That status code should then be returned to the SP. Throwing exceptions ------------------- How you throw an exception depends on where you want to throw it from. The simplest case is if you want to throw it during the `authenticate()`-method in an authentication module or during the `process()`-method in a processing filter. In those methods, you can just throw an exception: public function process(&$state) { if ($state['something'] === FALSE) { throw new SimpleSAML_Error_Exception('Something is wrong...'); } } Exceptions thrown at this stage will be caught and delivered to the appropriate error handler. If you want to throw an exception outside of those methods, i.e. after you have done a redirect, you need to use the `SimpleSAML_Auth_State::throwException()` function: The `SimpleSAML_Auth_State::throwException` function will then transfer your exception to the appropriate error handler. ### Note Note that we use the `SimpleSAML_Error_Exception` class in both cases. This is because the delivery of the exception may require a redirect to a different web page. In those cases, the exception needs to be serialized. The normal `Exception` class in PHP isn't always serializable. If you throw an exception that isn't a subclass of the `SimpleSAML_Error_Exception` class, your exception will be converted to an instance of `SimpleSAML_Error_UnserializableException`. The `SimpleSAML_Auth_State::throwException` function does not accept any exceptions that does not subclass the `SimpleSAML_Error_Exception` class. Returning specific SAML 2 errors -------------------------------- By default, all thrown exceptions will be converted to a generic SAML 2 error. In some cases, you may want to convert the exception to a specific SAML 2 status code. For example, the `SimpleSAML_Error_NoPassive` exception should be converted to a SAML 2 status code with the following properties: * The top-level status code should be `urn:oasis:names:tc:SAML:2.0:status:Responder`. * The second-level status code should be `urn:oasis:names:tc:SAML:2.0:status:NoPassive`. * The status message should contain the cause of the exception. The `sspmod_saml_Error` class represents SAML 2 errors. It represents a SAML 2 status code with three elements: the top-level status code, the second-level status code and the status message. The second-level status code and the status message is optional, and can be `NULL`. The `sspmod_saml_Error` class contains a helper function named `fromException`. The `fromException()` function is used by `www/saml2/idp/SSOService.php` to return SAML 2 errors to the SP. The function contains a list which maps various exceptions to specific SAML 2 errors. If it is unable to convert the exception, it will return a generic SAML 2 error describing the original exception in its status message. To return a specific SAML 2 error, you should: * Create a new exception class for your error. This exception class must subclass `SimpleSAML_Error_Exception`. * Add that exception to the list in `fromException()`. * Consider adding the exception to `toException()` in the same file. (See the next section.) ### Note While it is possible to throw SAML 2 errors directly from within authentication sources and processing filters, this practice is discouraged. Throwing SAML 2 errors will tie your code directly to the SAML 2 protocol, and it may be more difficult to use with other protocols. Converting SAML 2 errors to normal exceptions --------------------------------------------- On the SP side, we want to convert SAML 2 errors to SimpleSAMLphp exceptions again. This is handled by the `toException()` method in `sspmod_saml_Error`. The assertion consumer script of the SAML 2 authentication source (`modules/saml2/sp/acs.php`) uses this method. The result is that generic exceptions are thrown from that authentication source. For example, `NoPassive` errors will be converted back to instances of `SimpleSAML_Error_NoPassive`. Other protocols --------------- The error handling code has not yet been added to other protocols, but the framework should be easy to adapt for other protocols. To eventually support other protocols was a goal when designing this framework. Technical details ----------------------- This section attempts to describe the internals of the error handling framework. ### `SimpleSAML_Error_Exception` The `SimpleSAML_Error_Exception` class extends the normal PHP `Exception` class. It makes the exceptions serializable by overriding the `__sleep()` method. The `__sleep()` method returns all variables in the class which should be serialized when saving the class. To make sure that the class is serializable, we remove the `$trace` variable from the serialization. The `$trace` variable contains the full stack trace to the point where the exception was instantiated. This can be a problem, since the stack trace also contains the parameters to the function calls. If one of the parameters in unserializable, serialization of the exception will fail. Since preserving the stack trace can be useful for debugging, we save a variant of the stack trace in the `$backtrace` variable. This variable can be accessed through the `getBacktrace()` method. It returns an array with one line of text for each function call in the stack, ending on the point where the exception was created. #### Note Since we lose the original `$trace` variable during serialization, PHP will fill it with a new stack trace when the exception is unserialized. This may be confusing since the new stack trace leads into the `unserialize()` function. It is therefore recommended to use the getBacktrace() method. ### `SimpleSAML_Auth_State` There are two methods in this class that deals with exceptions: * `throwException($state, $exception)`, which throws an exception. * `loadExceptionState($id)`, which restores a state containing an exception. #### `throwException` This method delivers the exception to the code that initialized the exception handling in the authentication state. That would be `SimpleSAML_Auth_Default` for authtentication sources, and `www/saml2/idp/SSOService.php` for processing filters. To configure how and where the exception should be delivered, there are two fields in the state-array which can be set: * `SimpleSAML_Auth_State::EXCEPTION_HANDLER_FUNC`, in which case the exception will be delivered by a function call to the function specified in that field. * `SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL`, in which case the exception will be delivered by a redirect to the URL specified in that field. If the exception is delivered by a function call, the function will be called with two parameters: The exception and the state array. If the exception is delivered by a redirect, SimpleSAML_Auth_State will save the exception in a field in the state array, pass a parameter with the id of the state array to the URL. The `SimpleSAML_Auth_State::EXCEPTION_PARAM` constant contains the name of that parameter, while the `SimpleSAML_Auth_State::EXCEPTION_DATA` constant holds the name of the field where the exception is saved. #### `loadException` To retrieve the exception, the application should check for the state parameter in the request, and then retrieve the state array by calling `SimpleSAML_Auth_State::loadExceptionState()`. The exception can be located in a field named `SimpleSAML_Auth_State::EXCEPTION_DATA`. The following code illustrates this behaviour: if (array_key_exists(SimpleSAML_Auth_State::EXCEPTION_PARAM, $_REQUEST)) { $state = SimpleSAML_Auth_State::loadExceptionState(); $exception = $state[SimpleSAML_Auth_State::EXCEPTION_DATA]; /* Process exception. */ } ### `SimpleSAML_Auth_Default` This class accepts an `$errorURL` parameter to the `initLogin()` function. This parameter is stored in the `SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL` of the state array. Exceptions thrown by the authentication source will be delivered to that URL. It also wraps the call to the `authenticate()` function inside a try-catch block. Any exceptions thrown during that function call will be delivered to the URL specified in the `$errorURL` parameter. This is done for consistency, since `SimpleSAML_Auth_Default` never transfers control back to the caller by returning. ### `SimpleSAML_Auth_ProcessingChain` This class requires the caller to add the error handler to the state array before calling the `processState()` function. Exceptions thrown by the processing filters will be delivered directly to the caller of `processState()` if possible. However, if one of the filters in the processing chain redirected the user away from the caller, exceptions will be delivered through the error handler saved in the state array. This is the same behaviour as normal processing filters. The result will be delivered directly if it is possible, but if not, it will be delivered through a redirect. The code for handling this becomes something like: if (array_key_exists(SimpleSAML_Auth_State::EXCEPTION_PARAM, $_REQUEST)) { $state = SimpleSAML_Auth_State::loadExceptionState(); $exception = $state[SimpleSAML_Auth_State::EXCEPTION_DATA]; /* Handle exception... */ [...] } $procChain = [...]; $state = array( 'ReturnURL' => SimpleSAML_Utilities::selfURLNoQuery(), SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL => SimpleSAML_Utilities::selfURLNoQuery(), [...], ) try { $procChain->processState($state); } catch (SimpleSAML_Error_Exception $e) { /* Handle exception. */ [...]; } #### Note An exception which isn't a subclass of `SimpleSAML_Error_Exception` will be converted to the `SimpleSAML_Error_UnserializedException` class. This happens regardless of whether the exception is delivered directly or through the error handler. This is done to be consistent in what the application receives - now it will always receive the same exception, regardless of whether it is delivered directly or through a redirect. Custom error show function -------------------------- Optional custom error show function, called from SimpleSAML_Error_Error::show, is defined with 'errors.show_function' in config.php. Example code for this function, which implements the same functionality as SimpleSAML_Error_Error::show, looks something like: public static function show(SimpleSAML_Configuration $config, array $data) { $t = new SimpleSAML_XHTML_Template($config, 'error.php', 'errors'); $t->data = array_merge($t->data, $data); $t->show(); exit; } simplesamlphp-1.15.3/docs/simplesamlphp-advancedfeatures.md0000644000000000000000000001510713245225037022616 0ustar rootrootSimpleSAMLphp Advanced Features =============================== SimpleSAMLphp documentation --------------------------- This document is part of the SimpleSAMLphp documentation suite. - [List of all SimpleSAMLphp documentation](http://simplesamlphp.org/docs) This document assumes that you already have a installation of SimpleSAMLphp running, configured and working. This is the next step :) Bridging between protocols -------------------------- A bridge between two protocols is built using both an IdP and an SP, connected together. To let a SAML 2.0 SP talk to a SAML 1.1 IdP, you build a SimpleSAMLphp bridge from a SAML 2.0 IdP and a SAML 1.1 SP. The SAML 2.0 SP talks to the SAML 2.0 IdP, which hands the request over to the SAML 1.1 SP, which forwards it to the SAML 1.1 IdP. If you have followed the instructions for setting up an SP, and have configured an authentication source, all you need to do is to add that authentication source to the IdP. **Example of bridge configuration** In `metadata/saml20-idp-hosted.php`: 'auth' => 'default-sp', In `config/authsources.php`: 'default-sp' => array( 'saml:SP', ), Attribute control ----------------- Filtering, mapping, etc can be performed by using existing or create new *Authentication Processing Filters*. For more information, read: * [Authentication Processing Filters in SimpleSAMLphp](simplesamlphp-authproc) Automatic update of SAML 2.0 Metadata XML from HTTPS ---------------------------------------------------- The `metarefresh` module is the preferred method for doing this. Please see the [metarefresh documentation](simplesamlphp-automated_metadata). Using simpleSAMLphp on a web server requiring the use of a web proxy -------------------------------------------------------------------- Some modules in simpleSAMLphp may require fetching HTTP/HTTPS content from external websites (e.g. the metarefresh module needs to fetch the metadata from an external source). simpleSAMLphp can be configured to send HTTP/S requests via such a proxy. The proxy can be configured in the config/config.php option "proxy". Should the proxy require authentication, this can be configured with "proxy.auth". The default is not to use a proxy ('proxy' = null) and no username and password are used ('proxy.auth' = false). Auth MemCookie -------------- [Auth MemCookie](http://authmemcookie.sourceforge.net/) support is deprecated in the standard code base of SimpleSAMLphp and will no longer be available starting in SimpleSAMLphp 2.0. Please use the new [memcookie module](https://github.com/simplesamlphp/simplesamlphp-module-memcookie) instead. Metadata signing ---------------- SimpleSAMLphp supports signing of the metadata it generates. Metadata signing is configured by four options: - `metadata.sign.enable`: Whether metadata signing should be enabled or not. Set to `TRUE` to enable metadata signing. Defaults to `FALSE`. - `metadata.sign.privatekey`: Name of the file with the private key which should be used to sign the metadata. This file must exist in in the `cert` directory. - `metadata.sign.privatekey_pass`: Passphrase which should be used to open the private key. This parameter is optional, and should be left out if the private key is unencrypted. - `metadata.sign.certificate`: Name of the file with the certificate which matches the private key. This file must exist in in the `cert` directory. - `metadata.sign.algorithm`: The algorithm to use when signing metadata for this entity. Defaults to RSA-SHA1. Possible values: * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` *Note*: the use of SHA1 is **deprecated** and will be disallowed in the future. * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` These options can be configured globally in the `config/config.php`-file, or per SP/IdP by adding them to the hosted metadata for the SP/IdP. The configuration in the metadata for the SP/IdP takes precedence over the global configuration. There is also an additional fallback for the private key and the certificate. If `metadata.sign.privatekey` and `metadata.sign.certificate` isn't configured, SimpleSAMLphp will use the `privatekey`, `privatekey_pass` and `certificate` options in the metadata for the SP/IdP. Session checking function ------------------------- Optional session checking function, called on session init and loading, defined with 'session.check_function' in config.php. Example code for the function with GeoIP country check: public static function checkSession($session, $init = FALSE) { $data_type = 'example:check_session'; $data_key = 'remote_addr'; $remote_addr = NULL; if (!empty($_SERVER['REMOTE_ADDR'])) { $remote_addr = (string)$_SERVER['REMOTE_ADDR']; } if ($init) { $session->setData($data_type, $data_key, $remote_addr, SimpleSAML_Session::DATA_TIMEOUT_SESSION_END); return; } if (!function_exists('geoip_country_code_by_name')) { SimpleSAML\Logger::warning('geoip php module required.'); return TRUE; } $stored_remote_addr = $session->getData($data_type, $data_key); if ($stored_remote_addr === NULL) { SimpleSAML\Logger::warning('Stored data not found.'); return FALSE; } $country_a = geoip_country_code_by_name($remote_addr); $country_b = geoip_country_code_by_name($stored_remote_addr); if ($country_a === $country_b) { if ($stored_remote_addr !== $remote_addr) { $session->setData($data_type, $data_key, $remote_addr, SimpleSAML_Session::DATA_TIMEOUT_SESSION_END); } return TRUE; } return FALSE; } Support ------- If you need help to make this work, or want to discuss SimpleSAMLphp with other users of the software, you are fortunate: Around SimpleSAMLphp there is a great Open source community, and you are welcome to join! The forums are open for you to ask questions, contribute answers other further questions, request improvements or contribute with code or plugins of your own. - [SimpleSAMLphp homepage](https://simplesamlphp.org) - [List of all available SimpleSAMLphp documentation](https://simplesamlphp.org/docs/) - [Join the SimpleSAMLphp user's mailing list](https://simplesamlphp.org/lists) simplesamlphp-1.15.3/docs/simplesamlphp-sp-migration.md0000644000000000000000000002301013245225037021713 0ustar rootrootMigrating to the `saml` module ============================== This document describes how you can migrate your code to use the `saml` module for authentication against SAML 2.0 and SAML 1.1 IdPs. It assumes that you have previously set up a SP by using redirects to `saml2/sp/initSSO.php`. The steps we are going to follow are: 1. Create a new authentication source. 2. Add the metadata for this authentication source to the IdP. 3. Test the new authentication source. 4. Convert the application to use the new API. 5. Test the application. 6. Remove the old metadata from the IdP. 7. Disable the old SAML 2 SP. Create a new authentication source ---------------------------------- In this step we are going to create an authentication source which uses the `saml` module for authentication. To do this, we open `config/authsources.php`. Create the file if it does not exist. If you create the file, it should look like this: array( 'saml:SP', /* * The entity ID of this SP. * Can be NULL/unset, in which case an entity ID is generated based on the metadata URL. */ 'entityID' => NULL, /* * The entity ID of the IdP this should SP should contact. * Can be NULL/unset, in which case the user will be shown a list of available IdPs. */ 'idp' => NULL, /* Here you can add other options to the SP. */ ), `default-sp` is the name of the authentication source. It is used to refer to this authentication source when we use it. `saml:SP` tells SimpleSAMLphp that authentication with this authentication source is handled by the `saml` module. The `idp` option should be set to the same value that is set in `default-saml20-idp` in `config.php`. To ease migration, you probably want the entity ID on the new SP to be different than on the old SP. This makes it possible to have both the old and the new SP active on the IdP at the same time. You can also add other options this authentication source. See the [`saml:SP`](./saml:sp) documentation for more information. Add the metadata for this authentication source to the IdP ---------------------------------------------------------- After adding the authentication source on the SP, you need to register the metadata on the IdP. To retrieve the metadata, open the frontpage of your SimpleSAMLphp installation, and go to the federation tab. You should have a list of metadata entries, and one will be marked with the name of the new authentication source. In our case, that was `default-sp`. Click the `Show metadata` link, and you will arrive on a web page with the metadata for that service provider. How you proceed from here depends on which IdP you are connecting to. If you use a SimpleSAMLphp IdP, you can use the metadata in the flat file format at the bottom of the page. That metadata should be added to `saml20-sp-remote.php` on the IdP. For other IdPs you probably want to use the XML metadata. Test the new authentication source ---------------------------------- You should now be able to log in using the new authentication source. Go to the frontpage of your SimpleSAMLphp installation and open the authentication tab. There you will find a link to test authentication sources. Click that link, and select the name of your authentication source (`default-sp` in our case). You should be able to log in using that authentication source, and receive the attributes from the IdP. Convert the application to use the new API ------------------------------------------ This section will go through some common changes that you need to do when you are using SimpleSAMLphp from a different application. ### `_include.php` You should also no longer include `.../simplesamlphp/www/_include.php`. Instead, you should include `.../simplesamlphp/lib/_autoload.php`. This means that you replace lines like: require_once('.../simplesamlphp/www/_include.php'); with: require_once('.../simplesamlphp/lib/_autoload.php'); `_autoload.php` will register an autoloader function for the SimpleSAMLphp classes. This makes it possible to access the classes from your application. `_include.php` does the same, but also has some side-effects that you may not want in your application. If you load any SimpleSAMLphp class files directly, you should remove those lines. That means that you should remove lines like the following: require_once('SimpleSAML/Utilities.php'); require_once('SimpleSAML/Session.php'); require_once('SimpleSAML/XHTML/Template.php'); ### Authentication API There is a new authentication API in SimpleSAMLphp which can be used to authenticate against authentication sources. This API is designed to handle the common operations. #### Overview This is a quick overview of the API: /* Get a reference to our authentication source. */ $as = new \SimpleSAML\Auth\Simple('default-sp'); /* Require the user to be authentcated. */ $as->requireAuth(); /* When that function returns, we have an authenticated user. */ /* * Retrieve attributes of the user. * * Note: If the user isn't authenticated when getAttributes() is * called, an empty array will be returned. */ $attributes = $as->getAttributes(); /* Log the user out. */ $as->logout(); #### `$config` and `$session` Generally, if you have: $config = SimpleSAML_Configuration::getInstance(); $session = SimpleSAML_Session::getSessionFromRequest(); you should replace it with this single line: $as = new \SimpleSAML\Auth\Simple('default-sp'); #### Requiring authentication Blocks of code like the following: /* Check if valid local session exists.. */ if (!isset($session) || !$session->isValid('saml2') ) { SimpleSAML_Utilities::redirect( '/' . $config->getBaseURL() . 'saml2/sp/initSSO.php', array('RelayState' => SimpleSAML_Utilities::selfURL()) ); } should be replaced with a single call to `requireAuth()`: $as->requireAuth(); #### Fetching attributes Where you previously called: $session->getAttributes(); you should now call: $as->getAttributes(); #### Logging out Redirecting to the initSLO-script: SimpleSAML_Utilities::redirect( '/' . $config->getBaseURL() . 'saml2/sp/initSLO.php', array('RelayState' => SimpleSAML_Utilities::selfURL()) ); should be replaced with a call to `logout()`: $as->logout(); If you want to return to a specific URL after logging out, you should include that URL as a parameter to the logout function: $as->logout('https://example.org/'); Please make sure the URL is trusted. If you obtain the URL from the user input, make sure it is trusted before calling $as->logout(), by using the SimpleSAML_Utilities::checkURLAllowed() method. #### Login link If you have any links to the initSSO-script, those links must be replaced with links to a new script. The URL to the new script is `https://.../simplesaml/module.php/core/as_login.php`. It has two mandatory parameters: * `AuthId`: The id of the authentication source. * `ReturnTo`: The URL the user should be redirected to after authentication. #### Logout link Any links to the initSLO-script must be replaced with links to a new script. The URL to the new script is `https://.../simplesaml/module.php/core/as_logout.php`. It has two mandatory parameters: * `AuthId`: The id of the authentication source. * `ReturnTo`: The URL the user should be redirected to after logout. Test the application -------------------- How you test the application is highly dependent on the application, but here are the elements you should test: ### SP initiated login Make sure that it is still possible to log into the application. ### IdP initiated login If you use a SimpleSAMLphp IdP, and you want users to be able to bookmark the login page, you need to test IdP initiated login. To test IdP initiated login from a SimpleSAMLphp IdP, you can access: https://.../simplesaml/saml2/idp/SSOService.php?spentityid=&RelayState= Note that the RelayState parameter is only supported if the IdP runs version 1.5 of SimpleSAMLphp. If it isn't supported by the IdP, you need to configure the `RelayState` option in the authentication source configuration. ### SP initiated logout Make sure that logging out of your application also logs out of the IdP. If this does not work, users who log out of your application can log in again without entering any username or password. ### IdP initiated logout This is used by the IdP if the user logs out of a different SP connected to the IdP. In this case, the user should also be logged out of your application. The easiest way to test this is if you have two SPs connected to the IdP. You can then log out of one SP and check that you are also logged out of the other. Remove the old metadata from the IdP ------------------------------------ Once the new SP works correctly, you can remove the metadata for the old SP from the IdP. How you do that depends on the IdP. If you are running a SimpleSAMLphp IdP, you can remove the entry for the old SP in `metadata/saml20-sp-remote.php`. Disable the old SAML 2 SP ------------------------- You may also want to disable the old SP code in SimpleSAMLphp. To do that, open `config/config.php`, and change the `enable.saml20-sp` option to `FALSE`. simplesamlphp-1.15.3/docs/simplesamlphp-metadata-extensions-rpi.md0000644000000000000000000001036113245225037024054 0ustar rootrootSAML V2.0 Metadata Extensions for Registration and Publication Information ============================= * Author: Jaime Perez [jaime.perez@uninett.no](mailto:jaime.perez@uninett.no) This is a reference for the SimpleSAMLphp implementation of the [SAML V2.0 Metadata Extensions for Registration and Publication Information](http://docs.oasis-open.org/security/saml/Post2.0/saml-metadata-rpi/v1.0/saml-metadata-rpi-v1.0.html) defined by OASIS. This extension aims to provide information about the registrars and publishers of the metadata themselves, and it is therefore available throught different endpoints and modules that provide metadata all along SimpleSAMLphp. More specifically, this extension can be used for: - metadata published for a [hosted service provider](./saml:sp). - metadata published for a [hosted identity provider](./simplesamlphp-reference-idp-hosted). - metadata collected and published by means of the [`aggregator`](./aggregator:aggregator) or [`aggregator2`](./aggregator2:aggregator2) modules. Currently, only the `` element is supported. Depending on the metadata set you want to add this extension to, you will have to configure it on the corresponding configuration file: - `metadata/saml20-idp-hosted.php` for hosted identity providers. - `config/authsources.php` for hosted service providers. - `config/module_aggregator.php` for the `aggregator` module. - `config/module_aggregator2.php` for the `aggregator2` module. RegistrationInfo Items ---------------------- The configuration is the same for all the different files, and consists of a single directive called `RegistrationInfo`, which **must** be an indexed array with the following options: `authority` : A string containing an identifier of the authority who has registered this metadata. This parameter is **mandatory**. `instant` : A string containing the instant when the entity or entities where registered by the authority. This parameter is optional, and must be expressed in the UTC timezone with the *zulu* (`Z`) timezone identifier. If omitted, there will be no `registrationInstant` in the resulting metadata, except in the `aggregator2` module, which will use the instant when the metadata was generated. `policies` : An indexed array containing URLs pointing to the policy under which the entity or entities where registered. Each index must be the language code corresponding to the language of the URL. This parameter is optional, and will be omitted in the resulting metadata if not configured. Examples -------- Service Provider: 'default-sp' => array( 'saml:SP', 'entityID' => NULL, ... 'RegistrationInfo' => array( 'authority' => 'urn:mace:sp.example.org', 'instant' => '2008-01-17T11:28:03.577Z', 'policies' => array('en' => 'http://sp.example.org/policy', 'es' => 'http://sp.example.org/politica'), ), ), Identity Provider: $metadata['__DYNAMIC:1__'] = array( 'host' => '__DEFAULT__', ... 'RegistrationInfo' => array( 'authority' => 'urn:mace:idp.example.org', 'instant' => '2008-01-17T11:28:03.577Z', ), ); `aggregator` module: $config = array( 'aggregators' => array( ... ), 'maxDuration' => 60*60*24*5, 'reconstruct' => FALSE, ... 'RegistrationInfo' => array( 'authority' => 'urn:mace:example.federation', 'instant' => '2008-01-17T11:28:03Z', 'policies' => array('en' => 'http://example.org/federation_policy', 'es' => 'https://example.org/politica_federacion'), ), ); `aggregator2` module: $config = array( 'example.org' => array( 'sources' => array( ... ), 'RegistrationInfo' => array( 'authority' => 'urn:mace:example.federation', 'policies' => array('en' => 'http://example.org/federation_policy', 'es' => 'https://example.org/politica_federacion'), ), ), ); simplesamlphp-1.15.3/docs/simplesamlphp-metadata-extensions-ui.md0000644000000000000000000002552413245225037023706 0ustar rootrootSAML V2.0 Metadata Extensions for Login and Discovery User Interface ============================= * Author: Timothy Ace [tace@synacor.com](mailto:tace@synacor.com) This is a reference for the SimpleSAMLphp implementation of the [SAML V2.0 Metadata Extensions for Login and Discovery User Interface](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/sstc-saml-metadata-ui-v1.0.pdf) defined by OASIS. The metadata extensions are available to both IdP and SP usage of SimpleSAMLphp. For an IdP, the entries are placed in `metadata/saml20-idp-hosted.php`, for an SP, they are put inside the relevant entry in `authsources.php`. An example for an IdP: array( 'DisplayName' => array( 'en' => 'English name', 'es' => 'Nombre en Español', ), 'Description' => array( 'en' => 'English description', 'es' => 'Descripción en Español', ), 'InformationURL' => array( 'en' => 'http://example.com/info/en', 'es' => 'http://example.com/info/es', ), 'PrivacyStatementURL' => array( 'en' => 'http://example.com/privacy/en', 'es' => 'http://example.com/privacy/es', ), 'Keywords' => array( 'en' => array('communication', 'federated session'), 'es' => array('comunicación', 'sesión federated'), ), 'Logo' => array( array( 'url' => 'http://example.com/logo1.png', 'height' => 200, 'width' => 400, 'lang' => 'en', ), array( 'url' => 'http://example.com/logo2.png', 'height' => 201, 'width' => 401, ), ), ), 'DiscoHints' => array( 'IPHint' => array('130.59.0.0/16', '2001:620::0/96'), 'DomainHint' => array('example.com', 'www.example.com'), 'GeolocationHint' => array('geo:47.37328,8.531126', 'geo:19.34343,12.342514'), ), /* ... */ ); And for an SP it could look like this: array( 'saml:SP', 'UIInfo' => array( 'DisplayName' => array( 'en' => 'English name', 'es' => 'Nombre en Español' ), 'Description' => array( 'en' => 'English description', 'es' => 'Descripción en Español ), ), /* ... */ ), ); The OASIS specification primarily defines how an entity can communicate metadata related to IdP or service discovery and identification. There are two different types of extensions defined. There are the ``elements that define how an IdP or SP should be displayed and there are the `` elements that define when an IdP should be chosen/displayed. UIInfo Items -------------- These elements are used for IdP and SP discovery to determine what to display about an IdP or SP. These properties are all children of the `UIInfo` key. *Note*: Most elements are localized strings that specify the language using the array key as the language-code: 'DisplayName' => array( 'en' => 'English name', 'es' => 'Nombre en Español', ), `DisplayName` : The localized list of names for this entity 'DisplayName' => array( 'en' => 'English name', 'es' => 'Nombre en Español', ), `Description` : The localized list of statements used to describe this entity 'Description' => array( 'en' => 'English description', 'es' => 'Descripción en Español', ), `InformationURL` : A localized list of URLs where more information about the entity is located. 'InformationURL' => array( 'en' => 'http://example.com/info/en', 'es' => 'http://example.com/info/es', ), `PrivacyStatementURL` : A localized list of URLs where the entity's privacy statement is located. 'PrivacyStatementURL' => array( 'en' => 'http://example.com/privacy/en', 'es' => 'http://example.com/privacy/es', ), `Keywords` : A localized list of keywords used to describe the entity 'Keywords' => array( 'en' => array('communication', 'federated session'), 'es' => array('comunicación', 'sesión federated'), ), : *Note*: The `+` (plus) character is forbidden by specification from being part of a Keyword. `Logo` : The logos used to represent the entity 'Logo' => array( array( 'url' => 'http://example.com/logo1.png', 'height' => 200, 'width' => 400, 'lang' => 'en', ), array( 'url' => 'http://example.com/logo2.png', 'height' => 201, 'width' => 401, ), ), : An optional `lang` key containing a language-code is supported for localized logos. DiscoHints Items -------------- These elements are only relevant when operating in the IdP role; they assist IdP discovery to determine when to choose or present an IdP. These properties are all children of the `DiscoHints` key. `IPHint` : This is a list of both IPv4 and IPv6 addresses in CIDR notation services by or associated with this entity. 'IPHint' => array('130.59.0.0/16', '2001:620::0/96'), `DomainHint` : This specifies a list of domain names serviced by or associated with this entity. 'DomainHint' => array('example.com', 'www.example.com'), `GeolocationHint` : This specifies a list of geographic coordinates associated with, or serviced by, the entity. Coordinates are given in URI form using the geo URI scheme [RFC5870](http://www.ietf.org/rfc/rfc5870.txt). 'GeolocationHint' => array('geo:47.37328,8.531126', 'geo:19.34343,12.342514'), Generated XML Metadata Examples ---------------- If given the following configuration... $metadata['https://www.example.com/saml/saml2/idp/metadata.php'] = array( 'host' => 'www.example.com', 'certificate' => 'example.com.crt', 'privatekey' => 'example.com.pem', 'auth' => 'example-userpass', 'UIInfo' => array( 'DisplayName' => array( 'en' => 'English name', 'es' => 'Nombre en Español', ), 'Description' => array( 'en' => 'English description', 'es' => 'Descripción en Español', ), 'InformationURL' => array( 'en' => 'http://example.com/info/en', 'es' => 'http://example.com/info/es', ), 'PrivacyStatementURL' => array( 'en' => 'http://example.com/privacy/en', 'es' => 'http://example.com/privacy/es', ), 'Keywords' => array( 'en' => array('communication', 'federated session'), 'es' => array('comunicación', 'sesión federated'), ), 'Logo' => array( array( 'url' => 'http://example.com/logo1.png', 'height' => 200, 'width' => 400, ), array( 'url' => 'http://example.com/logo2.png', 'height' => 201, 'width' => 401, ), ), ), 'DiscoHints' => array( 'IPHint' => array('130.59.0.0/16', '2001:620::0/96'), 'DomainHint' => array('example.com', 'www.example.com'), 'GeolocationHint' => array('geo:47.37328,8.531126', 'geo:19.34343,12.342514'), ), ); ... will generate the following XML metadata: English name Nombre en Español English description Descripción en Español http://example.com/info/en http://example.com/info/es http://example.com/privacy/en http://example.com/privacy/es communication federated+session comunicación sesión+federated http://example.com/logo1.png http://example.com/logo2.png 130.59.0.0/16 2001:620::0/96 example.com www.example.com geo:47.37328,8.531126 geo:19.34343,12.342514 ... simplesamlphp-1.15.3/docs/simplesamlphp-translation.md0000644000000000000000000000656413245225037021657 0ustar rootrootSimpleSAMLphp Translation Portal ================================================================ ## How translated terms are referred from a template Here is an example of how two terms are included in a template from dictionary files:

t('{core:frontpage:about_header}'); ?>

t('{core:frontpage:about_text}'); ?>

In this example, two translated terms are included: `about_header` and `about_text`. Both these terms are found in a dictionary file named `frontpage`, inside the module named `core`. **Note:** An important use-case here is that you can create your own module, that includes a new theme that overrides some of the default templates. You may in this template refer to both terms from the existing dictionary files, but you can also add new dictionary files in your new module that may introduce new alternative terms. ## The definition file When the template library is about to lookup the translation of a term, it will lookup * the definition file, for the English translation, and * the translation file, for translation to other languages. SimpleSAMLphp will always fallback to the English translation using the definition file, both: * when the term is not translated into the *current selected language*, and * when the translation file is not available at all. The name of the definition file is `BASENAME.definition.json`, where the term is referred to like this: `{MODULENAME:BASENAME:TERM}`. The file MUST be placed in the followng location: `modules/MODULENAME/dictionaries/BASENAME.definition.json`. The content of the defintion file is a *JSON encoded array* of `term => definition`, where definition is an array with an required `en` index for the english translation, and the value is the English text. Here is an example of a definition file with three terms: { "header": { "en": "Missing cookie" }, "description": { "en": "You appear to have disabled cookies in your browser. Please check the settings in your browser, and try again." }, "retry": { "en": "Retry" } } Note: you may not include other languages in the definition files, the `en` index is used in order to at a later point in time introduce more meta information for each term, like in example: "header": { "en": "Missing cookie", "_note": "This text shows up on the error page when the browser do not support cookies." }, To summarize the pattern of the definition file is as follows: { "TERM1": { "en": "English text 1" }, "TERM2": { "en": "English text 2" } } ## The translation file The translation file is similar to the definition file, but including translation to languages others than English. The structure of the file is identical to the definition files, except from the language index, which now is not `en`, but the actual langauge that is translated: { "TERM1": { "no": "Norsk tekst 1", "da": "Dansk tekst 1" }, "TERM2": { "no": "Norsk tekst 2", "da": "Dansk tekst 2" } } simplesamlphp-1.15.3/docs/simplesamlphp-hok-idp.md0000644000000000000000000000712213245225037020643 0ustar rootrootAdding Holder-of-Key Web Browser SSO Profile support to the IdP =============================================================== This document describes the necessary steps to enable support for the [SAML V2.0 Holder-of-Key (HoK) Web Browser SSO Profile](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-holder-of-key-browser-sso.pdf) on a SimpleSAMLphp Identity Provider (IdP). The SAML V2.0 HoK Web Browser SSO Profile is an alternate version of the standard SAML Web Browser SSO Profile. Its primary benefit is the enhanced security of the SSO process while preserving maximum compatibility with existing deployments on client and server side. When using this profile the communication between the user and the IdP is required to be protected by the TLS protocol. Additionally, the user needs a TLS client certificate. This certificate is usually selfsigned and stored in the certificate store of the browser or the underlying operating system. Configuring Apache ------------------ The IdP requests a client certificate from the user agent during the TLS handshake. This behaviour is enabled with the following Apache webserver configuration: SSLEngine on SSLCertificateFile /etc/openssl/certs/server.crt SSLCertificateKeyFile /etc/openssl/private/server.key SSLVerifyClient optional_no_ca SSLOptions +ExportCertData If the user agent can successfully prove possession of the private key associated to the public key from the certificate, the received certificate is stored in the environment variable `SSL_CLIENT_CERT` of the webserver. The IdP embeds the client certificate into the created HoK assertion. Enabling HoK SSO Profile on the IdP ----------------------------------- To enable the IdP to send HoK assertions you must add the `saml20.hok.assertion` option to the `saml20-idp-hosted` metadata file: $metadata['__DYNAMIC:1__'] = array( [....] 'auth' => 'example-userpass', 'saml20.hok.assertion' => TRUE, ); Add new metadata to SPs ----------------------- After enabling the Holder-of-Key Web Browser SSO Profile your IdP metadata will change. An additional HoK `SingleSignOnService` endpoint is added. You therefore need to update the metadata for your IdP at your SPs. The `saml20-idp-remote` metadata for SimpleSAMLphp SPs should contain something like the following code: 'SingleSignOnService' => array ( array ( 'hoksso:ProtocolBinding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Binding' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php', ), array ( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect', 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/SSOService.php', ), ), SP metadata on the IdP ---------------------- A SP using the HoK Web Browser SSO Profile must have an `AssertionConsumerService` endpoint supporting that profile. This means that you have to use the complex endpoint format in `saml20-sp-remote` metadata. In general, this should look like the following code: 'AssertionConsumerService' => array ( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', 'index' => 0, ), array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', 'index' => 4, ), ), (The specific values of the various fields will vary depending on the SP.) simplesamlphp-1.15.3/docs/simplesamlphp-artifact-idp.md0000644000000000000000000000732113245225037021660 0ustar rootrootAdding HTTP-Artifact support to the IdP ======================================= This document describes the necessary steps to enable support for the HTTP-Artifact binding on a SimpleSAMLphp IdP: 1. Configure SimpleSAMLphp to use memcache to store the session. 2. Enable support for sending artifacts in `saml20-idp-hosted`. 3. Add the webserver certificate to the generated metadata. Memcache -------- To enable memcache, you must first install and configure memcache on the server hosting your IdP. You need both a memcache server and a the PHP memcached client (extension). How this is done depends on the distribution. If you are running Debian or Ubuntu, you can install this by running: apt install memcached php-memcached simpleSAMLphp also supports the legacy `php-memcache` (without `d`) variant. *Note*: For security, you must make sure that the memcache server is inaccessible to other hosts. The default configuration on Debian is for the memcache server to be accessible to only the local host. Once the memcache server is configured, you can configure simplesamlphp to use it to store sessions. You can do this by setting the `session.handler` option in `config.php` to `memcache`. If you are running memcache on a different server than the IdP, you must also change the `memcache_store.servers` option in `config.php`. Enabling artifact on the IdP ---------------------------- To enable the IdP to send artifacts, you must add the `saml20.sendartifact` option to the `saml20-idp-hosted` metadata file: $metadata['__DYNAMIC:1__'] = array( [....] 'auth' => 'example-userpass', 'saml20.sendartifact' => TRUE, ); Add new metadata to SPs ----------------------- After enabling the Artifact binding, your IdP metadata will change to add a ArtifactResolutionService endpoint. You therefore need to update the metadata for your IdP at your SPs. `saml20-idp-remote` metadata for SimpleSAMLphp SPs should contain something like: 'ArtifactResolutionService' => array( array( 'index' => 0, 'Location' => 'https://idp.example.org/simplesaml/saml2/idp/ArtifactResolutionService.php', 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:SOAP', ), ), SP metadata on the IdP ---------------------- An SP using the HTTP-Artifact binding must have an AssertionConsumerService endpoint supporting that binding. This means that you must use the complex endpoint format in `saml20-sp-remote` metadata. In general, that should look something like: 'AssertionConsumerService' => array ( array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', 'index' => 0, ), array( 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact', 'Location' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', 'index' => 2, ), ), (The specific values of the various fields will vary depending on the SP.) Certificate in metadata ----------------------- Some SPs validates the SSL certificate on the ArtifactResolutionService using the certificates in the metadata. You may therefore have to add the webserver certificate to the metadata that your IdP generates. To do this, you need to set the `https.certificate` option in the `saml20-idp-hosted` metadata file. That option should refer to a file containing the webserver certificate. $metadata['__DYNAMIC:1__'] = array( [....] 'auth' => 'example-userpass', 'saml20.sendartifact' => TRUE, 'https.certificate' => '/etc/apache2/webserver.crt', ); simplesamlphp-1.15.3/docs/simplesamlphp-customauth.md0000644000000000000000000003311713245225037021507 0ustar rootrootImplementing custom username/password authentication ==================================================== This is a step-by-step guide for creating a custom username/password [authentication source](./simplesamlphp-authsource) for SimpleSAMLphp. An authentication source is responsible for authenticating the user, typically by getting a username and password, and looking it up in some sort of database. Create a custom module ---------------------- All custom code for SimpleSAMLphp should be contained in a [module](./simplesamlphp-modules). This ensures that you can upgrade your SimpleSAMLphp installation without overwriting your own code. In this example, we will call the module `mymodule`. It will be located under `modules/mymodule`. First we need to create the module directory: cd modules mkdir mymodule Since this is a custom module, it should always be enabled. Therefore we create a `default-enable` file in the module. We do that by copying the `default-enable` file from the `core` module. cd mymodule cp ../core/default-enable . Now that we have our own module, we can move on to creating an authentication source. Creating a basic authentication source -------------------------------------- Authentication sources are implemented using PHP classes. We are going to create an authentication source named `mymodule:MyAuth`. It will be implemented in the file `modules/mymodule/lib/Auth/Source/MyAuth.php`. To begin with, we will create a very simple authentication source, where the username and password is hardcoded into the source code. Create the file `modules/mymodule/lib/Auth/Source/MyAuth.php` with the following contents: array('theusername'), 'displayName' => array('Some Random User'), 'eduPersonAffiliation' => array('member', 'employee'), ); } } Some things to note: - The classname is `sspmod_mymodule_Auth_Source_MyAuth`. This tells SimpleSAMLphp to look for the class in `modules/mymodule/lib/Auth/Source/MyAuth.php`. - Our authentication source subclassese `sspmod_core_Auth_UserPassBase`. This is a helper-class that implements much of the common code needed for username/password authentication. - The `login` function receives the username and password the user enters. It is expected to authenticate the user. If the username or password is correct, it must return a set of attributes for the user. Otherwise, it must throw the `SimpleSAML_Error_Error('WRONGUSERPASS');` exception. - Attributes are returned as an associative array of `name => values` pairs. All attributes can have multiple values, so the values are always stored in an array. Configuring our authentication source ------------------------------------- Before we can test our authentication source, we must add an entry for it in `config/authsources.php`. `config/authsources.php` contains an list of enabled authentication sources. The entry looks like this: 'myauthinstance' => array( 'mymodule:MyAuth', ), You can add it to the beginning of the list, so that the file looks something like this: array( 'mymodule:MyAuth', ), /* Other authentication sources follow. */ ); `myauthinstance` is the name of this instance of the authentication source. (You are allowed to have multiple instances of an authentication source with different configuration.) The instance name is used to refer to this authentication source in other configuration files. The first element of the configuration of the authentication source must be `'mymodule:MyAuth'`. This tells SimpleSAMLphp to look for the `sspmod_mymodule_Auth_Source_MyAuth` class. Testing our authentication source --------------------------------- Now that we have configured the authentication source, we can test it by accessing "authentication"-page of the SimpleSAMLphp web interface. By default, the web interface can be found on `http://yourhostname.com/simplesaml/`. (Obviously, "yourhostname.com" should be replaced with your real hostname.) Then select the "Authentication"-tab, and choose "Test configured authentication sources". You should then receive a list of authentication sources from `config/authsources.php`. Select `myauthinstance`, and log in using "theusername" as the username, and "thepassword" as the password. You should then arrive on a page listing the attributes we return from the `login` function. Next, you should log out by following the log out link. Using our authentication source in an IdP ----------------------------------------- To use our new authentication source in an IdP we just need to update the IdP configuration to use it. Open `metadata/saml20-idp-hosted.php`. In that file you should locate the `auth`-option for your IdP, and change it to `myauthinstance`: 'myauthinstance', /* ... */ ); You can then test logging in to the IdP. If you have logged in previously, you may need to log out first. Adding configuration to our authentication source ------------------------------------------------- Instead of hardcoding options in our authentication source, they should be configurable. We are now going to extend our authentication source to allow us to configure the username and password in `config/authsources.php`. First, we need to define the properties in the class that should hold our configuration: private $username; private $password; Next, we create a constructor for the class. The constructor is responsible for parsing the configuration and storing it in the properties. public function __construct($info, $config) { parent::__construct($info, $config); if (!is_string($config['username'])) { throw new Exception('Missing or invalid username option in config.'); } $this->username = $config['username']; if (!is_string($config['password'])) { throw new Exception('Missing or invalid password option in config.'); } $this->password = $config['password']; } We can then use the properties in the `login` function. The complete class file should look like this: username = $config['username']; if (!is_string($config['password'])) { throw new Exception('Missing or invalid password option in config.'); } $this->password = $config['password']; } protected function login($username, $password) { if ($username !== $this->username || $password !== $this->password) { throw new SimpleSAML_Error_Error('WRONGUSERPASS'); } return array( 'uid' => array($this->username), 'displayName' => array('Some Random User'), 'eduPersonAffiliation' => array('member', 'employee'), ); } } We can then update our entry in `config/authsources.php` with the configuration options: 'myauthinstance' => array( 'mymodule:MyAuth', 'username' => 'theconfigusername', 'password' => 'theconfigpassword', ), Next, you should go to the "Test configured authentication sources" page again, and test logging in. Note that we have updated the username & password to "theconfigusername" and "theconfigpassword". (You may need to log out first before you can log in again.) A more complete example - custom database authentication -------------------------------------------------------- The [sqlauth:SQL](./sqlauth:sql) authentication source can do simple authentication against SQL databases. However, in some cases it cannot be used, for example because the database layout is too complex, or because the password validation routines cannot be implemented in SQL. What follows is an example of an authentication source that fetches an user from a database, and validates the password using a custom function. This code assumes that the database contains a table that looks like this: CREATE TABLE userdb ( username VARCHAR(32) PRIMARY KEY NOT NULL, password_hash VARCHAR(64) NOT NULL, full_name TEXT NOT NULL); An example user (with password "secret"): INSERT INTO userdb (username, password_hash, full_name) VALUES('exampleuser', 'QwVYkvlrAMsXIgULyQ/pDDwDI3dF2aJD4XeVxg==', 'Example User'); In this example, the `password_hash` contains a base64 encoded SSHA password. A SSHA password is created like this: $password = 'secret'; $numSalt = 8; /* Number of bytes with salt. */ $salt = ''; for ($i = 0; $i < $numSalt; $i++) { $salt .= chr(mt_rand(0, 255)); } $digest = sha1($password . $salt, TRUE); $password_hash = base64_encode($digest . $salt); The class follows: dsn = $config['dsn']; if (!is_string($config['username'])) { throw new Exception('Missing or invalid username option in config.'); } $this->username = $config['username']; if (!is_string($config['password'])) { throw new Exception('Missing or invalid password option in config.'); } $this->password = $config['password']; } /** * A helper function for validating a password hash. * * In this example we check a SSHA-password, where the database * contains a base64 encoded byte string, where the first 20 bytes * from the byte string is the SHA1 sum, and the remaining bytes is * the salt. */ private function checkPassword($passwordHash, $password) { $passwordHash = base64_decode($passwordHash); $digest = substr($passwordHash, 0, 20); $salt = substr($passwordHash, 20); $checkDigest = sha1($password . $salt, TRUE); return $digest === $checkDigest; } protected function login($username, $password) { /* Connect to the database. */ $db = new PDO($this->dsn, $this->username, $this->password); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); /* Ensure that we are operating with UTF-8 encoding. * This command is for MySQL. Other databases may need different commands. */ $db->exec("SET NAMES 'utf8'"); /* With PDO we use prepared statements. This saves us from having to escape * the username in the database query. */ $st = $db->prepare('SELECT username, password_hash, full_name FROM userdb WHERE username=:username'); if (!$st->execute(array('username' => $username))) { throw new Exception('Failed to query database for user.'); } /* Retrieve the row from the database. */ $row = $st->fetch(PDO::FETCH_ASSOC); if (!$row) { /* User not found. */ SimpleSAML\Logger::warning('MyAuth: Could not find user ' . var_export($username, TRUE) . '.'); throw new SimpleSAML_Error_Error('WRONGUSERPASS'); } /* Check the password. */ if (!$this->checkPassword($row['password_hash'], $password)) { /* Invalid password. */ SimpleSAML\Logger::warning('MyAuth: Wrong password for user ' . var_export($username, TRUE) . '.'); throw new SimpleSAML_Error_Error('WRONGUSERPASS'); } /* Create the attribute array of the user. */ $attributes = array( 'uid' => array($username), 'displayName' => array($row['full_name']), 'eduPersonAffiliation' => array('member', 'employee'), ); /* Return the attributes. */ return $attributes; } } And configured in `config/authsources.php`: 'myauthinstance' => array( 'mymodule:MyAuth', 'dsn' => 'mysql:host=sql.example.org;dbname=userdatabase', 'username' => 'db_username', 'password' => 'secret_db_password', ), simplesamlphp-1.15.3/docs/simplesamlphp-sp-api.md0000644000000000000000000001611413245225037020502 0ustar rootrootSimpleSAMLphp SP API reference ============================== This document describes the \SimpleSAML\Auth\Simple API. This is the preferred API for integrating SimpleSAMLphp with other applications. Constructor ----------- new \SimpleSAML\Auth\Simple(string $authSource) The constructor initializes a \SimpleSAML\Auth\Simple object. ### Parameters It has a single parameter, which is the ID of the authentication source that should be used. This authentication source must exist in `config/authsources.php`. ### Example $auth = new \SimpleSAML\Auth\Simple('default-sp'); `isAuthenticated` ----------------- bool isAuthenticated() Check whether the user is authenticated with this authentication source. `TRUE` is returned if the user is authenticated, `FALSE` if not. ### Example if (!$auth->isAuthenticated()) { /* Show login link. */ print('Login'); } `requireAuth` ------------- void requireAuth(array $params = array()) Make sure that the user is authenticated. This function will only return if the user is authenticated. If the user isn't authenticated, this function will start the authentication process. ### Parameters `$params` is an associative array with named parameters for this function. See the documentation for the `login`-function for a description of the parameters. ### Example 1 $auth->requireAuth(); print("Hello, authenticated user!"); ### Example 2 /* * Return the user to the frontpage after authentication, don't post * the current POST data. */ $auth->requireAuth(array( 'ReturnTo' => 'https://sp.example.org/', 'KeepPost' => FALSE, )); print("Hello, authenticated user!"); `login` ------------- void login(array $params = array()) Start a login operation. This function will always start a new authentication process. ### Parameters The following global parameters are supported: `ErrorURL` (`string`) : A URL to a page which will receive errors that may occur during authentication. `KeepPost` (`bool`) : If set to `TRUE`, the current POST data will be submitted again after authentication. The default is `TRUE`. `ReturnTo` (`string`) : The URL the user should be returned to after authentication. The default is to return the user to the current page. `ReturnCallback` (`array`) : The function we should call when the user finishes authentication. The [`saml:SP`](./saml:sp) authentication source also defines some parameters. ### Example # Send a passive authentication request. $auth->login(array( 'isPassive' => TRUE, 'ErrorURL' => 'https://.../error_handler.php', )); `logout` -------- void logout(mixed $params = NULL) Log the user out. After logging out, the user will either be redirected to another page, or a function will be called. This function never returns. ### Parameters `$params` : Parameters for the logout operation. This can either be a simple string, in which case it is interpreted as the URL the user should be redirected to after logout, or an associative array with logout parameters. If this parameter isn't specified, we will redirect the user to the current URL after logout. If the parameter is an an array, it can have the following options: - `ReturnTo`: The URL the user should be returned to after logout. - `ReturnCallback`: The function that should be called after logout. - `ReturnStateParam`: The parameter we should return the state in when redirecting. - `ReturnStateStage`: The stage the state array should be saved with. The `ReturnState` parameters allow access to the result of the logout operation after it completes. ### Example 1 Logout, and redirect to the specified URL. $auth->logout('https://sp.example.org/logged_out.php'); ### Example 2 Same as the previous, but check the result of the logout operation afterwards. $auth->logout(array( 'ReturnTo' => 'https://sp.example.org/logged_out.php', 'ReturnStateParam' => 'LogoutState', 'ReturnStateStage' => 'MyLogoutState', )); And in logged_out.php: $state = SimpleSAML_Auth_State::loadState((string)$_REQUEST['LogoutState'], 'MyLogoutState'); $ls = $state['saml:sp:LogoutStatus']; /* Only works for SAML SP */ if ($ls['Code'] === 'urn:oasis:names:tc:SAML:2.0:status:Success' && !isset($ls['SubCode'])) { /* Successful logout. */ echo("You have been logged out."); } else { /* Logout failed. Tell the user to close the browser. */ echo("We were unable to log you out of all your sessions. To be completely sure that you are logged out, you need to close your web browser."); } `getAttributes` --------------- array getAttributes() Retrieve the attributes of the current user. If the user isn't authenticated, an empty array will be returned. The attributes will be returned as an associative array with the name of the attribute as the key and the value as an array of one or more strings: array( 'uid' => array('testuser'), 'eduPersonAffiliation' => array('student', 'member'), ) ### Example $attrs = $auth->getAttributes(); if (!isset($attrs['displayName'][0])) { throw new Exception('displayName attribute missing.'); } $name = $attrs['displayName'][0]; print('Hello, ' . htmlspecialchars($name)); `getAuthData` --------------- mixed getAuthData(string $name) Retrieve the specified authentication data for the current session. NULL is returned if the user isn't authenticated. The available authentication data depends on the module used for authentication. See the [`saml:SP`](./saml:sp) reference for information about available SAML authentication data. ### Example $idp = $auth->getAuthData('saml:sp:IdP'); print('You are logged in from: ' . htmlspecialchars($idp)); `getLoginURL` ------------- string getLoginURL(string $returnTo = NULL) Retrieve a URL that can be used to start authentication. ### Parameters `$returnTo` : The URL the user should be returned to after authentication. The default is the current page. ### Example $url = $auth->getLoginURL(); print('Login'); ### Note The URL returned by this function is static, and will not change. You can easily create your own links without using this function. The URL should be: .../simplesaml/module.php/core/as_login.php?AuthId=&ReturnTo= `getLogoutURL` -------------- string getLogoutURL(string $returnTo = NULL) Retrieve a URL that can be used to trigger logout. ### Parameters `$returnTo` : The URL the user should be returned to after logout. The default is the current page. ### Example $url = $auth->getLogoutURL(); print('Logout'); ### Note The URL returned by this function is static, and will not change. You can easily create your own links without using this function. The URL should be: .../simplesaml/module.php/core/as_logout.php?AuthId=&ReturnTo= simplesamlphp-1.15.3/docs/simplesamlphp-database.md0000644000000000000000000001013113245225037021046 0ustar rootrootSimpleSAML\Database ============================= Purpose ------- This document covers the SimpleSAML\Database class and is only relevant to anyone writing code for SimpleSAMLphp, including modules, that require a database connection. The Database class provides a single class that can be used to connect to a database which can be shared by anything within SimpleSAMLphp. Getting Started --------------- If you are just using the already configured database, which would normally be the case, all you need to do is get the global instance of the Database class. $db = SimpleSAML\Database::getInstance(); If there is a requirement to connect to an alternate database server (ex. authenticating users that exist on a different SQL server or database) you can specify an alternate configuration. $config = new SimpleSAML_Configuration($myconfigarray, "mymodule/lib/Auth/Source/myauth.php"); $db = SimpleSAML\Database::getInstance($config); That will create a new instance of the database, separate from the global instance, specific to the configuration defined in $myconfigarray. If you are going to specify an alternate config, your configuration array must contain the same keys that exist in the master config (database.dsn, database.username, database.password, database.prefix, etc). Database Prefix --------------- Administrators can add a prefix to all the table names that this database classes accesses and you should take that in account when querying. Assuming that a prefix has been configured as "sp_": $table = $db->applyPrefix("saml20_idp_hosted"); $table would be set to "sp_saml20_idp_hosted" Querying The Database --------------------- You can query the database through two public functions read() and write() which are fairly self-explanitory when it comes to determining which one to use when querying. ### Writing to The Database Since the database class allows administrators to configure master and slave database servers, the write function will always use the master database connection. The write function takes 2 parameters: SQL, params. $table = $db->applyPrefix("test"); $values = array( 'id' => 20, 'data' => 'Some data', ); $query = $db->write("INSERT INTO $table (id, data) VALUES (:id, :data)", $values); The values specified in the $values array will be bound to the placeholders and will be executed on the master. By default, values are binded as PDO::PARAM_STR. If you need to override this, you can specify it in the values array. $table = $db->applyPrefix("test"); $values = array( 'id' => array(20, PDO::PARAM_INT), 'data' => 'Some data', ); $query = $db->write("INSERT INTO $table (id, data) VALUES (:id, :data)", $values); You can also skip usage of prepared statements. You should **only** use this if you have a statement that has no user input (ex. CREATE TABLE). If the params variable is explicity set to false, it will skip usage of prepared statements. This is only available when writing to the database. $table = $db->applyPrefix("test"); $query = $db->write("CREATE TABLE IF NOT EXISTS $table (id INT(16) NOT NULL, data TEXT NOT NULL)", false); ### Reading The Database Since the database class allows administrators to configure master and slave database servers, the read function will randomly select a slave server to query. If no slaves are configured, it will read from the master. The read function takes 2 parameters: SQL, params. $table = $db->applyPrefix("test"); $values = array( 'id' => 20, ); $query = $db->read("SELECT * FROM $table WHERE id = :id", $values); The values specified in the $values array will be bound to the placeholders and will be executed on the selected slave. By default, values are binded as PDO::PARAM_STR. If you need to override this, you can specify it in the values array. $table = $db->applyPrefix("test"); $values = array( 'id' => array(20, PDO::PARAM_INT), ); $query = $db->read("SELECT * FROM $table WHERE id = :id", $values); simplesamlphp-1.15.3/docs/simplesamlphp-reference-sp-remote.md0000644000000000000000000004030113245225037023153 0ustar rootrootSP remote metadata reference ============================ This is a reference for metadata options available for `metadata/saml20-sp-remote.php` and `metadata/shib13-sp-remote.php`. Both files have the following format: array( 'en' => 'A service', 'no' => 'En tjeneste', ), `OrganizationName` : The name of the organization responsible for this SPP. This name does not need to be suitable for display to end users. : This option can be translated into multiple languages by specifying the value as an array of language-code to translated name: 'OrganizationName' => array( 'en' => 'Example organization', 'no' => 'Eksempel organisation', ), : *Note*: If you specify this option, you must also specify the `OrganizationURL` option. `OrganizationDisplayName` : The name of the organization responsible for this IdP. This name must be suitable for display to end users. If this option isn't specified, `OrganizationName` will be used instead. : This option can be translated into multiple languages by specifying the value as an array of language-code to translated name. : *Note*: If you specify this option, you must also specify the `OrganizationName` option. `OrganizationURL` : A URL the end user can access for more information about the organization. : This option can be translated into multiple languages by specifying the value as an array of language-code to translated URL. : *Note*: If you specify this option, you must also specify the `OrganizationName` option. `privacypolicy` : This is an absolute URL for where an user can find a privacypolicy for this SP. If set, this will be shown on the consent page. `%SPENTITYID%` in the URL will be replaced with the entity id of this service provider. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option in the IdP-hosted metadata. `userid.attribute` : The attribute name of an attribute which uniquely identifies the user. This attribute is used if SimpleSAMLphp needs to generate a persistent unique identifier for the user. This option can be set in both the IdP-hosted and the SP-remote metadata. The value in the sp-remote metadata has the highest priority. The default value is `eduPersonPrincipalName`. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option in the IdP-hosted metadata. SAML 2.0 options ---------------- The following SAML 2.0 options are available: `AssertionConsumerService` : The URL of the AssertionConsumerService endpoint for this SP. This option is required - without it you will not be able to send responses back to the SP. : The value of this option is specified in one of several [endpoint formats](./simplesamlphp-metadata-endpoints). `attributes.NameFormat` : What value will be set in the Format field of attribute statements. This parameter can be configured multiple places, and the actual value used is fetched from metadata by the following priority: : 1. SP Remote Metadata 2. IdP Hosted Metadata : The default value is: `urn:oasis:names:tc:SAML:2.0:attrname-format:basic` : Some examples of values specified in the SAML 2.0 Core Specification: : - `urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified` - `urn:oasis:names:tc:SAML:2.0:attrname-format:uri` (The default in Shibboleth 2.0) - `urn:oasis:names:tc:SAML:2.0:attrname-format:basic` (The default in Sun Access Manager) : You can also define your own value. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option in the IdP-hosted metadata. : (This option was previously named `AttributeNameFormat`.) `encryption.blacklisted-algorithms` : Blacklisted encryption algorithms. This is an array containing the algorithm identifiers. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option in the [IdP-hosted metadata](./simplesamlphp-reference-idp-hosted). : The RSA encryption algorithm with PKCS#1 v1.5 padding is blacklisted by default for security reasons. Any assertions encrypted with this algorithm will therefore fail to decrypt. You can override this limitation by defining an empty array in this option (or blacklisting any other algorithms not including that one). However, it is strongly discouraged to do so. For your own safety, please include the string 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' if you make use of this option. `ForceAuthn` : Set this `TRUE` to force the user to reauthenticate when the IdP receives authentication requests from this SP. The default is `FALSE`. `NameIDFormat` : The `NameIDFormat` this SP should receive. The three most commonly used values are: : 1. `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` 2. `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` 3. `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` : The `transient` format will generate a new unique ID every time the SP logs in. : To properly support the `persistent` and `emailAddress` formats, you should configure [NameID generation filters](./saml:nameid) on your IdP. `nameid.encryption` : Whether NameIDs sent to this SP should be encrypted. The default value is `FALSE`. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option in the [IdP-hosted metadata](./simplesamlphp-reference-idp-hosted). `SingleLogoutService` : The URL of the SingleLogoutService endpoint for this SP. This option is required if you want to implement single logout for this SP. If the option isn't specified, this SP will not be logged out automatically when a single logout operation is initialized. : The value of this option is specified in one of several [endpoint formats](./simplesamlphp-metadata-endpoints). `SingleLogoutServiceResponse` : The URL logout responses to this SP should be sent. If this option is unspecified, the `SingleLogoutService` endpoint will be used as the recipient of logout responses. `SPNameQualifier` : SP NameQualifier for this SP. If not set, the IdP will set the SPNameQualifier to be the SP entity ID. `certData` : The base64 encoded certificate for this SP. This is an alternative to storing the certificate in a file on disk and specifying the filename in the `certificate`-option. `certificate` : Name of certificate file for this SP. The certificate is used to verify the signature of messages received from the SP (if `redirect.validate`is set to `TRUE`), and to encrypting assertions (if `assertion.encryption` is set to TRUE and `sharedkey` is unset.) `saml20.sign.response` : Whether `` messages should be signed. Defaults to `TRUE`. : Note that this option also exists in the IdP-hosted metadata. The value in the SP-remote metadata overrides the value in the IdP-hosted metadata. `saml20.sign.assertion` : Whether `` elements should be signed. Defaults to `TRUE`. : Note that this option also exists in the IdP-hosted metadata. The value in the SP-remote metadata overrides the value in the IdP-hosted metadata. `signature.algorithm` : The algorithm to use when signing any message sent to this specific service provider. Defaults to RSA-SHA1. : Note that this option also exists in the IdP-hosted metadata. The value in the SP-remote metadata overrides the value in the IdP-hosted metadata. : Possible values: * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` *Note*: the use of SHA1 is **deprecated** and will be disallowed in the future. * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` `signature.privatekey` : Name of private key file for this IdP, in PEM format. The filename is relative to the cert/-directory. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option `privatekey` in the IdP-hosted metadata. `signature.privatekey_pass` : Passphrase for the private key. Leave this option out if the private key is unencrypted. : Note that this option only is used if `signature.privatekey` is present. `signature.certificate` : Certificate file included by IdP for KeyInfo within the signature for the SP, in PEM format. The filename is relative to the cert/-directory. : If `signature.privatekey` is present and `signature.certificate` is left blank, X509Certificate will not be included with the signature. `simplesaml.nameidattribute` : When the value of the `NameIDFormat`-option is set to either `email` or `persistent`, this is the name of the attribute which should be used as the value of the `NameID`. The attribute must be in the set of attributes exported to the SP (that is, be in the `attributes` array). For more advanced control over `NameID`, including the ability to specify any attribute regardless of the set sent to the SP, see the [NameID processing filters](./saml:nameid). : Typical values can be `mail` for when using the `email` format, and `eduPersonTargetedID` when using the `persistent` format. `simplesaml.attributes` : Whether the SP should receive any attributes from the IdP. The default value is `TRUE`. `attributeencodings` : What encoding should be used for the different attributes. This is an array which maps attribute names to attribute encodings. There are three different encodings: : - `string`: Will include the attribute as a normal string. This is the default. : - `base64`: Store the attribute as a base64 encoded string. This is the default when the `base64attributes`-option is set to `TRUE`. : - `raw`: Store the attribute without any modifications. This makes it possible to include raw XML in the response. `sign.logout` : Whether to sign logout messages sent to this SP. : Note that this option also exists in the IdP-hosted metadata. The value in the SP-remote metadata overrides the value in the IdP-hosted metadata. `validate.authnrequest` : Whether we require signatures on authentication requests sent from this SP. : Note that this option also exists in the IdP-hosted metadata. The value in the SP-remote metadata overrides the value in the IdP-hosted metadata. `validate.logout` : Whether we require signatures on logout messages sent from this SP. : Note that this option also exists in the IdP-hosted metadata. The value in the SP-remote metadata overrides the value in the IdP-hosted metadata. ### Encrypting assertions It is possible to encrypt the assertions sent to a SP. Currently the only algorithm supported is `AES128_CBC` or `RIJNDAEL_128`. There are two modes of encryption supported by SimpleSAMLphp. One is symmetric encryption, in which case both the SP and the IdP needs to share a key. The other mode is the use of public key encryption. In that mode, the public key of the SP is extracted from the certificate of the SP. `assertion.encryption` : Whether assertions sent to this SP should be encrypted. The default value is `FALSE`. : Note that this option also exists in the IdP-hosted metadata. This entry in the SP-remote metadata overrides the option in the IdP-hosted metadata. `sharedkey` : Symmetric key which should be used for encryption. This should be a 128-bit key. If this option is not specified, public key encryption will be used instead. ### Fields for signing and validating messages SimpleSAMLphp only signs authentication responses by default. Signing of logout requests and logout responses can be enabled by setting the `redirect.sign` option. Validation of received messages can be enabled by the `redirect.validate` option. These options overrides the options set in `saml20-idp-hosted`. `redirect.sign` : Whether logout requests and logout responses sent to this SP should be signed. The default is `FALSE`. `redirect.validate` : Whether authentication requests, logout requests and logout responses received from this SP should be validated. The default is `FALSE` **Example: Configuration for validating messages** 'redirect.validate' => TRUE, 'certificate' => 'example.org.crt', ### Fields for scoping Only relevant if you are a proxy/bridge and wants to limit the idps this sp can use. `IDPList` : The list of scoped idps ie. the list of entityids for idps that are relevant for this sp. The final list is the concatenation of the list given as parameter to InitSSO (at the sp), the list configured at the sp and the list configured at the ipd (here) for this sp. The intersection of the final list and the idps configured at the at this idp will be presented to the user at the discovery service if neccessary. If only one idp is in the intersection the discoveryservice will go directly to the idp. **Example: Configuration for scoping** 'IDPList' => array('https://idp1.wayf.dk', 'https://idp2.wayf.dk'), Shibboleth 1.3 options ---------------------- The following options for Shibboleth 1.3 SP's are avaiblable: `AssertionConsumerService` : The URL of the AssertionConsumerService endpoint for this SP. This endpoint must accept the SAML responses encoded with the `urn:oasis:names:tc:SAML:1.0:profiles:browser-post` encoding. This option is required - without it you will not be able to send responses back to the SP. : The value of this option is specified in one of several [endpoint formats](./simplesamlphp-metadata-endpoints). `NameQualifier` : What the value of the `NameQualifier`-attribute of the ``-element should be. The default value is the entity ID of the SP. `audience` : The value which should be given in the ``-element in the ``-element in the response. The default value is the entity ID of the SP. `scopedattributes` : Array with names of attributes which should be scoped. Scoped attributes will receive a `Scope`-attribute on the `AttributeValue`-element. The value of the Scope-attribute will be taken from the attribute value: : `someuser@example.org` : will be transformed into : `someuser` : By default, no attributes are scoped. This option overrides the option with the same name in the `shib13-idp-hosted.php` metadata file. simplesamlphp-1.15.3/docs/simplesamlphp-googleapps.md0000644000000000000000000002453513245225037021457 0ustar rootrootSetting up a SimpleSAMLphp SAML 2.0 IdP to use with Google Apps / G Suite for Education ============================================ SimpleSAMLphp news and documentation ------------------------------------ This document is part of the SimpleSAMLphp documentation suite. * [List of all SimpleSAMLphp documentation](https://simplesamlphp.org/docs) * [SimpleSAMLphp homepage](https://simplesamlphp.org) ## Introduction This article assumes that you have already read the SimpleSAMLphp installation manual, and installed a version of SimpleSAMLphp at your server. In this example we will setup this server as an IdP for Google Apps for Education: dev2.andreas.feide.no ## Enabling the Identity Provider functionality Edit `config.php`, and enable the SAML 2.0 IdP: 'enable.saml20-idp' => true, 'enable.shib13-idp' => false, ## Setting up a signing certificate You must generate a certificate for your IdP. Here is an example of an openssl command to generate a new key and a self signed certificate to use for signing SAML messages: openssl req -newkey rsa:2048 -new -x509 -days 3652 -nodes -out googleappsidp.crt -keyout googleappsidp.pem The certificate above will be valid for 10 years. Here is an example of typical user input when creating a certificate request: Country Name (2 letter code) [AU]:NO State or Province Name (full name) [Some-State]:Trondheim Locality Name (eg, city) []:Trondheim Organization Name (eg, company) [Internet Widgits Pty Ltd]:UNINETT Organizational Unit Name (eg, section) []: Common Name (eg, YOUR name) []:dev2.andreas.feide.no Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: **Note**: SimpleSAMLphp will only work with RSA and not DSA certificates. Authentication source --------------------- The next step is to configure the way users authenticate on your IdP. Various modules in the `modules/` directory provides methods for authenticating your users. This is an overview of those that are included in the SimpleSAMLphp distribution: `exampleauth:UserPass` : Authenticate against a list of usernames and passwords. `exampleauth:Static` : Automatically log in as a user with a set of attributes. [`ldap:LDAP`](./ldap:ldap) : Authenticates an user to a LDAP server. For more authentication modules, see [SimpleSAMLphp Identity Provider QuickStart](simplesamlphp-idp). In this guide, we will use the `exampleauth:UserPass` authentication module. This module does not have any dependencies, and is therefore simple to set up. After you have successfuly tested that everything is working with the simple `exampleauth:UserPass`, you are encouraged to setup SimpleSAMLphp IdP towards your user storage, such as an LDAP directory. (Use the links on the authentication sources above to read more about these setups. `ldap:LDAP` is the most common authentication source.) Configuring the authentication source ------------------------------------- The `exampleauth:UserPass` authentication source is part of the `exampleauth` module. This module isn't enabled by default, so you will have to enable it. This is done by creating a file named `enable` in `modules/exampleauth/`. On unix, this can be done by running (from the SimpleSAMLphp installation directory): touch modules/exampleauth/enable The next step is to create an authentication source with this module. An authentication source is an authentication module with a specific configuration. Each authentication source has a name, which is used to refer to this specific configuration in the IdP configuration. Configuration for authentication sources can be found in `config/authsources.php`. In this example we will use `example-userpass`, and hence that section is what matters and will be used. array( 'exampleauth:UserPass', 'student:studentpass' => array( 'uid' => array('student'), ), 'employee:employeepass' => array( 'uid' => array('employee'), ), ), ); ?> This configuration creates two users - `student` and `employee`, with the passwords `studentpass` and `employeepass`. The username and password are stored in the array index `student:studentpass` for the `student`-user. The attributes (only `uid` in this example) will be returned by the IdP when the user logs on. ## Configuring metadata for an SAML 2.0 IdP If you want to setup a SAML 2.0 IdP for Google Apps, you need to configure two metadata files: `saml20-idp-hosted.php` and `saml20-sp-remote.php`. ### Configuring SAML 2.0 IdP Hosted metadata This is the configuration of the IdP itself. Here is some example config: // The SAML entity ID is the index of this config. Dynamic:X will automatically generate an entity ID (recommended) $metadata['__DYNAMIC:1__'] => array( // The hostname of the server (VHOST) that this SAML entity will use. 'host' => '__DEFAULT__', // X.509 key and certificate. Relative to the cert directory. 'privatekey' => 'googleappsidp.pem', 'certificate' => 'googleappsidp.crt', 'auth' => 'example-userpass', ) **Note**: You can only have one entry in the file with host equal to `__DEFAULT__`, therefore you should replace the existing entry with this one, instead of adding this entry as a new entry in the file. ### Configuring SAML 2.0 SP Remote metadata In the `saml20-sp-remote.php` file we will configure an entry for G Suite (Google Apps) for Education. There is already an entry for G Suite in the template, but we will change the domain name: /* * This example shows an example config that works with G Suite (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 G Suite. E.g. if your google account is foo.com, and you have a user with 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'. */ $metadata['https://www.google.com/a/g.feide.no'] => array( 'AssertionConsumerService' => 'https://www.google.com/a/g.feide.no/acs', 'NameIDFormat' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', 'simplesaml.nameidattribute' => 'uid', 'simplesaml.attributes' => false ); You must also map some attributes received from the authentication module into email field sent to Google Apps. In this example, the `uid` attribute is set. When you later configure the IdP to connect to a LDAP directory or some other authentication source, make sure that the `uid` attribute is set properly, or you can configure another attribute to use here. The `uid` attribute contains the local part of the user name. For an e-mail address `student@g.feide.no`, the `uid` should be set to `student`. You should modify the `AssertionConsumerService` to include your G Suite domain name instead of `g.feide.no`. For an explanation of the parameters, see the [SimpleSAMLphp Identity Provider QuickStart](simplesamlphp-idp). ## Configure G Suite for education Start by logging in to our G SUite for education account panel. Then select "Advanced tools": **Figure 1. We go to advanced tools** ![We go to advanced tools](resources/simplesamlphp-googleapps/googleapps-menu.png) Then select "Set up single sign-on (SSO)": **Figure 2. We go to setup SSO** ![We go to setup SSO](resources/simplesamlphp-googleapps/googleapps-sso.png) Upload a certificate, such as the googleappsidp.crt created above: **Figure 3. Uploading certificate** ![Uploading certificate](resources/simplesamlphp-googleapps/googleapps-cert.png) Fill out the remaining fields: The most important field is the Sign-in page URL. You can find the correct value in your IdP metadata. Browse to your simpleSAMLphp installation, go to the "Federation" tab, under "SAML 2.0 IdP Metadata" select "show metadata". You will find in the metadata the XML tag `` which contains the right URL to input in the field, it will look something like this: https://dev2.andreas.feide.no/simplesaml/saml2/idp/SSOService.php You must also configure the IdP initiated Single LogOut endpoint of your server. You will find this in your metadata XML in the tag ``. It will look something like: http://dev2.andreas.feide.no/simplesaml/saml2/idp/SingleLogoutService.php again, using the host name of your IdP server. The Sign-out page or change password URL can be static pages on your server. The network mask determines which IP addresses will be asked for SSO login. IP addresses not matching this mask will be presented with the normal G Suite login page. It is normally best to leave this field empty to enable authentication for all URLs. **Figure 4. Fill out the remaining fields** ![Fill out the remaining fields](resources/simplesamlphp-googleapps/googleapps-ssoconfig.png) ### Add a user in G Suite that is known to the IdP Before we can test login, a new user must be defined in G Suite. This user must have a mail field matching the email prefix mapped from the attribute as described above in the metadata section. ## Test to login to G Suite for education Go to the URL of your mail account for this domain, the URL is similar to the following: http://mail.google.com/a/yourgoogleappsdomain.com replacing the last part with your own G Suite domain name. ## Security Considerations Make sure that your IdP server runs HTTPS (TLS). The Apache documentation contains information for how to configure HTTPS. Support ------- If you need help to make this work, or want to discuss SimpleSAMLphp with other users of the software, you are fortunate: Around SimpleSAMLphp there is a great Open source community, and you are welcome to join! The forums are open for you to ask questions, contribute answers other further questions, request improvements or contribute with code or plugins of your own. - [SimpleSAMLphp homepage](https://simplesamlphp.org) - [List of all available SimpleSAMLphp documentation](https://simplesamlphp.org/docs/) - [Join the SimpleSAMLphp user's mailing list](https://simplesamlphp.org/lists) simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.9.md0000644000000000000000000000227713245225037022400 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.9 =================================== * The OpenID client "linkback" URL has changed from `.../module.php/openid/consumer.php` to `.../module.php/openid/linkback.php`. * Support for CA path validation has been removed from SAML 2.0. * The X-Frame-Options has been added to the default templates, to prevent the pages from being loaded in iframes. * Access permissions of generated files are now restricted to the current user. * The code to set cookies now requires PHP version >= 5.2. (PHP version 5.2.0 or newer has been the only supported version for a while, but it has in some cases been possible to run SimpleSAMLphp with older versions.) * It used to be possible to set an array of endpoints for the SingleSignOnService in `saml20-idp-hosted.php`. That is no longer supported. * The `aselect` module has been replaced with a new module. The new module gives us better error handling and support for request signing, but we lose support for A-Select Cross. * There has been various fixes in the session exipration handling. As a result of this, sessions may get a shorter lifetime (if the IdP places a limit on the lifetime, this limit will now be honored). simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.6.md0000644000000000000000000000273713245225037022376 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.6 =================================== * This release requires PHP version >= 5.2.0, as that was the first version to include `json_decode()`. It is possible that it may work with version of PHP >= 5.1.2 if the [JSON PECL extesion](http://pecl.php.net/package/json) is enabled, but this is untested. * The secure-flag is no longer automatically set on the session cookie. This was changed to avoid hard to diagnose session problems. There is a new option `session.cookie.secure` in `config.php`, which can be used to enable secure cookies. * Dictionaries have moved to JSON format. The PHP format is still supported, but all dictionaries included with SimpleSAMLphp are in JSON format. * The iframe-specific logout endpoints on the IdP have been merged into the normal logout endpoints. This means that the metadata no longer needs to be changed when switching between logout handlers. The old iframe logout endpoints are now deprecated, and the generated metadata will only include the normal logout endpoint. * As a result of the changed metadata classes, all metadata elements now have a `md:`-prefix. This does not change the content of the metadata, just its expression. * The deprecated functions `init(...)` and `setAuthenticated(...)` in the `SimpleSAML_Session` class have been removed. Code which relies on those functions should move to using `SimpleSAML_Session::getInstance()` and `$session->doLogin(...)`. simplesamlphp-1.15.3/docs/simplesamlphp-idp.md0000644000000000000000000002517713245225037020076 0ustar rootrootSimpleSAMLphp Identity Provider QuickStart =========================================== This guide will describe how to configure SimpleSAMLphp as an identity provider (IdP). You should previously have installed SimpleSAMLphp as described in [the SimpleSAMLphp installation instructions](simplesamlphp-install) Enabling the Identity Provider functionality -------------------------------------------- The first that must be done is to enable the identity provider functionality. This is done by editing `config/config.php`. The options `enable.saml20-idp` and `enable.shib13-idp` controls whether SAML 2.0 and Shibboleth 1.3 support is enabled. Enable one or both of those by assigning `true` to them: 'enable.saml20-idp' => true, 'enable.shib13-idp' => true, Authentication module --------------------- The next step is to configure the way users authenticate on your IdP. Various modules in the `modules/` directory provides methods for authenticating your users. This is an overview of those that are included in the SimpleSAMLphp distribution: [`authcrypt:Hash`](./authcrypt:authcrypt) : Username & password authentication with hashed passwords. [`authcrypt:Htpasswd`](./authcrypt:authcrypt) : Username & password authentication against .htpasswd file. [`authX509:authX509userCert`](./authX509:authX509) : Authenticate against a LDAP database with a SSL client certificate. `exampleauth:UserPass` : Authenticate against a list of usernames and passwords. `exampleauth:Static` : Automatically log in as a user with a set of attributes. [`ldap:LDAP`](./ldap:ldap) : Authenticates an user to a LDAP server. [`ldap:LDAPMulti`](./ldap:ldap) : Authenticates an user to one of several LDAP server. The user can choose the LDAP server from a dropdown list. [`sqlauth:SQL`](./sqlauth:sql) : Authenticate an user against a database. [`radius:Radius`](./radius:radius) : Authenticates an user to a Radius server. [`InfoCard:ICAuth`](https://github.com/simplesamlphp/simplesamlphp-module-infocard/blob/master/README.md) : Authenticate with an InfoCard. [`multiauth:MultiAuth`](./multiauth:multiauth) : Allow the user to select from a list of authentication sources. `openid:OpenIDConsumer` : Authenticate against an OpenID provider. [`saml:SP`](./saml:sp) : Authenticate against a SAML IdP. Can be used for bridging. `authYubiKey:YubiKey` : Authenticate with [an YubiKey](http://www.yubico.com/products/yubikey/). [`authfacebook:Facebook`](./authfacebook:authfacebook) : Authenticate with a Facebook ID. [`authtwitter:Twitter`](./authtwitter:oauthtwitter) : Authenticate with your Twitter account using the Twitter OAuth API. [`papi:PAPI`](https://github.com/rediris-es/simplesamlphp-module-papi/blog/master/README.md) : Authenticate by means of the PAPI protocol. In this guide, we will use the `exampleauth:UserPass` authentication module. This module does not have any dependencies, and is therefore simple to set up. Configuring the authentication module ------------------------------------- The `exampleauth:UserPass` authentication module is part of the `exampleauth` module. This module isn't enabled by default, so you will have to enable it. This is done by creating a file named `enable` in `modules/exampleauth/`. On unix, this can be done by running (from the SimpleSAMLphp installation directory): touch modules/exampleauth/enable The next step is to create an authentication source with this module. An authentication source is an authentication module with a specific configuration. Each authentication source has a name, which is used to refer to this specific configuration in the IdP configuration. Configuration for authentication sources can be found in `config/authsources.php`. In this setup, this file should contain a single entry: array( 'exampleauth:UserPass', 'student:studentpass' => array( 'uid' => array('student'), 'eduPersonAffiliation' => array('member', 'student'), ), 'employee:employeepass' => array( 'uid' => array('employee'), 'eduPersonAffiliation' => array('member', 'employee'), ), ), ); This configuration creates two users - `student` and `employee`, with the passwords `studentpass` and `employeepass`. The username and password is stored in the array index (`student:studentpass` for the `student`-user. The attributes for each user is configured in the array referenced by the index. For the student user, these are: array( 'uid' => array('student'), 'eduPersonAffiliation' => array('member', 'student'), ), The attributes will be returned by the IdP when the user logs on. Creating a SSL self signed certificate -------------------------------------- Here is an example of an `openssl`-command which can be used to generate a new private key key and the corresponding self-signed certificate. This key and certificate can be used to sign SAML messages: openssl req -newkey rsa:2048 -new -x509 -days 3652 -nodes -out example.org.crt -keyout example.org.pem The certificate above will be valid for 10 years. ### Note ### SimpleSAMLphp will only work with RSA certificates. DSA certificates are not supported. Configuring the IdP ------------------- The IdP is configured by the metadata stored in `metadata/saml20-idp-hosted.php` and `metadata/shib13-idp-hosted.php`. This is a minimal configuration of a SAML 2.0 IdP: '__DEFAULT__', /* * The private key and certificate to use when signing responses. * These are stored in the cert-directory. */ 'privatekey' => 'example.org.pem', 'certificate' => 'example.org.crt', /* * The authentication source which should be used to authenticate the * user. This must match one of the entries in config/authsources.php. */ 'auth' => 'example-userpass', ); For more information about available options in the idp-hosted metadata files, see the [IdP hosted reference](simplesamlphp-reference-idp-hosted). Using the `uri` NameFormat on attributes ---------------------------------------- The [interoperable SAML 2 profile](http://saml2int.org/profile/current) specifies that attributes should be delivered using the `urn:oasis:names:tc:SAML:2.0:attrname-format:uri` NameFormat. We therefore recommended enabling this in new installations. This can be done by adding the following to the saml20-idp-hosted configuration: 'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri', 'authproc' => array( // Convert LDAP names to oids. 100 => array('class' => 'core:AttributeMap', 'name2oid'), ), Adding SPs to the IdP --------------------- The identity provider you are configuring needs to know about the service providers you are going to connect to it. This is configured by metadata stored in `metadata/saml20-sp-remote.php` and `metadata/shib13-sp-remote.php`. This is a minimal example of a `metadata/saml20-sp-remote.php` metadata file for a SimpleSAMLphp SP: 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-acs.php/default-sp', 'SingleLogoutService' => 'https://sp.example.org/simplesaml/module.php/saml/sp/saml2-logout.php/default-sp', ); Note that the URI in the entityID and the URLs to the AssertionConsumerService and SingleLogoutService endpoints change between different service providers. If you have the metadata of the remote SP as an XML file, you can use the built-in XML to SimpleSAMLphp metadata converter, which by default is available as `/admin/metadata-converter.php` in your SimpleSAMLphp installation. For more information about available options in the sp-remote metadata files, see the [SP remote reference](simplesamlphp-reference-sp-remote). Adding this IdP to other SPs ---------------------------- The method for adding this IdP to a SP varies between different types of SPs. In general, most SPs need some metadata from the IdP. This should be available from `/saml2/idp/metadata.php` and `/shib13/idp/metadata.php`. Testing the IdP --------------- The simplest way to test the IdP is to configure a SimpleSAMLphp SP on the same machine. See the instructions for [configuring SimpleSAMLphp as an SP](simplesamlphp-sp). ### Note ### When running a SimpleSAMLphp IdP and a SimpleSAMLphp SP on the same computer, the SP and IdP **MUST** be configured with different hostnames. This prevents cookies from the SP to interfere with cookies from the IdP. Support ------- If you need help to make this work, or want to discuss SimpleSAMLphp with other users of the software, you are fortunate: Around SimpleSAMLphp there is a great Open source community, and you are welcome to join! The forums are open for you to ask questions, contribute answers other further questions, request improvements or contribute with code or plugins of your own. - [SimpleSAMLphp homepage](https://simplesamlphp.org) - [List of all available SimpleSAMLphp documentation](https://simplesamlphp.org/docs/) - [Join the SimpleSAMLphp user's mailing list](https://simplesamlphp.org/lists) A. IdP-first setup ------------------ If you do not want to start the SSO flow at the SP, you may use the IdP-first setup. To do this, redirect the user to the SSOService endpoint on the IdP with one parameter `spentityid` that match the SP EntityId that the user should be logged into. Here is an example of such a URL: https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=sp.example.org If the SP is a SimpleSAMLphp SP, you must also specify a `RelayState` parameter for the SP. This must be set to a URL the user should be redirected to after authentication. The `RelayState` parameter can be specified in the [SP configuration](./saml:sp), or it can be sent from the IdP. To send the RelayState parameter from a SimpleSAMLphp IdP, specify it in the query string to SSOService.php: https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=sp.example.org&RelayState=https://sp.example.org/welcome.php To set it in the SP configuration, add it to `authsources.php`: 'default-sp' => array( 'saml:SP', 'RelayState' => 'https://sp.example.org/welcome.php', ), simplesamlphp-1.15.3/docs/README0000644000000000000000000000041213245225037014765 0ustar rootrootUpdated: January 15th, 2015 All you need to know to install and configure simpleSAMLphp is available at: https://simplesamlphp.org/docs/ simpleSAMLphp homepage: https://simplesamlphp.org/ simpleSAMLphp mailinglist (for support): https://simplesamlphp.org/lists simplesamlphp-1.15.3/docs/simplesamlphp-authproc.md0000644000000000000000000002541213245225037021137 0ustar rootrootAuthentication Processing Filters in SimpleSAMLphp ================================================== In SimpleSAMLphp, there is an API where you can *do stuff* at the IdP after authentication is complete, and just before you are sent back to the SP. The same API is available on the SP, after you have received a successful Authentication Response from the IdP and before you are sent back to the SP application. Authentication processing filters postprocess authentication information received from authentication sources. It is possible to use this for additional authentication checks, requesting the user's consent before delivering attributes about the user, modifying the user's attributes, and other things which should be performed before returning the user to the service provider he came from. Examples of neat things to do using Authentication Processing Filters: * Filter out a subset of available attributes that are sent to a SP. * Modify the name of attributes. * Generate new attributes that are composed of others, for example eduPersonTargetedID. * Ask the user for consent, before the user is sent back to a service. * Implement basic Access Control on the IdP (not neccessarily a good idea), limiting access for some users to some SPs. Be aware that Authentication Proccessing Filters do replace some of the preivous features in SimpleSAMLphp, named: * `attributemap` * `attributealter` * attribute filter Later in this document, we will desribe in detail the alternative Authentication Proccessing Filters that will replicate these functionalities. How to configure Auth Proc Filters ---------------------------------- *Auth Proc Filters* can be set globally, or to be specific for only one SP or one IdP. That means there are five locations where you can configure *Auth Proc Filters*: * Globally in `config.php` * On the SP: Specific for only the SP in `authsources.php` * On the SP: Specific for only one remote IdP in `saml20-idp-remote` or `shib13-idp-remote` * On the IdP: Specific for only one hosted IdP in `saml20-idp-hosted` or `shib13-idp-hosted` * On the IdP: Specific for only one remote SP in `saml20-sp-remote` or `shib13-sp-remote` The configuration of *Auth Proc Filters* is a list of filters with priority as *index*. Here is an example of *Auth Proc Filters* configured in `config.php`: 'authproc.idp' => array( 10 => array( 'class' => 'core:AttributeMap', 'addurnprefix' ), 20 => 'core:TargetedID', 50 => 'core:AttributeLimit', 90 => array( 'class' => 'consent:Consent', 'store' => 'consent:Cookie', 'focus' => 'yes', 'checked' => TRUE ), ), This configuration will execute *Auth Proc Filters* one by one, with the priority value in increasing order. When *Auth Proc Filters* is configured in multiple places, in example both globally, in the hosted IdP and remote SP metadata, then the list is interleaved sorted by priority. The most important parameter of each item on the list is the *class* of the *Auth Proc Filter*. The syntax of the class is `modulename:classname`. As an example the class definition `core:AttributeLimit` will be expanded to look for the class `sspmod_core_Auth_Process_AttributeLimit`. The location of this class file *must* then be: `modules/core/lib/Auth/Process/AttributeLimit.php`. You will see that a bunch of useful filters is included in the `core` module. In addition the `consent` module that is included in the SimpleSAMLphp distribution implements a filter. Beyond that, you are encouraged to create your own filters and share with the community. If you have created a cool *Auth Proc Filter* that does something useful, let us know, and we may share it on the [SimpleSAMLphp web site][]. [SimpleSAMLphp web site]: http://simplesamlphp.org When you know the class definition of a filter, and the priority, the simple way to configure the filter is: 20 => 'core:TargetedID', This is analogous to: 20 => array( 'class' => 'core:TargetedID' ), Some *Auth Proc Filters* have optional or required *parameters*. To send parameters to *Auth Proc Filters*, you need to choose the second of the two alernatives above. Here is an example of provided parameters to the consent module: 90 => array( 'class' => 'consent:Consent', 'store' => 'consent:Cookie', 'focus' => 'yes', 'checked' => TRUE ), ### Filters in `config.php` Global *Auth Proc Filters* are configured in the `config.php` file. You will see that the config template already includes an example configuration. There are two config parameters: * `authproc.idp` and * `authproc.sp` The filters in `authproc.idp` will be executed at the IdP side regardless of which IdP and SP entity that is involved. The filters in `authproc.sp` will be executed at the SP side regardless of which SP and IdP entity that is involved. ### Filters in metadata Filters can be added both in `hosted` and `remote` metadata. Here is an example of a filter added in a metadata file: '__DYNAMIC:1__' => array( 'host' => '__DEFAULT_', 'privatekey' => 'example.org.pem', 'certificate' => 'example.org.crt', 'auth' => 'feide', 'authproc' => array( 40 => 'preprodwarning:Warning', ), ) The example above is in `saml20-idp-hosted`. Auth Proc Filters included in the SimpleSAMLphp distribution ------------------------------------------------------------ The following filters are included in the SimpleSAMLphp distribution: - [`authorize:Authorize`](./authorize:authorize): Access control based on regular expressions. - [`consent:Consent`](./consent:consent): Ask the user for consent before transmitting attributes. - [`core:AttributeAdd`](./core:authproc_attributeadd): Add attributes to the response. - [`core:AttributeCopy`](./core:authproc_attributecopy): Copy existing attributes to the response. - [`core:AttributeAlter`](./core:authproc_attributealter): Do search-and-replace on attributevalues. - [`core:AttributeLimit`](./core:authproc_attributelimit): Limit the attributes in the response. - [`core:AttributeMap`](./core:authproc_attributemap): Change the name of the attributes. - [`core:AttributeRealm`](./core:authproc_attributerealm): (deprecated) Create an attribute with the realm of the user. - [`core:GenerateGroups`](./core:authproc_generategroups): Generate a `group` attribute for the user. - [`core:LanguageAdaptor`](./core:authproc_languageadaptor): Transfering language setting from IdP to SP. - [`core:PHP`](./core:authproc_php): Modify attributes with custom PHP code. - [`core:ScopeAttribute`](./core:authproc_scopeattribute): Add scope to attribute. - [`core:ScopeFromAttribute`](./core:authproc_scopefromattribute): Create a new attribute based on the scope on a different attribute. - [`core:StatisticsWithAttribute`](./core:authproc_statisticswithattribute): Create a statistics logentry. - [`core:TargetedID`](./core:authproc_targetedid): Generate the `eduPersonTargetedID` attribute. - [`core:WarnShortSSOInterval`](./core:authproc_warnshortssointerval): Give a warning if the user logs into the same SP twice within a few seconds. - [`expirycheck:ExpiryDate`](./expirycheck:expirycheck): Block access to accounts that have expired. - [`preprodwarning:Warning`](./preprodwarning:warning): Warn the user about accessing a test IdP. - [`saml:AttributeNameID`](./saml:nameid): Generate custom NameID with the value of an attribute. - [`saml:ExpectedAuthnContextClassRef`](./saml:authproc_expectedauthncontextclassref): Verify the user's authentication context. - [`saml:FilterScopes`](./saml:filterscopes): Filter attribute values with scopes forbidden for an IdP. - [`saml:NameIDAttribute`](./saml:nameidattribute): Create an attribute based on the NameID we receive from the IdP. - [`saml:PersistentNameID`](./saml:nameid): Generate persistent NameID from an attribute. - [`saml:PersistentNameID2TargetedID`](./saml:nameid): Store persistent NameID as eduPersonTargetedID. - [`saml:TransientNameID`](./saml:nameid): Generate transient NameID. - [`smartattributes:SmartID`](./smartattributes:smartattributes): Generate user ID attribute based on several attributes. Writing your own Auth Proc Filter --------------------------------- Look at the included *Auth Proc Filters* as examples. Copy the classes into your own module and start playing around. Authentication processing filters are created by creating a class under `Auth/Process/` in a module. This class is expected to subclass `SimpleSAML_Auth_ProcessingFilter`. A filter must implement at least one function - the `process(&$request)`-function. This function can access the `$request`-array to add, delete and modify attributes, and can also do more advanced processing based on the SP/IdP metadata (which is also included in the `$request`-array). When this function returns, it is assumed that the filter has finished processing. If a filter for some reason needs to redirect the user, for example to show a web page, it should save the current request. Upon completion it should retrieve the request, update it with the changes it is going to make, and call `SimpleSAML_Auth_ProcessingChain::resumeProcessing`. This function will continue processing the next configured filter. Requirements for authentication processing filters: - Must be derived from the `SimpleSAML_Auth_ProcessingFilter`-class. - If a constructor is implemented, it must first call the parent constructor, passing along all parameters, before accessing any of the parameters. In general, only the $config parameter should be accessed. - The `process(&$request)`-function must be implemented. If this function completes, it is assumed that processing is completed, and that the $request array has been updated. - If the `process`-function does not return, it must at a later time call `SimpleSAML_Auth_ProcessingChain::resumeProcessing` with the new request state. The request state must be an update of the array passed to the `process`-function. - No pages may be shown to the user from the `process`-function. Instead, the request state should be saved, and the user should be redirected to a new page. This must be done to prevent unpredictable events if the user for example reloads the page. - No state information should be stored in the filter object. It must instead be stored in the request state array. Any changes to variables in the filter object may be lost. - The filter object must be serializable. It may be serialized between being constructed and the call to the `process`-function. This means that, for example, no database connections should be created in the constructor and later used in the `process`-function. Don't hestitate to ask on the SimpleSAMLphp mailinglist if you have problems or questions, or want to share your *Auth Proc Filter* with others. simplesamlphp-1.15.3/docs/simplesamlphp-theming.md0000644000000000000000000001160213245225037020741 0ustar rootrootTheming the user interface in SimpleSAMLphp =========================================== In SimpleSAMLphp every part that needs to interact with the user by using a web page, uses templates to present the XHTML. SimpleSAMLphp comes with a default set of templates that presents a anonymous look. You may create your own theme, where you add one or more template files that will override the default ones. This document explains how to achieve that. How themes work -------------------- If you want to customize the UI, the right way to do that is to create a new **theme**. A theme is a set of templates that can be configured to override the default templates. ### Configuring which theme to use In `config.php` there is a configuration option that controls theming. Here is an example: 'theme.use' => 'fancymodule:fancytheme', The `theme.use` parameter points to which theme that will be used. If some functionality in SimpleSAMLphp needs to present UI in example with the `logout.php` template, it will first look for `logout.php` in the `theme.use` theme, and if not found it will all fallback to look for the base templates. All required templates SHOULD be available as a base in the `templates` folder, and you SHOULD never change the base templates. To customize UI, add a new theme within a module that overrides the base templates, instead of modifying it. ### Templates that include other files A template file may *include* other files. For example all the default templates will include a header and footer: the `login.php` template will first include `includes/header.php` then present the login page, and then include `includes/footer.php`. SimpleSAMLphp allows themes to override the included templates files only, if needed. That means you can create a new theme `fancytheme` that includes only a header and footer. The header file refers to the CSS files, which means that a simple way of making a new look on SimpleSAMLphp is to create a new theme, and copy the existing header, but point to your own CSS instead of the default CSS. Creating your first theme ------------------------- The first thing you need to do is having a SimpleSAMLphp module to place your theme in. If you do not have a module already, create a new one: cd modules mkdir mymodule cd mymodule touch default-enable Then within this module, you can create a new theme named `fancytheme`. cd modules/mymodule mkdir -p themes/fancytheme Now, configure SimpleSAMLphp to use your new theme in `config.php`: 'theme.use' => 'mymodule:fancytheme', Next, we create `themes/fancytheme/default/includes`, and copy the header file from the base theme: cp templates/includes/header.php modules/mymodule/themes/fancytheme/default/includes/ In the `modules/mymodule/themes/fancytheme/default/includes/header.php` type in something and go to the SimpleSAMLphp front page to see that your new theme is in use. A good start is to modify the reference to the default CSS: to in example: Examples --------------------- To override the frontpage body, add the file: modules/mymodule/themes/fancytheme/default/frontpage.php In the path above `default` means that the frontpage template is not part of any modules. If you are replacing a template that is part of a module, then use the module name instead of `default`. For example, to override the `preprodwarning` template, (the file is located in `modules/preprodwarning/templates/warning.php`), you need to add a new file: modules/mymodule/themes/fancytheme/preprodwarning/warning.php Say in a module `foomodule`, some code requests to present the `bar.php` template, SimpleSAMLphp will: 1. first look in your theme for a replacement: `modules/mymodule/themes/fancytheme/foomodule/bar.php`. 2. If not found, it will use the base template of that module: `modules/foomodule/templates/bar.php` Adding resource files --------------------- You can put resource files within the www folder of your module, to make your module completely independent with included css, icons etc. ``` modules └───mymodule └───themes └───www └───logo.png └───style.css ``` Reference these resources in your custom PHP templates under `themes/fancytheme` by using a generator for the URL: ```php ``` Example for a custom CSS stylesheet file: ```html ``` simplesamlphp-1.15.3/docs/simplesamlphp-install.md0000644000000000000000000003674713245225037020775 0ustar rootrootSimpleSAMLphp Installation and Configuration ============================================ SimpleSAMLphp news and documentation ------------------------------------ This document is part of the SimpleSAMLphp documentation suite. * [List of all SimpleSAMLphp documentation](https://simplesamlphp.org/docs) * [SimpleSAMLphp homepage](https://simplesamlphp.org) Development version -------------------- This document is about the latest stable version of SimpleSAMLphp. If you want to install the development version, look at the instructions for [installing SimpleSAMLphp from the repository](simplesamlphp-install-repo). Prerequisites ------------- * Some webserver capable of executing PHP scripts. * PHP version >= 5.4.0. * Support for the following PHP extensions: * Always required: `date`, `dom`, `hash`, `libxml`, `openssl`, `pcre`, `SPL`, `zlib`, `json`, `mbstring` * When automatically checking for latest versions, and used by some modules: `cURL` * When authenticating against LDAP server: `ldap` * When authenticating against RADIUS server: `radius` * When using native PHP session handler: `session` * When saving session information to a memcache server: `memcache` * When using databases: * Always: `PDO` * Database driver: (`mysql`, `pgsql`, ...) * Support for the following PHP packages: * When saving session information to a Redis server: `predis` What actual packages are required for the various extensions varies between different platforms and distributions. Download and install SimpleSAMLphp ---------------------------------- The most recent release of SimpleSAMLphp is found at [https://simplesamlphp.org/download](https://simplesamlphp.org/download). Go to the directory where you want to install SimpleSAMLphp, and extract the archive file you just downloaded: cd /var tar xzf simplesamlphp-1.x.y.tar.gz mv simplesamlphp-1.x.y simplesamlphp ## Upgrading from a previous version of SimpleSAMLphp Extract the new version: cd /var tar xzf simplesamlphp-1.x.y.tar.gz Copy the configuration files from the previous version (in case the configuration directory is inside SimpleSAMLphp, keep reading for other alternatives): cd /var/simplesamlphp-1.x.y rm -rf config metadata cp -rv ../simplesamlphp/config config cp -rv ../simplesamlphp/metadata metadata Replace the old version with the new version: cd /var mv simplesamlphp simplesamlphp.old mv simplesamlphp-1.x.y simplesamlphp If the format of the config files or metadata has changed from your previous version of SimpleSAMLphp (check the revision log), you may have to update your configuration and metadata after updating the SimpleSAMLphp code: ### Upgrading configuration files A good approach is to run a `diff` between your previous `config.php` file and the new `config.php` file located in `config-templates/config.php`, and apply relevant modifications to the new template. This will ensure that all new entries in the latest version of config.php are included, as well as preserve your local modifications. ### Upgrading metadata files Most likely the metadata format is backwards compatible. If not, you should receive a very clear error message at startup indicating how and what you need to update. You should look through the metadata in the metadata-templates directory after the upgrade to see whether recommended defaults have been changed. ### Alternative location for configuration files By default, SimpleSAMLphp looks for its configuration in the `config` directory in the root of its own directory. This has some drawbacks, like making it harder to use SimpleSAMLphp as a composer dependency, or to package it for different operating systems. However, it is now possible to specify an alternate location for the configuration directory by setting an environment variable with this location. This way, the configuration directory doesn't need to be inside the library's directory, making it easier to manage and to update. The simplest way to set this environment variable is to set it in your web server's configuration. See the next section for more information. Configuring Apache ------------------ Examples below assume that SimpleSAMLphp is installed in the default location, `/var/simplesamlphp`. You may choose another location, but this requires a path update in a few files. See Appendix for details ‹Installing SimpleSAMLphp in alternative locations›. The only subdirectory of `SimpleSAMLphp` that needs to be accessible from the web is `www`. There are several ways of exposing SimpleSAMLphp depending on the way web sites are structured on your Apache web server. The following is just one possible configuration. Find the Apache configuration file for the virtual hosts where you want to run SimpleSAMLphp. The configuration may look like this: ServerName service.example.com DocumentRoot /var/www/service.example.com SetEnv SIMPLESAMLPHP_CONFIG_DIR /var/simplesamlphp/config Alias /simplesaml /var/simplesamlphp/www # For Apache 2.2: Order allow,deny Allow from all # For Apache 2.4: Require all granted Note the `Alias` directive, which gives control to SimpleSAMLphp for all urls matching `http(s)://service.example.com/simplesaml/*`. SimpleSAMLphp makes several SAML interfaces available on the web; all of them are included in the `www` subdirectory of your SimpleSAMLphp installation. You can name the alias whatever you want, but the name must be specified in the `config.php` file of SimpleSAMLphp as described in [the section called “SimpleSAMLphp configuration: config.php”](#sect.config "SimpleSAMLphp configuration: config.php"). Here is an example of how this configuration may look like in `config.php`: $config = array ( [...] 'baseurlpath' => 'simplesaml/', Note also the `SetEnv` directive. It sets the `SIMPLESAMLPHP_CONFIG_DIR` environment variable, in this case, to the default location for the configuration directory. You can omit this environment variable, and SimpleSAMLphp will then look for the `config` directory inside its own directory. If you need to move your configuration to a different location, you can use this environment variable to tell SimpleSAMLphp where to look for configuration files. This works only for the `config` directory. If you need your metadata to be in a different directory too, use the `metadatadir` configuration option to specify the location. This is just the basic configuration to get things working. For a checklist further completing your documentation, please see [Maintenance and configuration: Apache](simplesamlphp-maintenance#section_4). SimpleSAMLphp configuration: config.php --------------------------------------- There is a few steps that you should edit in the main configuration file, `config.php`, right away: - Set a administrator password. This is needed to access some of the pages in your SimpleSAMLphp installation web interface. 'auth.adminpassword' => 'setnewpasswordhere', Hashed passwords can also be used here. See the [`authcrypt`](./authcrypt:authcrypt) documentation for more information. - Set a secret salt. This should be a random string. Some parts of the SimpleSAMLphp needs this salt to generate cryptographically secure hashes. SimpleSAMLphp will give an error if the salt is not changed from the default value. The command below can help you to generated a random string on (some) unix systems: tr -c -d '0123456789abcdefghijklmnopqrstuvwxyz' /dev/null;echo Here is an example of the config option: 'secretsalt' => 'randombytesinsertedhere', - Set technical contact information. This information will be available in the generated metadata. The e-mail address will also be used for receiving error reports sent automatically by SimpleSAMLphp. Here is an example: 'technicalcontact_name' => 'John Smith', 'technicalcontact_email' => 'john.smith@example.com', - If you use SimpleSAMLphp in a country where English is not widespread, you may want to change the default language from English to something else: 'language.default' => 'no', - Set the timezone which you use: 'timezone' => 'Europe/Oslo', * [List of Supported Timezones at php.net](http://php.net/manual/en/timezones.php) Configuring PHP --------------- ### Sending e-mails from PHP Some parts of SimpleSAMLphp will allow you to send e-mails. In example sending error reports to technical admin, as well as sending in metadata to the federation administrators. If you want to make use of this functionality, you should make sure your PHP installation is configured to be able to send e-mails. It's a common problem that PHP is not configured to send e-mails properly. The configuration differs from system to system. On UNIX, PHP is using sendmail, on Windows SMTP. Enable modules -------------- If you want to enable some of the modules that are installed with SimpleSAMLphp, but are disabled by default, you should create an empty file in the module directory named `enable`. # Enabling the consent module cd modules ls -l cd consent touch enable If you later want to disable the module, rename the `enable` file to `disable`. cd modules/consent mv enable disable The SimpleSAMLphp installation webpage -------------------------------------- After installing 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 e.g.: https://service.example.org/simplesaml/ The exact link depends on how you set it up with Apache, and of course on your hostname. ### Warning Don't click on any of the links yet, because they require you to either have setup SimpleSAMLphp as an Service Provider or as an Identity Provider. Here is an example screenshot of what the SimpleSAMLphp page looks like: ![Screenshot of the SimpleSAMLphp installation page.](resources/simplesamlphp-install/screenshot-installationpage.png) ### Check your PHP environment At the bottom of the installation page are some green lights. simpleSAML runs some tests to see whether required and recommended prerequisites are met. If any of the lights are red, you may have to add some extensions or modules to PHP, e.g. you need the PHP LDAP extension to use the LDAP authentication module. ## Next steps You have now successfully installed SimpleSAMLphp, and the next steps depends on whether you want to setup a service provider, to protect a website by authentication or if you want to setup an identity provider and connect it to a user catalog. Documentation on bridging between federation protocols is found in a separate document. * [Using SimpleSAMLphp as a SAML Service Provider](simplesamlphp-sp) * [Hosted SP Configuration Reference](./saml:sp) * [IdP remote reference](simplesamlphp-reference-idp-remote) * [Connecting SimpleSAMLphp as a SP to UK Access Federation or InCommon](simplesamlphp-ukaccess) * [Upgrading - migration to use the SAML authentication source](simplesamlphp-sp-migration) * [Identity Provider QuickStart](simplesamlphp-idp) * [IdP hosted reference](simplesamlphp-reference-idp-hosted) * [SP remote reference](simplesamlphp-reference-sp-remote) * [Use case: Setting up an IdP for G Suite (Google Apps)](simplesamlphp-googleapps) * [Identity Provider Advanced Topics](simplesamlphp-idp-more) * [Automated Metadata Management](simplesamlphp-automated_metadata) * [Maintenance and configuration](simplesamlphp-maintenance) Support ------- If you need help to make this work, or want to discuss SimpleSAMLphp with other users of the software, you are fortunate: Around SimpleSAMLphp there is a great Open source community, and you are welcome to join! The forums are open for you to ask questions, contribute answers other further questions, request improvements or contribute with code or plugins of your own. - [SimpleSAMLphp homepage](https://simplesamlphp.org) - [List of all available SimpleSAMLphp documentation](https://simplesamlphp.org/docs/) - [Join the SimpleSAMLphp user's mailing list](https://simplesamlphp.org/lists) Installing SimpleSAMLphp in alternative locations ------------------------------------------------- There may be several reasons why you want to install SimpleSAMLphp in an alternative way. 1. You are installing SimpleSAMLphp in a hosted environment where you do not have root access, and cannot change Apache configuration. Still you can install SimpleSAMLphp - keep on reading. 2. You have full permissions to the server, but cannot edit Apache configuration for some reason, politics, policy or whatever. The SimpleSAMLphp code contains one folder named `simplesamlphp`. In this folder there are a lot of subfolders for library, metadata, configuration and much more. One of these folders is named `www`. This and *only this* folder should be exposed on the web. The recommended configuration is to put the whole `simplesamlphp` folder outside the webroot, and then link in the `www` folder by using the `Alias` directive, as described in [the section called “Configuring Apache”](#sect.apacheconfig "Configuring Apache"). But this is not the only possible way. As an example, let's see how you can install SimpleSAMLphp in your home directory on a shared hosting server. Extract the SimpleSAMLphp archive in your home directory: cd ~ tar xzf simplesamlphp-1.x.y.tar.gz mv simplesamlphp-1.x.y simplesamlphp Then you can try to make a symlink into the `public\_html` directory. cd ~/public_html ln -s ../simplesamlphp/www simplesaml Next, you need to update the configuration of paths in `simplesamlphp/config/config.php`: And, then we need to set the `baseurlpath` parameter to match the base path of the URLs to the content of your `www` folder: 'baseurlpath' => '/simplesaml/', Now, you can go to the URL of your installation and check if things work: http://yourcompany.com/simplesaml/ ### Tip Symlinking may fail, because some Apache configurations do not allow you to link in files from outside the public\_html folder. If so, move the folder instead of symlinking: cd ~/public_html mv ../simplesamlphp/www simplesaml Now you have the following directory structure. - `~/simplesamlphp` - `~/public_html/simplesaml` where `simplesaml` is the `www` directory from the `simplesamlphp` installation directory, either moved or a symlink. Now, we need to make a few configuration changes. First, let's edit `~/public_html/simplesaml/_include.php`: Change the two lines from: require_once(dirname(dirname(__FILE__)) . '/lib/_autoload.php'); to something like: require_once('/var/www/simplesamlphp/lib/_autoload.php'); And then at the end of the file, you need to change another line from: $configdir = dirname(dirname(__FILE__)) . '/config'; to: $configdir = '/var/www/simplesamlphp/config'; ### Note In a future version of SimpleSAMLphp we'll make this a bit easier, and let you only change the path one place, instead of three as described above. simplesamlphp-1.15.3/docs/simplesamlphp-maintenance.md0000644000000000000000000003156313245225037021600 0ustar rootrootSimpleSAMLphp Maintenance ========================= SimpleSAMLphp news and documentation ------------------------------------ Please check the following sources of information to stay up to date with regard to SimpleSAMLphp: * [SimpleSAMLphp documentation](http://simplesamlphp.org/docs) * [SimpleSAMLphp homepage](https://simplesamlphp.org) * [SimpleSAMLphp mailing lists](https://simplesamlphp.org/lists) * [SimpleSAMLphp in twitter](https://twitter.com/simplesamlphp) ## Session management SimpleSAMLphp has an abstraction layer for session management. That means it is possible to choose between different kind of session stores, as well as write new session store plugins. The `store.type` configuration option in `config.php` allows you to select which method SimpleSAMLphp should use to store the session information. Currently, three session handlers are included in the distribution: * `phpsession` uses the built in session management in PHP. This is the default, and is simplest to use. It will not work in a load-balanced environment in most configurations. * `memcache` uses the memcache software to cache sessions in memory. Sessions can be distributed and replicated among several memcache servers, enabling both load-balancing and fail-over. * `sql` stores the session in an SQL database. * `redis` stores the session in Redis. 'store.type' => 'phpsession', ### Configuring PHP sessions To use the PHP session handler, set the `store.type` configuration option in `config.php`: 'store.type' => 'phpsession', Keep in mind that **PHP does not allow two sessions to be open at the same time**. This means if you are using PHP sessions both in your application and in SimpleSAMLphp at the same time, **they need to have different names**. When using the PHP session handler in SimpleSAMLphp, it is configured with different options than for other session handlers: 'session.phpsession.cookiename' => null, 'session.phpsession.savepath' => null, 'session.phpsession.httponly' => true, Make sure to set `session.phpsession.cookiename` to a name different than the one in use by any other applications. If you are using SimpleSAMLphp as an Identity Provider, or any other applications using it are not using the default session name, you can use the default settings by leaving these options unset or setting them to `null`. If you need to restore your session's application after calling SimpleSAMLphp, you can do it by calling the `cleanup()` method of the `SimpleSAML_Session` class, like described [here](simplesamlphp-sp#section_6). ### Configuring memcache To use the memcache session handler, set the `store.type` parameter in `config.php`: 'store.type' => 'memcache', memcache allows you to store multiple redundant copies of sessions on different memcache servers. The configuration parameter `memcache_store.servers` is an array of server groups. Every data item will be mirrored in every server group. Each server group is an array of servers. The data items will be load-balanced between all servers in each server group. Each server is an array of parameters for the server. The following options are available: `hostname` : Host name or ip address where the memcache server runs, or specify other transports like *unix:///path/ssp.sock* to use UNIX domain sockets. In that case, port will be ignored and forced to *0*. This is the only required option. `port` : Port number of the memcache server. If not set, the `memcache.default_port` ini setting is used. This is 11211 by default. The port will be forced to *0* when a UNIX domain socket is specified in *hostname*. `weight` : Weight of this server in this server group. [http://php.net/manual/en/function.Memcache-addServer.php](http://php.net/manual/en/function.Memcache-addServer.php) has more information about the weight option. `timeout` : Timeout for this server. By default, the timeout is 3 seconds. Here are two examples of configuration of memcache session handling: **Example 1. Example of redundant configuration with load balancing** Example of redundant configuration with load balancing: This configuration makes it possible to lose both servers in the a-group or both servers in the b-group without losing any sessions. Note that sessions will be lost if one server is lost from both the a-group and the b-group. 'memcache_store.servers' => array( array( array('hostname' => 'mc_a1'), array('hostname' => 'mc_a2'), ), array( array('hostname' => 'mc_b1'), array('hostname' => 'mc_b2'), ), ), **Example 2. Example of simple configuration with only one memcache server** Example of simple configuration with only one memcache server, running on the same computer as the web server: Note that all sessions will be lost if the memcache server crashes. 'memcache_store.servers' => array( array( array('hostname' => 'localhost'), ), ), The expiration value (`memcache_store.expires`) is the duration for which data should be retained in memcache. Data are dropped from the memcache servers when this time expires. The time will be reset every time the data is written to the memcache servers. This value should always be larger than the `session.duration` option. Not doing this may result in the session being deleted from the memcache servers while it is still in use. Set this value to 0 if you don't want data to expire. #### Note The oldest data will always be deleted if the memcache server runs out of storage space. **Example 3. Example of configuration setting for session expiration** Here is an example of this configuration parameter: 'memcache_store.expires' => 36 * (60*60), // 36 hours. #### Memcache PHP configuration Configure memcache to not do internal failover. This parameter is configured in `php.ini`. memcache.allow_failover = Off #### Environmental configuration Setup a firewall restricting access to the memcache server. Because SimpleSAMLphp uses a timestamp to check which session is most recent in a fail-over setup, it is very important to run synchronized clocks on all web servers where you run SimpleSAMLphp. ### Configuring SQL storage To store session to a SQL database, set the `store.type` option to `sql`. SimpleSAMLphp uses [PDO](http://www.php.net/manual/en/book.pdo.php) when accessing the database server, so the database source is configured as with a DSN. The DSN is stored in the `store.sql.dsn` option. See the [PDO driver manual](http://www.php.net/manual/en/pdo.drivers.php) for the DSN syntax used by the different databases. Username and password for accessing the database can be configured in the `store.sql.username` and `store.sql.password` options. The required tables are created automatically. If you are storing data from multiple separate SimpleSAMLphp installations in the same database, you can use the `store.sql.prefix` option to prevent conflicts. ### Configuring Redis storage To store sessions in Redis, set the `store.type` option to `redis`. By default SimpleSAMLphp will attempt to connect to Redis on the `localhost` at port `6379`. These can be configured via the `store.redis.host` and `store.redis.port` options, respectively. You may also set a key prefix with the `store.redis.prefix` option. ## Metadata storage Several metadata storage backends are available by default, including `flatfile`, `serialize`, `mdq` and [`pdo`](https://simplesamlphp.org/docs/stable/simplesamlphp-metadata-pdostoragehandler). Here you have an example configuration of different metadata sources in use at the same time: ``` 'metadata.sources' => array( array('type' => 'flatfile'), array('type' => 'flatfile', 'directory' => 'metadata/metarefresh-kalmar'), array('type' => 'serialize', 'directory' => 'metadata/metarefresh-ukaccess'), ), ``` You may also implement your own metadata storage handler, in a very similar way to how you would implement your own session handler. Your class **must** extend the `SimpleSAML_Metadata_MetaDataStorageSource` class and override the methods needed to change the backend used. This class **must** also be located in the `lib/MetadataStore/` directory of your custom module. Bear in mind that **your class name must follow the PSR-0 autoloading standard**. This means it needs to be named in a particular way, with the use of namespaces being the preferred convention. For example, if your module is named _mymodule_ and your class is named _MyMetadataHandler_, you should define it like this: ``` array('en', 'no', 'da', 'es', 'xx'), 'language.default' => 'en', Please use the standardized two-character [language codes as specified in ISO-639-1](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). You also can set the default language. You should ensure that the default language is complete, as it is used as a fallback when a text is not available in the language selected by the user. All strings that can be localized are found in the files `dictionaries/`. Add a new entry for each string, with your language code, like this: 'user_pass_header' => array( 'en' => 'Enter your username and password', 'no' => 'Skriv inn brukernavn og passord', 'xx' => 'Pooa jujjique jamba', ), You can translate as many of the texts as you would like; a full translation is not required unless you want to make this the default language. From the end users point of view, it looks best if all text fragments used in a given screen or form is in one single language. ## Customizing the web frontend with themes Documentation on theming is moved [to a separate document](simplesamlphp-theming). Support ------- If you need help to make this work, or want to discuss SimpleSAMLphp with other users of the software, you are fortunate: Around SimpleSAMLphp there is a great Open source community, and you are welcome to join! The forums are open for you to ask questions, contribute answers other further questions, request improvements or contribute with code or plugins of your own. - [SimpleSAMLphp homepage](https://simplesamlphp.org) - [List of all available SimpleSAMLphp documentation](http://simplesamlphp.org/docs/) - [Join the SimpleSAMLphp user's mailing list](https://simplesamlphp.org/lists) simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.5.md0000644000000000000000000000235213245225037022366 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.5 =================================== * `SimpleSAML_Session::isValid()` If your code calls `$session->isValid()` without an argument, you will now have to update it to pass an argument (probably `saml2`). The reason for this change is that calling `$session->isValid()` without an argument can easily create a security hole. * We have introduced a new module for SAML authentication. This authentication module supports both SAML 1.1 and SAML 2.0 IdPs. We have also added a new authentication framework which should replace the previous redirects to the initSSO-scripts. Relating to this change, we have also deprecated the `initSSO`-scripts for SAML 1.1 and SAML 2.0 authentication. The old methods will still be supported for a while, but new code should probably use the new code. See the [migration guide](simplesamlphp-sp-migration) for more information about this. * The `request.signing` option has been removed. That option was replaced with the `redirect.sign` and `redirect.validate` options, and has been depreceated for one year. * The `aggregator` module's configuration file has changed name. It was changed from `aggregator.php` to `module_aggregator.php`. simplesamlphp-1.15.3/docs/simplesamlphp-hok-sp.md0000644000000000000000000000440513245225037020512 0ustar rootrootUsing Holder-of-Key Web Browser SSO Profile on a SimpleSAMLphp SP ================================================================= This document describes how to enable the [SAML V2.0 Holder-of-Key (HoK) Web Browser SSO Profile](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-holder-of-key-browser-sso.pdf) on a SimpleSAMLphp Service Provider (SP). The SAML V2.0 HoK Web Browser SSO Profile is an alternate version of the standard SAML Web Browser SSO Profile. Its primary benefit is the enhanced security of the SSO process while preserving maximum compatibility with existing deployments on client and server side. When using this profile the communication between the user and the SP is required to be protected by the TLS protocol. Additionally, the user needs a TLS client certificate. This certificate is usually selfsigned and stored in the certificate store of the browser or the underlying operating system. Configuring Apache ------------------ The SP requests a client certificate from the user agent during the TLS handshake. This behaviour is enabled with the following Apache webserver configuration: SSLEngine on SSLCertificateFile /etc/openssl/certs/server.crt SSLCertificateKeyFile /etc/openssl/private/server.key SSLVerifyClient optional_no_ca SSLOptions +ExportCertData If the user agent can successfully prove possession of the private key associated to the public key from the certificate, the received certificate is stored in the environment variable `SSL_CLIENT_CERT` of the webserver. Enable HoK on SP ---------------- To enable support for the HoK SSO Profile in the SP, the `saml20.hok.assertion` option must be set to TRUE in the SP configuration. This option can also be enabled in the `saml20-idp-remote` metadata file, but in that case the endpoint will not be added to the SP metadata. You must also send authentication requests specifying the Holder-of-Key profile to the IdP. This is controlled by the `ProtocolBinding` option in the SP configuration. 'hok-sp' => array( 'saml:SP', 'saml20.hok.assertion' => TRUE, 'ProtocolBinding' => 'urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser', ), When this is done, you can add the metadata of your SP to the IdP and test the authentication. simplesamlphp-1.15.3/docs/simplesamlphp-automated_metadata.md0000644000000000000000000002020413245225037023127 0ustar rootrootAutomated Metadata Management ============================= Introduction ------------ If you want to connect an Identity Provider, or a Service Provider to a **federation**, you need to setup metadata for the entries that you trust. In many federations, in particular federations based upon the Shibboleth software, it is normal to setup automated distribution of metadata using the SAML 2.0 Metadata XML Format. Some central administration or authority, provides a URL with a SAML 2.0 document including metadata for all entities in the federation. The present document explains how to setup automated downloading and parsing of a metadata document on a specific URL. Preparations ------------ You need to enable the following modules: 1. [cron](./cron:cron) 2. metarefresh The cron module allows you to do tasks regularly, by setting up a cron job that calls a hook in SimpleSAMLphp. The metarefresh module will download and parse the metadata document and store it in metadata files cached locally. First, you will need to copy the `config-templates` files of the two modules above into the global `config/` directory. [root@simplesamlphp] cd /var/simplesamlphp [root@simplesamlphp simplesamlphp] touch modules/cron/enable [root@simplesamlphp simplesamlphp] cp modules/cron/config-templates/*.php config/ [root@simplesamlphp simplesamlphp] touch modules/metarefresh/enable [root@simplesamlphp simplesamlphp] cp modules/metarefresh/config-templates/*.php config/ Testing it manually ------------------- It is often useful to verify that the metadata sources we want to use can be parsed and verified by metarefresh, before actually configuring it. We can do so in the command line, by invoking metarefresh with the URL of the metadata set we want to check. For instance, if we want to configure the metadata of the SWITCH AAI Test Federation: cd modules/metarefresh/bin ./metarefresh.php -s http://metadata.aai.switch.ch/metadata.aaitest.xml The `-s` option sends the output to the console (for testing purposes). If the output makes sense, continue. If you get a lot of error messages, try to read them and fix the problems that might be causing them. If you are having problems and you can't figure out the cause, you can always send an e-mail to the SimpleSAMLphp mailing list and ask for advice. Configuring the metarefresh module ---------------------------------- Now we are going to proceed to configure the metarefresh module. First, edit the appropriate configuration file: [root@simplesamlphp simplesamlphp]# vi config/config-metarefresh.php Here's an example of a possible configuration for both the Kalmar Federation and UK Access Management Federation: $config = array( 'sets' => array( 'kalmar' => array( 'cron' => array('hourly'), 'sources' => array( array( 'src' => 'https://kalmar.feide.no/simplesaml/module.php/aggregator/?id=kalmarcentral&mimetype=text/plain&exclude=norway', 'certificates' => array( 'current.crt', 'rollover.crt', ), 'template' => array( 'tags' => array('kalmar'), 'authproc' => array( 51 => array('class' => 'core:AttributeMap', 'oid2name'), ), ), ), ), 'expireAfter' => 60*60*24*4, // Maximum 4 days cache time. 'outputDir' => 'metadata/metarefresh-kalmar/', 'outputFormat' => 'flatfile', ), 'uk' => array( 'cron' => array('hourly'), 'sources' => array( array( 'src' => 'http://metadata.ukfederation.org.uk/ukfederation-metadata.xml', 'validateFingerprint' => 'D0:E8:40:25:F0:B1:2A:CC:74:22:ED:C3:87:04:BC:29:BB:7B:9A:40', ), ), 'expireAfter' => 60*60*24*4, // Maximum 4 days cache time. 'outputDir' => 'metadata/metarefresh-ukaccess/', 'outputFormat' => 'serialize', ), ) ); The configuration consists of one or more metadata sets. Each metadata set has its own configuration, representing a metadata set of sources. Some federations will provide you with detailed instructions on how to configure metarefresh to fetch their metadata automatically, like, for instance, [the InCommon federation in the US](https://spaces.internet2.edu/x/eYHFAg). Whenever a federation provides you with specific instructions to configure metarefresh, be sure to use them from the authoritative source. The metarefresh module supports the following configuration options: `cron` : Which cron tags will refresh this metadata set. `sources` : An array of metadata sources that will be included in this metadata set. The contents of this option will be described later in more detail. `expireAfter` : The maximum number of seconds a metadata entry will be valid. `outputDir` : The directory where the generated metadata will be stored. The path is relative to the SimpleSAMLphp base directory. `outputFormat` : The format of the generated metadata files. This must match the metadata source added in `config.php`. `types` : The sets of entities to load. An array containing strings identifying the different types of entities that will be loaded. Valid types are: * saml20-idp-remote * saml20-sp-remote * shib13-idp-remote * shib13-sp-remote * attributeauthority-remote All entity types will be loaded by default. Each metadata source has the following options: `src` : The source URL where the metadata will be fetched from. `certificates` : An array of certificate files, the filename is relative to the `cert/`-directory, that will be used to verify the signature of the metadata. The public key will be extracted from the certificate and everything else will be ignored. So it is possible to use a self signed certificate that has expired. Add more than one certificate to be able to handle key rollover. This takes precedence over validateFingerprint. `validateFingerprint` : The fingerprint of the certificate used to sign the metadata. You don't need this option if you don't want to validate the signature on the metadata. `template` : This is an array which will be combined with the metadata fetched to generate the final metadata array. `types` : Same as the option with the same name at the metadata set level. This option has precedence when both are specified, allowing a more fine grained configuration for every metadata source. After you have configured the metadata sources, you need to give the web-server write access to the output directories. Following the previous example: chown www-data /var/simplesamlphp/metadata/metarefresh-kalmar/ chown www-data /var/simplesamlphp/metadata/metarefresh-ukaccess/ Now you can configure SimpleSAMLphp to use the metadata fetched by metarefresh. Edit the main config.php file, and modify the `metadata.sources` directive accordingly: 'metadata.sources' => array( array('type' => 'flatfile'), array('type' => 'flatfile', 'directory' => 'metadata/metarefresh-kalmar'), array('type' => 'serialize', 'directory' => 'metadata/metarefresh-ukaccess'), ), Remember that the `type` parameter here must match the `outputFormat` in the configuration of the module. Configuring the cron module --------------------------- See the [cron module documentation](./cron:cron) to configure `cron` Once you have invoked cron, and if this operation seems to run fine, navigate to the **SimpleSAMLphp Front page** › **Federation**. Here you will see a list of all the Identity Providers trusted. They will be listed with information about the maximum duration of their cached version, such as *(expires in 96.0 hours)*. You *may* need to adjust the below php.ini setings if the metadata files you consume are quite large. * `memory_limit` * `max_execution_time` Metadata duration ----------------- SAML metadata may supply a `cacheDuration` attribute which indicates the maximum time to keep metadata cached. Because this module is run from cron, it cannot decide how often it is run and enforce this duration on its own. Make sure to run metarefresh from cron at least as often as the shortest `cacheDuration` in your metadata sources. simplesamlphp-1.15.3/docs/simplesamlphp-nostate.md0000644000000000000000000001431713245225037020771 0ustar rootrootDebugging "State Information Lost" errors ========================================= **"State Information Lost"** (`SimpleSAML_Error_NoState: NOSTATE`) This is one of the most common errors that you can encounter when configuring SimpleSAMLphp. Unfortunately, it is also a generic error that can have many possible causes. This document will attempt to describe what this error actually means, and some of the situations that can cause it. What is "state information"? ---------------------------- The "state information" is data that SimpleSAMLphp stores in association with a request. The request is typically a SAML 2.0 authentication request sent to the IdP, but it can also be other requests. This state information is given a random ID, e.g. "`_2da56e07840b59191d9797442b6b665d67d855cf77`", and is saved in the session of the user. What does it mean that it was lost? ----------------------------------- This means that we tried to load state information with a specified ID, but were unable to find it in the session of the user. What can cause it to be lost? ----------------------------- There are several ways that this can happen, but most of them have to do with session storage. Here we will outline some generic alternatives, and possible solutions. #### The domain name changed during authentication The domain name the IdP sends the response to is configured in the metadata of the IdP. This means that it may not match up with the domain name the user accessed. For example we may have the following scenario: 1. The user accesses `https://www.example.org/`. A session is created for the user, and the session cookie is set for the current domain (www.example.org). 1. The user needs to be authenticated. We therefore save some information about the current status in the state array, create a SAML 2.0 authentication request, and send it to the IdP. 1. The user logs in on the IdP. The IdP then sends a response to the SP at `example.org`. However, the metadata for the SP that is registered at the IdP uses `https://example.org/` (without `www`) as the domain the response should be sent to. The authentication response is therefore sent to that domain. 1. The SP (now at `https://example.org/`) tries to load the state information associated with the authentication response it received. But, because the domain name has changed, we do not receive the session cookie of the user. We are therefore unable to find the session of the user. When we attempt to load the state information from the session we are therefore unable to find it. There are several ways to solve this. One of the simplest is often to configure your webserver to only use one domain, and redirect all accesses to the other domain to the correct domain. A different solution is to change the session cookie settings, so that they are set for the "`example.org`" domain. If you are using PHP sessions, you should change this in `php.ini`. If not, you should change it with the '`session.cookie.domain`' option in `config/config.php`. In either case, it should be set to the top-level domain with a "dot" in front of it. E.g.: 'session.cookie.domain' => '.example.org', Or in php.ini: session.cookie_domain = ".example.org" Note that if you use PHP sessions, you will also have to make sure that your application uses the same domain when it sets the cookie. How that is done depends on your application. (See the section about mismatch between application PHP session settings and SimpleSAMLphp session settings.) #### Hopping between http and https If a cookie is set during a HTTPS session, it is not available when the same URL is later accessed over http. If your site is available over both http and https, check that you're using https consistently throughout the configuration. The best and most secure is to make your complete site available on https only, and redirect any http requests to https. #### Mismatch between PHP session settings for the application and SimpleSAMLphp If both the application you are trying to add SAML 2.0 support to and SimpleSAMLphp uses PHP session for session storage, and they don't agree on all the parameters, you can end up with this error. By default, SimpleSAMLphp uses the settings from `php.ini`, but these can be overridden in `config/config.php`. If this is the cause of your error, you have two choices: either change SimpleSAMLphp to use a different session storage method (e.g. memcache or sql), or change the session settings to match between the application and SimpleSAMLphp. In many cases it is simplest to adjust the session storage. If you decide to make the session settings match, you should change the settings in `php.ini`. This is to make sure that the settings apply to everything that uses the default settings. The following options in `php.ini` must match the settings used by the application: * `session.save_handler`: This is the method that is used to store the session. The default is "`files`". * `session.save_path`: This is the location the session files are saved. The default depends on your PHP installation. * `session.name`: This is the name of the session cookie. The default is "`PHPSESSID`". * `session.cookie_path`: The path that the session cookie is limited to. The default is "`/`", which means that it is available to all pages on your domain. * `session.cookie_domain`: This is the domain the session cookie is limited to. The default is unset, which makes the cookie available only to the current domain. What those settings should be set to depends on the application. The simplest way to determine it may be to look for calls to `session_set_cookie_params` in the application, and look at what parameters it uses. #### A generic problem saving sessions Sometimes the problem is caused by SimpleSAMLphp being unable to load and/or save sessions. This can be caused by the session settings being incorrect, or by a failure of some service required by the session storage. For example, if you are using memcache for session storage, you need to ensure that the memcache server is running and that the web server is able to connect to it. The same applies if you are saving the sessions to a SQL database. You may want to check your web server error log. If the PHP session handler fails, it may log an error message there. simplesamlphp-1.15.3/docs/simplesamlphp-reference-idp-hosted.md0000644000000000000000000003706013245225037023310 0ustar rootrootIdP hosted metadata reference ============================= This is a reference for the metadata files `metadata/saml20-idp-hosted.php` and `metadata/shib13-idp-hosted.php`. Both files have the following format: 'idp.example.org', /* Configuration options for the first IdP. */ ); $metadata['entity-id-2'] = array( 'host' => '__DEFAULT__', /* Configuration options for the default IdP. */ ); /* ... */ The entity ID should be an URI. It can, also be on the form `__DYNAMIC:1__`, `__DYNAMIC:2__`, `...`. In that case, the entity ID will be generated automatically. The `host` option is the hostname of the IdP, and will be used to select the correct configuration. One entry in the metadata-list can have the host `__DEFAULT__`. This entry will be used when no other entry matches. Common options -------------- `auth` : Which authentication module should be used to authenticate users on this IdP. `authproc` : Used to manipulate attributes, and limit access for each SP. See the [authentication processing filter manual](simplesamlphp-authproc). `certificate` : Certificate file which should be used by this IdP, in PEM format. The filename is relative to the `cert/`-directory. `host` : The hostname for this IdP. One IdP can also have the `host`-option set to `__DEFAULT__`, and that IdP will be used when no other entries in the metadata matches. `logouttype` : The logout handler to use. Either `iframe` or `traditional`. `traditional` is the default. `OrganizationName` : The name of the organization responsible for this IdP. This name does not need to be suitable for display to end users. : This option can be translated into multiple languages by specifying the value as an array of language-code to translated name: 'OrganizationName' => array( 'en' => 'Example organization', 'no' => 'Eksempel organisation', ), : *Note*: If you specify this option, you must also specify the `OrganizationURL` option. `OrganizationDisplayName` : The name of the organization responsible for this IdP. This name must be suitable for display to end users. If this option isn't specified, `OrganizationName` will be used instead. : This option can be translated into multiple languages by specifying the value as an array of language-code to translated name. : *Note*: If you specify this option, you must also specify the `OrganizationName` option. `OrganizationURL` : A URL the end user can access for more information about the organization. : This option can be translated into multiple languages by specifying the value as an array of language-code to translated URL. : *Note*: If you specify this option, you must also specify the `OrganizationName` option. `privacypolicy` : This is an absolute URL for where an user can find a privacypolicy. If set, this will be shown on the consent page. `%SPENTITYID%` in the URL will be replaced with the entity id of the service the user is accessing. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. `privatekey` : Name of private key file for this IdP, in PEM format. The filename is relative to the `cert/`-directory. `privatekey_pass` : Passphrase for the private key. Leave this option out if the private key is unencrypted. `scope` : An array with scopes for this IdP. The scopes will be added to the generated XML metadata. A scope can either be a domain name or a regular expression matching a number of domains. `userid.attribute` : The attribute name of an attribute which uniquely identifies the user. This attribute is used if SimpleSAMLphp needs to generate a persistent unique identifier for the user. This option can be set in both the IdP-hosted and the SP-remote metadata. The value in the sp-remote metadata has the highest priority. The default value is `eduPersonPrincipalName`. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. `contacts` : Specify contacts in addition to the technical contact configured through config/config.php. For example, specifying a support contact: 'contacts' => array( array( 'contactType' => 'support', 'emailAddress' => 'support@example.org', 'givenName' => 'John', 'surName' => 'Doe', 'telephoneNumber' => '+31(0)12345678', 'company' => 'Example Inc.', ), ), : If you have support for a trust framework that requires extra attributes on the contact person element in your IdP metadata (for example, SIRTFI), you can specify an array of attributes on a contact. 'contacts' => array( array( 'contactType' => 'other', 'emailAddress' => 'mailto:abuse@example.org', 'givenName' => 'John', 'surName' => 'Doe', 'telephoneNumber' => '+31(0)12345678', 'company' => 'Example Inc.', 'attributes' => array( 'xmlns:remd' => 'http://refeds.org/metadata', 'remd:contactType' => 'http://refeds.org/metadata/contactType/security', ), ), ), SAML 2.0 options ---------------- The following SAML 2.0 options are available: `assertion.encryption` : Whether assertions sent from this IdP should be encrypted. The default value is `FALSE`. : Note that this option can be set for each SP in the SP-remote metadata. `attributes.NameFormat` : What value will be set in the Format field of attribute statements. This parameter can be configured multiple places, and the actual value used is fetched from metadata by the following priority: : 1. SP Remote Metadata 2. IdP Hosted Metadata : The default value is: `urn:oasis:names:tc:SAML:2.0:attrname-format:basic` : Some examples of values specified in the SAML 2.0 Core Specification: : - `urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified` - `urn:oasis:names:tc:SAML:2.0:attrname-format:uri` (The default in Shibboleth 2.0) - `urn:oasis:names:tc:SAML:2.0:attrname-format:basic` (The default in Sun Access Manager) : You can also define your own value. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. : (This option was previously named `AttributeNameFormat`.) `encryption.blacklisted-algorithms` : Blacklisted encryption algorithms. This is an array containing the algorithm identifiers. : Note that this option can be set for each SP in the [SP-remote metadata](./simplesamlphp-reference-sp-remote). : The RSA encryption algorithm with PKCS#1 v1.5 padding is blacklisted by default for security reasons. Any assertions encrypted with this algorithm will therefore fail to decrypt. You can override this limitation by defining an empty array in this option (or blacklisting any other algorithms not including that one). However, it is strongly discouraged to do so. For your own safety, please include the string 'http://www.w3.org/2001/04/xmlenc#rsa-1_5' if you make use of this option. `https.certificate` : The certificate used by the webserver when handling connections. This certificate will be added to the generated metadata of the IdP, which is required by some SPs when using the HTTP-Artifact binding. `nameid.encryption` : Whether NameIDs sent from this IdP should be encrypted. The default value is `FALSE`. : Note that this option can be set for each SP in the [SP-remote metadata](./simplesamlphp-reference-sp-remote). `NameIDFormat` : The format of the NameID supported by this IdP. Defaults to the `transient` format if unspecified. This parameter can be configured in multiple places, and the actual value used is fetched from metadata with the following priority: : 1. SP Remote Metadata 2. IdP Hosted Metadata : The three most commonly used values are: : 1. `urn:oasis:names:tc:SAML:2.0:nameid-format:transient` 2. `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent` 3. `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` : The `transient` format will generate a new unique ID every time the user logs in. : To properly support the `persistent` and `emailAddress` formats, you should configure [NameID generation filters](./saml:nameid) on your IdP. : Note that the value set here will be added to the metadata generated for this IdP, in the `NameIDFormat` element. `RegistrationInfo` : Allows to specify information about the registrar of this SP. Please refer to the [MDRPI extension](./simplesamlphp-metadata-extensions-rpi) document for further information. `saml20.sendartifact` : Set to `TRUE` to enable the IdP to send responses with the HTTP-Artifact binding. Defaults to `FALSE`. : Note that this requires a configured memcache server. `saml20.hok.assertion` : Set to `TRUE` to enable the IdP to send responses according the [Holder-of-Key Web Browser SSO Profile](./simplesamlphp-hok-idp). Defaults to `FALSE`. `saml20.sign.response` : Whether `` messages should be signed. Defaults to `TRUE`. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. `saml20.sign.assertion` : Whether `` elements should be signed. Defaults to `TRUE`. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. `sign.logout` : Whether to sign logout messages sent from this IdP. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. `SingleSignOnService` : Override the default URL for the SingleSignOnService for this IdP. This is an absolute URL. The default value is `/saml2/idp/SSOService.php` : Note that this only changes the values in the generated metadata and in the messages sent to others. You must also configure your webserver to deliver this URL to the correct PHP page. `SingleSignOnServiceBinding` : List of SingleSignOnService bindings that the IdP will claim support for. : Possible values: * `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect` * `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST` : Defaults to HTTP-Redirect binding. Please note that the order specified will be kept in the metadata, making the first binding the default one. `SingleLogoutService` : Override the default URL for the SingleLogoutService for this IdP. This is an absolute URL. The default value is `/saml2/idp/SingleLogoutService.php` : Note that this only changes the values in the generated metadata and in the messages sent to others. You must also configure your webserver to deliver this URL to the correct PHP page. `SingleLogoutServiceBinding` : List of SingleLogoutService bindings the IdP will claim support for. : Possible values: * `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect` * `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST` : Defaults to HTTP-Redirect binding. Please note that the order specified will be kept in the metadata, making the first binding the default one. `signature.algorithm` : The algorithm to use when signing any message generated by this identity provider. Defaults to RSA-SHA1. : Possible values: * `http://www.w3.org/2000/09/xmldsig#rsa-sha1` *Note*: the use of SHA1 is **deprecated** and will be disallowed in the future. * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256` * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384` * `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512` `validate.authnrequest` : Whether we require signatures on authentication requests sent to this IdP. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. `validate.logout` : Whether we require signatures on logout messages sent to this IdP. : Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. ### Fields for signing and validating messages SimpleSAMLphp only signs authentication responses by default. Signing of logout requests and logout responses can be enabled by setting the `redirect.sign` option. Validation of received messages can be enabled by the `redirect.validate` option. These options set the default for this IdP, but options for each SP can be set in `saml20-sp-remote`. Note that you need to add a certificate for each SP to be able to validate signatures on messages from that SP. `redirect.sign` : Whether logout requests and logout responses sent from this IdP should be signed. The default is `FALSE`. `redirect.validate` : Whether authentication requests, logout requests and logout responses received sent from this IdP should be validated. The default is `FALSE` **Example: Configuration for signed messages** 'redirect.sign' => true, Shibboleth 1.3 options ---------------------- The following options for Shibboleth 1.3 IdP's are avaiblable: `scopedattributes` : Array with names of attributes which should be scoped. Scoped attributes will receive a `Scope`-attribute on the `AttributeValue`-element. The value of the Scope-attribute will be taken from the attribute value: : `someuser@example.org` : will be transformed into : `someuser` : By default, no attributes are scoped. Note that this option also exists in the SP-remote metadata, and any value in the SP-remote metadata overrides the one configured in the IdP metadata. Metadata extensions ------------------- SimpleSAMLphp supports generating metadata with the MDUI, MDRPI and EntityAttributes metadata extensions. See the documentation for those extensions for more details: * [MDUI extension](./simplesamlphp-metadata-extensions-ui) * [MDRPI extension](./simplesamlphp-metadata-extensions-rpi) * [EntityAttributes](./simplesamlphp-metadata-extensions-attributes) Examples -------- These are some examples of IdP metadata ### Minimal SAML 2.0 / Shibboleth 1.3 IdP ### '__DEFAULT__', /* The private key and certificate used by this IdP. */ 'certificate' => 'example.org.crt', 'privatekey' => 'example.org.pem', /* * The authentication source for this IdP. Must be one * from config/authsources.php. */ 'auth' => 'example-userpass', ); simplesamlphp-1.15.3/docs/resources/0000755000000000000000000000000013245225037016122 5ustar rootrootsimplesamlphp-1.15.3/docs/resources/simplesamlphp-googleapps/0000755000000000000000000000000013245225037023136 5ustar rootrootsimplesamlphp-1.15.3/docs/resources/simplesamlphp-googleapps/googleapps-sso.png0000644000000000000000000002526313245225037026616 0ustar rootrootPNG  IHDRUBhiCCPICC Profilexڍ 3wϙ?3?- ""aQ,[zw+AA6nn)c]t9C)fu髼X4$́x^昨($@ruXvHD`J4GӃ6D]\ f$>7B!Y744:d LE!>RyU}nMFXjppq89{Tҷ0"FBH@I$2GZ -#+'HSRVUS_g`hdlt,,m엯pXuOϵ^됹ni@ y֐аm1q;wڝ_&KޟzසIK?z2NgWVs9=_pWҲ+oܼUU};wox'-O={eWwOo_o a81izf3`p b@8 U(]=}} PV6$0n(/(|1" 8~!8 Ǖ9(ufơB@A Fj%!Bbu^ X)~)r1T J *0( @y`@"[B&W,(I ^(xcURZ@YUG{ ͏`} &qI V"#> 9i5M.OX_:N+ѵt{wߍ=l8_BCe>{ {Γߢtl 2$v ]%j\,0Ⱥ";˖,`}>x2&~OiN{~L0v7?HxL38}3yqEB['|_|A@d8y۾E~H|)z@B6zyIx<'yۺ1-cuu[#<fWY?q.]9 Zy=S?y*)H`: c; b9P[̀ju~6x:74+[y||*%čSz:e~3} Brq}>y3?k1_ 1tX;7}điT?G6]prjdԣ 'x:įua70Гx -Yo:/BIlY}և_|SdSLb:9Ƅ>a[FsCez&5yyggr{I;Bc2><h 3IEv>9dĶtDrv=#r'Ccjѧ:f1gc2v1YO%n'f٣рa0PLe(IԓqmC7^L&sj$y4~O?X#ݏ ֣d&}?Oޑu,VY WXUǟJ$Y@򲟏!CLrI]&L|& I⬧~1J8)yTH\vrT5 `},a"UJz}n=c2\;%'f=]ѵ_X_fUdžI8?o+Jra`r픬vl>D!!y]< '3֟~]AK>zU˹v~{>&+>c ?>cXdmjӥORς1oH;6"ΰVOT_a /Ss/OYѸa笿td1g;^bzn=|b+7n0zshZqNjbbt/e^[ϲwκg򩒐n{_ 林=DO\|҃l2|ɾ.ohI@zsdӟgxGf *gЋ٣7x~pŧ)C_1THH'7*~򆯓l܇(❗nAUb^K(VTATFwɜsh5?)<..iC(&9Kwn?㳔 zZB$,w})>N9ےظ?)1hTtc"3!|TWYEF"_K(qʎrwAw8 0eq%sEvH߂~no)K`}eI# ;MR>fFCNZh ߡg*wqqR, FI*r{>!_C[Z_A~t#V.̖"v{VO%]^&~AA? Q(H+ nmTbn5c{.m[D|#*{-33߱kzγF O# {&I'[sDҨ@9 9GMRc_.N,[5n)(a|u{tQt@|huQV:6[mt7s-m©źJ>$3}'^ϖ-"w=JRWki$5x eUQS\O¬Dx4#3uڈK$w(n!ՎpA鵇#8S"Tc 0kʃvJ"'=JH?4³۝R7azggS\̉MDGHm0w6HqO&'~fx%itZUDʸwg 66)4ӄ_IPg2voAW3Ƥ rzџEn1'Ym͚udd=gjDeac ʯrQp3dhF(YAt<ڹ e6B#7P͹ov,6ŠIQ]H!䂬^&_ZaxMf6f%Qܠp6HfCA`B4nZq#zghߙdvoX={)-< C,iӭ'E9&"lo1BY@R.7қW rJAW gIrx"gzJ&E[3N@Gf*v^\ Hrh-eUy(I:HB2Mt=DJPU0NS/ŶeGxvPTKG:qu< 尙/FPF]Sԧ9ThΩf>J‰YAd`",T70й]s#y(HsS7ad}RkH= Q88Ip\K\;evdqq'YmIsĤH\HÓ }:6fkD SqT5JfI +E-73ϸ9e}ZTէiPr)MP2vŶ g>6oAxkaҍn8^$cNFhEWJ'LDj2h~<3t*aO6=9C=8kd6E9 FpyxRzY<)p-yiuw&3KX_ybAK\p+Iߟͯ78>m1UQ~d.e6y8¯Od= u965g!>HZtdϸ,[]$]%/n))`llG[Ĥ 6.y&COy8MzkINPc^$̒0UUBcGw{JG;0Lض3TY(t4AGNBwa.hllm&] h2&4KNfĥ`#U4a} g"?-@dJ]H!9I7%+~Odd%:G4$}8SuXJA`Ayn"TS|rMR铓>s6l㏅USl-˯tp$;Ɍ0-J-0&7MQJ/4 l:C˽尸ߢݧC7l)>]R2_lYx&4g ս+ ,?8awiC4Kfj9F!Ϲط|n#dۻ䇨Mϟx y>'N@ [ D'eA0O06I۞:Fa0N Azx5Ouڑ*or۶ؿlqUiWX&#;]jX= Qсmot yCm%$F>q۵C۩ln + 8 h2{] o+һZ/z P3,M:#7,|@NrVQ=W޸/KC1φX_kf_Q [*nOE=u#V+prl%^0/V9|鍻̗(fe}|Oυ79*.}ԕdqҮ`]-yUB)cHgW%Ȳ4:|TY'5餒tUgvq/"?ɼW<)lVn}uмRx9aO<+]4o;>Ƈjn"ljWM >8]y?zO^.YXKuPƾ:E f")BUTޫBTmљXxӯRD?><^2YՌf rFqHU#D묯:gS$~|Ja+iWY_gvNo( [vTDRpuڱYlqCAz쪂zYVܤ~l7OTtU\_Rqil%ɥ+xL]ᖄ+č}bQvً S|t+!0v暝#i7ŧ`Qa?x}|`z\=.jiAOE3oTʑ9+--o'Si:nm`]jұ9aJHʳE7Q5 dd\4wCj$T|xL]ᖄ$}ut5~U-aUrzdDa0Mk[N*%{w}{$U tVɾɼ*8b쑋3֌ QU,+?+(g˴o;6G=dLne՚ e̯N[p}}A>t{nIl쉋d.~ۿE Ini|`F]5ZguTUEE]C#NEEZ)-ͳ@~7cE`%w@kpl6{'a]QmJ[SI'K|[ᖄ+*}ӌ&_ꇕM/JR_!]{ʿS?7Y~#hƞө{NOH~zǾ;`}g<H[mĜHz RzJ<8ת9I94*ޠ'['1TX7n%ӷs"Mdr[0/QݝEVxYD% \,z Xese ϛ F ,WRfL:g r[M!wciذ,_sD) ]vzџP7a~.56lVbifW8DsH:(S ˋ܃{ H]R^6Q{Q{V^r~ajs&t'oZs}xJ~1>C߾u]to_o?wO!?spY*s^3X gڹ6Gd>SkgΛ);AnXj*jai*8ÈrsqnKlrh2k'gs3)+4$є|eLV2L_+~iru kx2Uu*ߴIXe|ͺ[e WI|ny?㷈{ONg߶w~2?*^N>1x5|(Hx'hSdyp(i\=m䶾7.7[$Ί3e2Hs(}:-O/Թ<_9ѼҙXck()o)Z@\xdW?ԊV(a`/r9t$cw_Lw;Y]ǟ'!JneO/zޏZ/||Kڃ,?hp'/`?d:,Q;WQ^ERuv/r LAذa34EuN-vМ7 PJ.UԹׯ9OZ˲]gxWQ-gGv ^.suxv _58ٗ7ObyY?>ӟ#|g#dQ> V;P?oHsXp+(iEsIsV|;g lAX63U0|*Lx3UԹsv׭986ޑ¿LWҢ._`%~.AtC}f6B5{>'c`}X>`}X>`}z@K}\=FOhIENDB`simplesamlphp-1.15.3/docs/resources/simplesamlphp-googleapps/googleapps-cert.png0000644000000000000000000032066313245225037026751 0ustar rootrootPNG  IHDR %iCCPICC Profilexڍ 3wϙ?3?- ""aQ,[zw+AA6nn)c]t9C)fu髼X4$́x^昨($@ruXvHD`J4GӃ6D]\ f$>7B!Y744:d LE!>RyU}nMFXjppq89{Tҷ0"FBH@I$2GZ -#+'HSRVUS_g`hdlt,,m엯pXuOϵ^됹ni@ y֐аm1q;wڝ_&KޟzසIK?z2NgWVs9=_pWҲ+oܼUU};wox'-O={eWwOo_o a81izf3`p b@8 U(]=}} PV6$0n(/(|1" 8~!8 Ǖ9(ufơB@A Fj%!Bbu^ X)~)r1T J *0( @y`@"[B&W,(I ^(xcURZ@YUG{ ͏ȝ%.V'~RF {i x/ lF  3 C+W\%7[K}C}@<O"סwDsoB=K0btq:Q ;d cWxJ;7QeZ jf_IRJg$db*I-*///---J*bZbDEEETD!Z!z-t9jT^:KTr_%5UVVVTaP]]]2jU@v$<`%VpT"OFj h uA!6b2-W$Dx q>=s~d@= pEQ $oSR?3 f5j k *g6iR|Bi413Dϰ:D:̵lDNyvj6RT-DG|@ JRD6,^ %)#( m (A3;J%|x W&q*4PMUQ/Q[ Quh<بAd!b**"|x7P uarA4wДXJFN rKLB[zNS՘%Tp2%0mKC$\ $V-s͜aSʹ)2 )e.1<\K&uJcjgb%Qi譤 {fyȤ>k!9jC.k}J^ع~s9e|*IW@!%9茗h_2Vyւ:*Q4ATE=J[$à=bFDa#TΔ\^/ݩR)YMW + m::xsP7'ӊFBpe4iǥ5C9f*ig%F;N҄1M0}Wf2~u J-W&ajUͤ)hc#sq5FUܡbq~DVd&ߤTuFaIg"2_)NU*Xgt<NVD:W9PkR PE6H@- Evv6|hTm۶k׾/og;v@ MW_|l"D_>k=_%v֬YAEF4 V3&Up b!11SR6o)g#?~iHy1\^A\0meyvh?.0%iNR0D-\1ΰ}&n`IvVeyR[]QfeXOh0l .Oш,d0ىl\vZulɥ4$_=9U`Ux*Yմ4e-M: ̚~~ͺA@4/>U#h&|_2S0dEV۷~ݻw'%%kΞ={>C x` ַ?nߑl/xY>H6m`fǎxxyO>>V4ʣ;7pi1Ob;~hH1OTIO* H1o:.2&@ hg7hsn%+BHmS)鹈̳hҺ,uoyvZS4*=.mk``ѡ;O"=T3 Sê⿫z!oXh0V  9 +`L%$  ๯Skoԥwuڮ^_ _ůaz|k_7Ҳ:w^2_ѭOJ{7Gqa0Q< h[T.Di%+E hX͐LBj)f.PV$`b.5F+1 hcJgY޴?doa!jJ:.ĩyri>.yXpq~3tP<6h.1՟WVLCSopzHoe t`CLW r;چ a*`ʤ|y)t`_P}h-s?#~4ƚU:CIw.w zP ɯTK~Y0]4h}uU!E||g@ H7~؁8M9rQO.M**5ת=Ç+| 5?c?}c7kj/6U¦XQI柾<22zטn,J:aXK1F.XsbBVPjii1u=Lci2̫X)L -.C8+^0+ z2E#F:..(-o7K;`;se`fҩu =-z|_+EҦQ GZYUo-!֮6Ӎ&p [ <Ɗy2y+XSEZFc@7XGJbհ3?dXKl!ݟL>ka&Kx3 /h&}eVЪ1!?=aaa[nMIIAB ?ɓ}TIz5dݺkk~YSor~7{̊]lڟl{_BgVw}ߞ_n񛉿Y)5Uqwg_}~B?y\oQ6?;X3_SzW0S+XKYc`ajJ+ JnV *Ex'\z 4BS;f]`0 ,w*2ti3r[J(zZ\BgЩMg{M4;L<_GׇG?.=۱׈MNpd=2d){zGLtNVeŞ;*Trp)@ XFqZ&!/Ǻ|IL=?{{Dಛp8s-4e*NhZY/ͪ` 5TC"MѢmOOO>$==D$_-$7ojYW@?3 /o}FُM՘h i1w=^˨@I"c~'oY~<<0甹J ,bYML?P ɿ~F_47hY8+-hu2TI)uZ*Xt!":>#PGqn4u^Y;)p;: QWQ/i/-o{T"]"μF>]q 76ݭ S#_?/?^{rk7hoR.~o7_8moz~o ŌFeXRʥ#AV^71+rAoт^\xWw}^'*P 3xӕXc^Ѥ;m ʉԥV<2Ԝ C$$MxI$zvW7Hg C˙/vi!x=x|EEvx@;JED:ia7G ЯARpWvڽ>0k\3iFDE0s1t84|&E7d*_0Ni \Y=Xy1]DY%Նj |Imu~BG[OtFZKtAgR*8՞Bx* % -N35P'Ogq3% \x=x]aRB?VEGGQQCp?uW\s?i95P8[Lo_?};C&tmO'B6f^Yo_S`ʡ=*,25QA3Kz*%#ⰎG0_%LW5.aZsA Z #gʦEO}Mc3šrN$ڑnSx:4ۈj-5YԧQcOk5(B ic{ϟo;rJ/O MI%.Okvt;ě=yZ$ړߊ~FH9ohR,[N׳gp\c Fg22< )1x^Xf=s(!B325x47eHB0ZpV}>;f<Ҏ.mli<oF@\z7--5'v8G ?۵wH?u╤Z,/\75źp8DLQpTE?:f4{wN9Y.ߍKpFyŸ46W~|BgEX]Z&It q1σůR_1cg"5fgu7ky6" u3 sA+\xֶ34-0?ĽXX"S͊J.2œP^cx2 .\kN0 Wϟla;#؍&}o}7/'գUxz`H+ /`1z{k>U,߄cFZ.P ^ID u+K53,9oPą3hubX-BI5:'k j ~b\ο-ؑ&Bj, M*򮮦{{9IP#%(%7Sz>x[qEI-o"ѳ>]AQB#لG<\$/{ѵuLɂyZiyprhfRM9VU=\Aϵ}D?/oE`: u918dK<&Z4Z|#e)nPѝwn=~Vԍ3;TNb* 8iOҿ^;W'v`.i`Dǡ0ӂ0p5D`501+p9fGѣBZSe֕i\ XVS&fh~W gcIJ 4-O%`%Y2͛&2n/+SYSSWF[7Ѭous緷==欎7Jƚ zgt3::_Ht҅%ۥ yC!aMAj|\ KKyR -|Uff WP/c% ,~cum_O8Țsx qա>[F#yw~x:ó?'Sx/+w'BqiŔԹ cQa* dD\Y[[~qfT$&4` . r~̊ihZ AN Q];"|ɇ?~'i q֜&y;j91lg͑6A`5NQwf"5,98-n rڠE-*9Qz%L.ZM<;Ϊ&Yl,/0 H4y' z`)N>,*.Na5L<#>"~:.&Hzp?Z0j:]s%#9 ?ԱwK O+̦Ƅ&\ZҠ,h87)bf&c&g#c[Q&&Њҿ $вIxF$H\B~Xp9P7֭{[H HG $`@UixT |dRbRĄD KqH &> H~]v}'!'(44LTW&xKy-@ $@3 j|۷o߱cN`GMNɑYPQ,d2Yeeemm- A8777$""̙3 >3вU$tE^k絀H Z.ݻ8Ec}.\hmm`OVD do51\[k~.~ؔG ͎MX&0WS##%&&&߲oj>p‡Τ xbbjY-LeܾN\q?}ӓ#,O;`7AS>9&LNN=pƥKFFF8q/oVKKbqrk8rd?c2͊\OA86ޟq@ `PrL7s23##S#;X&j?j2 +})*e/{%C< ?jc s c }j1*?11>\qo V ._1E8bx^I*Ș`V0"6"Ň2}VbWȯ߁/GzPVx0ܮ7o pƌаaÆg-X-_AJEhm(!FBd6h[Xm.pT23g՘I~LJ&֡A Cg/+mi:8h­3]¸i _[lj2#X,TȾgaW)~V[Ň##>l[gdZ◵ޯ|v6+ x2Gidiʋ6hw8 T-D( QCf. z64_mvd^ifqf3h䧉lVb-cH#ɹs.\rgll3-D/¯k3dAO4%%eϞ=۶m۾}/9| s _e2ݻshHyƯ}mx %%UBqظmt^F y[hGp6Ȩf%X<@m[c"XtG[<] {6Z<@> zƭãFl#ёa 7?:t(222***F TLlDkE_=?_j˱5f!p7Ly[ =!HMM@ѓv $K L b8#33Ӏ3 E8Cөj~ \ȑ#UUUp`e}@W~Ֆ+[٬7c|DvM<|brר6.G&ޤjX&pP0ې:urjr% 6TwL'1MOplozMq£d܇hGY#ĸ3*Hnn.|uwƶóWfMWWCᕡW9{u^J/M׏n\yK ;0f|x.\/..OLL˃A"+@d@-` `````Wi7(AE QqF[[[ee%΀%''Xq\M---oz^8q5HaۓV9u]_6iqY\cۿ( v2oȘ$VBh`4k+žƩfo8@ #)^3mdla5yϙ1%^ǡ~)žPgHC,x֑ѱԓ'O㛝AM/575HP}}fBp2M  t)j4|.Nk Suݓ%#?55#c$@-M0C1`6q Si hXXF.wuu-g3fØmL=ǟs\P0C&={ ljoz^AߠԞvݻwsGuј53̭4橉;J{R_}ZC 0efrgؑjm22Hn@eh6F[=V㟸jǍ;vC G_a||QF.WtرNx2 ?@qqqcV39TnyȌ@>\RNXܦ&OadvDz'+~huxMZjLOK-,,/ Dn{nn {L l̓ٹ, ϸK իWTUUa>/~H…3b :qm%~Ɋ^Aߠ|%gXVsH~EWq܊Sʶ1ϦCY1U1u}*qOfyĥ`ʍ3F|pư"Jq]vα!'(y' hľqBp[ȟ'ΰ\rЛсQ z Hj"("M~)dvZ} Irκ}v'2Oԍ,ua y24yvmݧ3JKKM&`;w x!HYD!*06Cf<Ҳu^_g?YUuՙ3̗ We[[d+gWd9z}Rўfϑa PN趌v&3W6 sۦ2/xc[(un U:&I?Ê L K-(GU&'H_ DdX( rtDŽGKS\@22mNl"Ʊ+Fr-:=2/'|%B)ohV=u+oԜ?~|bjd66[&zBc]t'MME$"lpz YG/(6A^rf@''mC-m4*ԐHamnnJ:Qf6yfUrZ)Uc քrۑɲRrdNwGkGk&?"0YW+ybGY0 /No2QY SR 3T*ջ;%v+~APf80Rvv6%"g wřkVؐgzK:::닋Yw'Tnoo?u|N7 i4C' *//1CR̕J&!&=Ǘm[,p{{{| J\r-]7DGF44Ǘ :Sh<DZ=m>Ԕ5ɛMASùffk]z~n :5tk[Ξ=K$EEIkww0lÆaYa^qgrd|رѱ >xQjQxkhȋIb2Sl$lG5\wAH==ݢW*[ZZlpEPS2n߾ Ңuݺu7n|B8 uA?PX &d<۷nݸq_jAqcGa8C.w5Sp |_Kt)B . AQ|{g,ffQ,&1Ğh!=|WW'><?8[bul׷Ұ/g-료P?6 +tB>o&HSSSee% ދxqFud801l=i zng49Qָrڛv0Fu(^u]f~L (OxlĪ@4Z#q,j /ܨV XR~:ZWo>}% pjsVvMFv*|RD8*ez j8# gܾ}5/ 0nll#ǃBϊϟ?{U骗`̙۷?Ϡ:tA8B!;^AzFd79/!V[FPG^CMZ+p6獇d ?!?L IۇDs68>{'r7wv57XI32 '{쉣@@UUU1/_ SSdd؟Q9ldwcװ!,˝T猩(Gﯽ9=CWZ>ơ7#2[Zꔙ+C%"&<N5/@,)ZoiĖԵ_H _v^^X{K>g\6`xP:TPP`22F?C6;wW(CLa.8ګ72,NF%|Mt+MV=mDfCZlZi: /W}Vmo:])j3{LJ jY4 'g`&eee\3b QX1<|ٌKJJ~A\Ѹ+**%qP{{;Y@Pt[ZZ=G_pE)ThUlxmr;`Ww*GO}ˮZ:CjB43PDQQʜƶ;X-rScƹ;JN:cTam1KW$W\/jX#'xg뒯5?7\!Ogk9خznKr.q_wM y*΀PX;r_ZlL$4~|7yƑHMjo3gHõ81JB$SqD$`7n3 QVVVvvvJJʑ#G `:%-(^Xoo/;Ώ3a܇%ibF8 /B =$V^iMg0{N]og#(ZopL#Fnu_gëUc}8JԱGߖh^:a-0ڈ3y \#A! ItQAIzX4ĪXK;/Jڮtz-t m/O-yZ9i"OU#@|s_w{3wyh9lP|' nQ UxΘHe˖g"U L+B ]a Hp0ैX#Rn J(R+?[xxq0h,2dB@}^&Eӱ}Y/__nof͚W^y3$j0C'P*@*\:K/\u5Ci?yu Z"FJ{{;c ^|% W(ijeP]JNhX5!Ygj7ߖ갘lo9c@r$E}j\ Mss3Gx {MR8#;K!붖Z0LX~N]5X% g ! qT<`)0ئ EV] f|"QTàD[꟠mB0m DgD3F ۭCZ-؟2&"0<=p ANABc8V2=[C<8CMl=0N[0|גp3(uMx0S*EEE;k4]#mܹs d2( Ekk+fؿ~? >|hSS!9.=@oCfQ ?b8 { ]7҃ۉqhهIO:sւKMC!a$&Y,mGep@8#-D7ڃ^Kd&Lv^xmvO7~q/I8Sʢߟlw"Tg#{F $/-q m C;$ic^{p0<F0]p"9cWiiiTT`k׮ݷoOrۖuձii/AEÅIs2}.vGi,cb!堯YE&Ʀ':$+&(<8w=qDFFB@Px >}p3ltryrrѣG:d S.r: ko/, z}C^~v]B>TiQO]-NR#JT娊KUR+ԁ;N9 $p0=<8*ԋJK!83v,'ǔWT) "T;qI-r cw Ib~u?st&0Ɉ5`M3tD~2==M%{.Rd P^R6o#ICݥq @m׼RZCo1cc̯U!NJCg$d /!dffT@\Q1vrrr`_ -@.trI7z}25^T533"}1 (ԁKoλ[i.J;H鰟llIۇPj*rbVo)eV`8 7nܠ ۹C}$ֲ!q} )9`'???uΣHX,Ō8Z %_8΀>@O?+WaK簼aΜ < .$''>|xE@*%85ưV3o풜8 c4! $DOO(BzL&;uTNNΙ3g`@?B ͛7}f͚Q^EFFnٲ_աQQQ!9Q 6g ,~׿H/3Kc{R͗.]iV2B!=IUa@֘LQW_}{pM6}'[$jgP':::ZZZ***- N-%r> _S}:o?&Յsտ08ň]K@(/^H ck'a6(wJJJ^^JԤHŷބk]kVVVFFƉ'I XyŌ` SX<,.oijI;dP Sh#=w EN R9,AD ̤酆GFF0hZ^ /_ CRKHEj<܋bd ,\hU-&_$FZoA>s d Y0Ux6hsttfAKoA|e%:\{sj )ͅ5~R,,,ER{^ZZ`hx'ݻz%pw`>X}!O@4XǮ[/^=?nYP.rjۊlI](ڠ PGr+%oetȖ =CN.s9pcX&,mE}[-={S7DTfF;D%դ>lhZ A BQء3;N5~y?%>?z$ϐuP!Dڶ(V1'MciEO&Q?5/_CQHa,J@9RfV5XhI<}gY̧"%ߠO}֨/&%j$'璟ԳUg֧vZPji}?.b@P) CT"|hxj.`/ zgw 8c߾}-N$I~REEEww7 x}O&ixxnᘝBr* Ws䬬Λ7o7@6^Y7TS]/8<-ܟ2H0p&HSjU'M2}Ӵ:ǔ %*_A',3E^߷l:v40 O?jOtˌpVC!8 X3&`⌘88Ǝ+kk%g7J.RvSNF]/,n}q0l$:;;A?>}S{~Ⴙ_)* MAH(""2Px& "5ٳgB؁s8|0jUV 0}CqgUUܜbIQGg x `1;; %pO4 D\vX8)dF]Sm;sViiqT%Z+"5S7W^dN s"Q):@!MQQ@9FSj۷/11f >wKc=>^a3!У3݄U HRSSO>&xq2-F 2/ KTE+ @ uvAa*5EDDa0Vg Q& .\rknw 덹a=Wj̷QGnp^G7\Jy?O T&eUp;G L2>|LYkv=O8[Lu9Wt)$Zw rxZx7{a=WɞqC*F]v53~U4CH{m/{.;Tw \px^1"y#n {RFGe-{ !fv'X. \q Rs!C  ."p0A Ӕɉ/n?Lx␢a|Mʇ+r}KaƄ_rU.Eѥ%Ws(K7| ׽T[6p@.ΐVCyԨ8_ת&D&*qMQhzvuEU{L4 d<~$. E [6D^2<<sIF4#lI*Ϝ0=;KF01 !,0"TSSCM=_n/pk;Kvj>76p3ƚ{m:ڟ?h"PvR&0Nr f8#d|#є^ *# /dZuf̀uzT@q /kn:3X x\A1CеkR&~I<^c&%P:Bu6 w_5l}Y} *`-ECKszF{g8K\,.fy:noNu)0-5RO$+bn_ Վ3T8zjqY@8" k $%%6<;˽$ZC{G&h%S2)++ kC[dd={3"l?޻݇O]'JtSJawB" FcZ #iMfms'jbۢ}650jJWp" 8goK^xA*.蟡﷨:Y@++sz`sXuL=璚*S8S=.G* xBgvp(ճj}|qFI?gTǗ'~5@߁|}xxqٕg@}` p{=<<̉3|(HhcԹ=Jp,[3궙w 0z\ @!wQ{-׽8ROg=}cJh8çK݌.}P^ ֳ!Cg7ygAZ zWazKC>X]AQ꓂2ɓ'j/nK{vg+19{d(EfלNH=Ɏ=)}fmxvVgؐ]xn| 8í 6Zvs[ 9\-0A0)jtSD8^o?͛0qCMh 3g_j5NcNʎ4ʩM;iR07;~5f>|{RqF׵IXHRx >É?f[P>I߇gtuuX @rD8~i>;;<g 7 !LfPpګ-ãΙm 8W_""^I1H{n^p_&:R-;hHqNGK8CnRl4deM6};|D"=zh\\|ޣb8m,5lٕv61=Gs*\Kov9٩xetbaӰtV!g.> j3oRlAYBbwA-_-`g^rgN0g3qFvBm-`j;V9噰$Xs{jvG]{jGju/kBhz%Z7~+N7p L  Q\ A@=QnQKg$ECIh v,F^g8u8:Q8$><\/玼L5b7p6A.c \3H=@2 m{Cvc#rv:ԃ/PJ8Zgck$WEki9I*pnB6s9h4z/!qPr]8n GT2&?]:f[eV21e$* θa)hA=8Fׯ߼y3@ݻw8pСCb>EG-_+5-IzzzBB̫Ht*%L`tdftB<ܙum@IeUٰeI}g#]ץ wh*P1lOˈO3.\@'=πk7YTLjzKc^,m0{M"\8&k$,$9 Eި Cb "TDԛ&'qFυx`EIb-0:m#A<0y׸Wp((r+ KF(,mP |93 |&[^OeRHreCbu򴀔H_{ 1?p|ܹ3WHvo\GΡ$޽H]1v'fq ˼8YoB3^F@nazvs a@L裏>3-"##%ILLLtt4|"̵lYiRZ[xBqUb]ӅSy"XgPq!h>7 Snb2 97 dXl-L.Eٽ8gM?#*R"Y{Cv+p`gXZ]v8_gZpvzvpP/y |.hVZu67+Np-5=v3\_"9f_}JSD܅`\#xWO_)5scWSZ!N;k7=\! 0{"W8 >~ "=X]Xgpq6>m۶g|ᇤF!%'+kEM`ty42UUcls=K75p}v+zpA)&g΍TWi^lkjgKWK533@ &!}p0M}}ùsVxZ-MM5æ6jk£_=lh¡ţ<#xIj};kvS)y-)o!E $@cIj"("M~)dvi׈ q19:@ #, xp 〉aij3VgAHh*AsU AW7_7  0` 5pqqmtwwhյjqhjW-= ( ^"7svvK5x@i8gķ??o>әy͝Zx+x,`x?_S=jB-Q6ё`ӳsn,2݆ 2qhDD$ MI'3l&oÌ7J\N3j,ԚPn i9o$3{)xۗ #}~Gz3ƚzEXɹsZ4aoݸq֭9Zs6<<!DBu$k|#xʓ ndj}OݮL׌GWكGϹyMX%,cK׵R͑#N5YZ;;}fjq&LƗ+?TQKD׋z;蠟 ȱ@ * IIIΝ880UZ;}g~~>t8M A a:̘?Hp8OP{{;x_jԨjy}̙/ԠMZCBB>ӝ;wT2j3ҸRp`Zd?[n7755u5WWV*J ++$8ex0b1 :@J@B 4%B1ŢyPD }٨yɝgt{Y0Y7]]ŴcXK#GDZGsJocFaݳfpvFSZ1Uh+ﭺc `;\%wKGHW}w&]ٹz]&@?} {8L|a 5`T+5(]P84(0LC%`|UMhji.8eѢ R9mцy2 jȨ )@4S,h04yBcv۶=-- g+UrlaaiRRT(**fx5JT.7#sNCX`@՝ >ԡx^P}"+)i؝oNҌ'j1Uג-_]tb)X ֛8c؟Q9ldwcװ!,˝T猩(Gﭞ5\`R4.+R3P3> j'3K0,3FPj8N{TTT,p8pc:PZ9`E9PH$5z"٭3)&AWQHvQ\ke8 ={ 2ɕ%jŀKP5~Y2L4-Zyȣ ht"2J$&JCEڍ7:"DSn޻lR%hKD"`V%ů%m]3I-(pBg74 Z13fff庺߆CjPW455@* (5FFF^`~>qС΀ UYHӧO'''F%&(,,j[ZzF[]]W^^]RU*Yǎ%eees V̏l).! -;EɁyσ^}ިۀɅ_;xBcVۢ+HY3l/aV,g@ገȮ֜ZbX4V/ֱZSMqJ]p{i%.Z8Z'| Vgm۶7߄qqzB h nga0:M 8J ެgC+&V7e.ʻ,9[ Ha0ȁ.ìA :6)EO2SfK$&RC^k|@ 4c |"D*[QkxXW~3HA ,wy73 gkk+*sryKryvV<+==[&KgHNe I|U4 @?oMJDR8cGqi͸D˻ӟa[q!,w)8#""2{ 8\Xq.Ρ=S?U#M @<_Ɉ~w[g*㌣G QpC 2" 8m Ή3|($dZvy8d,Aꂽ/a? p3S) Y=oB+qXkgo WWÄ́f^?K*BD8CF܈"N)f26 Aޕl8Π"}̰Z#U˄p^LB:::Z[[2ʒ|IIIjp4D"IHH h\*:tࢶݳgΝ[nݴi{g%΀q…styyEgϜ)*..+HONK {c(>~]qx8_:Xy?` 5HHx}juQQB@8#&&O/D VY `Sr.\t|{{ׅ F6تu]hm8p<%ߔ=gL폭ZZ~i}Ļy`t;[ xG%g%%7Gg8Mr4554@qq1/;B`0f$[|൫ I mtN졦g.hu+,/V  hIUUUIIINNNzzzBBBll, 4M6lغnG7غ+?/³Ͼmx樨7~p8G۶ /9`|?Ap*rJ_INk0/ /9ز"h4=05Ȁh i:@r>P , g\v-4tڵ ?[믿f[k;1|qϞ,~_zAgD[ťcLT4GEq8Rp@x@"I $/g,5YB L'Ud:?uTRR Qj}ĉ]/x3Zgd]=^c=Zm;\6td]&P0R>q!3zpeE;\D AZOHӉV...NNNhjPLo/N\2G=y3MgeeuttƒjHJJx1f5RMum,qC$|`!^s.`vw_|pƒ2˂V Q[[ clnnnjjQ?1 6>r+3+ vgyO'$[7̈́Ua1z#&&tB+kk%g7J.RvSNj:E'  0RAS0tFEEpP32勏)*ʞXΰx: 'z u1Jhpiͫ2g\mz_qF@ MgϙQEbL+r :Έq:2qi f{=Z5ec#].j1X$=,t*Z677ΰy 0h #)JJJݿ8+q5(ZepKʇ3t`0 _ j>C R\4%444TVV@h.H~C5Hi}pn!WWn☞O8lvu)bÿ 8cQ8cGqi͸D˻ӟM.aCK\k))A q ubMNiDT5.}wZ RnNI6z"4(TVQсaV@@ NA!JGVU`@LjpaC076a0Vg199ykkM{}2nKC555&E ){8TO`&̓f,(Lkv@.UmS. i\ )S9ɣ i4([` 0677WTTp ]j05POR̭fRo2TDVwd45/<~b*$J٢/y2Hi@!RVnQ86:ǡMMxBdFI- 兇:rV%ů%ڙ8ن2+ -HuvhAVjPHӃTUUB!48464bXǀ=WmL;N58< G֞rl.5`1D< KYޚS5]pkJe:V<|vj9nHntqX] ol<. JX $"Zrn>z^ 8&=8c3?a ffq@X+_+ c@$CӳJšۭ&x5"g TvOS0q0'ۤb\' _Oz/ӚiMfpאt)@>mpfkܔ= <>>,8clyd#vAu@8:ԄBhdgg䓍7'?ܰal޼944??~xAAA[[ 5_,'xEJ٦n2up}䒇3tWww[6,HD~2AS'6wcx~~)8#&&2{ 8\Xq.Ρ=S?UrŃF (@ " :F0(|"!'X΀A Fg@}Yj=QHzn i{O ,' '3i˼]:<ıTfoGET 1ۮMucLq-kSx.&c >]1D+,qWx =t.H~?MΠcd``PxWWGd4QQQصk@-[D PuV8cǎݻwCp\Sj8^I]3ooWms߾i6W)x<8)/9BX0EWXʭ{F_Ku/밸>,s)vE%ZcXcf,(krjNZFuc< %%q&Qn%>P˜B8PS{A 1 8cq8Ʋk׮@ *sj*6oܪT(s pZ>J) Y=zQX| Tv RAQ ͂1˜z',RxmW&{[ze IOT2q?ga8L&b~Â3|ll|l85h >/iEEEAAӧ= o.;wϷysaݻw:|D"Ap'7x|rt[wl+?)6E;G" /;Sc+,ᥖ=E0L#TpFw'PAu۩})4v7G T !C& &!E! Ӯ%=d6@wRqkpTv2_^э+jݤ r-,F"lʰ %4-m 1d!0XΠ+Wn Ԡ:mС@d:1FuuuII Ri$&&h ` >/ bT/Jӡ)puxs?okٓem L:w6 @Bb+ h)wbz,~^$-D7ڃ^Kd&Lv^xmSbȼ3.-9 zE %gXS=8#3G sV) =>3AZ%$oa:PgAR@xr8ψ.Km >hnqHIZlg eci}7w):>ypd|i2\xpEK|SMJăºgj@pV]CSW[)8\Tap8R|.pG[P§^ 0{W'>]Y@ᅴUOaL Z8r5 j @L W(r<55@CNxZ"H%(B:~ZOΨnI/OGfiW\\pUg+2JK*GnyUZcכ^͜|޾sXybEd0ْ˳G6}v0+;^*|!FՀ b:0AMMM>B5"l};B`0f,!=ސaarBqJtX!=V!VT94K122JzE^ĪP™3grrr22NS(K);NVV.R7 YƏ tEg~359D]UUUuuբpF^ޙݲc|bj> }aS)ǵfvˁs9{ +;XL vJ3KkGm_t|zvP0R>q3(I|O lk5jZc&ErMbUkX|-L/핝g[od-p>=EWz.I[>K Jk3_ԂUg(&׏'c&(ߋa'.c>y!3z4qp,,j A===]]]:GccV%?Id=HH?.`}DdqFyzK(n4i{$-祯lLwšϐ<..́ =jFXD +> p$n69Z1?؞k>Q??Z3F5X\'ڏN`rqF?OL](nIIIUUJ) Eg"08Eb@F ./t =uS<o":A稯 h-FrN_g MbO:^TT?$?|`@qQN]%1z;rXz0H< Cw"NCJj:v{.y5n4Mg%fwv/ gH"T3(h.*oHi;z=@ x+~m㌧FwW3e7mܗYD7nTSu?~_Ђ7}TuHⶢY<"H!YPNa`5+ 83vd֌K*O=$>䳮uQvd~x`kxuAaQQt9x!Nb~Muev2 c2ܲ~V/\Rz]3Zu}%&&fffswС$* %s`2faOuvi .6rs>uV s>8cܲ*RqEp\bI޹ XA5hK^/UAs>j R5@;_ l}3X4ɑ.#郔1XHҾbN>z2T#0EWXʭ{F_Ku/밸>,svxӞ7"2C ־ٵy$[])U [̮ rૃTfP=PAJ8cB[N zoiH1;YpZt]}@u6gH0*<Y1IN`Yar->dxl[Az- :BzhcP?tdž€p7?X] ˡj[گ<b58c%q]nڞ|3}rS 3 K *fggoc اhKr`PÖ `3XTwp ]HA>;w 4g,g,[TD6!.H^OTb>F ͂1л܇ vKtO1B^l!Z d"?R3"F̆Htl! nJ@+5T%oknn~p_+O!v̫[,ttmâM..n<{׭[h $#TNQ S:17D$⋉q7q!H! S u 8cY+J0•?(8:Okq/ Q؝v-!#P*оC_5t"Xj;v}(̌3Zm0Y,RKIAp9?s*Â*u-nŨ7~X B 3jP@5v `;I2sr\r_2q#tN;.Y:.ٹCM8H1/+Nnt!H ƒg`{7X!C%aqqFdԮE 1A6BC@RgX& 7wx'8y>{V,@S@]@\<-TL15 8R &HjFΡ܉WC%a~ $#!ZHz-|y<4pn3qaA3][8aW֥(fƼƗgܺ}nJA Jw?O}3IܿIȘf_n%ǀXn+<AكI6i=3ۈxҋXԷt 24)sU^ANI[Ջw~_BRB<ϵkDĢp R|yg-:ѺӀ5)eϝ̞p1M\xW8Z޻;3?\8K+7f6Vj ֡P5v oiE!='ca3Ȉ}sM'|҅џQf(C?q= "t؉X5554Q\\,Hhg,[b!0E-[~  IP[Ko3l`cE-+`B 68hڵ}ƚG?7omC>bNئlSp M GFF[>z?<*<{nC'؃3rͲ5 y3aUX],nj{q pi1Z56Y_~ d;F3Sƻز"zh4VZp:{u'u VA˶g5f|?i. 5|hLPƸ/԰@!//݄IPVVr"aO;#c#o#֩Q۔e 6΀br8ukR+ȉ/t窲O(x{Oԏ'iƏLjDy}o ׉cܼD!3p!-~u^r.D׈PLV;zcd=+ʗgɶ,UOܑQ lCf7аuh+΀7@Ǝ7 _ەkcįqk4. +bJ=^c-,W+('ب _h=3/%-yX˰z8cw@FSe-QXL'n1ɲ-n" 9!У3A~bX& uTe Mkoo/..NH8N]Cq&΀)**jۖ憲w߻;m^ũJ5ڊ1.$3ԎzɡzzC,rFee{B*)C=ZjrкL2;z hc 8#999++040%$cVM/T׆0O #.9\RҥK0088X\\ pnypX=@jzpƒhj۴6ۤ}l231-O~3ϲ4x : 8>8-/%<ܺ2xL[tCkO96bn"腥Md`qIMEO5ŪFStG\r uuu+ 8p pMڸ6s, j0j ?*veZCG}3_??"Y ,Bx#3HC}93& mi7:7f כ8t !bD [˽8c>5k`je_7/% 24H6$,]a*->m~-ձ`ZLΥMbbb Nf5٦M޺3>o`ƹgJXB5b,R3;usAf}L1<ݯn6v݃oe7c783tz_Kbw4H^SQQbo,J\BV_d}xqXQ6 5q@8#j#2;Ԅ#tVWV4zoa簖z5%59T:pj {B]'dd3d4DaT= mFWg Tޱ8`)w^xv>ajc ʨ]yJ8Q҅D=I\/Fc V\dE6f.m,j /gd&ڤ5 +GSl#J/b#XRֵ_j5NcNʎ4ʩM;iR07`$D0ZGtB`)aY,rheI@K]]]0HIԍ 8xpquW\#|/p>Ht' Jܮ[BETg)D8C̓3v(&׏'c&(ߋa'.cḲ8#;K!붖Z0LX~N]5X%ս}FTmv22XBrhNŠ3]ˬ0E5*"Ƒhpep]ĀNT rMe w& & LJ=H3a0 e1L&V0*pB,NŨ'qOP;nrAC>n.8Ro'H9 5 >{k5UOInț b25u`vLjzKzALff6E(v;So+WX`N$ȫ {deOH$<a\% (CmA0,r 4c&TCKLb$7Q5ό3ۢA$z<8vƒц? N1Ql|8cUc`zLey8g+&I5o>Q^[ubX&7/gDEJRd;koX)p`gXZ]v8_gZo/G0"CE‹)F.Z`!B1@iQ^ v Z6ˍ;n:x\>8&5_ѕm5g7Yn lb$q.$@p`=y25*kɖb:qImvgK/Fk\H-$> /HA0 YMd7rC5 ~ΰ4*-nX|bw_<qE2lJ: 2q!BL.s|晠Z 5N&Zal1Fvs=](m1vji&8ȍ7q?Ǐ_pۧw е cL_mD XR lbZ=,H 38C19yQdD쥶y9QXdkL %nNcΐ\I<u$utwX,nNs?gl*Wfby0J݅hk1PcPy4ǥK0F jt"4l)VVV Яbic>0|gbQqF:Xz 9D")"sHgOpxx]vc=E7YR̤P\hTsD`~8zGضmKJJ}e%9%D etx 1db`?\!JSyxIIK%ʹB֛\_d1K$ΈN,GT4*K/><DC CCC7olii:}t~~>A9sl\7Nsc7ѝv&k~q鵼91E+k9wo}`WM)%g&?kƄQ$Jm=)8ci1K9cQѨ^\pHTXԛThUzx&H9c5r- L^Of>*MJ]0H`xÆ8cGh-V&K:Wsϲ Π1C]b[!@ ёA5ɷr[:`H,5fI"g<(* DL&^[^΀:^>7:PbNS[[;tv1Ãս̈́ƆǏ4XNa00:}۬zX+z% A41hl\(uNM08 E58KΘX*j,6Qp(P1hRz@@6m|&x͗w /S Π27gl46ع\u膳S.TЍJ%jlĘ%)8chTV3@Ο?o>w9Ciy hMΞU:+o < 8֡OTKt#QLY&yY8ȣU5.Z٭._'4FL' vBRܔ33xx41GY8` P! sqƶ|LJo>0yʿvw큛eBS~V-9Fng.=SMd.HFe9#٫iL1000G^xU&p)aXFlut|)z"" ިJ<~ Q9{`#dʁN΀Zhcn x&_}=` P! sqƖMk6ޑ7)mۧۛo Ro޼ l_(mڌC&7)c CކŁ,6=m&* #HXR̒8#)*v e,96ЈgMM3ϼ¯fĦ/cM 8rqoaIi`dvy)&KY2g$GEr!ΐR!g E#kJi6Z;">\bқo_ygaմa\q6ؔڀcgr} 3̣[7fZX&gg !}Ac&n Z}}3,#* $gwxl9ȓٸ$-_=wO\ _w]'iN=>o8i|J EC];m?^3 =8hɽ/߯F.ܫ$ip_A|[S1KXp:?я~~f*t":eA9c9cbc~j7KdwXO8)qs#݆3󛄢͚߄ AsPsÛX"oNl<s @B ,rvݱ#W ^sN'MF\Y۶nt)וhOwA~+깏y_a\ YR`+Tœq %b|aL)+ܿ" ##?8ٲu)akn,O@mU3vG݉ql79˫kۭ0 Y!nn:n nnkM|-DRVG>mBGKzqFC1m<&u=d(` P! sq;e-_~jqgƁ1ә194ۀ3W_^?P޶|pnz1o~π3zOgç.|k~FIku G/{O~`v .d44AO;:w3z[RNi?YI?V׽Iι?s֯ܓQxߺ!oTBm~&mrl %cw7ވJl9wEo2g˷ \v-o5\GGQ~4g$Щq`mpc|G59*hfyTh3& brRg2ɤϤ;9idOٚͦo]"mDcz# ! ! QH+-1}xvY& 1l3^+H^9/x-S-n 4[qcՏؔz;Ҧg;|ͷV@6喔(1t&G7vnT2G-V99󌍖( VgV(2=$*X2f/U ]xr5?dy!x,zCG)u= @B WH^6eU繽-~sl3DRɳ>keReBS~V-9Fn'.hҭn7hƉ)kĜat: JJg9*2V)$:5$2Àq LJ i :*T6dG5 *ה)'Rf#HdZM1:`mx<T(g6XW122h&)u= @B ⌗'oGz_,~`3{.bWړ *ssƆ21m?_xr{kӆo021yFHF3`uXטl^ Ʋ=EN#ds=ŋp6Vo2w@劬Eθ@d2>T(g,6&"p,6Ҋ3 b0gdO^<;!&g<Ÿd2:sܳ'ooGmbRX$7oѲ/nMĨ!1HX|dz7qX?{܉ ic q )TqvUEeL|w)6g?~߾}/ *=gL|(icNd^v@@6-p^?7nk]#̺%s5g8@xhw :xQQ4g$t36ځ2JI%V+3 "^2L#gD!qeS""1H)Fέ^1>>N3Έje Ïh>T (` P! <6>63h̨9cc)pFqOd_96Yor:ܟ5V_&X:P5oѲ98C"3|ҙ R3Ì'$|&BI8WMxS`HU33 q𪒦LgB9c9#Yn, m5gr&_E1p 'xŹFk%z6ԾK5h}6V\߹8@'{Z||Dp^E (B.iDxCYDJ|>l,3R쟑& \kI[FRpN#0:|zi*3nݚCC`zroKGѶ (*zmvc2oڶD ܝ9Ã&iP">[F{I*R"H K 0 V&MjZB=0RZRӮb/ j`i T(g<޴& 䌥Ō"Mku,,dBcY։wEwzAdwXO8)qs#݆݋h~}SdbE|DH6[Ҿ E4{f$HD9]Hrɴ0Ը +`g \vƌ&=jg/&/m>RPk .3U:l:J&ѩ4{fZcuRO ު'P2tNk7pffPedb/ۢWŹF>3G|9lYJsg7*&?# BYmlݬb8o*_юce~~ K5 *=g, ØX(j<*X-##g㌥Ōw+:X,Ԙ,%V{;X4WD"\ z3l(MklN-bg)C ѫ)$`WTޏ7xXLx2xCgVf*dSh ?ɦc}ˮ~Uߐx Z݄`r4 `~Щ555++3!.61w '4~F Xr̨XD7u&So=zp?[:`rI~ͣNePA7*Jȣ6HT(gPe&gLLLhoo&bԊåp 'pg]Z+ XQi&2g1 FnO}B[巴P.X7Z582KBý[\|]K NO9 -ጱz3e1<< w ?+i߄ƌ361|GhoC{[О6T|Yd~%`]!n1 +MAhPxGW_+53n=;Æ ^Mh̨9cI 㠃==[Ⱦ}zۆ-&EMu" Eg1Jf~OԒ0{:밨KEx%//ŋ3<ޜ.ݼy˵ǎ+((?;vlpO2P&&&߷[uW=ucTe}>C38~kc-݂@%~T=ɷn*5g,-+Q i 1Ct6sg URҟ)3 8[r BӨRL.>"0Qj6CyUf&CϔGAjXcrI#ŞJܔZtx_Rh2(3,۔llqHhPΠrT*vZss3ؓ'O+kQQkCJz}D򷃻b*MJ]0H`xBONR9rƂ8cc)pFqOd_96Yor:ܟ5Vf7Yj V1*/јlP^ ƲEc#`0"Nj0Z zчrlD g$׌(-)CtQRdzǸh79Rt٢l N'ƒaNWP.!>3T0r,8ɤ(gPy8.N8AA׃y|E@R^jvpn=&pVnRCߠcB<Π %z6ԾK5h}6K*qJ6 Ăww$a8uURwK"&*QQشQ}1QMkJV1=!;xQx|dOQGyY/o&"att u,v%_!0n2{oSD{SjEzx8)gPy8CҀ7ohooo[K W w 'pgɌǟ3s Π0qgh@CO(~5v^%@MsF?c1Xf9 + ۗG<&D#K8eΈqFrON >(^n4Gٌp0}`BtWgvg<+09Z]$I^E9Js:n@gt Ĭ +녫k;Z _ 4H)u*ˁ (ힸœAdwXO8)qs#݆݋%`E f31Jg9)TfkqI3'rNxS`ݜfN}iQVlc1Ψ;3ʉ;* IpmJ¸-*E,΂8^q&4@ 8 @jP0o]"],\5\;p7d3q+jJAZZ+OJ}Θ> .Y#ȳ>g87 pyO!m n H`t9V*y>M50H+?3]Kjp$ ^K@^dth4g%噟?a%tXb,/lF2nedmfK!-!TlQZ`gHfiΈx*4&3|Jk՚ZyE4TzOq*&bFrg߬7N7׃v_]dc 'blp V3V)$$V>"J :@/g;MuFAesG.䟱XH*-d Aȴ0rò.M //M9(i8ɪ@)/E~pZ =n-b5㱒rJgQ9g}/^W9Ł?cg1Ac.ExDϧ[BG LJ3d7Yj,xMj.vzw)1|7bl$6hE*$&=:CJPQNG1pQc-v^CC 7 9BerFgP36{S_.נN2aXg,1ֻ4cTd!OV.yyy/^ *k3d7>CC`zroKGѶ (*zm1v%z2q/E'Q[/sf4,/j$"Cojxh(39uUw^m+2LkgZ$++ І㡚 *+i㟁˹97ɮeqSF 7ѻK485JHnuEřRt)IT5&A erZ V7zqE`%c3.8p"&IQArǖ3d7k, _N-}ֿbɏHB{PVۢ&K"<ՙW㥪Mn0't!̨![ijLP/4gf^gVYܬ(668MiDxvPW`7/ k^Sq!B9ʚ_9#:!OW$# T@\ 3gf[v^CC 7 #HrBE,jŢbjܬkKAFYi4JQb Vg,4/XA8 js6|閥 2L.9(cBܞsoiϡ\72v|=Ӝ#|6[nC38~kc-݂@%~T=\8)3hN9(gPYAY ;UEu2g8'3d7<6vv)óJ0kMx%~xK]H&9 DXU/2PXQdjyv+Z!UK^#&f7!% xX5!CCH_=}}GdɁpVYLZiN9(gPYac߁.Ȍ_!y`WI]x|D(%kjŭ߆ 8TY,xu> #CG-D=Ȍ2șb(ղJgc('08xوh3UssFg536{S_.נN2aڈ 6=AGg|3hN9(gPYۣ>\3>._Q01Dd A}fŸ.pC|">b:z3#Lg#|dڡ0UaD.p J'Urg{p<Г{_:ʿ_]@WI9PΠP9#m3mXXDb05FFY:Dn UNnql7Yg,1 ݦ7 w3n CʐfJ9'MD1bƖq?\* ^gxfqYK_$:9B9 ;Ms rrER.36yxIIK%ʹB֛\_d9eȽV;!r*{ TQ ǂ'8C438q4>/d*ǴZ8 bq_!^ёuj*3V&uHӫ3)>HqF7"By&k^gR.3J{7vՄ2 \xffLELBqr$F'87-fr(SgDj*t<"ΈKp'̗K^hFLNzDL>8cAv%}pdΘяPPF)jHepЫ$Sj2x!T?W2Z{9#088OPJr$HMRlλBD(T:(̷#BdȻRC<y)Ԑڝ䟱z {]ϋSivXcW FL9^Ϫ90romE[ď9o %b ~T栚 -3dԨosYkq>HK_Yt 2x)CΌCII6݄ʸz&K3B{ ʱяz79׵p-&T栚 -Vli?ck"8(7ã0_~"Ve?w[r pא? TsP!kR?c><~"g{p<Г{_:ʿ_]@WIZԲg[+?a-ߨ؎t. e-&宣B5:ZUH *ssƦyjv) ïOo@]H߉vdPA:Z(g9ca_^__n (yh qt}=hM ThI" 7g>ss9U{-{<%殣 %//ŋTsP1?n,/0D+ 4^wke݁ǔPdg.=$2:E*O'eҙi4m3OܜMO^*}ԶPb&K]Zet?F48I.UxHQtM&+* E>#HVV  C9 匹m*(o=-AxY`~Da=,8F i:}B9IJJ䟱,>CgDq˜Qڻ>&a35c(b5'g,%wd$PƊX|q2/kb4"nћJe)qd}}}3PΘM%艣 TT*` )O TTPԏ?vُҎA2"8%jV F/6<[|jRFIKf^1+g,0}9v:3+3.Wͷ~C#(kQva !V arEs|D(L>Q?',&(za_G2qRΠB9#4dhh KEEŕg;:A4ރM[ifAȫN5@J OZO3T:=~bjtid)>ҝp3O,6?[:`rI~ͣNePA7*Jȣ ?Х#ӎ6VGPZ50x"s}x ۩WMNKBmvHKÕ.'gttt$GArF*6J{&1YGТ B#hבYTƍA Ier1bg2dlPLͬ 0D}YNkfx>SlpG׬RjעRj=336N%59>S/9N-, ]/sFc*hԇ+U~K{庑k-Yܺ%D?#_#3}%Yt,ԣ9PX(l>=vlX)tK8w<+XĢP*,l'A5IvcgF7 } lPqi㟱TD%jL(d^mq$bR&,nd*F/Ls;n%Z7: ꘾\1P4 Ynom@{PMTF&fuȥ4i#J ^CJ %gzE`_:ty9PΘZZ:{cc&Ӝnf/:Y?8&M TIKGb~ȉB.d?HqǣQx0g,ndD@ ^^D$$l: )"#eΈ,-b1ώF'qǬXAE9/I 㠃==[Ⱦ}zۆ-&M.6Dq`h ThYo4 ʉ_'  KtZ|l{JXS Jdz]+سPDe,6Ϧ{W:g,'QiU*S4 D"384D+:ꄙ.rOv,g8@xhw :xQQb8殣r5>Z&Ge^/6hx :tD?a݉2`=Q 24uW~D֛/r]+]G5 *tPY"g}(F|2Q3 Ë"@6D֤ѱ =8hɽ/߯F.ܫ$MPA5:Z,3)~}E!x3 _#jLGZN|~_9%nnDp~r9BeqƂ&'qnԲg[+?a-ߨ؎t. e-nBj9Be6'qnT|3Yn9~7ta h ; *TsPMOܜ/ίo~&a35c(btAn8´1 \;쯧T(gPE&v SFQg4j*08xkEQQEc{S5#˙R39$W;91^4r{*=r؍gBa.8nRF׵RAihwT`Ԑ@SpƋ"8Q4=3R/e=NNM`^؜ő͗cw&N):fYqY :4"Y,!<}0"2̫yJoR4 Ynom@{PMTF&&? xΰRΠB9 - <"-rD3dтbA1f7˭4 N.rƒ8caysr#1㡒{"40<鎉K†}5քALr,B5CJ O|hL#kM<.'.l!ۛQn&?s~nB׵RA$ΐ׵uIMq3L2g^\`hV΀h-138N)q} 8#m3rrǫ!Bhp ]GɊ#Cm8YN8qrΈK&h,DWab8pbcT.k.3 C} gqZJ{32T(gPe9fE!2'FA|G:ggxoD覈ZP}?*kGrkGӡ]NyqC$W[`"雮5J귛l,B:+F?"M@ֵиTBG i㟑rdPz#jEU7\[{dיOO? g_LI;&38,KJ4sq>nppbCC`zroKGѶ (*zB5:Z<H ʊí7ZnQt1q4]v,# (%g&+N,:e6^:m7ql^N@855n.j-eMk;'r~Y~甸nMnTBG 5 ]zlfCuaTCIcbu|c} NT1#~F>!&1e*h\d:BosY}=˩eV<[̿Q1mӅڨ݄ Th8#g:u_C؉!v 2oڶD ]M!>HXoղ;-f. R>mw!}'ᢜAj*tPYc In"&c7 ׯo*ص{8>Fvþ:ֿQLF;FznBj*kwcظ0qeN83l nŦ_ƓtغYk;Su{<{Wv+}ZӜA3ruՁ'r^8ne7F_465|u|s˅ =^?[:`rI~ͣNePA7*JȣF9 *kcgUjSE]]U)ɸ|LL96OݤT:n's&q5Mq`T( hpH`T&i5O ]fNp^'EkcΐCF$m3$:f57Ҷв+KmΉ{k3PA#>\[s(׍8_kqPAed53e9KopkYA {MJcc"2MVkpk#ׅ–DM'5(/(k +fO(=essu%GK+C38~kc-݂@%~TM9 *kwcX皢?} &6l2ŒeBR,zۨRlmĸJNK#'WTjkk;un潭/t`6Θg{L^|}ZNu2 \FBmckV)$f*5d rĜat: dZg9#[YdU[ >J9-RB _sQf̣slzUrY?8#dh븧79pZPΠBG+GZpƗյU0ϝ;wۑۗ8/8#k %Z˻ xjdY+z9p8bb0{P}A R2 VU/qds`hP: E кYց2}KҳYVZ:iq≭ GLo\F8cC=ޯ <~} D{!c TBGc*YK.wx~C;_|ŧ~/Ne̟3 ^4 q-#bT*tV+^ѬsH=.|>vQܱ5_`NY2Ia$ӉMzhf%˘9O+|@9g^4 X>u}I&TBG GiX+; e`r:6thnwrT#֐ #X]yNBgH bwFb({;"'R 0ꨄssY6=ןe;F}A+3PA*k3a7Qk>Eӥ&'5 rco8o5gih76F(m"2"^2sr|͆mJdoBBM[XdXg?9VO^k>tq퇻: >D9 ThI? 9$>5G)3f7! #ŋ:G|׈e-U|FTÿxnt{j7B5:ZҐ3IgPI/^lmmmii[___[[ ^t 966

766w~#Aj*tPY InH$t7ee%%eǪOsVܩqrU,u.>GÓ3O?#l]WR".Bn,3BIu`Uk? *TsPʲsFgx9#/Fdž M"T<~)Rt.x61>m|2 0YϬPdzmo3mgP -T+X$''73*}O(BS3. L()lWmM(Q)BBjULYN3"fVi6,Ԛ\QlzyN$Cgȓ_)fS*3qzuJI_^+uQ4s3M+RSq@ Arir52WψATϘ&bR(oBtVD(8MPΠs_*h2I1/`JIz} &H~271CH .d6 *3Z22BH`p04&gO&T(gPy,3(;{q 6!*yzV?S!zU8[P9)^^uōAXOS҃8CT1@^\ʌg6 JɈTW3ؒ聱 ; ҀڸB~iAe8ÁAYO3^CϠB9?ZO@ęĢۚ_2>xQHrd>!Z jK0[12s(hzgPY.ΠZPΠBGc%/9q(^䟁y Doݺ%5[OZ3PA5&BQXݤ۠ \|6* vͶ,( w Th9#m32b0!&&DvmγukM-ρIr9Br8cqD"7= vsYYIIٱ=wnܵ\Kqyl`.IS{jD9V ipE M!j7B5:ZVXD!EƔ9f~.q*1x9#/Fdž M"T<~)Rt.x61>,Cy`(Gi |>ͬ.3ϠB5:ZKXQ[$LYغW#"h2PE*Y ܾ'Ch)GNwL^6 &(bj iԙ,hs81]o^xh6) G>N`)|=ql&_oҪ+ P -%ff E˜`Ea=ݦh2pKk%VF:gWC#:}G#p)qdJ`dexfaĹcxLcrQHNr)"0*e=z9 <Ԙ4sSeVx=d6F9&6X7׹A/L:9FUPIfr%|$~ (C2s%FY2EF|Tt6ibeFOkΌ-R 9ђ^BT5(-#R&[EC0DزV]Wf3BU܁ndDчS;e7m]~B.?{ B&d|RP #B"X $ 8ݢL(gP -kd̥%bc՚$3w7QNpP׷~}SFУ+y_ 0j7B5:ZZH3UswhG2ߧ2r9BrR&Ջ/eutx] gP 9Be8#m3PΠB5:ZPXg,nBe8MPA*+ink [nIVGr *TsP M KKK Oam^̷QikeyF)gP 9dgbV+8#m32b0!&&DvmγukMwa9EҠAj*h-5 MB`('[Ew?PW O^MD"(n.++));VU}✳NՍbv9.>Ln3VWdFLOKk*3FK?D9b1"&BmÏ[NYΠv*3g5.8&Dtģ7-&ܓT$<8uHMDKD gEY)K&0d/^lˁѱB{y'_  v|4!Akna|X u3T " U3O}^*Ń͐"Y8C 2@wZËsfq2)gPA1 SM%2n* GcOCy"kLGΠ%{9.-0IF !Q^D*vqR^EK:hElJiL&l{5(Ke#i1ՁxQɈa$ #& 1bLK*C j7B9J:p4.Nlo!em%$@!aJ^3L !KQMgWgGgggW}3hS3zIN,]=2H!Bc8Ò˾^<;<~л|>V$ h&2wauy=V- "W厬 I'ܼ֖v'3Dsy&P(9u#Ό|z2ie8dӓ CMIxJaR834a;1Ldj$ݟrTg({[i Oo(9>[A&8cSc=u)<η?N^@0BBP OQ9;=vQG*p3VG6u7ivƚx"ofN6!xҤ+)h: GE+3H?#77x{.>.FT̿x-F>9}hOq`ThL8|6pʈs`C j7B9J:pƴuuWH^UrR3ζ*ZkɫPГ*W5܏(D]P`K Me( Au)@PF:UMU!ڀZ"UfTG6yRɈ-Tׅ–DM'5(-( ֠ pFeE-Q(A8tALe; u_^AxU4o>eDm3_=3ˁU^Z_oȰzXrTh]@ P(L$[ⲉT+[Iv`<:ԁI-@D!3 JP[ oj0[12)h;Vj6^Wơ0*|1:΅zt|q}g*dcR#~hYMԪ-&? *tE8|/9q(^i?С/[:FNt}=l`]yӶ'*n tQZ(gP -'37Zę 3a7ޭ?TddcF<:C]~bMv;A͟:P -TV-gZۙ9ۻ}X TBG >|W/Cv)A&TBGjQHa1OYcYn"ϱNj? *TsPaT+匞s/U󙠈G-I0DVߕ 6I\LPwr9Ѳ8m6iR, .a PGċKH.T8[Snk [nIVGr9#$_w VS * W^qF__4J4@`\ZThY=dHY)›ܤRM^HD[šǚ3d7),,,--6hz x3FޮٖA&TD"vBGGLBx4\F%N"Rdpmk:gd`BMLd<ڜg*ZC`ǰG0ߩħ8I|L)RD}6D ʙdp3\"Y^:LVxi1+νiӰilN^ieJ(RƊ鄰+8LSٳg7%I֭[p2)gP -;sIKKr5IZggxoD覈ZP}?*kGrkGӡ]NqPqqP= DZ %v0=h JL8"~3-2xNDZý&r< %Y[UzII5556 gKK˕+W:;;{zzpzi7|#MB>,j72Oͱn:9Be69y-Nf u>LQTus廿Gvdp{132=C8>X|D76!gU]E[&"Ha'TF\gfF|;{lmmp:/_~7o18xv8#8O Ϡ7T -TV;g,4/|}]P]t}]l;gգ[N|rh?_ +3LB˄|H\:LQH⌨&i_`g3D3 PEOOOww7x<& n‡ %Fxj7B5:Zp҄W Vi$^\#~3Ա iþJ-Y)3ѣfϟojjr:V\(Dqh aI>PBe9#g:u_C؉!v 2oڶD ]h߄Y| A `qŢ/<*_XXxԩKq>Y8}ɷFQ -T#X${PqUu}7ڽh\?ysĉ۷oOMM8kB5:ZgZۙ9ۻ}Xi>Dz{{vwwMp7?xTBG 5 >|W/qL444x<l|XdΘ;ӧkkkm6[}$sD@r 8 *T?Wc G||o_Oֵ_T+~~r4+$pŋ]r|jX,GB +=^$tB9#m3@mm$|-|Rx gUJ(w+|O/r'^T^@C3^;B@#t04ԲyaJ7P9 i}Z֧iYÿ{ԲlvE/_g߫QWmiRkq v8l6\TT3:"8cAvүnFSl|栟6* vͶ,π{|O=ԏO>`XFeet{'c/ZOqz(O]`_HPzs?|sOvO>?[sYjv˙v+_0ƍ=q"ׯsp!T|邂'NPT@S8:;;^ /Š9x<>up8 D;w!p,~ziDJUHv7?*sQ6C`SDNsf~V'oq+F_._:8l1taT_5zO?+WãOrYo^VaugiZBxռ}6q,4tuuր=hHس='?< y:{F)tu9&+:&4G S=g9]sŚߦQ48{|FšQ"=$Ʋsad0EsgS'%N>brB9C3,@x0drrRr| g\.07hAa>#D)X{X7^([W6C^@Qn+mC[ЮÉ]@vm-+Y34ۅrhOO>?kO~0㜞~|ҥKd$ht<{zz^jۿK GSg>T?'ц5ggxoD覈ZP}?*kGrkGӡ]NR(.Զ \Z<Όiΐ;Jl0E #kYj ^S'ΰ<A ?<3@XaLWP7> ?ݤ9)~0Fa8`0)yi@Ǐ?}w?^1? ]En c7*{ܫhwt tl7ʹ]G먠W.I>OZe_A^BkG~:AP|۷oKo\8}-x tww_rأҤHQ/ϐfD=9y-Nf u>LQTus廿Gvdp8˻ *ಁ7Fo|C𧚥KV<( Ĺ4ϒ)g!jF$:qbـLgoHһQVBb0xpdbh}Z֧iY: Ј|FWmyEE-p&#@^cۭV+O<+ tcQYQuFm4.F9fС+SaedݗWP17+g`eΒe&Jl כxX!rBJ3M-*h-yZ> BwWW)]r0 .6 } g3ˊ7&0;x}6l/ҋ/fPB ;Cj8ZE %OuxWM.x9lnnqo"d@Y|F;Vj6^Wơ0*|1:>A/LFVp5N, gİXL=xL^wz7aE 3(V6~>"A-{)oӧO< p ClN p@֔v}}y.8Qr}H+ Lȉ'UB e x30<%wx|ꃃ ;{|#8p:ynP+\@ŀYT$ꞡp5^}E+ OэntݖBT (UAeŤ o_?C7эnt۲lVfgPY1 ½CcF7эnK@Z*&30φFzBW ҍnt  P.bf>3PB *K݄ *TPr*TPB%=9c,#\?(K #aYN#\nEՓwgfR>-?$ wp84S|TPB%m&"ąK nn: .LX4$ݐ+HRw>I B *TG I/ІDMb Jq0Hv@UU#4!t? 7\ P H!$HB~C9?rX%!6@:?yr$Klyv΀['3PBcASGȴ?94l$T#;Dp!\HIyx{͌sal!x$x'gO͏avA_9 GRէB *T֢qcP8}өҧ 7<̱q Yd=L|?!#~ \x83xHq.h EPKFCfePXFiff#3*^Wl 贿45Xx?@j{LHұX';L$MA 23E;I%'~)J17~={5gЏ-12˔* δC?!n}$(Nܴ/ bO[*TPyoq.\(..ޱct: ]ů\^Uues?o~I9ٌ1t׾3o#a( P(׃W~i"E5\%|,Nu ɮ}GLt.'nw3zيH'( "_?=e)8#aվ±H+1ܙ@_A *ˠe/^~V[ZZrܹ3;vO>_t :k<|;̳~tUAcнު˿<,?H=>L,-Lhj_jaB Y @={6;;l`` ߾}{|U1$t: ݆ 3ϔfuY GЅK-FWam%t: ]̬nCwipFp .rU{P48gR*TP7:^___nݒ`l%nC`^V_~3WN]:dO wtwL/tlyE\{g<,dgm:I *1۷eaP DG*lwFi|_6t..dAF|W^9N=9T}w9l8|nйgn0t:_2&TPBe^@I:t+tU"O~,򃟿}& N\|QK1VnC狉T}+_ԛSVȩ!M_5KVwɩr0'ZmXǠUa:[i CTP2)VyyygϞ\RWj&8}yYEy=?P |(H K ˙-|^ToT}zi^5YՀ~ /Z;ǰۻ_ag6t ByeR΀{ *TxPPվ/h4655|@ j;N^%/?8y=8W<44`p ͓3$ԀCp p!G0j'Ͼ8vͱ2ߖ fkϬZw+[38CN*R.Bt By[v $*T"&:ĉnԾ4$:Jb}jTBg:jX/ڤ#~ySh[ݿ_~γ{߄z3v'1@|t:P^^>| ]֭Wy'6T l==\ ;? ;_^}2#ֹ?|H`T`N!.YYt nCBrsATP x.]jmmm}UP]]]^͛7rZ:OW}f bܰhkn~jmp=oyaf(qm\p7n^U/פ_*r3I]A'a6t....j>֭_>WomM_ө{'^o{V0F6`0ywI:*Kcg>\}7UYICɿCM,ˡK=ɽ7m<\\\|=]3- $*T"37p\Ag_~]R>H,TA]Uz_o+~ j4um3{+߅ۡƺ#7}Q[۫kJInCBr ]?^ h7qp(]>}0Kx`/K5ԷI;DZjR v!:B&@9t :݃NBWm<\\\|=]3<(~6H8<3܄\0v >PB91⌁w}tz<io0L\$ݠ6>Bn~ Xv#vO ROJ] {L̞c,oLY팓AǠ{I4K Yn\|8GF:tA3E//B/P:_b8xc_;I:v j>#Gja煒Gߓ'A*J݃Na6t..$3s|X١4냁ؾ>vg榃 1KN0f7Y9-J 4ϓ>Pv.84r}HJL|%~l`֟`0,?pmvl9P8ݺukuulfY)R *WcYNW`t 4aOҖ}Bz;:s~&@_o~jpB D3>U谄 gTE=0D3{W_2󯞈fw85? zQ؟}M/WiPni[cդ?g+;O ԄvfA'a6t..dxg ̼P# TA fZBsXK_usa0~_Gr~*TV3Ir J~SיDZik x8pСCi aXr'-㛨盪/]~Usyo&(+N[ <@,Hua#U 'hˆ>G2Dg3cѷ*7+mϡe?_C|lrsk}ҟTI*? 7H։7ˆz]Cp f߿t_ IqP~ rePrPʿu# hkR{$9~GP4l#㩂($ȍ#Ƒ K$AO_/ [b #p\r>.aRC>s;}f^7:[On) r=r=h|?',;SHq`)VO3w"uШڡg 9'-w7W=6H$IU%'L K9PAH(H~da饈]=JR~Tn_ArOw۵;2RQ$ܞMF7o޸yf`82ѽ@$ Y%6MXB,'RP|yyΝ߸q}hh(.ō5.( r;w}c?*}7wm@ hm iƦ# w9 _M~(γ|sj>K1t nCvz;MI ;}\?爐3#$$f7!C,;DKӰ8"Y\|H͡,@mĬk{d<"vWF3Zª%>rַ]OXrJHoWu>#;٦y%OOFT>)͸>H^`Rq iQWm* NI] %THlT ώO?՟;ׯ%Α~_|{y;vl{J.o$fRבs}00S`0_n ϟ'jF;,!_&9QqQ "G䄠wlP§ģ^"iӼ|:uA(jI+-a:I㌬2 EB_qjDXg'I鸶9 UxU10|deٳ̙3dOS|^JӏGdptt~;c?qwcgl$I`05ꉌ&>ZE~> |Ya'PjCIW'&&ܹ'8k׮{ow}ww ɔUO׿#T0埙]ý>ncwVt}Sx<1:`@` APg ~vTv#LT~GWW׵kך,EJCCͪ v,ҥK`F`r?&Lw\sק| ڃ*+}F!L`D|wMӳQg @ w'}E޽{ `A/ĽYtww駣|  ]d l8n߾w^dzg  ڳg,2ݎ& <9zmNPg  "C^sH y]bٔko  4avV:cbbbpp Ad"v>o`utF4y޹sxAd+bp0y"at{.T}ʕgVWW9r?/--݋  S z~߾}<|pUU˗/IJuÇc|0t===V  K5_H|W 2N>m6m60 @*Hgܿaa! yr}&5xBВ띘GAd=L0A @ $aDŠtRjlFHdaa,OAAdza2HxB!KD"HX,CAdSf|A0@+2\g0x( Y]%6:kEl1 AY@Z:ɉ;8@王 hrFSaI(T_)'. O[ީ SEmEkiz%A=VJ3GGbYvd$th2nUW\cszfes|~]Uړ"6Ǒ89 tFYeQ7te[  :mQ)?///]ƪlQwIt{! `~RPS ,Rt[μ-PR0TxVNUBۮBhQ1(GAH&d-F'V;o$SfQETJ&eHwz,t5T9:/oCD(LRj(ַYkEQ]+]DLTAbnp|RjPC%HM1ke?}oSH939CQrAd錤|… MMM0|>)xy4dqhsJ<Ȯf/_>`#m"]+#p5p\Qk1M5V5s,`Ij|q~ר=KKrr4KL6Jbx\qiL"wQ]]}k׮ t:c__ Lw a끗cekڣF Yc[?8Xc{%Nk-cAd댣G/ׯ_rX8qӕy"k{Hzݎغ >ҶIRO?S ;T{{;8n޿_ VaCvğ+I ׈?zWX#Db{T3"'E5n Jcrwjm3bMN*}&i'Ԧ/K?Հ۝-CCjjff]f_mL4v<UH?rcAd nݺ|CCC###Wӹz0zqo}Oտp܇!Kq܋swǻ«';FƻOÅ\P^/p}JIFH_NC~wtHe(_=t#w.C{З^^H:hPwGeEذNcꄿ`vO O5FSwOdy(\k _m7Bcx2GQ4wȗBU jTiB'd7ѐ/ģV0+uB4! jNCpŶ"~wqu D_u/ByyKwYv uCKnjnZEȾ*y3z.ۗ Rg성F@a4x<^jx2d:/, ng/^?v9N/'ܱNcd+AZrH(;:z{Apheټd1 )uFmm۷aG=6 OOOO{>IYBѦo BҶ Ap'Ge+d {'ywpqm;q ގ  yJ/^1 L ?1:13??G!^'}R0O}CB@&[_^ }|eOSĞ3ɾ\`#;8|F\32cR!eF.uoRe *v7O!-WЏO7) 7IinlQ(eW'\-$Ƃ-|>#kl!~oI\O=g 1Q{;Ffa?86+,V(ܜ0F.O: wgxp?cwON޺P ~YnL"lJqʕ `3330/...,,DݻP|z*^}ɣse7|䪧U)GgH^=EuߒR޺x@3 :#]6u|ZW}^U\QOc-,Iڣ+ڨ<{rn2a7*΁V5EB )q uT_K~lQ;Glk^U67x^ i(dn̫-c)Ͽ[P3bW e1 )upuu{ԃ1޽{$lمA"JGH*S~%ϑNep=Dk ƣ}ƳJT5CkhW۽Tl/r4v.p fhZјbu1 u Ξ= bX,&wВ_"-SO p5p8j>OW?<`m/(Gp \qz ر k1 VdgdvvppJKK9r%ӹAl0\Gp \c+g[uli; 7omڟR&ٞy^&27WX,lW]tFYяdJ=7Ѣgj[ :C|-%%e֛l\Y]?Rs ]󴘚:?SJwCY"lK[\ov [\B7H_eRwZAHIʥVZok{Cn2 `omo"_3uƣnk/śN)߃b`ZWӝv6< ϒW3&ʧu}-3-G>YhEYbuFJX"*஄B<n23K[|rp [0 `l0#̬ºv&_$AKA(o 1Mm0U\CbjϞY 2لH\FVY t7\?:B=yޕM-j2B "u#~gג#E.K8OD+ %&,L3.Y-t%>^ol* l]"JOIy^ m R]|D𴃦*3"KM~ij %f"-] ?8de6ITRv.ABR9Dڗ3mUD w[ )F*G(s=HlqvHӗ<3D4EcN|pDDt"6u~p=SkNll:C| Mz\0u@7cL_ig&c,?Gb2GD_S܎Z(&אêilٌŐ>͹-eiq\4dfgjK-($N%wDN wUG/vKW54%(I:HZŲcy)kCAݫ[J4%ݒ&.!]-]'G$6˧42pC*,?T}?uc10I_RmK.I{_$6YٶfdijX]25~N_&DKy ԾeƳ23ZƶUo ? Άcɇ>j  c'JN׭ GK"U4>KB , hL6 :C,B~nst4>fUsG2MgkcNUIf܈\C,S.ӂl i6FE`=i&[NjLJf}TSެѓ: FvJGTERwQݱ˻})W\"Ynz~lT%`i\+mGN.mO%-Wfb"k#uŲՐ"K$J%-KTO\8*yTӚq J]J-[XĒ5(S|-f<ש7)"{#s6pdH jCHS*Ǜ(_! }U"M,>x\ua$<=Sg"'蟧HL?ZXpAPg [Daqy|:'럧H,"GA   AA @Au  Ȫv0ZIENDB`simplesamlphp-1.15.3/docs/resources/simplesamlphp-googleapps/googleapps-menu.png0000644000000000000000000001675413245225037026763 0ustar rootrootPNG  IHDR2Rũ}iCCPICC Profilexڍ 3wϙ?3?- ""aQ,[zw+AA6nn)c]t9C)fu髼X4$́x^昨($@ruXvHD`J4GӃ6D]\ f$>7B!Y744:d LE!>RyU}nMFXjppq89{Tҷ0"FBH@I$2GZ -#+'HSRVUS_g`hdlt,,m엯pXuOϵ^됹ni@ y֐аm1q;wڝ_&KޟzසIK?z2NgWVs9=_pWҲ+oܼUU};wox'-O={eWwOo_o a81izf3`p b@8 U(]=}} PV6$0n(/(|1" 8~!8 Ǖ9(ufơB@A Fj%!Bbu^ X)~)r1T J *0( @y`@"[B&W,(I ^(xcURZ@YUG{ ͏9زv^)uup 觕m=d{,~:̢U2ڣ4nmq#q Եr|p̹m=,8k |CIݩ!"3RheHtn>*UeNE60=n/ҏ5Zǎ(sq~F)E;ݬY@˒G3hy{H{6/1/0gQX];Gi^|J&>gsφkȤwT̹aszIEx9?5t޹1qsͺiz\a-f܉(SZڠ 8h `,^,3{Knc6EC#ѹWXWK1SQ\m{Ar7ߦԇٙPP(i'_"i?#Hs;t=n[ s6͑~/|8r|JcicQ1; %9y"QH.Z+]v! cN,4dž2H%ZW|AnjF!}NU[@UwV~YX]@wX--:7YzxNk_/t=&{j }uT siŸi5˔+_W%\͸jmw=9S(Ic,*S#'|ћkȤ˙S }-f_m3!fW&9baf|A%|6hsrP3c60y6dYv!ԍ;CsD+v-+ΝOK]ҁL Sχ9w-W?q-HX1d =qaRW\3Îa]~6`~ݦvĹ\^Zt`"ʶz87Qgж Qoؐr\+uUZk)J=L+T>y`9M>QYp !zYzBW?'F >#6`1e[yX޼|6|Kl:I;ẓܑk澞TzKAg0Ν,wVzJq;+;:B(-,XjE%W8lv78 qUzU:,XKUHtw؈Hҁ54Ka.>Y.bŃ*Wwڶ*+6 ĮNc?[+(cӁXJ567:aC&}J݅JïT {MuctXkm哯F^0Kݫg.6WoλK37Ԍ4yՕ'Ƨ7uvCS=^|!ۣ`?5i&xkYBpOaK2M*[eoD8=~f#s;["xw4p^HLZzɒ9d}H:v2.&[lrW.hqsu'.Mg3zzmJ7Ui㊆FS+# )C016['=%hgE'TçO=M}J63$W?I/\͖\nK)F4C4FU;\|UOv=xmȵGv˵fhȭ;z yfWqkn/םlW= 6_R%q% wrQ4+6x_<;ZpU5"N6gNɠ{27rC7nQ "#e iِ\\2iwGzodL} x0–z]8 rU96hh$:ͽ$3/F,gwҁZ[6!~LA[$ә7|ܾvղz}bI [Oq+Eؐ`\d~ުǸg2E.6Hu_NdN'^=wZ_k5!ua殻fSyez3s oqWLX&h!Gtyk-4A'/O*XK[5qyV襈5WO ̱lSJ{KJ㖿w&9=^?:[7'Ҧ92id9s.YDl촐9##]ﱅVLoǾnM4ȿX3띬#KBrlDcBɆ{LdZ4hh$:O6n#ېS-jF-X#VXWm=)S"㫓 v-9ސ;+fKbIvD֓w \"ElȏZ!%v4]X(D1Ӈ`p~L9{QLN{7&:*|;gVO-T_|UR\9LV9Z=مVl ]w?|zL_3_9{@UΑ)|w5ZFK_({ NsHl?;*% ̑IKo~,ŭMg[BY UbAka.6uwiVEs1cag RϴZSFC(T~ɲ<"ː .EL +~ cfXe\՚yɥw]fڏOvijSqTqR) .CM2e7ݥS4} ̑IK" ^ekfwV<5 %kȤ7N8*O?}hӜ-/FAvAܾpTHt p3]LZzs# =?:1 vבIKD'q삸udҿ =?(Ѐ삸udҒv%HK%i i @Z #9Lj|O9AO{jiik;}lixjK&i \sTxװ_}}P^QMũCt2?f#LwZWb$01by*L!֣#'U:=<+jҊ4}B cTU6VܝHogXEq?t8d࡞[ossn~j-GӒn0Pª. a @ZRH+z LpR'ǯȈNZ nrP3JrϛZGjg_[Ȗzw| JhŇt[b'z+QqZu~ˌH -)]ov(zK|x/k"kog4=79fkCgv9p-ojvR,u-bȕ>xA?WYLEJ(.K#]o̴RNV߯C.bL4rnU)qŁ+CvD*i)si7trIAu^h7M65YX"Z<~rNyn飤[l\erƮa-?Oꉡ1@Ԏtvnn)*C)w>*NK:=+,.m"FHKʚ:#/~Y;܎!c{jK'g7v/XHkt.YtH}4i .C͆;C߱!RHKb(RwM7O>T_pԗT+i>^T=lqOKum~SUU^oߧs'VKō::w B픦%~$, zȑd ގ\DE@ZJC ҦAMZݯ+ycaͩ[Z+Ԧle~׵Buz Ӓ1񖨠vW"FG +|%cc ܜRf`|R+lǸ2` u}¾c2٣⁴r-v$^5 kG@ZJCb!)iS@m|pTL-6eؼ/g!ꁝ`+䟾lArJJ%$>'N 嶞<*9VnHb'!` -eSМ[ƭVQk,_Vל>&JA)ҡҒijA>-tK?Q/lo}%4'{$./'Je?h@o/kœɪ59 hqh V fZt7`Y|۶lw?.tGhfBB - -@Z -HK%HKwi! 2i vKHKAA!-AAHKAA!-AAAAAHKAA!-AAHKAAAHKAAIEIENDB`simplesamlphp-1.15.3/docs/resources/simplesamlphp-googleapps/googleapps-ssoconfig.png0000644000000000000000000014202613245225037030001 0ustar rootrootPNG  IHDRA]ViCCPICC Profilexڍ 3wϙ?3?- ""aQ,[zw+AA6nn)c]t9C)fu髼X4$́x^昨($@ruXvHD`J4GӃ6D]\ f$>7B!Y744:d LE!>RyU}nMFXjppq89{Tҷ0"FBH@I$2GZ -#+'HSRVUS_g`hdlt,,m엯pXuOϵ^됹ni@ y֐аm1q;wڝ_&KޟzසIK?z2NgWVs9=_pWҲ+oܼUU};wox'-O={eWwOo_o a81izf3`p b@8 U(]=}} PV6$0n(/(|1" 8~!8 Ǖ9(ufơB@A Fj%!Bbu^ X)~)r1T J *0( @y`@"[B&W,(I ^(xcURZ@YUG{ ͏M,ea+mQ#AdH&@I.A#%-% C)GjHMPMSi=`kkf ЬLa0T h˼a=}/U``@@~4>|p[8z-_Ar`/q-{=Fe9}Fp ϟq-{=F"9}Fp ?p[8z'_y9}Fp _?8#ZAr`?6=nq1O8A#0 [o<㐏}Wv>_Q?_A>>F;h+(ooa zf/sl' Ξ1ԗ}d]/^OȰZZ؟זv׏?-^o_7otwg{J_rfQ;~uLv}\Х?V6ɿU#E5n-'M!m8c$ /\:WbvU.m|Ok?8O?/3:~WrO%N?xa;=xv[ƛJ[n%ֹw뷎koۥ^S} 0w/+wn[[-n.gO_/Jr`f}!d+Ҏ]|wd7ՙ{]Z`W]oHK~/ hό֭7J}yj;j@PGeb]vHukrA^5Zt7u5zQ qs_ܚ;M##e;/n?^;zgk,ޑ:Kw%:{N3j=US/6ZWΚYVv~#5!W|U흣 %ճ{2^mb:ysu /\o v)q)`x/zg7xxxq#{W7x7jykƻfs׻o1w4uv=}MSxga&CS,S^i_=UGb"_;FsFG8J8tV?x ؊mT0m| +P{i@-MK/_.LOZX:m'zjma%8L4,W}n{_~oߠhwILq?qeR6cn&"Q@Ǟa;' Ε_aNx4/ ϐ?Cw~3D"٥wɷOsM^}5umZeX ,5/Θ]^{z)nf`U3LAi;\@J{m敽mԷ5vF~O owOI=#|c_פֿʵ|M? k ۠G-+ޭ[KosOqݼX4\UQ{}ot#Ǟֽ(ڀ$Ȟ+oG9>Z%=9ZeV_c\|, $V==i.LӿmZ#߻'/]*]4ึX#𵋏U:ۻͳf-&Z[eb9M]Hj\qN[/&ޞxl O!@Bcα[|9-_xf-_Z*">ݸEu=»7 Ҷqȋ $1qr-^㵒.[Q~'`{_ZihH#΅)~s[,?xoXh>Q <>OOv“ ,? bunA)]`+n_C2SxkWMyLq&0M>^ |Ŏtޑ_,:??N-y_#=nq1kq89 p8p8pB@9WxfO>=&oۧH;I[ӟ8c=1?i˿pGV@yuw@اO<ew;O}9 OӹG5um;@@GV@]viP?vqe}~̿$>w}['Qs$. zdO*gg}ٶgu>?ۼH7,TX|X'fJǿ@ݼlgN(һ$ WJ۹6Xy$('ӕb KW>$C_X8qs'WN#|فަɷ_ 9JtՏŏ=Wxݛ/~78 Qǟ}kb?zT[νGohu-.З${r͓':uFx<)ʥ*<~vt,_ړRS&*g=W. ^~xsDOIFŀ8px4ą.yD^|Ջ/zۯ&Cz~SOo޹܉<_/ݼ`) ?Jg%^|}V}{C'H/o]|T\rXȌ?M\HJyC"qK6p_:N򗖄I%1E (7|!o2,>ɓ_x T8p( mG_o 6BwW;씛%?g۾zM<oR?A{:±_˟zwxq*^yc|G3~'jŏ}Ms͹ aٍ'p OZWZ|U}! _e~8>q^]^"|'㡧;Ly\y56tKĹ"{OIS"x ZkO@s4(]1#OsD+8pxDQz[+nq.t=ϝ&1<u>K'Xϟ2.>`Ι+מK7_2 =y-\\q g‰/7Hщnɫo=D1V9 ^l8]1 Fr|A/fq qW&K4ĀrpoI)v2TWj(cZ  AU!DUk+*eUP2\IVWh5ʖozbud )Vauw$(֔\Fː5:j$؎31)ՔكawC$+k#A'NAyPd>~:{=MK-LgT5dYiTLc˥$OG"+TON.Q]L$\RSM+RMo`R.H(x*#$=ę벘 W֊Pfa/m֑Z/`0Ԗ\UYmĞ8{˥%Jr;TqN%( "D|BaĩWHB,P@LK5}23>WyܰĤ\? Wԩ4)oMa(-k @@#& XY3cSNPT+AR錅0@a +J)/$".pI1 q'#4&K5}@9pbU{0I@X31IPE,O-GM@荂XXkt:ZbVO=V2t?+ +wk ̇QOGK6qG< "햼^s9H(Bjsww]P UVOcNܲj5Hqʞ8Et:bbOhb,-@q Cϰ-/|~Aǹ jgV:_{_cІt&Uv讀.eJR"t=%ݓG]#бI G) xdĐ™Յ{c|c:ʻ}n? @!2B/+8;߱(c^EQw Z(Zgٱ::醘P,!5sWvϥSeLaC;*^t>%Rq"r<>@0TU |o}{):xAV4zF>8^db4MR9/F |`rreT};ey{o*|%٣cF$*W qsf.Gsbq0 ~]`eې?NMA[FL|i*A3\(R|'*8Zro̯RMR @b[ȂUM3dg0Ю`@b^7HwV@^`vd;B hJޱKC(M7P8k iHHei=Uʗ(5jp{_Y%遮ԡ\O}:>pؿq;<}J3goq\X:&[=f|w=C3թRuunz-GյJl;+ENL"*6}-b$#T]xQރ[ @arMr.P es-Y83# ) WZ*L' +ui\@x~]:g.FG/_ o %OIn}MĿUS$x霮w޻Q+o? a+0xyoH;^Fʮְ޳WۈKcf06z(DbRhiu1O˥REWNJZ+0 Vwn@e#xs\4R&fM_{'fcMc[ jh8/zCWkepzr\#_`lwWyvE]b !ʷ5U뚷%8r4`'sb6QpTŽY;_ʞbNJE\[)j5T87 h z[N]k3[uWV<(@!rsGb9Ğ&CH"gǞjHYmXNNы[4AA{@@& Ǫw{,)1)@w {&0F}.gȗEYWʑpFvI`u'i9֙.Pk,$hzHζj]Ӊc ; |CwfLthܙ{5-Fw)*-^<ӗsP3R/9G}^'yؠQul׆.)FIe1FQ_c=E;J1LGr"V+G_:+]Z]= eY X=}5?ϱ!I"uyguz.΄⦐ qrPI℟V՝  SzK |D?Vי+ժukh$yw-H@4 c!gJx.ǣea>c7ǑpɅx&_-#mmb,/I]`=wbLCIWkQœh! uF)0em.Ĕb4FLj1jZ-BF*Jg%D.i}QHo^^ bhUe:_+SBO5;A~0Ybe I& NH֘DȟիCxAo:~;+!&[Ѩ8ׯz@vCDos󝱜s<α\i"_N)lZC6Döʻ~f~"0NGQ3."T5ph=-*&7Sz&VI9leoLmǣH$WXԜN!ۚ[Zg]FzԺ,SoCw I&lap8j,to`7jnWHlh!=2󻁁aZhqKIBX M}xUD"AI!o8E+NM5,]g7P'W:#=`%&@ER!u 9zO^)4,<3g):HKح>zJ} ށELH_$ER^[3);{= p&twЕJqOsC@@<ZG} @@ @@<ܹs~xWWݟ_rWq;[?D~'?;wpU]̟ѻ?~^?zFSG]Oowg{dCTUӍG-/aN'O..>k׫ǿ_xKo}D !Ѽ6]Z=O%͟`^R' j>10UP 3.'W^#( Z֟ŗ>/-~ȅ?ޅS}ȅ_"E@(r9 U}O'f9~g\ԯRISeY2/Νw?Ql$>/_?/UX$Z.(FBD&")6hS1ޏĄfx|Eq;uZHK Fb)%PtD -8w{ !ǢHB^Ɏ) gjXP'}>=qx7/ᗔ|lD p@KiR6SV C([r1N-v2\"f_/)a(H&_Q#0N%r"j&$)@" QEYgkǗ_:SpabTpI`I yB.*:jtjr:r\Qs3Z8WSm\NhLX̳T3 &<'wzHMVQ! ׄdKX$Fb]c1Zguxxӧ7~o/?|_ȗC@䘟j9N=Eu,˳9Ҋ p=4A$w- /?$Z>ut?Ùs$ژx ?P n '5\lj$u{ \FcSRZ 7!trP*31>ߝ( SH;Ɏ->U|&B$O@9ssù܅i+eD cV=oT01:aM~9!Ŗ :SQLghe|NԂrZ3D)Ā{5&81j;ͨ(ui0nٓŏXe'hH(C4}` fwڔ/w".?UMoلIv54 C, nܸo|sOOO_ |sϑh-;ըeW,P۷op^?rȒW$0SSdd^@ܹsg{h @@@@ @@20zq XO,b z|IR<DHBYH$ d4áP܊a+MU& ”̭B\TCRISeYg;ʗhNDj.'S{{Pc="u Cp[Ҩ/r˥X$Zlhp4#FH(pL Nt#p, 5Zb*"1! 4>d3`@F3Iq•j%ACʑObU+Ĺ_,Cc3RZ R"!:VWh5&©() p4U>QQ"\GhchOmVݴPW>M_!7Qy!|9tmd؆pNqF]Vc}_@ѓɬ_@ZSTBwse7Lt2-ujͭPcfv>pO _N{LFSjS!-lH@kN64U+Ջ燎(VĨ3vg?s>HXRd]3R<(;J<.wy mmP8_#:h8W3Oh^rۺ"Dm*h\D]J5EXdo&Pq%^cA,IU&\F)ƃbb4S/qqlIF5hxT4#4 FKLԄcsUʂY,nF177b4/)VDMF\nۃK(PʦH 04D? j ~1wNa;GڑHE\:J7Hڟ%8c)8?DT䊦Fi>͵b<*E¿X(V5ἤך6kG:~utjB F L.+w2YX,=eeffn&MG'F~NviXfWI 3lCsJO^[Rw`}: 84+3Ĥ;Eم rm]ά8’ܛAı 9آ(|啙YqUmvquuanSu&mM1ǔfao i֋1ֲ9/2⿲9ynZnm5Ͳ!y:.iջtt*K.M7V]TՎ1Pܷi].C-ҽ E-]WB27| Xm#lʃ F%Cy Ugw\U4ȍ8Lĺtb2>6T8%)eA4x4*tVjLoPWan.׋mK*] 5vH'8?+I$ޢ{KJUh1- Y8!TYjI\vUft>ocmYh#dZߥ:B}TQ|:-}3;Du̥P2U7oaF0^ s_`sEIV6Q. eZך5Piї++4;م%".c-項^#ncf[-PR[X6(b:mWސfIwKiZv;˛^wx:'1/۾sS8ʿM_~;N8p>J|H&,#no৬?q* IPqojӴVWSvAzŬkFJ3YRZqSۖš6T,#Z3v&l-A!+Kmٮ,S&nnjA@-M1DqMn漆3]9<;{Vi k OHGT(mO_V*-HԃFNT #nZ]I(ް|9@r$J}G6R.'$,#RKK,X#Ȟ{:q2?#Ͻ!Q?* \͙($jq^n˘AUe0FeljkO|8VgkV|mDcq ޜE(|@7F@'(Hji;OUV$%lTü֬ӈ|x#3 X'1:isxҘu4L/>6=3^g}ѹU ī_,>'^ ј;u߹r)U_Q&Rڰ,$gñT'*Iaaq[xrٮZ_&߭y)K*y,]N+Bi&G+\a+m)F06y:Hqr' bk Nr֍h~v/=1ѹ*C:y9\# Jq:ZuBCfKr%^5Zg/5^ z4'Ygӯ0WJ{x1&˥TV?FE`h˶p`I.UꍺDOh^e{0TWTD6W\*FN߮B3F¶adc(1ەjz)ef䰶m np ]jK|b}R޼e-8ҵ;%ԅr9 \DͱQ *έssbTxD+ r4-ҦiQo߰{++l [{JAsww2iuւ :;|*>i !KQE K3+k_r]O< UH* a}mM/۹ Y"7g߶lh_ui,>])@vqЦ)s(ϋ @gog 0/sCg7p윟pkX7_c ά|z'-;'TimHǥ6ZtF0bO;U.\FGrWmBW *<wҋ[<:>~w{.8m߹{R/ŏY*K0fڽ)O< UH^SP#'-nb yqЕ& 3)Fus=A"JIz=8Oi۾Oά:`HsLJ>B 1:ofkH3iF{?fоDYF(ssuzL3bXv39B Wclc`<NcѠz9m(hsέ'bl߅l],Ǻy̙o* ["#38R}Њ}ְ<*9+MNe7/[@x/\c#ХJ9cH"iovFWmZ:T>lMf^GQŌjN5k(fq%@͝1 XkGFsᖡ+CCG3M^=]oQpqei>[ue.19a3I;=#XegI*skməSf_>zl?q銮ȃ54'gTHmm7+=` y1TQֲ5-d-5b-M{-ψ^B]Yo]luKrr]#mjWG-pfI QB@* ƣ;^^c~m Mӂ_0g(x+]BC}'ON=?xM~뎮ZV*!+>m+$<“*p]D]zږjħ{v3N@}oaG /2t[_ߙ6&//mvWJXll{Ӆ=tr./O0إ&W#zϒM(dWӟ&֤}1vpL:ׅ0ck]AZ/hCMW V1/ޗ\^}~8jqr 4#WYN*.m9w::wcU]ManA M y^}T{\k6wO{WcdA@@U@1`3- 8rțoy@nF2 @@@@ @@?a蚪jh,* j> 1y9' {Z{V/J ug=#Օ43|D8)8zC Bq+^d*7U*Й DžR(4tʲ~sJ%A$K@D-qr0!ʺ'5; #"\00ݤPjiğ+%PtD m4>cp$QH׼ EkI=`@!*fuÅ:pk폋DHH(yBCȑІeWD 9Z&NCz SZ*h>Wh Sɰy\Gbb,$=@!T״w5ͨdHNJR{:%1N;RZ!SV C(U!+UDmL=MRxSͱBo]itՎ ' XoZ*s9w-g/Q>:K0^J"X4.3IЩ <)`@~!\ZE2jţaVNoÙBgQQR@"|.x? :r7<4hD$w% 6H_"N+8/0Wu]IE Z$HM|x$ e+hb\.hr,& H1j @e A/!,JLOM#L|Rcp \Zd扬`ZU q0/U)b0.6ZB5 16!5s±h&©() p4U>QQDP=R(@@~x3U<z:xw ) b~1fT2V>f=(Ɇ Yk-+ "yP=TJs!T6)ke&G тhfIHCuINLgb說_$Ek$M嫚b~0)MM<) ߚC͚πdΞP66nm#N;@@E:ը ^C"l/|@@Lt{U3Ȳ?=O<0r 7 ܗp177wȑ` An/\AnF;h %~h#hCHbv7T媐JS)zJr SxMmJ,p8bS> ~V B}Vbu-*Zp%!ԦLR.͔^^f/oVQTޭ¾L7AN o47dldR.-Y&2={{.Go]o+ɤճ&=${b2ז&lraKm-$ k+͎sw`vi * t)HAݎ[C T(Vtv}4@Dk#M1vI^hCR٦AF# i +vNV]ߥ O swt+joP(Q_N&*SH(Fj*D%J: (ADO^J&~kMUS ͕yFb29F@7ri* Zss=هXgz}ZA4ŊȻ,BIv_/>Cċ*%JK!5(97Cr̯="@rx|4'HX4F=bἣ+B~+;&ƣr9u Z(<+:gRbQ[RIR8uhJJY0ǫLhRE=JM4uyEb4.-(X7Dc[,DLi+>Vf1t숏jG@V1 \)cU#ѝsjhKJ9$NdRKel$O@^8jPцLMj+T=918Ĭʮ LuW<,̱ŀȯBt.հQF$. KE\b׳i׈h`oK.IrWDmS_*3l29Ty5K<.Mɤ`㊱u7ǟ44Ǔl+)kd_f_@Sbk8?ak|N w"1ϦM{šЋqֽJj{:T'U"Q 6:R!'dH(7-=W')儝ްj cx*nG6R.'$,#Ry溂Fٽ_/{'=3`N9̮9؇zhxޠ)Iߜ/RF5MUAŃ]ZSmNZ2 ?E#ŰkEW*ZrPmZ/2Q].+5hӬmA2EN7Lk(ԚVg Ut 8VYn@*YR.·ԱM1c +to #9\^IbX4i>^a9˅NN$aaqC+,GwEH.lj<ijZ{5Y%9Ifkilb"Xe!]]vhcBMK~:K>f ?^PKbz.Z˷ᙜ5{9 TaUD6W\"FZ]qG- l4*lPt%fRQ/xFh *pu2mxH Rg咱-}U2Qh>pT:`K˴[ 35G Q,T:*7HH5[ތk M#)B{]DoSүp9Gy:pم &Lj7y̵ٴG@; ͦڳKf1KܓѸ2)_vUOgv慙-I s\m)ZD&p6V4?O& 1Wovߥ!1 Du~fBbݰM14T?|cpɞ RGStc #ZQ5Gc"8ZQI(ϥUru,/먣Άm ROXk [ ^P-Zi:/Bsbp毗=\x5;A7DzN> +WЏ^Cjz26?.wS U{j4C:pFlac\@C9;mJHcMb'_2>>W"Ysrn25NA KTIQ}DJeʪ, +RLRu"kMjMQi r\Lhjsq1PfɬZ{7̷ޑ yyIQR1/wn&t5*<;bŭ!цI>-n1vv@ݜS>?̋m0fI~[#&YRx~/ egiTqħWڃކH_Q<~&{ ^bw+׎''=MY)Wov:)iZ=:IZ)1wŞj/zo]L1QXKQ"J>2ƦYXア2IP8h͝. )3X{:{UpL1,YZ{Rapi% ƣXրis9tJ9!bCcU"$ްNjh%=KJO[1 . Oz/ p+X`S_]SpEWر̞ Zh4is=;,lxDʒnҵ=&H#_8L9 8/ h%qѝ2+c'wZil{:&qik\Wd[y,l}B+ nx֩:b^/|? a+TdZF)U˻6"0-6o+›(4WX޻\la x~g5=2fSi6c"#G曧` An/@@ @@@@f C' Z5rH$ZhX@K|BFb<|D8)8zC Bq+^d*7UZ:P@()bτbh<uiF/ A9QK@n e߃ cC' :Ud'ZT*cԠ_-1=HLhh]" w͢ j>΋cp$QX`kP"_u Y±8J`^C.X#%2|!"DCQB| Vi/H\¼\YM$Uڴр=hA6s`BM, j4t`C|]PP,ĩӍR5SV{ŋM,oJSɰ>y\HMT=sHM_^BIdyß!TtD|!g4nB1rKfx\" QdNJ>è{&RD¥R8Tl$B$` iUW)u<+Cm5*sTy55#$.OD|U[lU+r}89߭?4'h4(Nu)gU@0U3ըQmxǞx1[Q #Gc r}pX0r 7 FT()ʎT CgCK#SB*O+ @@5BPM; Zm> rh#%!ԦLR.&Fʔ^^f/܅YR| ЧC}e9z[dvÝ}O^L{c2;}s* v=^im-$ AɅ-*}nfХH("](7C T(Vtv4@Dk#aa #:jE/v(:fb4Pш"cV[%DO^J&~kMUS ͕yFb29F@7;Oܻ S<:N@knnt/]!(nf(VD:j$fw_/>Cċ*%JK!(CsnB厙_5϶ݴP8_#:h8W3Ohޡ3zrѰwyhMGs20Py/^`Rb`[RIR8uhJJY0ǫLhY.&Ƭ[z uYű%k<:!&RѬ'-1aI<3{. \Vzc2Æ*f{YXU$,¢skT3+ꬰ$XL.mb!QVffh,aI.J}I!KgԽkk92HBz¼R]RX0+dV {V_YfWwpefv~6K# -j௠?qk9[]MYcAzmbaYy `{cf,r䝻>2Z@@:<{šЋq?w*`zӗաJ>@e !~(rBĈ29FYHRNة ˗3 Tx!'TkhTKJK噰( fY8$|G"$<YQM"ֵasYHή %u!c]`be9H;|?Na m$ r!Wٽ`BI j*eG@' fgZ/B\# J FuQ'4dϖ^Vg/5^ ZzY) d9o<D,_2*DŽ,Ra[tt=@@t-XKbz.Z Lj>rUSLΚ=b rۥ=6-3WVhum YuNDvaccC"Ir:K\{۔זgneW]r$RSuML^p3+.oϲzgA' Ur>NQ"g\qr4ˆVuUsNUk M-(6:jEqZ+ QKJ[^QG ROXk b4)@?  C!:,肋:/Bsbp毗=\w*L0tbV1'][nTGt@!.'+u{^ wdB^!E>;/wn&t5*zhEHkX ERE<  -N^FkήE};r;],t; "ꑛ2MZKԜ'y+BV|t(EXY)5gvtOHUz:1!:OEYZNuf:Y'K[k471 aWl"E=l-ȬB.A_2Հ;V]fJNkY{Sc.+8f6ejxtlٚ04-}Ng(=nCckNt&=?xi1^,VCSbZ9"#1A`Mo01'ѹR`'K|wp"NIsU_m׵` E tfsE(dIEmȃ.y{ӤN@F5T0a 51_u+4Zżh U>: }MҨ*_̳f*=Z=`ə|VQDlJDmTxMC \<$@NC|wbܢ'vSU5\#±<ß{( Vr Jdr\N\p^\W"9iUZ]"]M|^9g+D /Ѩ3!ZUtEsӇXBnZC=ѯ>7: 1o7oZĬ%sV /#D; ~T$_@K!=9Ue;mjR/3ϷNLk"1Z]SqGY,&aPɇZY`!0{T<]DnHX  ڹ=VIjS g1_8i*<$%v9=v|FcOvΜ q>pZ@j6TUJ6qL8([@ͳʉ0cz,xR!broɫ*OXC}C[űbHBP穾sX3 䈒. fɉWN`@ SV*qr'osϙ #(5Dvf=e4?j$~vKm+„RC'L&(=hDny[ѨsjB(Jz9[բ6A@Sn;ɗ8ZRg߹DL@DRcNW1y>,x9ȡB ń[@sW@SM.̊/mJ>HTɱ99O\lGMR~ +]NJؤcg8_1stM~Evf=^4yF ܩ))L(@@1nO C { Bu o۶Z™wUZz& PJuL(v 1bSSU) PL:B΋M6W@)8T"=âUFx?)p.tP1:Mgr`t[U& YxX. F.y,Ug?w<.-KQ?9Nν~%?X5 ri( 6}I)L(@@X9M<ߡh/mՒ& {#N6t MmD80oPӼ(Y1α%"\ݟ/1ks$ ᨵp 2d2HVΨ1A4lL=YpxCH }oz|3y~ݜ0;cN $w7ֹrK]ϋg? \@8tE'5_<+ = ͻ}$bfa! &&fll絧a'ELwfy^XCy7t"vȈh\'A"wIPQ 3`߻ *Qεzs=u @@ n^?xX|K xpDo)^ɋRne-k$﭅dr7T=093ٝ[)tt,sMaî?A[\" ʴgݘqU;Ԋߵ{Tκ?/}K%#Zֹ~ =݊dFܼ7et6jW ka@br9{tκ?ʥ?}/W^J&[}D>4]jٽ|ef%4ǿ]À=eeffn',}w|YYմyjav&Θ#~oJ7T}eΝ ֢'̊swuu呞T=e9;"Wg%G03.mَ"M0(dfaE^[RwLSkR'^)ϲ(a!yYw\4D:;-Jݡ]vaC_D3ݨElΛCUqycd¬[Uz4fYI/nI/mi д=l$k6CMcm5H$a4Oք^#^q筣?s++c9K[vR Գ?Too[\8I-^}ՓWY"6_3>Ze2ř2 = ?ݾ&ϒ.ܖJo:7trfi]'Aj]?2OpaMڔz6-6{گM.Y鵦^%Eo/oMNaBd/1Opy) ib W-5?% i.%!Vy^wC`vڪG-Ye ꥹ@׎j{֝1=.-K$~OrEg:ޢ45nWU5"B54eVof3^8Vj_w1>vϕ*2 &4-g*N#c6-߄gMF@4.|sg=tϴ[CgNϞ6+_8ݿ~Ib8g_)7\xʋ+W7;hnpΠIi7އ864m_!.M/luȗ Bz[dvgKXyr'; ݱ>ƒHk%/e!8y7lSIzwsKR[8+5oi>itLY6Ͳ>Gxk=ӰS^>gdZ;:P6d.Ǔd6$Muֵa85Wغ[d)JǢynƶ?хѨ"spw1o;V+j6֖ܵi9o-`$ɉD)[~V W׾Co(?oҙ6\n{;c포hk9k<ŷtfPx_ FLg666$ښ$uwM|94N@SwYqi}c}Ҟ„Rc⏪N9\/]J)Ygh*VB܊Ǥz m㯵"SFWm.3H@3c1xffM+_0 Kc3/dyf\J1?j`81Y6"6]&$x$j6;Ғ87??77ܽiN +;GjJ\詫k+ko+ޙl“G:$Yoխk=-3}"^-ij.9_f/~An@lD} Bk3ɴ7Nq͐Os r/@Kmk! ̯&*P@l㱳He䦜]ɡNG{JM0Kj4迭kv:ZO# \K/Hmǻ)W}yXqڤ\4Ӫ0]8Wkb;m]ɦs-Ezjskqoe4e]pLaLh6EMJpRm6co+l=  3) W5G@j\uqfs߼l L' NշV|;Wߗ|_~?q\ E:GܢYL_1 ݅e mmz3+ʮ;j}Oy#EڱdzY[ojֲ,-dJa7Td8/ ֍}dg g ԈSgQ&E]ڧ.[64yc[kwEظ ;gXT$N:-X+U11̳ol1``I{݀"N6Awe~fL o<7,;.[kvk6OI{*({ĤiQ-HPwmZnk1B_5G9WM">*dz l>w(ݏߞM=G9V㖀hn?zJV. W:C_xĔEqMOLLzj̛M?.4>%?]*` dP)*7ްh.IzwϠ?5gHsU_m/lMq//~C;d;ܥ"wV@{e՗&;k(^`\o;һ0[w-DH`k$}A{~0^ܿC '| ˠju@k6Zl*fzs*iX7 @@ @@' z! EY/BGV8pyc~\p28\PrIݫ]J P(#W )E"_ՇZ!j{"DCQBG! ŨФx8/Py!W0*2K!Fp$BYBCWkH8It[h1!ŒgU|mT!$a.#<$[YW8Hh*q. ZeREP Q+ñtaXXR4Z$њj˔Hm=Aw+4 GQ@A5ăz$*4x(\vF#@@=Z# yhؑԚ=Rr.)cA͙j% F4CLl#G-mou gYm!.A@CoZgD EXbt\շbQWT- E[BL,tP_ xQ/ rDC Y/ڈ%H,LXp`4ַZl')0Q3ӂg #Rp&ˆB"c ?60-iYL])0t* Q& b chjL֢V=FiѺ$B$Δu>zk$n taVy–HjE!WdCtfH8"]diyhH\tAJD[U@@q7&GlbO{ӓիWAMD:[Z&o=QpT`#t"ɳdf ( ѕuz~S&b"Su֨w}=yYofpJ.jwIs%VSɵV{p8ڰS$_;}sj*Ze;t2-uSl:}MtĞl4O``Yl5'<讑f:Š8vxȊ[])_>S:H 4/%Oxk- f% [+\x,Jaoe?zZXcuXMzRs%+,9uFOYIS/ 5"ug6ԁ +3S,&J~} G"iQ53GMY#9fɭ5fَm3v,MÕ$S[`k./49~v,?9ۊ҆ ȅr6龻H,""感woQAׂ4}/zPȨm)Rę k"[ĦIaaq74m"4̮4CY%IkM [<}1j â|I1Ioza6Qys}uya~6m4*A`86"m%zs[SW]5[ ftp^d9niCl,fmk'LmcF!%?3Ӝ5cz[a4f}-1 :m6l. 5-yp"Vn0.j`R.o-/-//-Q{wooԗB7 o{MU,6Oh~[nWX[Yxfy+0P^iH=)_E_ `%8Es+{+F !4!$̦M"$1+:פ}Ӯ2]^2Eeqo,IjHD9t6e>4H Jg$ &f->.{k\잯n-b`^#+[9-;ºݓ6՞?!9跃?ę`PFLwTv<m)P@OgqgHghtVI'7&Z$1I:&)Gv;Ғ87??77MY@k΢w?nwibG@ <+,l)/ Dc # ̫_74jqŒi; ?{~_1!;I@l9,nIbp>fdZ`vϲ2}>wh}KlrҧcE 2 D/ =  G"ͯɭ-9/Үӱ^@86T?B}`M_ grUىo9ys9JoݻK7?dz?I` wf>;Oh~ 7-j7{j{%=&q`݌?ж6>?r'81Kl4ce&( i~>k te]z,x3"J rE ̏,=̶:#;Ew-la9?{_Q{<Oa8t}N LikY҆ZöHu4m.l6kV˕氞UWSf/#dL_{C[l.I 3YjJrUfv&|7e)dڼjWb"9"F_^C3GlW[p1Gu1KޒdϚcZJk!ײ$#kӽv<:Ƣ{jZ*ȃZTcqe+UkqI9sM:M{Ao}Qv,MßDS/OE&gƴ6b`"2}@޸@:4=ƹ{Ӿ6EH{oO&-jܵ{=E qb}<, UI&)ㄚ:t )Ro_3a[gOq!24gIc =6]wWd[,GkǷ''u~š4vk9S?H;ỵ)ڻ5?u!-l9|o YLuOq&ܻ\ +I?{z_q`/WaDsu~U>f:+ y!&O7ݻOے]{ xthfbj4W>E'.k,׫{. yy_ ~B& U={(Nn ^ Hbsw{|o=&~ΏXma+m$|]藦{|o=&m^;ycs Hbsw;ӽa7wmbh|k=o,w0=70tNyg7c@@ Ox&|Fp3{1q;wc_;~cg ;LH|;];ӽa7sW@00zOcSϔ|?v7ݕQ-j<,'–6e #c_WhCHrVEza~9L퍥4_n;wr>:>h y'-&Wxd6璂o.9߯*/FfGKfsfZ'(/'~hlykWYxΠBڅ7IV@LG!̩݁8Tw_w$ͪ~UzUnw&]6SD?uT7B<`+W K.HB6*O3+Mu%!sJ*d{gSګ5E/Q]{%}ir~}.}jvI#g?ɵdvK9bP S7\%3#8s" f]/<,̘= 6 ^hkٹI.a"23+α-fWWy.=l K޷N3̒M域eτ ZAzm><:K2ܑdm=  lT얂}ƠFgٺ⾺#YoҤ@U*+v+˒YǭtR쵷6dk!b:98oĘefĠU(VLםM f@~;WpB'jF5 )N_2:}Z9F@П MF S8s9-NVWSh/* bZM_ H^K_r/h' s΍3F})Ci@7qn.#oG _Y 8cU2;3;7KjY:ȿF뿎RpK_ʺs4Us_pLlxqPmeY_Nꛔ 2TVuYQ=ma)vu29 SSͮ6|!}`ORaB^ )XC?.73pxDdZ[Z6M\_]^㑋4[Ir5x_; = 82mM4S! Aۈ*$#kGxᯬOqbau&) ۭ%8c2`C^ja#ɐIJuj[tIHVj6CWq!f6R)&n5:}y)+ݯgYB4"1)h!^ ~2/ _Yeg:KiW@͑ %cw2AL؜;Yqi}޼]n%[}XjoNՉ;\v}}@T~˷m aJ2ۑ7޶AW[٠=AqNaԅ*-1vzbłoŴkgݷVVi!q~iMX ?df;q:`„FRKAO*[JWnBؔTp| -p~#p*]f}|WgX@ bZ ηSn.:S-7is gwAW=ht$]KH{b:-J}O[vi%Mٱr?[G]F-m]FX#w<Ի¦ ͘5 [ ӕ1oYsJ2~o}H2b+$ ]Bq&~Q vlbZVd_P 6'!I:% ,J3<:.Z+X!8B.9),0CyTt",[.m%M"oxV; 5~z5+'|,s@W="`S\Da{]'NS &g/L+%oTL.?z.g[$cshL(d`Ӿ)Sםw ,V,A?=&F L,6c_hWsu6dvQ?`]a62Jo.R-IC_ :S U=UA7rxp/nv{AbIf  ɭr7޷ӚbM=N`C`"ܱY+ ċ8-`7 sٹ w{̎|+S}ϖ/ݿT[-l[D8aDJʫYv6nN1F@`{ XmeC`v[k^6[wY* ĕյUšLiuw6`/6;< &gj7aܱ!R܊dofp{Me`kmyiyyi4-L*^6W_eR@Mܞz}N=q``#KJٹcY*z0µ9!D-Ғ87??77 mabD`E6_U͉;JuBvœ 4e]`SMjge'kY9Rvb!?n_F ʿ &| Gkm&[-_?h/tؙڹ|2g}9f1F7ow{G ߈al"cS@g/;Jvy-QsU_S]ؠ7:ugW;;藮aľ~‹'n\}n06k@@*~ӎ> `{l'"Bm6fEf vZ 9ogo+pԵ}}NЍ06| ge:2ggvnIOzւgg/]=!2]lw ~nJd3K]ܼɻ Nʇnhͭn-9RiyE MED% |,; `ZSTBw<;xEu҇GO@NB|ߣ@gυ In,"/[Ǝj}:p([BL]/nL~bm8,#,;4wspScyYYf]+I[rqaFYhna=8K/+2ݯVG [G[~49RWWʑs<#/gO[K绩Rߢ>ع8p&Z'W+Dmzg8}e3e]#={oJ4F@x7ogbwsm&# W]6+&7U+%tUxSƤ'؆2 Qo33&/݄̎-9&lA$ Q'nvaq@bR~ӸgQK}tJқ?Rn Iȗ9={ڸ~C\6?~t'H}ܼr=*/V\x|Aù;k$͏s/8= ,oHtx:K"`z]Q۫iڡJ"gy.fWh–mWM=~Ӎ] 2P} چ4 ce7qKнs KK Lؤ[~V W׾Co(?oҙ6\n{;c포hk9k<ŷtfPbjKs(ϋ @•j&zBš¼0%Ya~.kbvл Ƅ-!ޮ“M7v~bm8m>cp(ؘf9&l1anc ce&9L`e4WV. k=uuqemu;6MxHSb<ugߺeOë%x%g:2@@ Ry t"` ]ؾVtCsv 3UySwn 1v6ݘ"ipP~cp<-9l3~kR.6pZ"JGSb[ȄL*E}7o5.ߺso^6؄ES ?r[+g>W>/ߟ8"#\nI^s/8k|%r. ·F#NOzgcچa۬Cӳ BӬMߖv ek^H}Sv=)/51N1]2qw._h^24Vm܅>t?b~{6uln3g" |S[[5T*Y,\j\ }e{'`y *papL_lERƭu]V.;Ӗ x@p ?< @@ @@Ν;ouqFs~ ȟ~]#RGRӇbw" qE':u_~T*G""#u$5%%~-v._3oWjz cvG w~ECEjGHjJKjXU>!]Yҵ?#ңUUUȵkpWv[[[֤RE@@쟗^ztd|R9wjq®#)/5CgC}B>gyfqqرc.\xb ۼӵa$A[OgB+#)/5;!ZuՈ+̗" ɝ;w^y//ѣGOj69]:?}`3ᒃW> =Q>x:=_zڑ:Z L,$Nӕ3S#rSP[Rg0=s+++?7j4gϞ% B:WO]82pn>+tLvR'OZ ;b/H8O2=wڇX| ֡TyO?^[__?~oMP6,6_ޱOm$7>s?$ғ"//xt.絅/5[K?O?~wI7~y'y/}?Uw?s?3Yb?-_sO. u$5%][[#u' v8Ŏ/=I2=2*q{hk/i^Z_ugBơTo8qO:u鳧G׿J={4=}HQ;B'?7BGHȩӡ?9rs6ϒGCSg}ŗN:uѓ?'!Ǐ!KuґKGiHw$9g YgɇzRK'}ٓGH_ճdORloosKka`#_$Y/ǏtU/>{dLBN?zdPuNS?8?ZFqE!(/\noO}_a~O=0/C߾xx(,]lȿ?+tDzSsϓ$[oO {EG$|Wfa~}}f+q]+@m$3#oţJxs@*IMG_a.ɋof ~O,Xfa+}W_B3kg~}1T%^IMI}II݉$ YgH6gƻl^>zT}W\ꘑVycLj"hwvv.__K4˿ e*?oxBgsvʕO6K+;/F(.^4ܙs]zqUmkDgM=ΕWltφ2swv{e+UFcɘѮohZ_T_%~2,mS^:\ܹ[ A(?O_@jǽ//5;@&%ߨ"e\8o8:oAg\ ѕEbпt;ͫH~K:Hu^mOA" ϭ[.//;wj{Gkf?}⏶;ִ0dOPvAo=:t'=4U7ݤ$˽^}Hڊ&73\mF"'?nx,S3q{<)%q5nvuʁ6D,q4s8U%;Ԅpn5ܑ!xƄ5]w?ZI(.IpJAn, 7sZ/:'bD"BX@,p8VB]Fcr2 c0fԄa 3jc?qƫ{ʓK5ܑe,DB!, k٫+_wwẉ mSmjMݞP8!"DlۚcN|TRe0S$Z KB @ C!N6MRo=`$n;8^GKX"BX@^׫ &om0q Fa _XG.cBahyȶm˲L4 ùÿF"6^#Lva){B!, B+&V$B^\A!BaA!B!, !RDg|p>IENDB`simplesamlphp-1.15.3/docs/resources/simplesamlphp-sp/0000755000000000000000000000000013245225037021420 5ustar rootrootsimplesamlphp-1.15.3/docs/resources/simplesamlphp-sp/screenshot-example.png0000644000000000000000000026605113245225037025746 0ustar rootrootPNG  IHDR-iCCPICC Profilexڍ 3wϙ?3?- ""aQ,[zw+AA6nn)c]t9C)fu髼X4$́x^昨($@ruXvHD`J4GӃ6D]\ f$>7B!Y744:d LE!>RyU}nMFXjppq89{Tҷ0"FBH@I$2GZ -#+'HSRVUS_g`hdlt,,m엯pXuOϵ^됹ni@ y֐аm1q;wڝ_&KޟzසIK?z2NgWVs9=_pWҲ+oܼUU};wox'-O={eWwOo_o a81izf3`p b@8 U(]=}} PV6$0n(/(|1" 8~!8 Ǖ9(ufơB@A Fj%!Bbu^ X)~)r1T J *0( @y`@"[B&W,(I ^(xcURZ@YUG{ ͏װg̞Dz^kf{ 3?ݟ9خ׿oS_(ﱱ]U#.%>%N%^%nuٮ(;#ىU1QRS ĭįNUvfjeccccccccc #>Ճ-YGv{b~29px Qޣm)P9Ǐ~Ѷ魜͓*q+±! D# O<(-\=FR5ȑ6:I b_bT3<(h@W.nbD;\D=x/ٝ_yy睸o}f.Fi}|j2%ۘl{ždc RAccnlJ] qe&NЀoB*tNr`Kޖ TF^w 1 hgQ Ì <*t޷hcw>:ߘ(XߋQ&yS'<$n[8&'ZzŨ盱1QP==ԋ5fbbbh>&Ck ]3F(QR,q k+~\E( 4-ktOܹs', qPQ?C1t,Ayy:VXYxgϊeo~&}ӯ~a8<P1m ޛ0Nl><|2Vz1zMY'?Gq4Al|/fT܌_G܃~x+Ī Xw{p]7c.O}ڃkmI ڶ39\)S^l_{oK飫qRw1*-ۏF̣J^.1/{/1TkimGjI$'Kv)9z/%].θZƄ<+Ǹ'O6'qnبl;FXK?۵n,;AxG}';~*d%p&4~p\GY s>]K{5) {{Z]MkٞOc6]}\9|h {n[I3IbҪwH/ Std?"}mGb'>β8fS'%Zw|+ew%p(}:+zgY=>RlpX ŲLr$l>ܠm#k{vccc 60&TTTj :ui'Ot16ڗc)Hg3(N|8v>y~< N(V?)U;qw2E31Aqгno㳌oLw7gx&^q-OC~i%*Kʵ9 ^|#?嵧]bxD),Ԏ-]ae%v%L&rɗ\ YlXJH-ElUG~%wHYmvccc 6뮻*}=z}Fizt KyP^7f׾uAIhmZ_#`rYGgsh|dŦ c,/DŽ??u?ې2ѠmxǛ?47w,ۻR@ޏ,:~j{8u":gysw@x:+Լ}GDV̒#D_/Nq+Qܞ|w5cj:Omyv1u8w%TʺMuWjס.{W\]]{Qur]/Y7U'CٖA{{}1#DG-8Xݧ$^/ѰW\'vm=hwe|빬{,!x[=>kZ%pm-kO_.rc,x%dc]kAÑwp~xA"ymwƂ' *FG (Jw)Z;" "u0ܽȯ|S)!</*}v.,(|'E] >:?R#xQ,,ytv\V:̋w~䱜EQ+VJn&?m+xxl};?YN[_.}ُsSۍ-،gQVV&ŋFоt KyP^7kҒDs7<QKw^N:5> ]5&`wchz?yx_X \\M|9;Yw*7G`ف񧖙8 ǐjġ.ƶWl/w<؆XDqaTUB'Ь.E'u=jr3 Ũ*\.eGm;74i:Q%r؆BرC23(39btk}^J_[ 1aOrxXy蘇Was>‡e[16;K9hǾzPUX9 o~9k(߻cg!?1ye<񏿭R<{,#Oc[ž䙖núQz~ /Sl=jC=K<&M+O6`{qy+ %]e[96 r̽`d߳Jm#mw)s2anv"TQ]*ݵ/U*-lS'?j^5{V<(qݷ|k2s{DzE۝Ҷ.siMx6};M5^)m}M}k$6ff*`cs^k^N}]mJlFӼɓ'?9n&r-F8< L2cǎ|^X8l{9]te:~vCwkh4vFG~sQ❏ qY'L~b;,,a6""B/CK0KYY666666660_fiP?.`GmD{f؂,KA /c:avĉb*.0T0nܸQxe0;fg-8aVҀg ̀=lllllllllAn0KJ0+f؂ f;2^i%pSsllllllllllA3K&T\Ov& 3T_4hhh@UUEAcc#Z[[R,,g >C[Xbϟpk͛,p!owƧrFcXl? /--q0vUbů߽[^a?ۉacoK*G9KyXh>vY66666666cq=6_ 7ήu87'O'̲X,b.0q?ڊ=E~[>7#ǯV7O1nig˫`k0s_iNsӜ49KO+n۶ =cN??b[N<Lw80a>wCAAA>lxNsӜ49iN_z:XaO'yxpQD8>kx,ŊIEC쭟aԩ^}g /49iNsӜfn݊aw+i Xߊb+VL /os` X^L<9avʔ)Ӝ49iNsӗ0a?߉gj!q t౤sӅv_ěZ 4il0KOWNsӜ49iN_z:a?vcA~ ڑl@;ƥ}i7=lXw\eŠ٧zy8iNsӜ49}i順{`K+2;̎;ʰ"|_phrہ)>-̒lĉ49iNsӜt좂Vd;l;&㕂,*Q` l6W٠@6 8iNsӜ49=頇--X_fCJ z36½@ kU`V̊yYPNsӜ49iN@:av&X:ix)VQcf*r49iNsӜt6!Ԇ+06,^Bu:c@F AǏqӜ49iNsA؄ Et7vanHM>6`vܸqbN42NsӜ49iN_z:avCXNJVoYÁE* 0K=Ӝ49iNsӗvT`6~oXlN+oĬ.~Q,eK+%VW }'Ӝ49iNsӗv}V#(0wV<މ~ LٸA4/5,>!)$ܒ?˃~?rpy_:B61{zX|P TlΙ @W-ǭ;_FPxi%.*h VfG;&㕂,*0 G49iNsx-< yV F*AV' jo~VTXҹ'iKFjAh=ykE!--X_fCJ Zp/sH;Zm$0KFۜ49iNsNgX'k+FdX=g"}iz-#=>x]~ :<ߪU=!h.'vzi^䡽!ΘMHt N'R^7^f3HU`6 iNsӜ49+M%JSUqoL@wrcX<}0C=̒`G2u{`R7Jp61x%`+VAHaW=f6 i6$В6M>+晍P`vyt hQvP3KFJ:ϟgpBT;_;_LEtr:yg ?Oұ|fc|M;|L R3P̞i>3gk)349髛~G`/O +aN.(,k3xg$PaV>@z \.To*PIzV3zb%ĒwY0Z}-Í7!7pu= `bI:0XZ$j+0KS_tdW6=BS/4Ja۹'F8Yg %/Ǿ8GeEY 4wU؟`o0 u[J@K6MjZO@+1~7|kW%~ |=V,/bt| Ĭ,:T@\ QFQdW62>鑀ԃdfZwM?69T ,pJ#vIfe>O%=~ңfJ^|L_zZzŎ49顜AX߹:ᕥz%sz3uk_+t@KI #F׿ xY*=:MONՀՉ9vnnUjHT`6~o;bXӊ[11 3_@t%x X]0k6twv*ֈ/}kۉ}wZ]}u=MxpVJ~[_w}0[}`vKlfm_Ek3']c9iNsz(oM«(f.Ё>T2FV,=iӅ `VN%g 5 %.7~FWVGK&n@PNV~\lFEXǴNsDXe W?xUx466xq|Rr5Ul=?طbv+ظ'N4*Vݙo9r_?BcS}e~a=l: =]_hms4溴+oWyuiCY6Wmجto9k&)܌[wcsYH]xf{ֶt{*^pf毈|oסH˼6J?}D ~T*?jVT~Utx7eSb{q Ɏ6f COWJ܊7 ߧ2RS)#oo+k^>{R)G#⽭ܮK<nBG[{dK>iNsI K K&A `@LF%0zҼ ^{e0fu^Y Ȓ&M@ǜ&AVzi%J4]M&W *aMXYІ[1!3݁j 4S;`XLI'mwoFm g<Ī:Py ^^О"qnwUhRݫ:Ƣԝ*E+/8'7mdf"6]Ls}A|=y]i߹I6<7~スZ&OU:4{LιJy\mŁ'wG <ؿϾ|W}qڞk6>ȭOaq4nUˠ{Ko,9k+|94斕Ġ/$Ȋ0-R>ʹ[;K@r-c& Y=R]uZھ`Cfah\;K )cf㋭.atB w^2 ZM0K(v^"#.ݍn7yH؈nS݇ :RN̆steБ?:B>:xuاs:ח=ζEѼG0ag/뱲]/}طTO~v[̮lϭg91 ~~wkfUm\S{ۿ<5%0Ko?X܀8iN_[iQuz~W WUY J>`VȸSw/#GFe{,?яʸ]/#n^9+bYzzepeh0~ݕ3=#f3X#f3aLʧۍC@|6A Y:@[sHl1SS>ؾJyo렢lbʬĕ[k7cιc3XZKQp5zޚNlC(s}~Xm8oHh|@AϙxN&Juh[T-.6>'ח۰y眐C11^3+uo-wk/C);g Kup-۔ XɺsξsdG E}'9WA9| <9W^[Cqz_[e꼰Ԟ2RMѽKoWrwg'}$qg9pӜ߿A]F ȤW ) d'~i0+[\2$3 fF^Mu NȌFM.Uk.^Y N0K[eacYwVA+MPMn*ӕ `ŊVؔsbv_D*fE(F(DzZqweo74b;tliyNk;iHw.4i+Y}m;$[ZWuq}=ޟvVkp\VOr^V+n{Y3'ذ{'.z_93^3˹Mst泮ۤ Jx_fe s?Kw>+gbrrCU9/!gZ_[Y=Hg$_]g*H˾Y{ +DO HO9k'-HЖ}_:=2@ jYO^YvIϬ>98׬ % KAQ.Y+av'@,+kŦY8^;+=z/ rRuWfZԆ{xb9L? `) n8;5`v ?0%.oC* \a6gd0ʗWue +'q1nd7d5q`~_UK<WpLS|lV⌗ЊeiG\X_˹rNuV;tmC28P;yype[lyuyiz-:~嶏`B3¬2ZE<_]U>9.wrZ+~Oa/=Ӝ`Nz鍕ғI^YV=]+ X d~z˷m7W d ff'j0KJJK0KFh%qCK&C D*睥]ј3H?؁R;1>wcA7f/Oy63O5YBWu|G{xw|+H5 ==PXʞ0Pօ>`ߏgz>Hx ƍCaU"IH,9cUP~\ \my?s/7HΟlv!Ӿ3dKpan;#۞=}9No2r./g鼪K\Oqͦsg_ܜ4]߽z璯zկE&AV¬AVl`v3 ި<f X ^ DzYOyX֗WB `%-6he,yf4]Tx㕃-(v$tt+·G P`v :=`"0m;ѴJmm(K{=3i[Pⵜ}/򜤇Z<^2e /}HѶ^AG(۽i_UuSBSiLJ]^ˊ`g \.2nZx*m[WñޯT}2Y⭣rپ|20 <1ߵNEU-3jBw;[ ݨ Gݗ3.8ñevFCnD*H#etmsb2)e68~.}Y6e#U(-&@8iN_;S}dAFӫs$A8S]Amqg6Pf;?KnB4cL̋KPKYZNAbOX@K1t޻K$g4u"%W"@f$Y:l s`HT^-WGN {=Hovl{3V`7zҽ/@l3<'mty{|[muR%nDˀ*<(sZ7 48` {eMړbu6PI-n+e*y= =eEzjm{[/ne >i4nE@>@z`&7KYV%y_ޚ-d[vWx2Wp¬dJsNXSS*[}w O;ɯ = %#d̬ʘY rqYx=+Ef>d7]9\`Vhj 2dR0UlZ 옘ގW zHR 3;( h{GHzfb>Z mܠ;ky>{_z~辶z'rrʹ67?b⼳+ ՝N},ҝD'n3f܍>wU=>>iNsHobX24FJx!K^YhXd{yf)܀[?t߆yG|N)=5.ϥlzE(\V]MS1z;axݥLXqdRaF 9/[կY>GԢS?y6*y>CsSWR˩Lk>ʕ)ux7)Ǵl^ڷ0.Rl\7 |~hj1J;8GyeލB~m=aƏ\`V's/\0NsӃ;}-@[(9{y` b Ne+Q*m_Pq0 %Jb0IhJȖ0D'*0KerHgr ՁTr0ʗA}V.EoT_Z9<qC 1f;b@O㥼nflA25< :/t`ҥXx1,vY\gx\y̋Ky|21/S/U_2q Ӯ1F.;ݮҞt31ORb`g^|j\ϙñt^9*'oq.\*z{֌خu~UλTA_49}57Gz@x>ELA+A*"&y'f0 #$#$PKYu3 f%%XRY>z% fuR(^䝥sɺ`64+W֝BeK0KOH;3ۄRJhI[&F(0H:dP0Ksȑu]<\F.Wz[{ LpT>e3\Cݨ7־5۟Ӝt}[N'"*PEfwxYoMw1+ycq<㝳0ʏ@Rp4=j_t='\2qCKK N0KF"lOY1/A4^B ?y)>i )MH6`LPMr+jnl+%:0&,&o"`i9Pvl,-6/uqk^+=ͭ&i9{j;nNs;J IN(W"ʒA U6/+-AA řR)#<%`<4; E!̀%/OyB¬aY}H\KPȥx &f/#dr`Օ ar8avCXNJVoYÁE* $̒ OO+:VحHxZJ?#n6;.k`u|&p_9 M3Vh/"49xB ȋIPGFKI^X5zb +=^5P4_NcEHG$#+&'a<~fɳIQ>yVIOO齉U7C YX:KPLp,=TNZL©TNFd4n %F[,WPnl{[lŒV<ߊY]"(+SA䞧)88iNsӜ4ub1%I{# ⼁,M]EFӣx;%/-MsEp.Yz_z4)W- mpdE6p.iK0K+V@:SHSD-VCuP+UKKsВW[.0AF N?;mV#(0wV<މ~ LٸA켜49iNszhGh!dC)AV. zHif YJ61{$.R|)šWT90KԒUVZpছoJJP.>\YZ,kV+=2@Vz%4/|5zf%R>䙥S.y rj 3nš4ˊe6<ߊ ,,VTɧA4zӜ49iN 1xY&L'hf}9\X@>ާ8T}%ϰXAZ,FIKJJJ@+UZiz?IYW954OXh)MKJ^eAp#vj aV,-M.!P7Hl|E6Nhsٟa΋X\V1ufru3sӜ49顛0E[d#q zffl5 F0KKfQ(9ݲr,EN[k ) /*yNeS V4mJ%p%.#MK"VɓɅD#=ìh,ՋQ;aVC&V6Y zr8 rψ ֈ l)&vc/_fP?`F-RWNsӜ49=tӷv(;H: [zO|4%Yz^diWU:wVZ9+&5A+P ߖ<4KNuEd 7;$%,)ynk'k!f@KS|-ՉG@+N@Y1XLf AVdTgv靽Ntk-V$ذrOƦ̾"W!6u,@wĈΎiNsӜ4f?($!Sf%R{f)VB,Y5^ *Ji^ 4V{ru.8Ef\mhr\R diY S /SUzΒwZKVzg%g5mJ-Ke NkRR,v㉵05 W]r2 Ї Ӫ[r^Yi4_`X\:', Vb*vYK2VX'%@uW+Y-`g7+C.C}}8&p0i 7,X BxՙVĂB=Lf$Y:l s`HR  O_8iNsӜKK,y=!W%#8QOFJ&= ]H=̒ђTN ARZ5@GP;Ie uz#Qqn'ۜ~dt~&};Ѽt=]70UlZ 옘ގW zHR 3;( ~򓟈d49iNszoNP'O,y)fPQ.vnƦV!| ,@l?I%/X2vTr&zo,׿u_xY驥4!ku0-[@^V9{]_o9،M! fmv"ł ڔg. h*7~o`avK ِ֗RbC3;cy, NVf 0NsӜ49=Çwze!zq4RX<]dr2 #[<1K*V>ޗ YZP`Vw +|__MF! <-՛ BMJzSM⇀4jKuE;PP;Q{ѱ@*szgu0Kh" D0ہK}2/uum6Tf(;49iNszhoм2Ӕ\[4@EFGU,Ƀ*4mcj Hi-uKS?n,~Qz%X%%#%pip 7 ! CkZY#u'Padd|.sڋ --Ǘ[NGF5i4f6 i6$В6M>+晍P`vyt hQvP,*Ӝ49iNu!hA,JcҴy&i-XE}ƊW2F3&Б!%#o%Zgf`Y2뮻`o~k ~k_wY/+T7f]yhB Y4MrʃڏO02Pȁ`N&XXcb.- `?^9iNsӜIr-Nϟ>@>])MJEGx_5@}'iFf]­ZSʏУ|M?_Jܬ*͹]7ہf'B#KI/b ^4cئ\jNː z)|+=} MH>`E+Z1:OgubbF z,MAvmqӜ49!zh-9JzhU4`JnVo&FǍ~X5[@ 7M1Շ=2V_:o 2WN%DOK&˕2V?vVoYC' SBk3 G [K@+ ^yuz`E0 mGlKvZ`|+&fuaxO\dԩ9iNsӜHz뭸AY6s?x̀V_xQ!A"E@K# *^SOYY ַx2?/U fia+KuvOTVvPb ^¬4 ”:bii岄?-ch8aS^7xDyaJpK63w}9GiV*u^T=ݯ">Slblf}^T̒VzY Tg'ySU9{ymAp+gx /4O ;gqYgl@j z󻱠 VOFY666666 _u,-01d%4Z@<3)K3BL-M^Q s4M`]3~ d#HSLYsz<Al:柕i 7-(v$tt+·G P`v :0dEI0+I(h#K3 H@KT^HX9:Y\#(.VQ>;+C $Gz5,gfidb)@ K /AV;.AzziԆtmGH?,AP_.P`v+@f0g[X*b-@N+,[,ŤJhj,%x% +d5^\ݫ CI@~>lZaG>>)`Yg0+cfif 5 /_̒wB+d+6,՟`V_wҫx_FD7K-=(]T*@6̎vLLo+=XT$T0666666 3 p%,@0+K͎f@V5h;)=zOX=V~ҌړmbѯY}|> C{}7`0+'xԃ+`ǾŨ;r: aV_wie+yf t\fhK ِ֗RbC3;cy, NVff؂ P "Xbuld3^po)L(Ԁ`r^%oF+I0B$xj!^ fi[%H$^=RaVTgJ#0oLJxכni_jc*I !C/$fvlR,S`ɴv׍׵ R>;HbX,54yd'̆>,Ђ+XM\ @v.0K:W0+rl h)t`VřL{Y2< X,Bss.h;WPP89)&W¬\(`V K6 f-^y- dhhGZ]h?8P9f)̂@&ڐTBK06g6B@1 EAW~W~W~ס:k,7PYE^0K gY=JY m$p3«0LFo<]m;G@K@/T'pLD6,.zJP/=z˥}{, _66 `t7vanHV;ƞYbXA&@9 ~"%#Safe?0+W"#FS'F`GK@GHF*K =0gӪ3d\r~]٩Nu鑕0+Z9NZ 9ЇY4XWߗC§g2@) GEFG3G=9 e`VZ29\Εb9Wm;=f') ТVL?@afpf/};xaO@KF K"f|?Y2Pmp/L49 :7.}9۬FQ`vEQ;h1x2/m`qǁ ,bDz{W08L 4I]0afi.U9|tN^GJj^GJ끖 u?٬)sPK+g W9 r0EД JVo0kq؍ r*0`3JҫrZ2 # 0+g7 !TaT_ AYi T>Yoۗ>Av_V,+VL /ow`a)H> 2̲X, B [rz's+`MwYV$a;Q@/ #W6YҪ^ NY +4 d*IJpIpM"sR`< |wt$xMrYX|WVn9 Y, SЀ7Sv:-qdp7 cf㋭.atB w^2 ZMceX,0KZ4Nk5FU 5)#+猕Ftb;K K+` @Vrd䩔S_I/-A6%yeXA48헜%@PZ*#,Lrfic@j7VLiv-yeM-NhK0&SL_`! F͠abX,V,Am @0%/a%%LzK;`.`'%H&%ڝ (F0jzYc%˘a'VB.|&'0+VX缳wcA7 ,bf i1MӞ|h;+bJ hi`bh9Sr90K^YJ &^Y,?K ?|p7,3^Р+g쥷ZEC¬L?q:?5՛..Ҋ?hGbI'J"|yD f7ـ ,bd0KJX՝g7 h@K RJO@`<s&2̟3fTKYYwOYH'qZ Z;crVG#;Ax i7&8=|U/ ̮?؁ueHt,l X@l#ieeX,D0+e%БS"pz_:='%g901fJYY:'yH¬ 5u%#@KS7P+6O%cd d7ZUfzg%.*h VfG;&㕂,*rbXY7*Ā,CR2܀X.@`*W&~P }|,SqyY, k/+P|+&d~;XQ $bjfY,b)Z w1^ZOFpKJzOUVn+{ɰ=f@m Z#Y jX7zKY-f6؊"F'0{E,.b՘:YbXhW l&nuiX}HXiFW̚'O.aglkl6Is1wXzf3geX,5i hyjoS`dhrդnTnބ'O*֊ K5>*ZQvee(+CuSu}m YAV]vy:Rm/rRUTǵzѵl#Y,k-Yǘ&ڐTBK06g6B@1 Eڡ Vča<۔}!)~uƽ. ek^#]Nz)r9n* G{fPPi]9ʽ)eA6soDZQLG= ZUDeX,`ޫ ٍMbDX$][,-j IupoИDPScQ/.d2YrqpmM#R.cc^byhf7oc^߲X,k0+ |=V,/bt| Ĭ,:T :9+pB+@02] FOW%u)y;/ 3uok5R_#S݊8t]R2B$ZNd]%lmRĶy.ǎK$0W5DgMT=]F1̲X,Hf#؊%;x00cEQ DWUCf{1 EPiPQ a|/'}z*ƹq 0{.1~'WGD:29@mTP3/`vᅑζWc樾3j\$rx)V"?#dGþ m@QN *:'=t S"3P5õup-R)Ģ/2 /S]}~e9ďlK)TzIظjK;r&S5mb1g9۬FQ`vEQ;h1x2/m`wX^9$ mq3SXVA-+yDavTtZ7j@aV`p+X 2w3XkX9zj}d<ʔ+s,r:F ˷g MMh2X{W/\+mG]nl3 Y%2|EL!>۟bX 3nš4ˊe6<ߊ ,,V(|ˢMb9!n[ W&*/S"=e3>K 6bJt߶KY*Sxm0{T_32i3W)ľBd`8Wf]y߇}.'{?DPVmܼ 9kuFDN~"]1'{$TOlY(fGSG::xw4w2"#rK|oEf*|?OybX 3_lEt Z\g"+([& ᩹S*48rZh.38g,gUs̜260[:BJ;z s}U/<'uu郰x՛{_EqVX'=̾a=䛹Mu\qǕeeZhJDԕ~׳'ywof~=l@*.|(2x(0].~t\}PyrշCqY=8Pp O:CΪd׹t ͫy 9q2jǁz k;VYkJ=3^ۿX,l?`vXTb=rN3z_!h:_ їh`X,~f$Y:l s`HR  `bwL'5էަm:Tx0N7+'`^=2nƁF)ys[<=8Ww2|(l3F@=amMnǔe|ϔŒ]%s[+<{^%Y: 1 ݪh37^B=u:b&R\$MۧwC_,b.*h VfG;&㕂,*0#vt.u\n-08%cu4{lv{J$RG;ձk{ERώq%ڟbXCf`} )%6k136½@ kU`abxfXb1f;b@O㥼nfl!:5r F1"abf~lnJmH*%mm|V3 ѢB-,5ՋBu1}u bٍMbDX$][,-j1̲X,b1nhB+bXЊ-x:v8 S6q ,bX A mGlKvZ`|+&fuaxOfY,bfm9۬FQ`vEQ;h1x2/m`qǁ ,bX f݄5i mx(2?XX O1 ,bX A3_lEt Z\g"jlb.m{6666666bC f{FfFf`ØO19swm6z̲X,bg6`vXTb=rN3z_V^4bX,a6a6JmH׎'֞󘯭sS ,bX 3;ZjG~;Ƨn,( S abX,a6`vK+2;%x*݊yH2dN3̲X,b1.P`v+@f0g[X*b-@N+,bX,l좂Viev$cbz;^) J / ,X,b1ni2RJlbfgl>{#@ תl,bX,ln"@q >֎6Agyj.bX,`mBZ I% cϊyf#]^$2ZTeeX,b .&XXcb.- `dž":ڐKsyQaDE׿,{hv٘hz\ eH9^+-@DX(BBP{e!f#ێb+VLŒF)] $VW EUxҍ>:]ɯ~8P/~9บ?ܮF }5 :}"5LzGEm"P+6JCvCa7|k0|B5 Se}` ېϾ:l?wWP/gՈ5 ̮(j?Ow⥭#,S`68rl['ͻz{P>RȂw)2*~x%X QoXH NoZN4x2S%[y9a_g=q4#*$y⁴W1B?rop߶HD J.> r7^)X"ͮm]cT\߾GQܮzaYut¬Mi˸:UkX6ꏦ|^3NfGߵ&L f}gмo\ì s"<\קJDW7۽7}oC]w}nz#.}3YN_Ӯq7u|"^l{a2d7a~eŲBońv+@LY Czz2ґ}EAzߣNSoX4{*.RU>|ۊ#D M7$㍣R6"ńFܖ|=&Ҭ s6dkMh굣G>3{O^Q8fEͮ8FXn5-OOCH)_mȋToa5OOHo[r3Tr͇5+WԶ{$ 5J4\܈CuF^z,2f/yDj^1Q>X>]5[UC>f}gqq0kþktl/wj(W!_b[g[]-ǒ]mύ^yfm篯ν>?o]yW}q7uکpO|6z0^ޘb+lЂ? Vcf0̆#""BpY?z}?FxK| /aFv|q>;LҦiɡM xpgV9=ʍF@Oo5x,u0x{Dqvu1)OMBѭ󸑷0T@Gܐ.#F{|<2}ѐl/ Oѩw{QX\D+!>3}5m `@nk 9C(܏A! Qu[7#tP \yi0=~XP%i^g}PO5~s̚otq7ש0;PI?g2RG+,^|?;zAڑߎ0= @l%^O!3T/ Rt#Ck>2ʋtuH 6+h&R6ᇴl2Hmzo=2&uPq_ɺGᑺ: ,}PnH¡dZDEƉk/}Q6@mTy멿~kiY{7lAD룽pma^a>'@0Jj{j޾i\'Pu2fm!aֆ O&bk3Ly紤#gz0xlooa>>fo[yz<:H}ìN\=Whv0-|'{gf[ZQށH,SVo=@B5&uKK髓?Y` 一UFOzNzL4#oҨמn!NSy,ɈP~hWsel;_'?U^~պWlqiWf\A11 cR>nk "ʵŏea :K9iz}+s\z]'_ }`@NAGz@aVwLW3llҥ|JWz T}0}g߿V#,٫`ŊVؔsbv_D*fEԛHDFEWd wfµWcrqaE(V "㊛_ S7\py.SljG""4QͻtY,}P_ ͏Kz_Bpﻟ۾^Vyw4+m*{{@ۘC-[teK.>v;sn(vym+m>u' ^`9Y3-5O(C{K7Ñ r^K?m` :߻Pd7=`և؈%ցq3~a{?=n}6ý/z_Ⱥa6JmH׎'֞󘯭t u0̎T>D 8\%O{CCC[((iQ,&y*ƧI/hyiZE.LVx\s]e_KVi+}캛]^CLS`B}Yr{@kO H}f®X6GP }߽?np_.<5Z3wNt~}vWkͮY6}YȺava@j z󻱠 VSCf[膳5Rn-GЇ/-QRwѸPzFmZ,2 Ywi@T<¬3p(78`_! B,YX7c4}}t=G(}hs~Ag;oa"miSnGww3Cվr"wG <ڼ7z,C|^pX^@kO P|\u0;VmHƊCe5La/"P?3c5/9ߏkf"ڑX҉ҭzj CM6 Yfey􁍃އ@/IB7ɂzRp5FIx.mʔ~X՛H^gyM+ JSFqr#Ss̆yc{/6CI~{CA[ Pir{|8>><)̚\gؼ/[y,i`zikO P~(%\cUf'CCG~7 sa9p?0Ywyf޷ #چ ̮?؁ueHt,l X@l#ieU?SuD"͎)z{OVyzӇ5] ;Y< fg:У^գ=rn>5w&k 0x~-4ycu xn9/fM 0_zVSftV}Ǥ}캛]7Ϭ z?Þ@G<եZ:{yf^i3ȽRٱscڮU=_Oo/ꏢ0eȚ]E<um좂Viev$cbz;^) J / 3p/<-{qj̓6ت8R,#O0ݿmk: 5H:*CE.4X{aVH"pXٯM P,BPPӆnb 0whۥj;O5_0m9buw,$h@mewW=ͮ5> Kr׸AaEkr=a6lnm FgUn z T j?+ߥ\c{ V?7G wAsoP }߽Ϳ_}3Kf~t0 i6$В6M>+晍P`vyt hQa6 Bvv /*bXצzv!L'͞5Dav&XXcb.- `fHDDDwfXkfwՀ! |=V,/bt| Ĭ,:T@\5,bX,ll{[lŒV<ߊY]"(+S*YbX,`[6k]QԎ;qL+LK[?GXlq`y%,bX,ld7a~eŲBońv+@L-,bX,l[]d<f(e@l3X0bX,0laglkl6Is1wXzf3geX,b 6tk-V$ذrOƦ̾"W!6M`X,b FkRR,v㉵05`E+Z1:OgubbF abX,a6`6R-bN+oĬ.~oѕ@)`u,bX,l-gՈ5 ̮(j?Ow⥭#,S`68abX,a6 f? bY ŷbBgxy K@i abX,a6Hcf㋭.atB w^2 ZMceX,b 03b65b6Ƥ|ɹݘ ,=h3̲X,b1n:+JlXcSΉyf_}Qʋ&X,b1#`] );bsRpXwabX,a6bf `RKHosߍ_ X~ d62̲X,b1niEFy#O[<" @l@iYbX,  ̮?؁ueHt,l X@l#ieeX,b 2]T*@6̎vLLo+=XT$Ta;0DD&Roa=t4w+w+P#6eHDe-zGF iphFRӆ]q@d\6kEqv2"mQhv塠@gz=z<"]6}Ͷ gr_;0 f`} )%6k136½@ kU`aH¬a#1rH+|х'[/[V=jҝ3=wO B=k}Y.>Q!"q4}m#C"^pi ۀkd(}wLH)|~Wې!"*ҡq4#rz\r\HGٶA^kbfbf7v ҁ8fLkKyx] UCujUnQ=!na{/p E 89~ORr\',"!,[p+g9$(m8C^n/?Omő: Y/@8,t`QsX,ìilnJmH*%mm|V3 ѢBYWPjd#.f[K4 `Iꄄs[o.D ^rq_1BחOx5Or4 "B]˨J/-BJ1֧аCK'ڸ[۰<:{o/ԥGkP" ¬͸\>۠y6|54o אU ^d*uOWfO\$jp~y9”tyv od6OܿelPd7Y07ޯõt}(]<]s*[/1D*Tq`f3I(V s@Bر!fЍpxQ9rݺ{Cr#NhTm8\xV!\`m>CMl[O}Fd1$RɿYޞ(K ۲=pyۨlBp>Z|l#ѦhqZwKO2Os[=ҵ0@>5L! W_9F_m]1k3d^R5t.n+Bȸ(-$ Ыd6z`?qdۿ`W$6G߸_;)(Gpz KEa ޞQ@b}T#'pjȌY, MH>`E+Z1:OgubbF zfЋ]닛{y,ntnFrzGC]zYu|GZq4NVʏزX  mGrXӊ[11 3_@t%x X]5a xYj>h(p\ՋG(NoQj&_*i@JFoia~XXcW:9( wȨ_qzC\Ґ.`P{)0R^^zom}25T?G5O]hTӣp];ڑB剀7Fih;qو S}aP={o̯Op팟@g<,Ǭn&>,~#R,bjj+ a6XjfWcvӊ';ϡ|uaqǁCfv7rn2=#q5mFgQ/P{|TzM{\o^s{8-e_0ȱWĈ/r\~ᆹ}n<+ @X[Cyq+np7!PhT=v܊:5((8;.pͶ Tê]QQ xپsRY׺y>cXA}G7Rsa6 f? bY ŷbBgxy K@i v(k ‘g9.Fh eqhvƉi>5mFyz7l+ik{Q_Ȏlz6 ֫38hubakziΨ@F'"R+,xI6ңn=3Y㗨Y{G̚(0FKiqa[,(vޮz3m(WaDW94 jЭE:Ӂٶ_98Zz {o̮O@RYs^@AqͽQ,앏/"Ȇ -x.3yˀj5f6n4=&t OG&lI{_F@|_6#=<呜5+ ڠ\~f]5+7xiR}#Թ;#/ |n{\X8q0@"J=tz؀m}`tVz-\e q\Oe~2i2 "-Gěnmӗ;Jؿ o|ﵟ{#\V!gOrbf`A11 cR>nk rtw+{vtwx+396/zG}ym/Z |qr}2kqٶ_ml+302_`;'0{`vXTb=rN3z_֡hbX,+a6JmH׎'֞󘯭sS ,bX 3;ZjG~;Ƨn,( S abX,a6`vK+2;%x*݊yH2dN3̲X,b1.P`v+@f0g[X*b-@N+,bX,l좂Viev$cbz;^) J / ,X,b1ni2RJlbfgl>{#@ תl,bX,ln"@q >֎6Agyj.bX,`mBZ I% cϊyf#]^$2ZTeeX,b .&XXcb.- `fY,bf f74!{X^h<ՉY;XtRjYbX, Hf#؊%;x00cEQ DWU ,bX mV#(0wV<މ~ LٸJYbX,` 3nš4ˊe6<ߊ ,,VTɧZYbX, /"Ȇ -x.3yˀj5f6abX,a6 rψ ֈ l)&vc/_fP0bX,0l0 ZH*aM9'}mEDBlf+/bX,00ׂu6XkkajyVKQ`v9`)YbX,`?H-#aSaz~7J`})0bX,0l0XЎĒNtS Z)uTZݤ׋upgbX,٫y-XWjCŎؽv<|mf7֝r&6 äme#:NKQ9 HsD .Tb0|,bf}Aڑߎ0= @l%la3t: }h-+`/͔e=?aSdee)bX,Y?`vK+2;%x*݊yH2dNɘYE꼯#Ѯ]θ>1o_?jՕw'Q]X]+zt9bX,a7.P`v+@f0g[X*b-@NPvAn+lEWu+;:-ȉQғ9lDfP{31ě.y5 ;Z!?z 谣,'F{8Dы}cU5y{)XŲ 0sL̋S(,LхCƾuvO)KJ|5B-Nr#DFa@6bX`EdH8`vRЃE%@B^@!f վ/)[ ]F K\Ƹ>f\W z]`Vރ܊<% ϴ{teu|O6%Ϻ&#SFy^+kW^feȩbX:ni2RJlbfgl>{#@ תlÐrxq9:oc+"]`l&rs9N<Յ(pV# 0J7vS)K_V AzW(7o^~i{6&yWH]q}^nj?|Lu 9q2jP}AX9[Ӥl#\ʦɘ?aֽ,zh1bu[װQSQ"X,u{f76 `t7vanHV;0֔>(M6l4^3]aV?׸꧵r5W]n(4/Rf0 zpwYz(6àN eQbX4nhB+bXЊ-x:v8 S6q 0v>ցy}ŸJ]ޫ_3f - 7a p2Tf.afuPgvf~3J+vmgqePGHt!,b]0lv[dƷbbVfl7JJ afa}`ԼTö).^ʮ .1հDL75N郾))襥f S9qʅ|18mD$*ZQ2xl_{u{S]PX׎ ޘkBu9v0bX,g5b+ڱxG;iœxi爰;,d5YEMR2,p22RరS_LeJfM sD_W6oR>c䣫}y#E.X,b]00 k/+P|+&d~;X [i a'u#q6ƍ3|I6_|ɇEd vgtdw̌O,#X33(;f[ SТ,-- H]c>CC?1VOWtN#s]&z3ǫ.ٝKp3Mj/DMa#}gVڈBtN3\Rƭ}wFwx2&ksѣ8mo7v@C}&|nWw?Rz/B!dȬ3;ұlO %68uN3NW5Qsw`wr !Շ@0x\E.ϮSc,ߪRh]~k7OC!'Ofrnoy,!Bed tN%<3 }=D>w$:}i!BsaTcݽWş7 =2K!B٣7\Zž) ݚ"wEE>aa$0|I/#L3肕WIˇ죺.X9я}]!P p0T^I8Ԃ/kmcymc_$HgOl*b˞/˲=O@4OufMm(r©n{}J6M#hqd=c*T%ZO{|]s Ӵ{ ͿHEy"B(Ifatce 9}|[;~LBf UDPsaV(~Wo|MfB,<hGByrVZ̀_CSU]u12QB(Vyh"j~7@/xMX)$ԚRGgo 2Ft#re6b#>9}ZAB7Uf/*҄PfQ[f0-!*dvElۛ!MvͅRFz}$WF޲_j%yaRE+ P=biNFW|>' e] ]OɋSvc,js489w#Y2.7Ղ0Hf\mק͂4ڨ/EVʇ8΂vNSfU=Z|Snہh玖s k]" [~9zlpzuX|ܩwXθdiusܨyn}1HeNbT+mx&vS323`*p`Αu+ ? NRsqq[y5ѵ3δ4ID8:@:_@:զX<_t/-æ sR()'2`FlAG2p#51d9.~uūdDZӠ!-SpX`,TJ\kY-VOvz9ͮJ2RG*_^:m}z6w Er_2U~j\Uw^FMhpl@k2WB>n&[9ᒑu̸h/;+O#d1NNTHU:ļEY_iCdb~ݜeR⸳T*T:߸o(Y |^Pfad|؈|[gmi`K `WױqB)MQ{˹'|srtOd΅0sC+0 YDL`ٽhhzVX33Nt"U-8Z4UK9T,q%e= RJHgڹ|d6Exk4ni/eVJQܵɬW{&Dݢ Mȑ/D7ݹBAѣ^ny|y7n5#o:ykwUkBSkYf"k♫G=s>.}p̙{s?ۏ7;玒6;I N\J}" 9uX''ُ"v[./?D:/3P;nVP]9$2hd6,dv"ůN<6k')`e`[/Wֳ_͉z.a pȞ'Zlj)Ҳ#IrAP#*dC\ [Vʼn\ģg:6Yɬ>`vp"q̙$^mE&Qv#n]nu-HGk;V;Ef}HsGlA`(<%Nw>!颁Lζr2,4쀨:dqGvbnOpB\|_Z$;`:t1FJI`2_g;~ ~pv\vcMǿCH2~ylUO9D[Jp)bRdV큋v BɾUqN|vw5֚.HN)mp.!dt dpZ.H^u$?ԦnNmmyiyR+(]y7GH|HJTL\ ƒ(蝭2uzRS-"NLPzc;rsGNj]IIpwE9g: 9!RpM='%F>}u?e-"'FGGӮx;޲seAL1Ґ7?V:ۻ[WrPf<@Ů _:?]/񫃷Ul>bׁ_ |'pT^"LDb-|ܹ0Ry7!]YF@큎dF_DP!A{٫}VG/2|Lފr2Cc )n >ڜ١gxjx/Ʃ{xwqrfGZ2kI&M5.ĕ|Zi*ሇR##!kkoaa$0|I/#L3 B2&rsf_.v~Ѕ(B!~˙=bƴ%DnػMqt{3#dVɮ!B(3;1SeNگ-MlHπѫ9Gj)B!oo[ӣ7;x,- *eB!2o2{h:~ZSCs2^?YK+k@t2UH=]ULFCLA"o U=HXA anaG8 5]}gT"-R˱O'  GоKl>}~ llXE twJχq^;qL;.#B +b9s{A9{!QG+>dKJ9QFm=Hdr]Yc88UHkIgc(@BLIxܧ?@!y52E{r?9 26! x_lk`e@"_-HIPV0WrӵjPJWT?:zʓ9Y Ԇ$QUGB|S-" kG15Ώg=`]tBvJ3PgOymD[fu`\fB A+D>(Z>V_J1 /ܸ*d!#Tkzsb oD9hJȝn@Yq %[ʉM6 4X\z}*ej)VFp^FDkm Zc;姤ˬ0ob="ؼ~ʪ!ҴTi9VPё6NVYեMt.'AQ#FI5w}m+5J{mScf-om !uQgGh/")$lQ4v_IeT8fֲڗWcf-.e|xn ό-~f>s=8_PHV O\Ѓqd7Fȉ\źl-2NdP,PQmOo"k'$ƹ>n(Χ4Q\k漏V[=Vh ;mXtٌ^wED4buY6v)D"j\ʺײVYg}P$m]׽M##=TD+Qx~:YP6aZWjYByd6>}Seixv^9zr6&du,N9((-J{) s5ݺWQ:9"喲@SS+f-ܭ7,]:z)h.t!@OmB)Wf-mf#4=xԽ,u٫<8&nT&]&)|S̅-!> +H{D )ĸO#ek}^3^i?>%qnmBȓ%}KՖxo~b= "$plh̊|iªTvGvLg9}ttT;X^25[Pcyr! ~,tnz8U|u8f6O!ه*[RdNh`',l8vdv]7yGM4 ShS?J[BedO16YƐ3w< `7.d@%?U#UIc Y q%p("Qt' B!پș=bƴ%DnػMqt{3#dVλ"Bi,N1:iҶgb7e?!!?FB!Pf+2{xVlDl Oˇ୳40YB!lY.}Seixv^9zr6&d[,!?nyjMCpRXA*D `e oOD89.Bev9Go@=S/x~^=jbs{ ^[kYB~x**2;@@q^!~!J>nyj{Vv6BeG=6%d`dr/(:"|=RƯs5rI}~X*ϖMVfe5,:X2 YE^.2=Be7ݚ"wEE>aa$0|I/#: }4ԵTȎb)YA53@V eKf=Eo|](Becs?]dCnkŖ/:|< .Q4Ǟ w>^c~ۿU-,TĹe^zS^Z=eB()g-KӖ2a"6M`Y:ˉ̶ʬ̒O@,}))35;Do+H|T)2q5Rf !2Xrf'fwIe<) 0z80HY+@Laȧs !t}HKM!_("N=alT[A/َi!%t: MӚ j0B>Y%<=z/ X=J5 Q"? 8A+Xu1SmK02JUs}׊*A@kd{bׁ_Rf !B(}3;ұlO %68uN3NWYB!lLܐ쒽- YE^.2=Be7 :N<6k')`e`[/Wֱ;/ƣ1UBvKA9 ZE6y2+#fF=Hzɬ<>_ YB0]Bf?8wO.g;AYƦ! ~ le6 xͱO(,yT#m XɷHgRNy25O]x{O,!PfS:]쇿t_ Wo'2|0Į;\V"4\e!j!XRRg,kvh$Czao8sf)B}9C);Ζ^RoS408̎|e=`X&JQu%"%$#NCYHG݇"rAvAe5z+%2qCHI"NCӴ!*|B(%``͠n o>rnouޛA!G V:l f@- 8A+Xu1۠ږ`d2zCCYB(=ح,sKxflA3{|Hy4 LӄU4xY&L{j%e6>}Seixv^9zr6&d[,!Berfހri { _0z /(B!~c8^KF&#x.#e`:eB!2g2YKK7Qm/'Ӏ+Bbǖ)B!>٭y){ _4QYNW;24B!l9.cl!7gby`o ]l2K!B#.aL[BT솽7G7=Bf՛욋B!2ۏ9;UJ2ݔ̆n  s2K!BY%<=z/Rf !B(&fcu<54_/UlDg(B!>ٰ١Lxcvޙv\Fr2K!Bَ_.!]Ļ'ؠ,cҀF_FUG<AhyOW)BESHea[ 6H"GUdKac -[Dypjtv=oA$Xyr*FEs?oBi,v]2~14_ߞb 2VrtB~.r}zc*J~s,i]@|_$J4~ضxXyU'⑕ѬsoBGPJǎe<5(Z>VJ'WŅՎj}aԯ"">]:Ykg[e{{[Pζ҈)X%P³z|w[ޖ-$â~.s=8_P5yIDF]}Seixv^9zr6&du,y5 /ZɄi0"bv~k0ThdVx~ӹ8+2[6D]v/)ZZL8ѤhB  ΂s4.UB*(hW7T;Vu A"'nV@)Mf 5Š $`v/;7TvF9Go@=S/x~^=jbs{ ^[kXf JٷʋgF䩾 "DB|^ 4HhsD*u,z^&@_HAPBG"ꬣ/Hn{U=<I _Aq 4=RB,h)ĕ+Q Nmc52sd֫U 99pkN`ÐY ++w0DAV~E?mཞݏ!B251/Evﴁ6*}I`^`Gfu:M*E-@B2t7UW ?6"`51\@mᢐF_KҁB*rkǂ@[:UצxZY`yш1W؇P4b-n-jmdu/i/6⦚k[j#VkKݏ!q7.s%/ی/{`w;!29.cl!7gby`o  gV`&Jj,1z4m [\Vճ zDI 9M5.n?蹟Sh˕Je}?y2(Aog$Pf&g-KӖ2a"6M`Y:zF17BD/t"Am)0(!Pf;NbT+mx&vS323`*p`ΑZ,#F,xF$ fB>Y%<=z/Rf !B(&fcu<54_/UlDg(B!>ٰ١Lxcvޙv\Fr2K!Bَ_.!]Ļ'ؠ,cҀF_B!Pf-@Ů _:?]/񫃷Ul>bׁ_Rf !B(}3;ұlO %68uN3NWYB!lLܐ쒽-aa$0|I/#>,QPWJRqO )¨_۫ED|~VCZ5J1(+;Dr"k4ndK9T ƼYf=^Oq٪@X*Mvtꑒel b !&SS AQF@E>y6 7/@o7Uf^_Y]8QX1}틆^wxؖӠ,Rp iĺfSNd5&c2\~Ε4SFa}H\HJdUnp UWtp<-rge5l^ճqwQi>ES*u?{-y^]{ۗO!2Xrf21m Q!.bS Uoj@JV2aL;L[bQ7oPKX-JzCT+eW*R%b‰E1tusB)(hzmH-w:Zmh?w[vN>UPU4sX&J*Ifk*cXVDCIЅxЕ[y_.'SI>\{׫X}?{-y#G%PuO7GE]a55rjտBea!C1)?;x=3 | :J1/H4DnKeV|}~oX ,' /E%-$Z[iM OM6g4l_ QuJC$#bqzDN ׸g' r|܎Q +;I5vW_ M"`^٘T(ZAnDυ.mrm7Bǯa.ݓylPwiBf_/cuӾD)䑌#1d9~y/x3mHMMșE l2KK[L8P* ;b #*8vz6p;THΗʗNӧJjnNsRy,E-%uqkYzh(mz8f>{ڐ\!5Jm,gk!T!}:N]Ta+UuM#u&quƀԿsr9^ۍB~2EŮ _:?]/񫃷Ul>bׁ_RfLܸuՂZpU-qAJ`?QLrIyj\̒Gs_JoCWHSPSj.i^mZ`;Xpф&dL|'L7e:Õ"鍺%c0equj-_*$ DQ_J)n>j&"/]:띧˫9V06-"wֵ4x!?zusfq2Kmqޝgֽّ6.hr:@%-:ԣ0/TWކs2BvSPw>!NJ@' `l[X:5'jb1]E[RR=- ط5uh>-эz7+$B+ aտ}}Vkj <B~27]72'Lu{lʹ)ms^ }ժ8y)E'*0ZdIކ+bRbIGE&HQ=zwª#֩<Սd:e$!bt'ʈIӵf/*J2[Ga4/вET߾>lr:o7!?z=r5e|xn ό-~f>s=8_&2[%$#(D2Kk[1 e"rrOC}v!+R1VҨr#٨[)g VV_0D^bBBi!h9[nT4E s[ux0)ĺ3Dz+WYF!?zaTcݽWş7 =v=f sgP-C(q82sq Pf#k6Og^Y"%@IBbM 4 9n-| 9DI%n az $i$:\^~8lzֿG!e<eE`x8 dHN%<$ހߎgd>t0bUUm׶]&2YKK7Qm/'Ӏ+Bbǖ)DAU(U6UB!!DC$ `ԒB~T251/Evﴁ6*}I`^`Gff@!B?=6eM1̾]l9 0 -Pf !B(3{Ė%iK ݰw&fGȬzswE!BӜىY*ct~mn~fCBf^9RK%BWd|؈|[gmi`K `W)B!~C]1xN:ïǗ*^v^\3YB!llXE twJχq^;qL;.#B%Blǯa.ݓylPwiBf_/Sf !B(fb/.C*L/)B!>͙Jqzow'gv+CA& G UHTPK|!B~27]72'Lu{lʹ)1bJV>XΔkB!e tN%<3 }=D>w$<_("$DR?hjS@^-Gf,!BaTcݽWş7 =vlB@8ĀYC$C>Ώ`/% 0 )}>(ZALX@x`>P˲;Nۡo帔ҪV{?=lAk'K'h44NU[^1/FC$Y%)""b=ysy^ezm'KlX*30!'ZfL`= xͩ1x/ap:Y+:r [ґKFhZo#*8N[HL""2 (fi4 % U]2cCxsBbX,Y-2[)8 iDT)SL\tt<Rmy#%'S~dke5<2ۓ~w_XlRiJ< Byre<eE`x8 dHN׾"BL`r}!i%a4i+ռs SzM^bGoc'* tfULf(aVd tD\) )RfUY\Mf<2=:ٮa V hfPՑWې&)zT+ӑHB.}]d.*s NU,!'Vf&60|FeOX: _q , |-2# pca4-вETDkZ\k ˹!œʗxک$JIDe~47˴Pfk.2E(Bl=6eM1̾]l9 0 -02 _HErrf#ن\U)Ĵrnoy,!Bed tN%<3 }=D>w$:i!BeOe6>}Seixv^9zr6&d[,!Berfހri { _0z /(B!~c8^KF&#x.#e`:eB!2g2YKK7Qm/'Ӏ+Bbǖ)QD*J.6D* ZYD"A@8"[w@k>Ւn*ۭg +(ocEkoMc -[Dea7?3mˇUC9=mBAevkb^i lT'2͠W|U A9]07B[)& Tĕ(XF}}>{^!k_3*y Y6 Pifqtcgnޖ̇u{ 29.cl!7gby`o , ?yTV;UySᄬb>[Mj QZY[{ \p۰6@& @@mh哇te?6lB~2-KӖ2a"6M`Y&YH!s%hE+Ǖj=Z8QM/ZDKU'yr~͕n˭SAGB4%N7_ P#l)'7D4ژ7 .і~;NPJ6}$=ɬgjԲLJ2-.4a{(991\RJ#f]b-C4@ʼ=Խ-Im\P퐒?zݖxH.'کG x\c=t9o2P6yY2yb{_i[3ِWsԮ[ꚼQK$"mv.uq1)PΉƖ"+FYɕXŪu_E,!OyBZsk^VqUvFֽE좎s:~=OV Y2[HVJϾGV0$Ou8l8AP@>CA:権Tyzۜ|?o(Χb9Vw@ͻ٨s2l6-m_uj.*2.Zm8>?.]f״/ڛS;p͂g[uxMՖcHD+Tp1Znu-}5y?; ^jk4kQfzٰ١Lxcvޙv\Freˬ\ZvtzSϑAC8"Nкm'=E&_F)d4Pz"]/bJ"f8K_DC;ZȻۤb#gՕ`,ۖtne;_U=/-ðV[q /*j/ ңU"2錼u:Ы?j.Q=mīU;Ǫ8Qڱs'eꂿ?o8l:ՖD2.=mlUyQfz?_.!]Ļ'ؠ,cҀF_^2[ϏP2M3ίk_,[abz ZPIC7JAYr.C"WBb"둤Z1MQwsBɢS^N^/ [(TףB#eTPD$|!}B Q+5%K'ܔ pz[Y $ U䄔`B\ uݭFGGfg{[yHxDF}?mī;BFԽb5`?4۶"YE]EM..y߾Z}lNJ1ngلs%z6^ufb/.C*L/yoEMY*/2jB2t*!ұہ4.ݖ4h%WT_KbH i훵)!GVd}@8*e1RK_f.5:D܍ R3A~=q[ZnzΫm׾:t]y~n^FMwz#տ{'dˬ3;ұlO %68uN3NWgVHXEoU]U;fq֣YnPbU,>^e7ߺbB Sl veZ'fRAuU<僴nЖcg;GY$68YWaxg%=<&<=56xIz?Oܐ쒽-Bf_Z¾%jxq7?\{ 8 |4O%BnMK;m`"~uؑYB!PfOfatce 9}|[;~LBf YB!lev cBf7]Ħ?2d\B!~̙ީ2F'WږLg6$dvgU#YB!BJA86^=M=<+6"GowY`[}U,!GA%`@E6E,cr{B]E좎s:~=OV S-!  H.ǫGwE: JJDjuB;AZ]9XVDMA^-_1(v{㖽>"9`} ۼJZ :$E G&rj>5XopRWe*< ̮IfBf/b0S:~>4wډ{xg q,!?gȏNf%Z29ɬX+iR>E+rYHQD>e1U/xף͡7f¹ÉV.JN [~9zl?$MhKJJ>.?G3ƊH/pƥ&3MhӾ*N[S?v> g(Bev9C);Ζ^RoS408̎|e$#~%Ŭs;̧ʋWщtt<,?<G$vT2$4hrHA;>#LևtX̺m z*ive]s'GA/!c'&ӳ\T Er_2UW ,tdvIbKd|ʙB9ŕ J:eTR>9RPmgSNNpBzI9hbޢ,ǯ4!21\g9.L0hvIH(+d.zB(=Lܐ쒽-{l-: 0t EFs#`Pf{ʙ=zʥ%20|{QSc2_6e9ȕPH+-(dSN9!٣2['#I!r,(U{-C;<V@4}+<㈍1W`,*QvXM *"NBs^{ZEf#zbwș՝1K89nOyWsz/,sR2maӇ۳b4?V?t ȉY*摈:QX4.-!T4a1⇃:,ϣPf{c8^KF&#x.8)sA%kUXpމf@6*,Sk̪9y|EWTnwuV_)eEMig?Ӵ-!jEW&JuM\RYXXlӪF:`Z^lgq:=XȲ;_U>^Z]zR1 σPfLf&60|FeOX: _q N3 5,vDA`2eֻVBJ=6eM1̾]l9 0 -Pf !'"‰/PHMI̮3{Ė%iK ݰw&fGȬzswE!B˜ىY*ct~mn~fCBf^9RK%BWd|؈|[gmi`K `W)B!~C]1xN:ïǗ*^v^\3YB!llXE twJχq^;qL;.#B%Blǯa.ݓylPwiBf_/Sf !B(fb/.C*L/)B!>͙Jqzow'gv+,!GD! QP&Pf{M3!{3%{3([xy[gǶOۛA2K!QP!PME˘Pf&Gn`ct-ᙱg!#Bj hheD"gt9^0>*(*9 DKHFZ)*lզ^n G,+;̳1(.##C|t`TX)/B6o`!5NFIn~?8򆉜/Ol>敍fcB/$0S*Ѻ| jk8!>w"YljQn3{m7Bz-Go@=S/x~^=jbs{ ^[kYB~x*)ɬ_-Hh@~t2.yՂHf\mHj:|PB}MiS@Y)RF0QOVw_6V,ߔv y'ZֺD*-;_c(l_i$Bi8& -h@S]RrUq91VDj}3.7,2봯ԼO*YBQf7 i 0-2{0rEH52zYC'g8JEh1,Uazt"/ *Oɬ )I5 R2'"H$!7&n[B.r]Yw+DWz DD;1!M%#/A/f[>g~=UHP4*%^{իx ˈR ܑn5iVli̕O9B(U=A^l;ZQʧ?ST Mrlʉ)NTHU:)'M[F:D&y!!%ڪvR$R|#Jo(Y |^l/251/Evﴁ6*}I`^`GfuA^ N#>#O o4.Hy,yydlp=#xsGIhj;$L޶B.QsC`zXD4b"#QYϴ~M"}I)qW2 !Usf21m Q!.bS Uo󮹜D0HHvm邤Pf#Ys}lrn˧0g:UsR[.-[.f2RgHF[[]w{On4Ӗ=ӣ|z+NY_=nڃZ-ʀF8*28ۚ`gHaD2ª83۸ D9$w?n!x '/guϙEcZCahn׀lO9;UJ2ݔ̆n  svl^8 e"p esji76dAL"Tڀuruwh7ϿuLAp{ҰO̮*fcu<54_/UlDgϬ8V`&,>fK?*+,a$Ӵ:>^TVbɲMet兑}>عY-Nr:_U>^::=,zbmF22;t~)ylN;Sȷ_Pf !EN-H<LzJ!Y:k%dx"~sel:B˔YB pbK"z6RsSB(u~K<~u6~{-S3@:K,!BeOsfR:v-9i`pə2K!B47]72'Lu{lʹ)B!~#7[1:YƇ؂g3ܑ؃ B!>M1B!PfLf&60|FeOX: _q , !B('2ܜ>-灝_{&t!,!!Sӈ+BϜ#.aL[BT솽7G7=Bf՛욋r |ZBϜىY*ct~mn~fCBf^9RK%<\ͫBfQf !2{ó `# `Kxz&^>|o/7]f``>YH!烢b _h^f,EZm|&-ӆH"j5rɖRv!Lb"~QYByd,bu ӱe~W=׀ e'Bd{ڪ'G(%dGZ@:jAG2p>bss "ȖtQGvzV /,W* 1;⫕PH"@ͭ531)iû^!<22;t~)ylN;Sȷ_Pf YW2WR4 0`Eb`ˮ#,Uώڹ5&#l.Ǒٜr\j4R![Xf~QFXREž?]zx3gdǯa.ݓylPwiBf_/Sf YW2[HV/G-=F9ZٌTv< %Vty(ct**9sz<1MB ݢb/.C*L/)<1B N4)J4lfG"Z[dUfȀ+vN9H%U+ldȬ3;ұlO %68uN3NWYB@4iq"XS^;ˬ/(ɧH[άwR1 ՘.,5]5Bȓ!!{3%{3([xy[gǶOۛA2KȺ)~-'Wm;iVM(ˬQNHI˜UG`RrPms:RW{j!<2{vk:F'[̾}";{p/M dR4MX3Z>ݳAղuy+G#> zׁB(},9*cL30xѻ1!}Rf !#-_BXF%l#g (gYGMlN}0q 8x2KFDª҈BP9>E!V=6%d`dr/(:"|=RƯSf !B(}&}KՖxo~b= "$plh2K!B3ݚ"wEE>aa$0|I/#L3 B2&rsf_.v~Ѕ(B!~˙=bƴ%DnػMqt{3#dVɮ!B(3;1SeNگ-MlHπѫ9Gj)B!oo[ӣ7;x,- *eB!2o2{h:~ZSCs2^?YK+k@t2KibQ4{U& G?Bޯ̆_`J{t|h20-+YBHd ۸ ĴbJJgnNB>lǯa.ݓylPwiBf_/Sf !5*=ULBe Y`?ebh:x=QŖ) v%e5v4zÂQd>lNMiX fpwR V t`D)3W$)EBev=r5e|xn ό-~f>s=8ϗ&d0 A_Pɔ)RPu$ːۧmL!2 2þ24 <{?o2{h-e'Fh{,f鶩(呌dAjFW_ Ȕw5oQf !2hsfހri { _0z /(+A!+R1VRXsZY)XB^ !#X%$#~'~82K!G)q L.EG]/3q):RƯSf Y_TP4%]rGqXU=-۪ME)'mEf>B>n2M/aT[Ƌe4`䊐key,!i-iӻ*JUX RF#c٭y){ _4QYNW;24B!l9.cl!7gby`o ]l2K!B#.aL[BT솽7G7=Bf՛욋B!2ۏ9;UJ2ݔ̆n  s2K!BY%<=z/Rf !B(&fcu<54_/UlDg(B!>ٰ١Lxcvޙv\Fr2K!Bَ_.!]Ļ'ؠ,cҀF_B!Pf-@Ů _:?]/񫃷Ul>bׁ_Rf !B(}3;ұlO %68uN3NWިG"H-*Ebh4b ,>ląpDAjB?|4A$X|iQ1(Uw>y4Gr<Byd7dodoe<=v /Ox)0s{3Sf{70jrw%BNBbH1{/*!k٫476NѬEzrT၁|R|cX2MAklC)rwDa|^돁V6_:ucd6i\>!+[ހri { _0z [kXf&o#q22; XC 8X[ReIJvjYB"l_R 눡0⪂yRb֑J*VG_XS*"Vj)'/ wƩu[L:%uS[8iQrge4l3OHM$3r"mh=wi(`ֱ*Tk/m鵍BY2{lK2^PtE"0<2{ __2WBL,"fGŎ9n.#m+2 Պ.wٽЧJNń&4!bz!,8'AHl1QPv8|B<(ei":ej+~svj I`?SLrI)8r;y E]]fڔ~io9c6q2̀B(n2M/aT[Ƌe4`䊐keu,Dh  ƑhSiϹv"DB|^ 4Z&R}BBr2іԀWZu`R;|EfeԶS(toS^=x}%MB٭y){ _4QYNW;2 R)rih"Gnq7-2<frͶ Y"et(呌}rZO t>Eߚl].(݈Cټbnζ*NԯmԮٵze9^mj~h9cd-me-B:cs?]dCnkŖ/rveMdP_,+䪖abF>`)D14td5^!!+R1Vm&h"ֻٲ ò#macpdV MBjl`cItY/FzūMuöBY2-KӖ2a"6M`Y&BQSdg-'v8B2t4{ұ4nv[nwĢ~[%k%ZpjE9[ (m\#5/S5c(-&!_]p̮WWj/mYCoه'^ev,N1:iҶgb7e?!!?Fe? LӄUvW8%YfarYUr0XJۇ'Gm}xucd-mvA!d]ó `# `Kxz&^>|o/7]B!PfMf"vQ9Oxjh_'z ys Pf !B(}&a!C1)?;x=3 | eB!2o]Bf?8wO.g;AYƦ! ~ lL%B[:]쇿t_ Wo'2|0Į;B!Pf4gv(c2Kmqޝgّ(B!~K3!{3%{3([xy[gǶOۛA2K!B7=r5e|xn ό-~f>s=8ϗ&B!PfQfs7UƘf`gw/w' `cBf-B!Pf-g (gYGMlN}0q 8x2KȺbQ4}S! =6%d`dr/(:"|=RƯSf Yg&G``ѬBvBf_Z¾%jxq7?\{ 8 |4O%d}Qu$TWB!251/Evﴁ6*}I`^`Gff@ȏ!au,$É,վA 5%T"kTLFuED|~GOB@ PByBdO16YƐ3w< `7.d@%^VT5 QK$"r|,cӏTE^OաڜWYE~;3=W #*HM6ByBev[f0-!*dvElۛ!MvEȺbL,"e&DO M/R7嬔YUl!Erl}O!Onީ2F'WږLg6$dvgU#YB֓6IM!ZYo`Lb}B@sv( ԧ^W}ʲrL%'^foo[ӣ7;x,- *e'Fh{•PՍm,вE,tMF)d4Hh+g4x6ɯQW}vA&Y.4wډ{xg q,! .6EM]"p}ϿR(RrZ  6 ivOw2K!Oyv "=e/d652euIRA˻ ^,T{)jŒvG^!u.xtǯoOTe `]v~I%BiPJǎe<5+{3<m}?s{3`B!پ'|huDS:SLNNʨ?? hiΝ;YB!/t/_??o,氰5Ϳ7, JE;)YB!lȬ {u)aHa?G7eB!2۷2{-,//tjJ%BRf)B!Y,eB!2K%B!Y,!Be2K%BRf !Be2K!BRf !B(Y,!Be2K!B(YB!,e2K!BB!,eB!Pf)B!Y,eB!2K%B!Y,!Be2K%BRf !Be2K!B(YB!,e2K!BB!,eB!2KB!Pf)leB!Y,!BRf !B(Y,!Be2K!B(Mf,7eB!Pf)?~'S\Ql1׃B!Yl2s5}3?Z 5փ2K!,eG%U%!B(6_ю?M WF=vec\17o~7j wNn馜?Ϲ;/ 06-B!2Ks4~$Oq;rOe4T1C?kB!2evQ>:5 <4sg񻟶z,T?!T_GBRfQ&!Զшat;ڸM) զ sf !B6M$-\m[tW_VsU2۔v !B]2{ fٟ2،VscM2\Vg񇟴+!B(')BZ?>nt5=eS򢗛 ~mˍV, !B]2.;v 0;`/f>^ ~(kY7rVޝІX+Vѣ'5!B(Ox't%dsls;q͞5[yU}"Bev߬.p_!Oן-.^=W?Á߬<~*;j߹ӭ[B!(Gai˵WK~}E512![+!"˩0oB!Y,5'ccѦW|}\VB!Pf)GHY)NRe !B1e| w ~wc'Be2K!B(YB!BB!Pf)YB!,eB!Pf)B!Yl_[8p?2K%B!Y,!Be2K%BRf !Be8qny7 LQRѬ*f)+Ydf<{)X 'R*Jd%I0Ni* $8 LGWM_hyh\.W=>>[,2",2 , ,2",2 ,2 ",2",2 ,2 ",22 ,2 ",22 ,2 "Ļv\-S){'4J6+rE=~iȃ]9oݒ{2dΩ3Ȯd1XBnvFmES/#'+{_w 6|}INO5Y d֐(l>ÿS@f̞2]eωY2i{{gʥLR\mkQx\ꩂդ! ω~q2 ,2;a{wO”DVIVJcrYsorM_fXǓ, 2{W\i_0u3{tGSWdv_2B{g䦶 "ev vc|zl;+̮Pfi2[}dYdv=2{B(G<5cbr=vkj3Xkݯf2ےs 22 ,2;Uf'5.~4ZMEaO좕`B!7;"[ŕք1|gvY9gƆV͔t:, $(Y-X)zNQ=I]Jgd+ Vߓ9km0ޚJqt؅ZdufσnG"x- STVnV?lma]uW=N~3 6K9x~lӚw' wdvʬ>-'s1Z5MLJ$w'k}O>:0'.N^2 ,2 ",22 ,2 """,2 , ,2",2 ,2 ,2 ",22 /}O?ٳgr@X@`Uh5ݓ;w͛7Yd'Nr%_Pdg}ƍ,222{)9s\xQZV jYX|-i,22{9~ӧh [ :v>Yʭ>[d,ǎoV[ :XYUr 2 nn+)Jr`,//4͠@+*_~%ڞ?@f72{!)RTɓD0=w\P* y…rAfM_-V'iuV{g@VUd\׃ZdhE?B0l7`?HVj@ož{֙@f7?:KיĶJ7UZj Vn/b1MV_{ɻ+~|'+_|E;7P@fMV]巴O?_~md2;[m-;=|%J%軨T85?ozҬϿxB翑czrRsf_ܬ8>5SJ=+_w38N!iJwK^ #ʬATnu—VXyȬҰ-b#ҭJb=QmHM+>G<ה\pLiPy OoJ/g5&.vJ.$z5"2fHP\jd,:#TVپԧ풔th=O.nHPafJf:Þ۸^ٶN]7.Ksb?b~2;X@eV%W_/Thwxe|坞"Ef&E/!5ϧ{#9VvyǽИ{f\DRY[J,N|b{ݑjfN UaKgDݰdť09kc.̯|_}zff|zE2:ax =jaʗ%o6(3b>f&ztY]g6\6Vg26]h/o. 723-PPO;6̆j*MmkHi4ˬ[_t5AT=HjJFCHZ0"bIYr1/)yo`,u ôvbYE?d\8jKfuň̪ڵ+^{M_>\?ؙ2[3"2Y KӔG =6yy&!G6*IN{?Ya̮j25/L38*6Gd}elUu]Q }fВaӯAu&q\)d5p@f=zܸq#s+XN|>ё6pbx&{V34Ojaj3Wz7gmn%N&bI/[)J.Z^[_Ubq)&@V/OZLSR*N%6aS뻆#i7>"^8>.3쪯G,l_՛& dV?L qsv̶+d]r שED)cedXJ*TTdҹ*d^jN] i3wkn9c3s!E)*-ɇ*X$4VHGǢ1 ;R-[iV}9BܔKXd#k82:HWndD,+tz$ˏ4.j q`gTf5/^k׮olm: LCd֗sI%1"ֲUr_^yR3ԖY设4̮^̌U%Uf`d67uHuz,foF!f[5L~";"n=?1]kDud]"g%:K{fo߾jիmuJ` Zܝ!5VKBhbտʿ ]3vt`Q)ج#^Pk,)Y3*wl~eY~(| ԎS1.F2'iM{͞ᄨ%bqK+*p1sjli֐-F&T`F'cw6 臬3CEdVUԥ\|9v:@韺}GQqSA;6nwVN;xZ n>]t"7X܎Z>g}u6>.~嗲jU t8[; ,O"w/c\8Δg}7b1m?(RTZ»$gΜ o!B!lĴDK!BV 2K!B!BيAf !B֕YӄB!YB!BPN:E,!Bz7ߤgB!lTdaB!-[o%.]gB!lGDYB!%rĉHk=B!dKɓ'%B!OtWV3K!BTUEVN,!By2J0B!-)B!-`EQG|IENDB`simplesamlphp-1.15.3/docs/resources/simplesamlphp-install/0000755000000000000000000000000013245225037022444 5ustar rootrootsimplesamlphp-1.15.3/docs/resources/simplesamlphp-install/screenshot-installationpage.png0000644000000000000000000035366413245225037030704 0ustar rootrootPNG  IHDR(9iCCPICC ProfilexڭgT۲wO $!,YrP!  3D A(Ad%*$< "" ֽzU{vuWuʏ#pv+z@&Ç mm9~>1*O%^*PI䁾3L$!~l@a]vpb80Y5bo(Qb !@' ߦ%0g ̞3<1?!#gF/_"/\Ar}lg;Lof=Q-T*.yz*̱-0 O-9*A))2bTRRfҡ2mr3 LJs*Kjbw>U;bbho|es* nKi+6쒰9e޹ O."{ ->|{:AO#msFG :.yB"V$o{ |}5.G˞W ߤMž y6|o6g2oA'/1_3ju-Oos-WӍ-m}Nԡby GQP҉Ћ11211oL6@G+'KF%fH~H`~$R"J3_LғZn W- 0XC]5~ژzNSJsIM;Nl˾IJ} # 02lǁw-,X;z漭}SsK!3WaW۔{2D/+7 <=7 =T|:W4 Ha/{"" %E1?|:x4wJrB]_H))ϤHOJJfxu,5=\۝t<-BCACT Y .WdƖ*+U'S+e5'z^ӯ!>5477\'pk6mQjIus֫ڋ;vBxT{{k<XxH|DpH Wos[aAn *u. (Z/N2tZS @6q@J@P?4C( 'W,D!UYUZA렓TTT ԇh4n4M48v:teS3T321>c2aje`.b`IcEƱnŰg's$sqfsqUQm'Oo?H%P$,@KhsAxH(M1W-FiR R2<2r@\D~N!MQNqH)FDE@ꋽj54uuTY{ Ts6 ;,1kozm{2Ug C[nT^A^_ )gUE@ƖO'MRI~rlfZX6S5#g#o2nA"bRWJR-74xw^;m]7z \}XbIcSs&^=}ugFJ n_Y.W,iyC}eVD>B@ (91@ҐDrzC"&H]d8 RF%<-"IajQ4"44#)3tFteG ɐʰˤ,\"R*Z&a1+}p?;]+M0KHYh -,$@( W0$"%$JJHOʤȪʾKגhT|JCUoQ}DSVT@'k_n^~AѬ Tj3V+leb:@NΓD\CZ7=L<ӽFy>:_6YALs$('ZNč"&$8혲z4&}\}Wo .=*-v*/ }}߀hrیhi&~{.Xׇk}!,0ISg/'&ozye6u3ŧ◍gq-W}WѶ:ackN 60@*݁bD1R|CUЧ+[>CIƋf֏v.Iɀahd4ffa6efic gSe~$1#+^q^2Kv!8Z|MG)2/dF**ͪT%P;Ѩ9ôOQ^~AakmS^3^,XIY_yfǂOwtu6qI>Fn~8'ջc;O d>r'7T8A Fr.:s/K $%ϥ<8۔Vq<%[)*o(|%~%L7ʱ_\ivMcfVps-{yν}>c8mcTd,9틳ů^WLM6c5{<ޟN\HBZcЎ?Y?7|# BQ( -Aqx: pHYs  !IDATx} \W󟙗Le^&oI&3&e&$f1Qc}7." **AdoTPVPQQ@Gp ! [UT7 (SUuoݭW{{6h6h6h6h6h6h6h6h6h6h6h6hal?"@ nh#E@ @ B]I#@ 04&2hX((=SzJO)=7?n$t@!"tO@ Q@ juG{ɃYp$t[+Bgk@ P@ @pj  (8Jz 1PC 䍦D(?W_XjA)=SzJO{Vz{GK^+A QPK%APC Ԓ5d 0 ?p4SzJO)=W sb4X0c(CA k d@oC/@ AX# ( +B%?v Ȁ-A/C_xJO)=SzJwLEY J$ $^k%Ku@ lTVdSzJO)=wGDX"Ȃ%k{w 1P"JgHX#PeGd0v‘4SzJO)={D溤yxȂAND*, r`A-c[y9)0X#JeQ+u@)=SzJO{~zK:5+3`0X" ג%AdEPb?-5\-DQٯr |D @ h)zA4I_5讯1Yl$X XA0YԐ J +`-02u@ Rl!Y>ӑb,ٲt 90w) b`u9ȑ#AVVv? 2Ǐs3s!30 SkFN?|,c"'O@ Bρ,*Q N"\n<1<>Tpgff{=K}`aK~L8R y8/Ŕ¼> AC h@2=k?Ǟ~dzp< JJPI?t0-NIODPh,%w'9PsAx,FN#VKȖf%Y|a}V 'g XHހn.+\Oc4 rq¬GWG5 Eķgm9|YovUk◽ӟ}!nRS7S<9\[yw vy[ݐUHz-)*O-)ev!->ٽ}`YއcoaLohsӜ*g߱vV#4xxx`ر:t(  \yl] '麏KxT65 m.5džV91-Ó 獬;1_G(|5\6LN7II;viyJo%_ؘHKɁ-wmW01|jWYo$)?;\yY|P7mL {?(7iA.WJ߾UXl{9\F)G≿l}3l)tFpyI 9\nZO3ȵx 23;x5Ut*V˔Pt O8[t”@l,Tٱ]+^Yn/?#y%2e:;::XvP feP\ HO _E A ~.xl9sϟǥKM` @\pAHEy^cb!~1\!OI~폰쌷l;Id4r 6#YXvt85s7ѩMIݮu79[}x] Yzc> KʢЅ5j֯_/p PXXSN Vc~cDŽܵez\ܧ%`A_Zp-u 9@ 1x܌Nj>;tsHdJ^D}?_/_G_D˛ oOqŗKg'sz[F_G&4'9\F)??x| '>nO c o&{}k1~HY< m>fKH!싊B|x,fRK,OR^GT6-`TFAi ov'eo]#Ό«N˜װ)۷IPLeDzqx&:o~i=ҷJ8^ x117yGuvEHge4i'|7[cȕ{>~<{|>##r c*iig)O]Nl1s%[ Vl}iԽg4.Z7<ƌcqkP~ {u0/f8MsP4d  ZO@Y"fSc"v.O#-w!'#|z4"M)Y:ύ;9^'aR%=!*=i(8qGŇfūb=}A;qE'D:AaZΜ:' A̒s8/,*W^]g6cz$HDbvxNǿn&i-q`뢅 J+'c6Qw~1Hx_9`7FR3yu\sqx>qݡLd%mȃSUj\$ez{rO ?ꑲE$iq~.ȥzZ U[xH.:c8a mcO◈\?\ ɡ) ! ӧOGzz`xbѓ5oQˈh=3.ǭ"|v}bp?7Qs1 A 0Rxy2-aX˳}a ME1rt۰Up^/ JGas|pl In%¡JrAaڕ#)|wJn;n1YY;R۷T;2g nXjv[D$뼜)9E_CaVWDZ~0!;Ε"ܟmeꕺo-Hksɂ2j3ː"k );E+-¨*({CnĞDo\(1{a||.? =&bD}Ӯn)9:۱=6JΧ?ݬ,C0w,6}h)F%_+\5HIك_Ŗ#ȴ&e{w/kKuym7hvBg?少6y-.CDK_Y;tb;tv=۵vQެxEכ#tTEق,NK-eM؍ؓN`A˗QQQ \yl]뷒I}A҃7܋'[!K3+V'dqHEE{a4_)X <0@1r6oƭ;pk^ڷvĿBB|92reV#6"lFz'AN& ů &m1Wn`$2bq2VUȺ//,ۮALyE8VBBjwotxf"ŕãWEW!Y:7z{Br2AYH6%*`K#/@ۖsPPxT:;}ˢSN b0S!)3n:#,-EU5[±9!mjYYKYme&幡B?5?8;)`yLTdJ҈\}5a1Cb2Qȅ>2d,3/;Ws)"Ӕ3!lq&mqPLsK ǤYi4Ov6Þ1m RQvd]m`c1Wӯ⋰z(a\e-d 䪭?FX =Cw)**Bbb"/B< Oyٺo%= %<@n='?la탟Z!r&>3"Xf'D DU|#jtd_sT0Pۯݰ13g sy5y&E`BNFmԞ+q(z"W#cO񡘲/V@hDʛ0|˧5y= Y8} `ΕF~8sugJ չsgq!c8:y쐤pnw8gNS8[T`Lϯ4Ν:!k'N3zQx1GGq9# ,&±3\ܙ`N.Zxv(9yLPfgX;d:Jr~UJXz˱Gn_&X;e)gff $kc->^bBޞfcZi梸DB񓕓|l+zZ ji+%())ɜHc6k˛ee!IxP1w3o\UC $r0Vs_wfɏ=/fOA{d<k`yQ 5ԟ{׸ oh=== $^kܰ sυo14>C~Ha#ykJ,8 KcOoٓ'Ӡ<-A_YZuib0o6ӬqCA*i;[DFf0Y|ee( vI9m~4# B0`GǞvZM`/??sV (vV; Bg2|̵71ZL/2s/z,8 +փ_Jzfgf"?B],DeD^gZ xc_َ|?f-OᇎԽ۰qKRzAؗ`wH'CLK9iXAjW2!Y^_sI@l͵VPrO-YHn5o V̬/Y xaRZZ !|zbc(AT,+G6<ґW,#k&<䩓(+޲XץF .Ì@ @d=x]"rs6\~%s-Z! Ɂg!b a0+)) @ Jz;؃We%3"EעGȁ.#?6 FF0)Lg)zAeL,M0{,D];ceSp_XKi""~ :`^oci(!`Α¾Y޳᎕@ B¶]iXvsaWl(hBL.ǰ&0vݱo !/pdƑ53%?>+h+&[;|N(TtkĉC{{O6!: Q Ǿ~25ĕ#╋ؤ.+_r6d|5(NC8L>'q&%I{Pi-IH 29...|2oR uuuP[[+ڵk9)#cPw ʦ15_Y<Գ2r`|"nja}`/w2b]'˷C[ℌKl`)2J S)x_XWޱǮFԧdo,I$K%܀7#8n#0*K`` =Ǐۍ'NAG̟ŎSV^3f`Æ h4cPPPPy5osx_>}Y.߀ome c>XNv,tZ}qu6N!X vlAQLdm8i-Nɀl_N2qdBp'`Ȑ!prr߾{# g`ϓoz*|||.o%=Y̧4<)=.#X!90AđWq5 G#p1Q};!XA_eԅؙ&bv-'j8w;Tp}hھ]Tpp*++qMU}еm۞ 'oxK wFtM}!vvH`oqCdcfu8b:t(Ʃ` (ܸRn'@"n90ߺu׸qڌnl<9xk׮I< قh/YX-ȁ!! 3kr` ֦15'Ϟ^*#HWwE{=pysf z$ƁS_s$G /uu-lyr^~ ?>;ͿA8[&9|#G!> )١2_v<.]ݗ]KI{2V#6e-||JFFmѦ~`,͕%K_ϟ?!hmmy}+g)"Dɋu?[Ь-{c e;G8<s\yn.X 0O:[cS֠t!4~*f#Qfh7WaC0r ^By@mrm۶\Hk#448U#ae9_Fx>}}CFIoR8 bMY݇GɛZ!1g F$D-1n%`iQ_3rQµm☾TgD@PDl  "<WF@]cA8{4;g#wf<%0Pك]cp-{DZk.rf#f<:C׸2RRϓCpGru֯_ow~Xs?,}%=/fdg2rrfS,#H~X0, 5vSدqp >+w~1b<% _Qecje@x;6FM *䆸47(i/ۑ E'q2{ "g" 43[W.q1B £ ȩl1j]"#bv%nD|hB ©oK]JXϮhE};އjћrl2?sG7"y,A5sFlBoEȹ9"4LeXb@E뷒7zrl14JOMgl:S%A{,rrT-Lc4 @v 5p ut80JΜ K'IA  ٯ?< Bm : [ 62Evv%D؞Fr q_Nga3%n.'.`2̙3W7|}] z@?^-e֫7+-E즵No2[4ai yr_BB au\l ge\@h^w||Y+ȁgƍ7j{%gܸq9s&"""PXX(|?q9d$ GS&i;{n˩X>5>}P;zO{ V+-:L25ǀQkC)ȬS< 0헟'σqLLIߕw["ә>&9ȁe8xLEY!rro$5X4}Mg~ 8q!p˃p.# IlJ}J@)) |r>t"9a2,-!c9Npi#Rbs >~Ϸ"18zJڌӓx>}P;zD|s~V !514*c0eu 0)*F׍V :ˎ_3?# CW|int ";i-m\cpDbk!wvq_%. `Đe`H,#<;b/?O_|!,ƭAK%=9xJOVX%d!sr𪅕 yqsL2GO)a~^8`׍a`'L1w^-n#% 12D&,}ȐݍM3I ay4Zj˥HcK-rAV>"<*ԟ& i⹽ ]4K4UJsCO@un9=и ;c6rp>S͏/03Rc(czcA|Y{P 94iӾmڽ䁒RӎA@j*\z2~m*R @WKe\6,#!/KŴ%t^clzqj/x pI4^z=7?o[2ţoH͉5#t2Z$zN3q[<E tnmu!i1+㵳4(S6@^_֏m]Ķ~_6J6/0r0F<-:hX&n90<ɂ _J j_}l|={,+-6>~9hIl3Z(G/q;&m5>8|;2\Rgx}G[X+*_%QWPUӄ6cNeuLZpS1%CeRJJb[iѕ@zk'W, mt>)8_^i2+*M#*ʁJdE,OնiSB}BrF1헟'σOO{%AOLdF +%%s+d9r le@:f Sf?HTJ`Fs 5؜#o I}S J3Nۮ&'^ jeMcyؾoOĖ9vC9v Bvm~-F4<_5X6ldfX}šcbI_ ON4~6EԪk4 }^(M򶴡$.JN3ŵ)joցTžH--Hu1%FRvem&GL;0TE)ՕHw L m#PvANY ")SliL(#S~,Z,}6(RƊ,y?g+B( 'g(kĖAdiں7#2k&˔6w3z}) ɪnFkViῧ 3%Vn90<_cDk4;x[9n*9Os/ď @ ;f vuu2B9ΘX}>xxeX!2ir"q__>*?$V/Wx}G3;ʱ4N1W!;' -4מ IL ){sJLʱxEq ޚonwXpf$E8l*z[gr#^5YVwmBxw3?5l: ^gKϓ-D>B䖃?YR Y(u90_ٜFF1#5Y 5|Gxu#^ラQTl'9x< 0ƭje*ϱeLlp3DQQjhw!`Hd-99D F#~cqK'ap(I A k/*V}M\[>ɁdT;R?ķ}MC2vZzbLDuK9.ʳK96-LmSMӚiɴTåe,;WnVMFO`^ Y#Ѥ<X<.'? Kr`F+'fוyc(~ޗfcB"L}Yw}C#2h>&61ܕANӅ5WDz^PZc\61[@ DE<6\RR/iO9r B NXot?rwb1wkE^8;k%\!Ɩb\| .b9$i{?53B1f9L\ν$0Xu9cpm!r-- "A?˦^-Nqx4^9c.1K ]|:1^ĆzSϓu/ I"ȁtU190_%-b_'VG6Lc48ssZ,w=_Q~)ą϶} 1?gVWi~?Ǻ/܏kr57[Bw1)Ә?1 C;y”d)ba 5Tu]#)XT/o8⤀saӨ فf_%+AΨ͑}M5sqb^ WEMY7+r,ps4y;+"20&2OkhiǙļQky^Pu&mA.^(29Pn99P/6s俔!VN-r$dX86zQdUQ_E/ƄIڇF/R|| *o`󟈌' 2ǟ5_>eׯr ?;xsH82Y Hzے^l VI~ҌڊVbn[99`~-s+?3ˁՑߖV@Z倛` l|=+{9> ^{ʨ{_+# Xu_Iv%B/[ރrnTn]Sl9N_ӎ 5/r0 uqX24lr9|q%'|Q3N ,XhpXGଙgXWW{-^_*gg`&F[_!RTMɬ2 v 0\ ޢ _ 1߶FzQyjH[t)z+k4E"%&(1{N*ϧJ_bwCQV[ZVӬ _iPY_,);TmOLyMeB<+/bt2(Qm䊧r=$?t5Q~Z"1~A_ zhƈx[TkES9RSunjAi *9 Yec}.9{?`J{3 b<|^jZw-_Sq녯/}͎Ljq5BRj)U9XM|]=*/nǼx}=-1;"_¬-yfh)愜܊D@+4W0ӯ: "kov(Xm $|WDۭh y*'Ъ8$f-ݱFF#pkN;yY!e2# STIعhd̜p8|Ø5}0rbr`PYY)u0k,rlwoID{?`9xUF^4rK_ɘKO,rr rAj{1";l?x=~0b?f7F/m5VIXOvK\lM1R0 ~ &h 2og8Q(A3 @3nAacr咓n0,J3ޤي`~Keq1Z %xClD_ :_Zy MYs\,NQZ_h, ilELo(hp1E$ u\c`Z}-Yg^ wOjs! 5'g1qӑaPPpKRv,K{/@~DB}`hZqUJiߍOz/BFmtHF|7(M#L<<_-DžP%^" VToI>-I>[m~QK1dp3u3j#)rXoh@3MNNȀ|!U[\%2 3+Wt 48 :Oȿ&p*UXKnE6[jUAwr}Ch͑i1={KݓS칷D,0vy]Qv6~ϡUspu&,\~WLy[BIIq:Sir Փ"c Ոvi;ݼ),T_,-q8n`x鎺3qR g,j;{uwfD`O&m*DɁb0C=_Vb=bV`S{F(߳F .[ӷc{DCM̴Hs/&!iҗEMH_2FI+Q{\s3~~\yiΓkDnwLsr`P9@ ^jܷ:j6R _ g)Sŋ.rOQ nK-Z O,xlzaC#X6/ w9u 'C6qQgpL셵;489f@j8fFH pxm3; sˁ[@lشЮ29Brγf֖TX8.."Y/=wvmC n5rlxwf~`A][zcVxGE01783F3%W1za,ST k iǿƬM1(pv¯6{Ka&rrLF#1`DtH[2Jgv,mHƚf~1{Aʥ>H8Y6X8WLq2:քR!p@<[m%?[p+r7{0i֡?cGX)e.cV-[~qΏex!cTʯ`etwrP_Y&̫Om=bke^!<5rhZk9Rrnuא8 qqKS@ x}p tXFFy n(b8!'1]k 1` _u֜:F N2qQx{jxuixp" b$SGಀQ_KE[,#;Tp{&+J0@n |AAn9{`ӹ .L*܊^Ui9P" }*bzc_`{*X [m@vl`9 ^3{/`xo kc`>NbS6hByhZ4Wm= cBzF ^qRb^ m.v?DLK'2"0) H]4\˰p`ܷhװ/%9iyulaaa&o\! q… )8s0ɓ',`r LvXz[ OXq+VIW a`OzSE7"03~-q#~<6.#eM+}/RT= *ˉτx)KRR[Af ?GH@ ܍?gAi<(y>}Mgk|` (plme߭m@C`#όwxol0R BH^8TMY( E9l?Ӗf`#Cm͊k˖-'I5bu:N8: >\8Oijot+R;[Q9x90Le'+]0pX kc"- eb~?"gsp [b0 f=4Ln/s{>tw ph3?;N G||¿lr+8yNVQAxy6FǠuq30݋ {w=FoJN/_Jl? iS!i lvn*K1غfe)44H,Y"qk}hy_܈ //OĄO-s̱Lf#S*=bv{:8]ud@lĘEǘ<}`%`ʿ^X"[ ѽc,ƭQ_sϵCG^Xv0?Oo[Dpޟ]c/#k?hPI&Cg@ ,|6Sl#7^ Uy'+ )(c98gß'aAw |phEⱌ$L@Q8`(s8`/eHHX<; Cx/N+_HHH;rD ܹsŀ/f |r0!'FFFAɁ +S>AO[ӎse܌@,sg=l^X]tr{R<Ž0h/u6n܈ u<y#1p\ʷz|,\5N-? D 3E옳G,\=+gߤ|rbБ@^0Foc#1P,p|;xq|^\6]U ;`ϜQ8>sD$3ǏEM"pv;ݦ#e8[Xt]x|'?|6mv/=sz:*,o(a`3 >>>5k,Q3^GIFII&D?1UL;NNNBg[^^^Bo޼Yț{d| k׮e9e`?-.0aX4˗/W\&Sku|@ t)ჱuxnz<1#/ Yc\1nnә3m̛#id8sa?znhx+Vn@<>`ԨQ#F x:N,Al2c91R[.;VHlE݆<ٖG)P6m2dFw 3Mes'|g@ ݏ nc?x>@  t (5몈9f9P݁`AnEΆC @ tH-uЭGe]@ .#s`Tr3rpI+nE6cl@ ;bz;V$'vGf+wd'D@ +$?S2VdrE@ vVbO]Z!w@ p7@f9@9x'-يz% @  !!qqqjHJJ¾}}BNNqI9sΝ~?no@ 92r="'@ N_WW󟨩+WPRR㫯BEE7ߠATVJˁ9NXSsЇ,@  Ġ ׿߯eeevP[[oV8q;,-'؀d{g+"r@ @V܄b,r իW }ZZZp-9X9x9x:j-VD @v9VNnܸK. EjCpr[#E~~=@ z*222P\\,KPZZBt:={/^Dyy@8qhmmUk9xTOwrV#$@ t'Y倻`˖-Xv-/^ӧcpssΝ;dCAsz:*,o[@ 'YlEi'{hw 3 V‘#G+͛7Y!#ur0Y@ w3Ӆ5 f/S׻iأ-ޅѓg_XAÉ5ˁE~cp+"@ [/6x #oǏ>9A_Kt#WcbL &EVtR!VA";h9xnE?.,&NFcOAo܊slEL1@ nXzn/f' 2Aw7cpMLot-p`rvv`OԖ[Y@ =έsX Ud`uy_LkQFa׮]VbO]Z!@ !J C/XöbhF._h9ȁrOQshOZQ&r@ @X,v i`M}14&i3# s"93f`g@ 92r="9 @ t?r$l/WJ`9X 0r08Ss{A$ H99ӫc+$w',HV9C@ BO%ڤJ,[7=1 b0Xȁg :_$B:V܊zl@@ n NʄيR1v{:8)VD @nEїO^ T`tt+S@jV2}vGi@ ]Cb.cSf2p?pc\t-ᅤ5GERV'g+QX#AnE@ Gv\fؐRc,>ll`W6AMO"@ 9\FȡV;^dM& @p)]]Tˁ99xXST"hr@ @`W9B@jf`HH~t[2r/s`rf9>*܊r@ @`)#n`s e`xs"-d"(oGwrX݊~]9x9 @ `Y\@ ^!׀ewN-fnEs`Tr3rpI+nEs@ @`gs0e7pJI&dI&dD#ڤJ,[7=1 b0Xȁg :!Ao +nE=6 YZkѣ1tPdI&dI&YLAmeIUXǻ^<&iX/EZY,݂r矓L2$L2$8Hb.cSf2p?p  k "hNVԣ9FWa9x܊:wAL2$L2$8Hv\fؐRc,VH``u`VHlE݆5 dI&dIV#9p*G>T ߌ oxPFb@%bY,vw ;sG[Y$, dI&dIV#9p,e lκ cN|Vd\vAZ[я::]K| ~I&dI&dD#bU j Č, Ew"n90s+w{2-{$OZq+. 7$L2$L2ɶd"]WG@^R)U:s"99<0[+$Ś/fotK2$L2$F&r`A '7y ĄuX$V2U;[Qo;VH~Lej˭,LB&~)$L2$L*ȁ1eQ=_9X zE$ VbO]Z!{ '|B2$L2$J&r LX!WX!>ё< xI+$h9@9x'-يz9u8N2$L2$F&r 9^*l8PՉUx `,;xO/t ;VD܊(NrЯ_?N2$L2$F&r9pa+b_*ZLs K[>{X_h[ѫ*-J;a9OE@RAt>9.,jA2$L2$F&r9pF&UbJQ!5ی*Fi< x׫=-pku%C<[-ǭsr@nEv,L2$L2$t,+ nIz}#Ĭ}< ؠc|O[%E~~=ʔf+"r@2$L2$9nEїي|ي80wX xf+:rLѭiZ>~I&dI&dD$1) ~z~ׅuᅤ5GERVn4'g+QX#AnEK8>I&dI&dU2+ج# RuL| ll ېrpg$L2$L2jd"h.# jVcKdž| .v/a Lc],\{CLvaL;f|麋1fMf<ϬvNgf͒5M/]+1+oݽ$L2rȁ`W9B@jf`HH~t[2r/s`rf9>*܊r`'9xy9n< M&y;0ݵ'EVw9]$]˕6f(b1fS.F iIf^u~Kzlցv6)m;$Lr'D#K9>tn/߀o!#]%Qގssеo߾¼W2Ԁؙ 3];fask:e盫uT63HK6D i`Q'L+jj1ഫHJuGԝdI&d"eq1ʪwz5TbF\eޅ;ٻ΁SOv'Q]CF!u55 <'_j;=spԖڛu }zx;Ft2r0ݘ1VHEa9h_SwI&d"w캂,=2bN)Gtbԡ%ي]!b$|wyGLy0,,ŋQRZƒ޸'0DGG3crž}3qAR~1JKKq7Y~!v"9b=ݺ;w\elE0IGc!me L?Y^(;r1ڤMfl-jek҆2ҝ9Wxùhz1rP`]N'O~DhHp Yu:n|h-vO /w3|KJJ~He}l*@14zP@F]? 3a+'qw9 P5(Է1skCflm%EyH o>nf4^5cM0ҏ6,OHv[chHW8.kI[1j_~s8;x uulBle={u {{>swsdI<ȁ11@@ x2r0 va4[Q #ۮTlEX!2-"t29v[\gZ* @Qm;d[Ze -Ю)wNvWIµۓV[qJJL.]¤h'}K0S",,\fd0ҮFq g Mk…3n+,5r`oF 0-B,oJKcl-!&k,tPC.<qw=ŴlHk `Z}M!ZbݬS8~v9,QqR~ޢ|?\09 ׭{6t3dIL2i˨g ,d`uw"g+zs=~ogc7h uu38J*VK"QleYTͶiY9타Ͷc:&^4zq61}L$}12DD5vMHZ!>lPmK ,up-:]}EuKc1.̈́m4?tRvg2@o3O+e~+Cl-YcU9o'6_oY{`y[S[,?\"^g>H&Α8h9,VHVHbtd-&+E g,Z)>*bI=jcD8l/ <ƈzu۳feBJ9Ώ*6E;YMfc4]OWr.6\X`2kS~06i!kuZJTi^.&G0//[ N-ݹ~FK3s`+skZ )vծm늱zg) ƃve0IE6K|An<MX'q#&/EŶ׳±2* ]cT:s5t ޚ*Ke?$~!rNR,|1fF4^1m^W$wMک(xP_+LyyS}kMK3SI$뷼v3{X_h[ѫ*-J;a9OE@RAt>9x曝$0QklzhfcN5URûLyDewˌ 9(QФG0ph^F6w!DobaV?zM490x԰BJq[>&J2| 5wfX.w9E(]QNشEn3K[e\_7ieEo_er6e0y&3%.nx8"u6+2%ǃ{TVDc>/ŨBg! ygカ<2yE½}V>H&Αt,+ nIz}#Ĭ}< ؠc|O[%E~~=ʔf+Br?ΐwL_е;KdjrC!&yBc:[?EsT S;/ž")R.;ڞME@B l-|k8[T #9Xx$2h@v[Ye<ʬl3ۭjJL ׀4kW|y{5Q'?LIoWeZƏYݧK [YwS6\Fe䑿tuw ``:jTӾ /L6[Q\_TM]Z ڭmHm_xC|Տt_vwU]v.H<Mr,+5'0Q$LrDt+,V#V]l4[YՖ7Let-?nEO:]O|o)ܝ53bιE2fv?ꝓ<30lOلY9'\OD9qH35g̭sB1tyvl^UA&eapBX`rXeZ/[}প\v}P~J2Ǽ_&k ם9jfK+nl7XMNRUc -Ѵ'hRZh7ʼ,.GEr0?7橆hھ:E2$wLArs2p]X`[XsT$aAsrp΁5r VyOG %XbjC[צLo_*sA)! 0p-FdN6 ,-*5]TFHckGN ,mWS 2CqO".Y%hЗ t&kɑ&VSW_9SiLxL695-)%Y 9+Lh[[+Ǽ_ΕmZ+\98{u-19]L-U2űVu6sdvx5o'KmnOd_1qx~]i |"OCy´=m.yFL28Hv\f=O!7]D7X, ܑي 9x,w;[d,]˗/ҥZo=tO _t>V'P9e2~&V['h&F|vױ3e XR.8v]ړ');âE㭜1,,!eё2B%!Ovnl\kjB͵(-==d Oy-Rz}D2?\ȁ19tjxU똬!.+j90'c SY?Y'|b^{ȿ[Uv9Oq8j-wA bRcPrtSI&D$zU7CB0=: =~ɮk5QVDq(j2],WWɓڵW? dGm޽l24a?$hȁc`)#n`s e`xs"-d"(oGwrX݊~]9x9u8?psQף,-*y^UUJ!J[ٸf/gʺ|"KQLrO8Fŕ (Rq XxDr`Vd:Nez[,?1#IVD1L^yaj2I&dI&d52+# C/)`oxA*FJ9[]bQ)Nr/otI&dI&d52cb89d`@P%&"i@F]m7يz۱B=e*S[nEd9rg dI&dIV#9p0 AZ=2G?: 9XB$ي^<@+$wro?ƛdI&dI&ٖLAAdBB |w#k17 Xy.VH>crOQshOZQ&r58otK2$L2$F&r 9^*l8PՉUx `,;xO/t ;VD܊(rлwoMO2$L2$lK&r9pa+b_*ZLs K[>{X_h[ѫ*-J;a9OE@RAt>9ӑqdI&dI&YL1r~>MxӣBj0! 3Ux@Vcu~*S~L2$L2$8V >i< x׫=-pku%C<[-ǭsr@nEV/L2$L2$t,+ nIz}#Ĭ}< ؠc|O[%E~~=ʔf+Br /O>H&dI&dDt+,V#V]l4[YՖ7Let-?nEO:]OyaI&dI&d52Aelʬ_n_uaoaQ[يz:*,o[Q璃?H&dI&dD$;`VjlHFM1Q K+$0r:C+$wdnC&%<~I&dI&dD9\FȡV;^dM& @p)]]Tˁ99xXST"hr9xg7=$L2$L-ȁ`W9B@jf`HH~t[2r/s`rf9>*܊r`'9x饗L2$L2$8F2r|6g݀_f 1'+2BF .jv-uЭGeh%|:2gydI&dI&YL1r,\ AYNJ̈k Pt'3"{9w*b99Nr"9肩L9N2$L2$F&r 9uYzd%L R< ]AC1p+{K# ٻB280[~;I&dI&dU2cb89d`@P%&"i@F]m7يz۱B=e*S[nEd9drop>$L2$Lȁ1eQ=_9X zE$ VbO]Z!$L2$Le9,VHVHbtd-&+E g,Z)>*bI=jcD</dI&dI&YLAr Up  ̽-Xv(/:_@AwЭQ́䀵@ ] r9pa+b_*ZLs K[>{X_h[ѫ*-J;a9OE@RAt>9(d78@ = r9pF&UbJQ!5ی*F{ZKGQ9xA Z [Y@܊@ rЭc{W0bMw ˳ :1Ur𑊩L]緛Li""@ |lE>lEz|Pu{NIUm9xTOwrV4s@@ \Ʀ*e~\9IAXyEY΁5r VD@ Gv\fؐRc,VH``u`VHlE݆K e&/`+.KNzC4k@dLZku^^I7sw v DSdAM{5UVZ&KT&?Ao{];o"90vBYeE`AiDEy*"#^ : y]ۂeB "s8 t9q݊[ArU2~0 FµmB|]pX 9h* ,3-zxu5e?߈$Wg'},6~_r,k0k[\ /W/DjֈkۮQ^w6**]a&+X2 ŵR?&b:wB pҠD+cJZ&ب6vwnUsa]elȆk8ÑG$kKZ}yI> *j?:&ֈjk9ӄRfm6vq(cI&Y,f-߬8 [µumO#IW!kGW{C7#F+ű|l?trȁWJ%6$Waž*Uᵘh|.WUZ}wrpd>d9`@rr28|Daw1M4N'kg)rp6XеdBk8\xlZʍ[#9IBvZm4RkG7|w OC)\ƯƯSZox99;G e+eH ^I:k(ɍ<9pUH/\ɽHb/ /{q‹VpʡKUm"KKJ@R&ڹezTʃkSj?^lH3(\JHCAqnˊmlG_5~ f{el,Kʘwc[_kso)\@#Y>b;0B`hXb5"'NU彍,E0*}5b׫έ=VX7㼦$M*c*jZ{ NֶspV 4D:)v(נ4[tjT>|99"|9XT{+G9`Bl3f9,V;d9m{ZKGQ9xA Z [Y@܊ܭF Y@} ƲckPݖ!v z(0*6ĉg)X!jZ_@@r5ɨZ/z&[kYWr&" ' %&B{e݊lI[=\id5YQkserVK'|Q+GCM-_ nά_]dzb?v9Hf;M>F͸cAP X[`o/:n mvɞyU|&Xt46X\J|G(yz6ƶⳀA9J[y_m71k-,6ĘVG*2wnrpO2يzH́%=@9JpR}ui"s۬FfDd_cUJc .t)e(G`a eIBZnnl /SfI2l,e5)UJMWTL ./WxlŬ5oumlG_9jhYԵR|_[YJu4 ؏a2QDGxޮ~2+ܺ)egbƹ3Y{FgG Vʵ{[|2 +d5q8[z !_s&6Hĝ:.Pt:9pr &dXNWm]𛕽 PX\gf$Mi9(iDs Um5HdJ/'Um(WH\-գ8۔? Q^^\)OG6 @M[bPb5meL͕4ףXgR~`|t"-!\"p{M>F˸bq#3U}щuSj7)sC\6Y9\XmEUrPWPulnp*H]c[Y@ؔY =@:š")+ouUX ;C{!~o*!*܊' R#.B~k p#VtMFWD|`VrXKb``σ-Yd2e(f>Ee|̏) wD0"<=y+^If_kڹ (i).5ukP&|W[}5V ݴ]({:UFn -hs[+  mEMُnWoF dvqg LNhPQ7~S8GdVrf~:aIV(H0#E Dn9qzdVcCJ5ooXZ!91ۯ/8BrGf+6arZ?z&M5*)"/iC]]Zz'=[Q__/dkEmXxٿm;67Jej6e4oqhǾ;c:ocu-;j8onn6b] h>v?f<~919tjxU똬!.+j90'c SY?Y/9hJkmdAug=:^oXݚ˳k+ĕu2RPRow];oYOҥ zU7CB0=: =~ɮk5QVDnJV}0׸`…ܖҺt&${ ֱ5u FBv)ZRMO޷HJȆ'RY7Yz̉oL 9Act+QwY9Z!q+,,1n[O?~hmCSz}}Req1ʪwz5TbF\eޅ;ٻ΁SOv'Qh6hlK9@v]ApzxI1Sv~#:DW1rPPAo܊slEL1Dh6h6"3 ȼOFUbŽ:,f+ d`vS+$?S2VD"FmFs] ~ULѣUaWDlE/uFmFmD~P2ad_a*FGbn0]$|Ƣ堟9"ўڣ6f+MmFmFGeO†UXX`hx fmCG!x9BbnEOtbFmFmD%9pa+b_*ZLs K[>{X_h[ѫ*-J;a9OE@RAArЪG&H+֚bD'` v\hhIH/4V܊F{ PujHDŽޝ4>n+B_5Ey8n͵oP;(}Is+REM\C^BkKC;ݽڮ}6uhPlVp{F J*X& Emn xwt/"? ]3r~>MxӣBj0! 3Ux@Vcu 9h,3 /2<_~P*,']~WpimH:q͕¦u^y^TEWQsphTT?6Rs:#4qcYcZ;Z$yg ))u5]lGy-Kڗ!2&~k" ǜaǟ|]^R'v}P hC&9t*/"w̭h[|x0rV%W9>{ZKGQ9xA Z [Y@܊4JEBY3Mox8{%D maúdj-^* e2HYlCy\PP*6@w] ڤ]G ݁SbCwI=K,[f~68鋳 gm PD,Iw41;,(VZgkQPZ۹Bx%W-I`Ķgtb́i#Sڻo79SlE=4W8X[$ ?.WPsˎ-6~!$]ezve82iDAB[ޮ^Զ:R8 gW|}5(X干"ũk+ k2pv^k/kgsE6]a>"cZY= 4^pv4֯"W#(sBN^7.2/mǴF`:\Y>e6hq?dF43 ήpH!rGppk-#Vq+II lnj,kJ9$\lާ 8m@JgÅK;‡5dQf鲡 2y&H$۬Ta7*{3E2+=ǜfHVRYwg3A!R\5;Gv\fؐRc,VH``u`VHlE݆ hleʷVV4+NvTz٩`fF: >\QQ,~u+a4bq*)U,^^BeXR f}ԯ:ԶĿ+a K]dp|*X>er6^qm1VhU(ZkxH-K {F\;4+l8fLJ8rv\UeZ5Q `)ޠˈR=Kk=iGj%WFgŹRp8{m}+=X{r?=ŲȞɁҘ$fz۵+xoU|YSJӞD9XA؜u~5ǜ| )b/9Act+QwY9Z砻2ũ/o/vN55U$&SyzyŸ]MH(Smqy d>jʳ52Шμ}j[GjY:, >\åt ܝUcJVS1NJxg׶|8Q2%~k宷|l^=Tjǧ>dVXn;rPftT~䋾Y,j\y~qO6}ZOv'\tQ3uss5pc>孱@6aƛ[x6(c}k=fLy)=`>k_FPS鬾W:yMeq1ʪwz5TbF\eޅ;ٻ΁SOv'Q"mhGc7JAl6龜gk3rIZMT^͍5:}[6ٱr mSƚS޷avҸknn6KUu\5t5N9zOe +1~65c*m6-=#Ԍ)gB% 8K =);S 9(u(样nErrpow9xDa"{WHnEܚ+)~m麍=<_ykAW ɕ?RXKr 麅TH}uUlxV'nnLB11@@ x2r0 va4[Q #ۮTlEX!2-"Ak ҠA+6!_5ګ[mql׷R_Zfl_Bvi'Mގpۅt)b/ )z*s9 HŞBr"= ^4vt?Q;]BZ"˄}oc;oK;rf.wӲOo3lz?qfZZņ(r)x%Bj*C&q`S'FB+-Jb;A|yёt^%H3sƖ9o#N3B~Y*`;) ڝъLD#@ R)?Pqz۰b/>K8cKMZE]@ 9XDrs ߝ G'`es;_gOJTъ*ρ9[`9gw|a***c O [>%N =-9x9 .aBO{V>=@ 9rP;=ml a6{Z7' ɕ 9CqJӉV1vdM=xP_0.. E։)~Іc]J,/ @ r|ΡIK-!Ưag ^ZTA"9=[4w2Փmi$/p;^E)x.S>NzTT}Yl79^xn?D0->^B=^\~}9`LOؿ9]8⎚}mxnWTػBˑ~e0Vpt :9gcdr0ӆIȵCi.@ rB ?~{TD3^r 9!%́$hf95E:̊Hsw ; XXX Ug6^Gs|8! O?-G G>U|CD(w#dMвw5xU[!gR='v;ν} \~m'*?&SJWS`iZ 7p`0!v4)@ DV!{ߟĞI^ĿL+i o\$r)M]iݘ)yNy2THvåGm`w &~.s/+7 vFO& 1S+)#`,V`ȁ}#sNbvj cp~mi+h>dn<#x_DUsy1֯ ]-IZ^~W?aNg3RHWÌvY+ʐ?G vj:]Ӽ99H0+2h(e|)lS0+"oŘ~LI\|- [U# ޫB9'{-9oۛLfc͒ZyDS!yrZz~*שDp)ڡ6D@`#>w'K9ɡ̊L9C%Z sʢ%[/~xi 9H?xy 9uHLr8́}|{CjH3u1L {Qჺ]Q;4~ 'Wy ItfUT"j}K"v@ 9X!?`1`qvOъ^eAPz dH^-L̊HsaBt!>a:O*,L|;6qqjxAq_6^4T<-U׉Y79!9y'p, $r,/ޏbRx"L~k V9P'*@m.@ rB>}6w< 9x(Gxbodk(Crv5h s,RχNU /ֳh(貦Bnb\}< 8E>ls6\ 1$8.vޜ'z#|~yJ{{wɗcA>HI_  i ɯp'с)x錐!_Vs 9XlJvF"&FpYz5'̽"ć !!*r%'٣ufӛr*ThZ}X ?PqErPFݲ+i2hED@ 9~hE/sъ&14~5<GV4[sA!ijfER"@ rs ߝ G'`es;_gOJTъ*ρ9[`9 @ AM5Bk 2$W2`)eHN'ZQƐIs@@ ޟīm!^ ivv{1ൡE$۳EspJ(S=I6@ "K9ʻOUA9q%7P͐?N9PIvk&[S9Xì4D@ 9Hr+}{'Qz?3+R0q45wiVtc9:9 r@ j ~n!c8uaKys"Vs`Vd4ρPˢ9R9 9X`VD>D@ 9NrO |~rhOvv 2ρ)")99S|PVd4C29 @ 3}ȒITNƐͻiTb o b<"{;;N6wsd4͊Iì|@ r]Ʊ)gS[_m@Y }K@Y j>Is@@ l%O 9ݑqq?6W]ď߼V 9 w90ɐ{̊!>s@@ UaVT-3rmww]Oߊ6wn,LZyKAs` 9[!ρ^9 @ }v5Qvd_Kv }^S$ej4 -Mf(SVD@ 7+:xV2h߯ÏN\ózhE5Bޛ˙hVt/9 r@ !<o ptV<Ik$hO((uh6Y@ DԎ`O*[Cy,MI!Cr%C|Rte 94D@ 9.|ΡIK-!Ưag ^ZTA"9=[4w2Փmi@ r ?~{TD3^r 9!%́$hf95E:̊Hs@@ $b'}/'9< r!C?lJSspWfE7fJS"@ r^:?VQ t7'b5 fEF e,/% u fEs@@ $o`o*ߝ.'f1`)`G C)R0+3hEF3$@ D2,9De$l 9ؼ{?BWrPs>)hE&fK(S-"9 @ ;}ma;6+syJr|PT$!hE<Qd"@ \W>.C+\I|<~t` ??<^:#dH9r ԕbli v䕢ʡ*8e{xK74g5) gֱFo3LUE4-"YG. w~q?|oww8 <}DVYs\hE~~tp?#D+Э9ؠ45_D{)A6QXd= (˳uf2,:5=(殯Ѳ#pu50u |f!"%!8Qʬz_~͓h3"^>"KGBe 0BAQx-8ك}qx%M4}'"5w'I1ӏ69hJ3;+mFd=`}VsiA^8w6:C>[Z5V::Pams2&s=#d$m_E jEtoG*mQkgZwUZ' Fڬ6JmV)|,>9h #Զ Zlfleoׄ@9~%=mf9 v;?w`=r' Yٸ]w?xr' ۶q$31`K ?[[h³~ A(|rI}?W޽ X/jq֐ρ@O[3ܚzfE9Xar0ժ8{Ȩ5MK1S o-]!'P' RAX4c#6LBuMKA>r`{ n[a^ByqXk!CLGp5P f6HKȒBUr m?#}'_N ۊP+'x&yVdMqccspBJ?*YsjmSnZ;ɗZMtZ_M^WZYۡ<fE#?!hzu7Z\gc» l͙ByTHͻqtV+rcǖI|A#;J6B2СYQvz\.loY";&<|<Q`~Zx:>@u.G. DV9C?=(o?V )޸I`S4+1S|d9HL~:&0Iϳ4 QV^.G,aTά 0a/prT& ~d!Fp)#e'Ms,Gr6ۀR0r8S<9hf#=YKDE[s \ZY\m.LxYk)o 6=&zhu 'OF`+MrTPB2r8'߶*mQkg9Pi\%U5@mڬ65䐼 -;6s1[uphh9}7sw@6ٳ w[3%oqrp ضd/G5ȁr?b&P9IRNF<X:?ᷧKĩ|4Շǝ[^ϼgm|DV9s`w{/c^:͛"y2]:"9ȂP*sma1 b;oX4o9+@-'GMĂB,9r0-; hj9~%[s!f_jMt'FˎNжB+LQU-D+qO+Bsh6fOUNC`\hDX'38\F(7.؛ۚ0:=-ɘBi=ᬊޫ#<w9xBUΩM-jɁZ#u@̺/eWmڬ6YZAsDo$mΙyxvm+gFuK'(gj9$L`FcUgļ(ެcP09XhE59 \:s9|Q${;PC<^; UA T;]OcSAxS90`V$%7g*ъfH&HGɋ3(%kF{c; D6&N. Bd e1Ibf&v*W\Z̷c.q0\5(,ł_,K ɤ0iEd8Ӌ*>\x2Wѣ\ y 9(MqƢԄᬐ[wm(L"\«Ҹv[qh=jcDIKZ ,Ȧ8/J$K`*imZ[T~+Un7/4A;\ DZd/j:Ө=+̫͵Z`<3%Xlr0ҾNb$0YכּMWz}V CqO-mh5G,<yRہðgdSt0߅ц6'vX[Lj@G4~d#x_cY/}={?!{O}^wv|j9#K&Q: C6ǏkUԜO e7Z@Tˬ4Y!Øf6뜨v^vg@AA1,*p-^!P#>zsL{mxFs &DF^k+[\$aܬIּx 9_gXt")/=F6ILڢvn1_uSja)ε< (}Raߞs\#.yG{0 笃r0gJ"8S$ȄX.q@䄚 QCh) wjmPG J.,`O׋xqdO/_{inwC~#Cuw;/9X >}6w< 9x(Gxbodk(Cu@ B䕤Čkl>HE#AsMHĄk6yX`" Bl1En_Jx <08#<޼8,1!GgLQ(U982$eHG#N3B~Y*`;) ڝъLDi=pT`X`ZQhE0L/{CRT5+5˜!7^Y(CNgZU{a3~7) j!x;|cwlqxuSvH? %#7iVtOfEsp@ 9fro/P`>׋pGks `R&D2Xr8v6 v ?y (^x!o5+9X!Y`=ij!{ v=˧/߱~;~޳g-D2</3wG7;\u?~*J[26<ߕ$CQ0+Z(@ 9X`B C̊j|xuFⷍ.?K[< TvI+})h2!w+9Ы9 ""@ r@AFj 64-5M;ҧHh[|%P@ 9 rfEqъ^Mcikx`Wh@`B({|9͊<D@sM`[(w/ă.|yR?) S)F+ʪ<Jn dVD@ DDԎ`O*[Cy,MI!Cr%C|Rte 949;e/ËYS(-,@AA!J-h^m20{Qcjhb(jm9*ѰqZuYcj͖pqbNT.91<Y1Qs9]i]+b ,o9MI\#Ye9;'j[/Wxnj]^/pp xmhQ5lܥTOM9lr07P5eYf5|bT81L(.6 'VB\UBa=̵l&犞i}b[qbA(..J3 QhmJ]s&d]1Qs]qPRTT92B] s"F"0ρ݇WZ$hm@K~kkx *`;c@Is' ڭ@nM`"\gfE z!Ϲ8A*%BQRy(Y y,yȫ@(Gθ*:mQoO'UFקB pD}3:Z({5u ^U6-ew1̑l̉wDoJV9L"CqȻ8Y[;;{>"|>$dHf?a3$,Cz45wiVtc9:96r0֪Rqg:W("X4߳`` ybV4ଉ}5 Z,9r0m5NsU7kF[k`a8YPWWNDJBn݂ᨐ,KE9S56Ӹ`ZF!_ol87ρpUm^ure(ɑč?7%߫whg\^y -fHoOCܵM"f y.JP(%Z1m )+}A%fgeƌ}ǯ4a̒+a̧9'6.j`r@ D$<۾Z8n<֒&>B~)2]#xho}[\@G I[oJLEsrpArN|26rf5Mx#{ ^9 4 υA9B6 h= 6<8q/Hy쎾(:VtpB4j/s\U$#\ ܎*~KSLAUkFx}ZZ@O9 ^UL]j!Ql͝'382PӊPXm}&eu5%L,~-OkdU: txty.U=ɳ0׎3$ֳW9<^G1OxnMs<8Cj2}!ffW%3~ ӊFz-;Z[L&j$XE?JNZE"[HOc#~8n󎖸ߗw0#gc9vl޼OHO@(lل@ۜL{&\[3$དwCwo+Qw=6vu= ODV9Y^8~0:cMN?qa#>̊L9C%Z s`2iSP)5_. o{) 1XA؁K2j {ZGzC!LO.,h W9GcPm DzxStN+zG6Usȁ\.& #pO}E"6jF7+o[uF4F{8a\*631Q9zzW%0 90oVzM9\sZ1ӝ:p; aV+wb&, yH?M3s ;@5ߧ=.DrDg_{?&&&pՀ-,AkZ"i->0t;d䨓G3la&Ξvޭk&z켖6 #ط'6מ쇧'{+ۯ}89#G X`93J$s9~659-B#{g yk SŦW0/go6i[cȑ#F dH^-L̊HsfEh(y̋;h.ApڔGw'A-^c2]<G1G0=ڃ NP,>0L>/' V_pjaL;Mj#"K$XaPE!jFM-X'D}c6y3>+VTvLW-yS'VK8~ug5#,qńcrAiɋ Ӌ92fE*ȯQaȸ''0oFCLp# ݛY[YA9'o'MCQsA7nj&\lwiJqϜƀ%,)xO?oO+JNleڄgOGP9C[kg[_?Uck~vS7 k"qQe^EOƐ׾mؒ`se;gjIl"aäsT욭3X G\۹zvH̋͊RE(~m%`3:&>Zo }ux8~zWp\_ϴǎ>C cǎ/Rgw[omni-~qQ1>>Ǭ[:5j`JhpHV9XOs:MOa1ы1LsZ30S7{]D>y_&ͱ5]o`ZQj3ZR[SąewZ%J=܊tS=k41g9i '1@ଋwZ&֌+v+gٸ6qQxw9؎{8A3P9O@j;02rsL`*4Дeh3zBi"9ou1ζs12NˋO4~d#x_cY/}={?!{O}^wv|A,1hoo玃Ò7q~Re lqqx<.KhL2uHPeɎ%}Zcq= y ;]w+ջP0[Dao ij2t!2 ۣk'`[V:{k {p Sz$P3EɁLЍ nK־r&Dv^|k,)]5ԏ7!*CnYHk#(i_xϿ6驪'Gw^ rGgg'Gp)|xw9ɓ•+WK!})h2!w+9Ы9 UH"T5`t%pMOq, /y 8QaxWu]ЃM媂]!. Z`"BlXnztp5?(E7Txx({x[71GL8Ntuu}|>FFFg… |28${s$!<#$h,79JL)ZuN䀎NvoCYp!U_3?o?=0c?Jmքٳ)qyj9"pU|XXXW  LMSsL4+9 H8go/P`>׋pGks `R&D2ŋЧ~H$"GJ1ZQV9P"wl ""@ r=zϝcwv~:/;ocG{̢AVӉV1v9 ,W[\ךDrp{hR e' &9 "D#́$hf95E:̊Hs@@ D9Xm`S4+1S|9 Ғ599c|@n6H)* `'* _پ?o=t/ˁX;ҩzJ娵Z4<'sJ}/׳WdPf+EEkUyl۞iS +1W ˭Bxy!}a[P[S &(Aȟ g`af3 /|2umMz8&yTorK/9`mu2cM,v,|kΆGPU ;Z4,|َѦRMxW'{5UC^֦rxJbvs,M6sKv5BN2u3a͊NX~.@@j-, .戩xm zl*kI^TPF'U=ֱz02v"ďS ~F#iӪ& L2\8πv!<۾Z8n<֒&>"O`]$A)M{0+"6+ Z9݃s ~)g^X(MA.Xٗx0j s4C!FEA}vmxpPoi r =U=#h*e*L$΄ȁ_R ! 5a,YaWk&ʏ+(!:w^mVFHes?cN^5Z4LPڴJi)<2>iJ2~ TsHL!5r5ȉJ29>/g.W !X~=ӹqH ցPڽNZkl}3#ٸ;vcB}قon0 ׾cvx&V7~9;q%ʺƮ54i! kVBsFCzd#q;/&3k{zϬpl>ZFc4G<5Aڵ2xxЁ9(p_-+( 7xjdɁ$ $*^+,3Fˏ qi4UH'x3ǜƽkBM*OZcƥ GKUp؊ ձKXѱN*I+k@m-ŭ]eXʈ>Xu=ӹqbX H}V#9yx;~LLLଫ[XbH/O$3ҍ~(O<-ƍtȐ-ۙsa>@&n\~x$voeۼOqYr041G>;$rg1vI%sm82R'~k%r@ъ*l˙eC} A59LNNlYAAw ;zhaL'ܧvu{O Z v"r⅏V+oJ0!E6u!RoߗT)ht:$-?4Z->rհ =&45)#&Ns ?s_ڔPιZmXZ1 (nv`B'O?c-:n4)ߨ@w; ad@įc0f`NɆ4XKQ5vr0 ֛p# [6onܶN[cNZn'?o<#<+|f5 91b[pRjj*#+D8XgNc<~''%'=ЄgOGP9 r!4AʐYQD+m5ETM/+vQm5ay yԮU0zv{jz$q` ݊4x{n)2;GV9 2Dݾ/9W:}81m,9' yp"*#r׿xsuښPmSB962vjriRtm֙89`+tlL >)5QyʯȁJ#AUq. jȜ;;/[p8qZfE#?Q ͺ-.^P-9B{W0(#.Tl[mv{͊eB$ڹ݅ 'FִȮX*rk GGRc9>1kZTt@+>#$h,79JL)ZQ2 9r!Ρ4V&B*N؊XFSV EZ. ɑRpVH$-!1 ,pwNic)1 yI`N5 e,?\"0#řfT) TӨ-͋#v}PmSr9/7*֜(&[m0TzOyr}AARvR?e^E򳣰E(4-6n][[Xi=Si;$-*#7N!9qaufn\}rEźyrqVl@1ѡIF{p`+K2.-FF<X9?ᷧKĩ|4Շǝ[^ϼgm|Di6(27M3Ѭ^s:B$=sȢ^2/ZyAS,Gs.K ~4ʎpu*ވ6%κGNSE.?Pczu%V{X\ *!9*"}۰eG ndflܑLk*vYC21M:&2'Zo }ux8~zWp\_ϴǎ>C G+ʪ<Jn dVtR (bWda\%*4ZD0)B #eL##;yp# κxgޭI;A`S~ؘ-wAh- -L9OŞ聕)gɷ=9`Kw xOv<?5?_ώ3Jr wE|gǷ@+Cr:ъ2N"BFauF!nDB=jh' 8Qaym5 NӘ`j*0;8A=1)9\~'<9ظq`R='E*ڹ{!m3 e%4{^# #¯?KvϏ <#æ׾yiR$۳EspJ(S=I6@ Ąo6X`"bbs LkF; "h<j}Ooނ_exk\3&()i}4zݚ }0+"@ ,:9c&+P3eBLO㏠~# 1xO񵒿&%A#r)M]iݘ)yNyj#8& \sGkp={[*eb@s`4h@n6H)@ 9X=zϝcwv~:/;ocG{̢us`JHJn;T͐L>D@ krud Cl eeVD"@ r@ieE(C2@ 9 r@ȁn*`;) ڝъLD?r c88 jV4 -]m]9e/٬N{:aTi^b3~gZY>Kջ 5U|>KFnJӬ4̊`AՖJԹ䱙{ł޶H"'{\V޹{)2͍3x,Iջ DDCo5+9X!Y`=i#rcۄv1RcѺh+A8a3zO{VrnRJL_+K2+|^o#YU90ɐ{̊!>s] <܄¼BXmb &5϶ioPeE68s-/w9+FsyAxPb$cP\ъ9mM桸'nœVOmJsx?seē94يWPZ'WHμD~nB= q}hPY(..-eWDrx_ }LZQsq cӀ5#k"DViRd9X+CVsWs@fE+Lxa}![@\X΁ZXN"VLZ op+/tt]M+Ca5g'Fk]M: bxtU@PŝSHIʵ19 u֤SaǵÂllrB kvZ^|Bf~`ѦRM9W#+% ?nyOÌimUYT-gx}LuS] }L^cjZe"j LMSsL4+d+9 ѝpy#bN"p&* ׳yAņPl5yQaD>Cf[_d9=mM4[R좴sV\3v߄Jی2bxtFRh-! =[07NFイ_wk!iɒC6=-U*bP>FlAYD 'kq}d9r6F @X<9[;bLt`S&ط9;:F0ڗnlGgbE{#ߺñW)`?zmz_CcߟCr*ъ*ρ9[`e9Iw8'Z]ÈAk8PS *I\YF@Ka8nu$ n Uy 'SaEmWW NPRoZm鐃8r4Z9BDLvyCQ3 s)^l樐XQeS] }H֊x%Z*M@5?lL'r-Ȇ< ׾~LLLଫ[X`w1$'F{G?F'l-!g[363B|6qM$ط'ybF8?[z7G` M #, ~z ]x9~pc_ok{@+Cr:ъ2N6ն&2uΫp188aQF؉9 Dm; 8UPHI@=j³r[mƇ8s a]- 9OF+MMDA0QpVLW?7q+heƍ&[R֠x@{=wk!P^+jQ\şs9x{pPFAk3kPk< 籷uNr0 ԛp# [6onܶ=.;l͛ZN_rȑ@ZN>^W&44 wnIir,39`%9K XRފW Ϟ2c{'r@T5lܥTOM9ȀhErD0Sy>kP3/C@YOly3Yw Qr#'nW0(G'k'')_ެ?\'DyYQ6'q6'krYrkw<5-kj7<|<Q`~Zx{w?wovo r@%%́$hf95E:̊Hsy"anυ&=9$UPH$-1X9]}iw?n>Y+j=;5R>l4¢pDB͜u_sS. -֍\~ϓط"gk@{-r0W[Yaw)lG/!ndȎ=wH?z?s~{O)oGS}xܹKx(WA&ܕYэ:MF '}۰eG ^dflܑLk*vYC21e[29X6JftL|q\(=(i!i/a_}it90tY4_J 7$̊@ vw4H`.މwk-F$rWT;6`>|] 4XLŞ聕)gPlm4 1"ur-*oe<^ט|k_`?;(e/`ߩ&r@>F3$3|sܡhd9 r@ E"YN}{Bʔl.?ɓ\lܸE0ampV"=m R? ބ\ e%#4{^# #¯?KvϏ <#æ׾yi dH^-L̊Hs@@ Fb$Mj`7PNhbe^5^gS?[o-k~|Ƅ>2%AfEY6ʐL@ BX$how: GPuEs@@ D9 r!4AʐY@ 9 r@`t25&_I3)E+"r@ %$k"Y9ؠ45_D{)@ 9 9 䔢eU%rp2+"r@ F:?s'":Cr:ъ2N"@ $Ke ܞ-TBI4D@ 9|4zݚ }0+"@ DV9ؔ4͊n̔<_<EN>MtAA r@NJ90tY4_J 7$̊ @ > r@DzR0+3hEF3$@ 9 9 dH^-L̊Hs@@ DDHs߬(+FݚUz>wfS;59 2+h]$A)M{0+""@ r@ rp=}K@Y j>Is@@ DDHs+ZWMw9uH9 "KAs` 9[!ρ^9 Jh[|%P@ 9HlPezo/gYѽ@ CrJъ*ρ9[`^r03/ړ-qq date 94DDD|(}1>3!apY_=i{uX߀o 0OLG0' Xw.z^ ^~'3FxuazY3YZ^d~+kwX"ٌn} 4hgs<3ڀ9 r@Gh٢9K%$hHs`ق933Cagrwa9ɱ}쁕]˼yaLKa<̍%iXoďL==0Xɪ swgp"#cxjkfilD[e_?1?\YKTodYK^y=dGR^?Y.:P"p3y4ZK{ K3"kaLs9ГL s^Yi1߄̋u$=pz xyApMo^[gF:xc'O8,oyTQhC\ 챧}o}~1lʹKaʻ#~zʽ2T'Sy[ekr;^=L=3ױJ+֫<>!oy9S,gr3fViIh̹~L2j5~mRyavMsC:~VVҍ_f)Bjx!|5W!_2'hͻNX'˛L~%[?'QμX[]\r3|+VvOLyѶ]A"\.)73'TuC=j헭`?KI& cmŒ3gF6~v;\_>Fy-t7c?sRr~Fp ǵϥ~Xt?s [3j[>Qй'NRz}r\{0#t2s Wm?Sv9HrM= *saߥN Z|24KlK:{]u4*J”rK_3__i)GIUl KhKɤaq)P<_ZiQ__yku豿{&erg@ 3$aqAR0+3hEF3$APicNX- 66Xy7v%f3<.tR9JރwkgA"[ۂ}ʵGJmn>4وK7rFMJΦRUYn?p%Nb55RJ4i!J$n-q.A39!}Dr&?N![F_zzrN/uߕr֛b{UnH~DC9ߖ?}33w  s/ .]3qN&T9GfڈKNΌAVF+2Ȑ6[Bj`Ց$й4Ƽ0}c?ĩ9h;r/d_θ]b9 aM J2Xa}[d-q y< QjRF8[bu&>ㄠFñoThB;뽄? icQ3zvuO= xKZ3|~1BN).+&Vpe}Ƚ8sW#gI5`e+oG^v˅ 43(\GH\7g^ ~SV"GV\*@_Sy]K9ŗ̝8s3q[3]{p|D!Z75m!Aٯrl~33 : 3|&s<>O0>Fo|^>/;nyڟNz@^R%ь$޼uA-uS[i3~km{|>[8h[$tmeʞg&}Ν~DDVDs 9XlJvF"ʐL 6c; hg:-V|-]7̊L9C%Z s@@ 1Z@Tˬ4D@ 9 ́~speH&r@ ݚUz>wfS;59 @̊tG+Z)InJӬ4̊@ "דYѷtjᐬs4D@ 9 ́hE_6̊!>s@@ "KAs` 9[!ρ^9 @`t25&_I3)E+"r@ dP9r&Ky@ r@)E+ʪ<Jn dVD@ ę(c9 r@ %$۳EspJ(S=I6@ "יρ@O[3ܚzfE9 r@ j#ܕYэ@ "9Нh(e|)lS0+""@ ρvuo;T͐L>D@ 9 dH^-L̊Hs@@ 7+ʊ<QJ"u=0x:y8:xհ*QmCo@v}'Z+>p_}KՀTc9۴Xmľ"t,Ar^ٔNhE&"BfQi]̑v01)TeͲmJH{Y rES/̖j"8m53s6l˕L(ʘ{21֬3iEWx!:\{LeqZLGBh!$wKe##:ɁjJu714j1!"ٞ4AʐYQpl奇ו(^c;z$^8>QX/(9`ύ_,Oȵu%9':3:F{tGi] }E`cP7: _:QEY2]oH|:ΤW]mKi>^W82_̬9 ɕ&ֽ cgxƄ@ d:9x@G(SInYnr4CRULeqWK"xB؄f4וIBT[Ra*eYN7juϬgF]{;NIɁن"I9l+G-Ir}Ķ8:;,qb <Ӓ6Ew͹6HcIMubה4 ,n :J}X_ې|j#'%%a{.յwZBd>F̔Nm?s5J@ rm LMSsL4+~䠻:RY>t43>=$"nG,:&T=a(,%902th.pϬCb}vI9eKȄtqm;ID|(}a^k! LFM(-8k#̼?#}t15oAIf ٝH7#Fv"9Hs5<@ 9XDrp{hR e' &d9:q& ,}PnqxholFkшJK\g{Iyᄧ ]A2NL6Ib\f {fuGu߬69̘6;A>x@x)y]sEIw]Ew䨞&c !FcN2̊y.@`(i$A5)aVD'*HiC}c=*vh-~tX$e̯Wj714 r@jwms#4AuQnB8 ՜@j8VV㡇$F!-C}9ޜ)h,ђD:Bѭj}uN霗%&񤞱Jj1 yiŭQ-r!1R+#\cN@ 9jr)M]iݘ)yNyh |^: l5:NI&4[L/19J912T :C۬TbY@'ZMRx":΋6ρX; ?kmwZMRs 2uvawB5J@ 9X y2]:"9r$MIiMyaOKE{urpdg[WQi/ҎwfVmO^K"/: | +K$o5Ln2<2uWJfj1!"Ys`JHJn;T͐L>BD!0PjsM⦄-{*3k)LMMqs^I~IP{lZufʼhu?E<ͬfәeԽL D0Z@Tˬ4F3JE0e&N@ r@NJE+F92$9 d82@ бUz>wfS;59 vJ42!4|ܔY=i@ D'o5+9X!Y`=i@ r@]ъ*l˙eChED@ rp=9/AF2n<z5dVD@ Jh[|%P@ "D5Bޛ˙hVt/9>r0%$a:蠃:2 rp9?[(uh6Yi@ 4qfED+rp;i@ r@`4lܥTOM9 r@ us9ГL s^Yi@ r45wiVtc9:9 r@ it90tY4_J 7$̊@ "s!yA3hEF3$@ DhE&fK(S-"9 @4͊"m!@ "DtkT!uܙMIԈVd"r@@ 2+h]$A)M{0+"|~ qqqe/8&49+c'gεb>h:~ .<^ImLݍL9'04V kk\̊SsF֬`d59 rK8oMmJa?j"|8!) \iߩ8u(&~%3FO ;Fm镭URAa[~-UnIuq {_"ח$CQ0+Z(<:0>Ʃ5w5/İDsXRPw'{^+|֢/F݆i 'GܱL}q]IѶ X&5r8(,q :2*})h2!w+9Ы9 ,&999a WaSCl5 a\'5My"n?5r09!w?|O&<8x|Aku=QKlx_|ql 6An\ayЍC5q <j?F޴#>58v NLu( m6,N lQ,{ z5R&n" Wh;|(F`eA2/.qgQSsnLAfyMktKh]_|:́M_f-6aDa1dq0PSt<̭隃z1 SQ;&Vki>,>qNh[|%P: Rb 0Ic_ü,0OxY /ă\ 8= &2L#j0=}Q6ɑ:9HT0/(:{ ]}Kj7J,V^0yS(H8pW0هƶ`Hr3&NY碟S'iy#&8pl#^VkQr:,! aW.{z"r Dr4Ɂ(cc&Q ;ѝyFPss#auԧ|q⺊59$1jmJq>V5 dW?R9ؠ45_D{)myn$ٚdԴ>}@\HFŗMة>)E81S6ռ=B]QaBYG$B$(m@#,]^6 _d`3+r%( V! )5=  bO#$4 ։@`8zBo;0A< FNA8%}8%\fś kDsT0+fv9xp>q:(>5KW^5tJ4äugF<3UZ iH~/2.Cr:ъ2N"]]* .nAW1ŗ{TpJ\vyu}>vxB4ᶃNb*}!VHPC/|{Q #R,pwq(IU"3;D́F,{q/8%V44*e_4aO/z_7 몱7-a:*0.kC-?̐Kv}5[*rp0As%KP-8}6L vjʚCB`RAt*+B ܞ-TBI4Y(+qFv6.+4{_XrkTgw>/lϟ@-(d_2Bp0-9!75- %xqHK\B}O%"3<ЍaA b[{Eц^SG# RRtyr5v4=jm6paфj':WzpṔ<$y:qnxlY׉i i=[QN뼢`Y^rpP 8^|VCNw0<:ȁh.%3wm?|bs4V"Ys9ГL s^Yi$h~wc]A40Ͽ;"y`! Nw84dMh$$Sd+.`j}Opmmmt1d#9~6s−fLg?$|ȐP1ҍLۜ c̤+p  vq\0YfI"$0D@ 9̊SsF֬`d59 r@ itE+.gVQ"@ 9|_ ke yjȬ@ "<#$h,79JL)Ze.à9/*C=.9ÈC1_P !{yj' {|J ELz6X ^Mw-*+F7ӣl^oR}XyβweooY0.))"rp=h6(27M3Ѭ^s@@~ pNf?ɬw*Wl{l/CzjO\e1f^i1z7{JEއq`.EMqMT|/U9KXƕHY֮Ulz#||j"rp=9?[(uh6Y :ǻgq-,2$˸~ΖseP29iYDϕyV#c,]L7!9hECn'(rrrCLg_@b uǪ f Y8.^=2Ƚ@be0h_ w(N܋'䆯(_0( faw=M7HvJrźs- iEqM\1fO34ȁqP=B15$ɍE|EY3wϘLpFz04<5bbcYN-twl* 8[JdWNY#!+K$sk{rȁnwU*qVnS>|a ੬_5t嫎sƯf-aPyxfUǐYW,sKQC#81Mݹf\GD͠7?W/*)*3ȁ,"$Ke ܞ-TBI4K@7A`^8ļl' BhG|12/q/ "xbuUM1/b& f b_\F lLhQJؗ8Λ5̊$ Qa4׆p[q|a^" rpc1kFxleK|,mbC̹{+H2| n@B=SA|O *i36!PW-oP"ZkvY"(I|3&~NZnjQο5]9[SSA1UIKdGZy/{+uכ0^[e?ZdϰclRcY"s9ГL s^Yi'r|lD- SQ)˕%b*üP i/vbnOo߽Nw͒xOVW_TOM0}3P g<\_g~ypNF0o|MlcbH 緷%K}66Bs3]I nʾT+ I#Lyuc!\Y֚LYhO5;cד$2" f/BIֱfyZfR'o9?9ɻس8ۑ=:k"i-~Hvn\3Y$q'$7@`45wiVtc9:9 rhV4\v"=V'$PĿ_@+v u>0H'>`r%^:ALtĻθ8g]7_B?bv dKT/Lq\TѿU6|S9FBIh ZkH8}YZwuY^r91Im# kWi ֝>~ w4 g&ݶ?~:~O[:oA90tY4_J 7$̊|$d5SI~z/ d@z>.is;\ i_;3/ID5wŽ梐gvam_Wl|ݮ;XHv;׸r1 :!+ %"lʫvDՈj^7ӈ*̷Zٟ}ވ`[_ĂYwh ZkvD<5=j7*kT@s&gˮ5F.9'" 9g?jzZ9o _(9)h.hgH^g!L9C%Z sp=2 5K^~,2pMK))R(g…yZcv q*]1IFh|+zW싖D`{3FJESu3!Lkk㍒1ɏsrgדyE]lE< Wsb9KZC:P1XV!s̯EWSK ̝f[e>1cVd2!ym22+"1w}cYIsfJAp`_@f]`m.1N F*8 }3˰Y aJݶxgs';90x儰jdC]"$"B~tDt^kKTɷ3=/?$BE9#q #? ~ݳeG8_3\T)$K.GUZluF e6qz^qg߮ӧ9|N=g2Ǜը!|UŞ|@buYwLmiRW9ʞ`̷WJĈ]$r@`D+F92$9XLDp9"Yp*V?0˶IgȬbr_zڱ2sNXnM\9*e_ fgȭ뤞kR va`!$k7<\^F9J=7_e3k6}mzVgAjT!uܙMIԈVd"r@ YAtA""Y|>KFnJӬ4̊:kS}TW#G;q7ec\]3> ?]clls@@ "KAs` 9[!ρ^9 @`t25&_I3)E+"r@ dP9r&Ky@ r@)E+ʪ<Jn dVD@ ę(c9 r@ %$۳EspJ(S=I6:#aF, V`nC2{||;KNɘ\X㢝_#U "|4zݚ }0+"DfUfYTLj5]澳jF}1Kڙ-An3qLk^c1\9V_rDX:r)M]iݘ)yNy(ϽpÕ>7lf cf:pϟoÂow3q,^{dp-Y9X5ZE_@ r e,/% u fEsANNx%l-͌`9Fm8#2#Y'j#*qVnS>|ad&x*qޒ/ܟ _/&\e)a*$ !XY"pw0,Jrs- %XÓoRO#j*jmG9!Y\ DmsG)3[0/TgFvףtO>]re ͎sbpt .K0({?b6i]y]S?ST$#G'gV2s=nԞ5N@ rj}L)I͙spJ" KɁ& /j^(`;՜ UMleo80,܀"(؈BRXbd2c FSL9b٬ ;6[xa.CZ #P7Z jVɁsS8c6ׄi&~`-cj~׹;7uf/1i@ڬ>1_$HWָ%5r?ki]<]D"3Z75m!bca2&% S};؋< mYv`%B`]\#*İuI #]$"QJZ lF IRTVX98)SvIIۉU,[B$f$}X1UaK׀RߵJN[:׽ɵȺz'DFs 9XlJvF"9Х9]QApH&SEaQq,i䅞Y7oOB{?Qr{#no=|E0\s$mǠŎar&K9&umT` +Obv0?a|׎ut~0?ܳ#ښ6j$Mھ\ýZߵ [5f'~Njq@`U9$ߟ4+' "9@n.Θ;E{/ظY!xc~F!?NICqޢmeDG`{3FJc]1P%f`B i3#6j 9X1UrV$˜HX9ըuܵgKropgs[%#c6 c1[w9~ | ɶ_k%oHzaA'r@ 9X=fEҩ9P#kVBsFCz9P"1K13%xGe$BxmV ) YfRI#ܰ)¤O8|I~\{Ubn/KydGEaxgMjuQ,ȷMmn}vqgB ZxoSR0wo_nTx>#q|FRmfYQX'Cu]Jv(P5?k_<^D"Ns`!(eC}##4%|}grX*y!#+vU0; 7] 3[lD>%sA:6T6.nL6"+Y\(;'Km\{"3cf߹jt0?})?+ "tdHRd9X+CVsWs@fE98:CA d9x@G(SInYnr4CR""˧Sw#PYOI < 50 A?M!P]ͅռF#DXZP9r&Ky@ r@)E+ʪ<Jn dVD@ ę(c9 r@ %$۳EspJ(S=I6@ "יρ@O[3ܚzfE9 r@ j#ܕYэ@ "9Нh(e|)lS0+""@ ρvuo;T͐L>90b`L<|J݈]Soes\6jxJ,8Wl%n͒+fxfc֬>@ @G" k%Yi0FRӢz^^[[*TTwklq4#r-͔@@ D,vodk(C25ns.|yK 8!~]˒=$"2\=ʛ@ r@Ar^ٔNhE&"䈇!roxX!|V]TC|[yB\<b _!> IMZ _Q"BzTO`MiݓYd)9A?3>O>LKDz 9G(k<D4ɁɌsv;3G~B%XSkVGKa|,i-)d!0X3u"ܗ #ZrpB2+}4xjK8+uE栤QLDp›"V@ DȒt25&_I3)E+hEɁJ9zA29F? PYGd E1S!"4Bޛ˙hVt/9 r䠟ݥ_Lro3`̓>!Y~tRGeS&}L "DRsp9P"wl "" S:E@ D!9hECn'TqZ% Q@ 9 rHDrp{hR e' &@ @Is' ڭ@nM`"9 @`MijJӬLsus@@ s`4h@n6H)@ D@;C:7g*ъfH&"@ \ъL2$͖PZfE9 r@ ieE(C2@ DDx}` P~؏2 ћ2㲗5Wk[ٌHR 1l}ev̳7ETV\u#>d-2z5|8m`_ɱ\w`-#+@ r͊SsF֬`d59 r] bBF/[~nRȄm']땜G&CW-)3gkH?/iV|1'mi^c, c]ɏwV@d= fEY|9 r2\ڒȺ GHܘչ"\eΏ|/\EqLeWrQbk͒{15+Vrp$4(`02̽lyL\WV9 БyyKAs` 9[!ρ^e19}J`l.9=2 zr1h1 yM%Fzmv+;Hzb;} ; 9x,_Y|vYFSvW"~ !Y'eo'?ֿ~Kht-䆯(?Fl '}੬yK_33$Ztݩ.E8o+A)c0ח~/zY;3Ѿ梿 B]mvFOY=\q=vZ0$'[yT[jO:&mE8ns5>%y@ r@Rt25&_I3)E+ȁ%6[xa\@](xcfM~%y0*9.~?w^ C0+.s/vi[J-J>I[B <'MtźYHLdϏT ls}]P8ֿ eK gX?Wag@DNz,sgsޢ) JWEspm\Bx`Y%ryfm!X_)skK,ق`X_#c1ZV$ڔkLQM gE б>J=Inrpk>uz e|I~|ϢJ ]텨A= 98#q`#D+h71)51hJ70aĝ/sf>w#.,!Q? sn /X e#%W}҈9o6-vٶ,9&&E>eѩiWYh'_V1"6 t>klSrlbIFVc֭R5@$̩%DhMijJӬLsus@@ ³:5EJڙx-_O-ar/Jiu6,{vaŕ.#:T GpH߉֑BJp y^m6r :V"ρPˢ9R9 9X`VD>DDV:V DX1S fERrpsܡhd9 ɋCI񉌻,)md Cl eeVD"@ @YQV92$9 @ȁn*`;) ڝъLD@ r@fE˔$h7iVtOfEs@@ ɬ[:5j`JhpHV9XO"@ @W rfEY|9 r@ 9rVܭ@̊@ rZ:BMvrʔ9G؇CeeU#껿CGk3e|o_"\4k67?渶]mseBøhgxXa U"FdIEq̼uo4"D45Bޛ˙hVt/9 rY7z2i 7s+qUSLj-ۦ}m߅2ϳɍs9. []ɟSs~{Jc ˯t%q-;O23qd#ܟy:4ȬF?'χ+}n̢psTH|6IBܵoOK6'vF0Ձ}~C-xx0o>d2GXfN#.w,Y2k%_XC ϐN!A999aL B\*7EwDsqݧx+3;ׂYrG)&| &E?Jx 9Ì ~U>xJzÓoIQF$!YNL+=SdT(Zio>S^8>us$`edWCTBM$Z̘f5㰻N?ST37q#C@qa'OeD΅'˞Bw-K5LW[Sk<;3Qqbv}H1[5rcI4iH\ }G 9XDrp{hR e' &d/9AaA*^|!Z,@sk*kG(2IL $qu_*2/{F@cq{+|7;T()* E&fxQԌ0o"gX̼@fͫuETf OeUyedg6M5IM_rq^\oL36[0(?q|f|Y/>LKew%,1͉g9lZyZ֖zʔ_yі31{̩ghe}G 9V%́$hf95E:̊Hs@ fX:xfK?)kkUL5F]⋾ l,;BICp4)kk$9np.7f.2W ݢ,ޗ4ju]VK[f0a)v6EGYLuB#>xc ӗkz֝XF p_U1IXKИ_wJeԬZ6( >eL|]!1Lҋ1̩p*}X@ r`S4+1S|9C{JQ@Xmw4UjwFWU؊9aKJ^"$g #.fƾH<A/T O83aΗD6$D.Ð"9R,GMn(_|y{vg@]=.I8k[dh-auYSkDj4[m>?dSA3*a%?s`4h@n6H)k>aזNU ;jƧ}8?n_lg]qm9PVA`ԓe5vA..#<cbK65*'&My<>p7"a'WIls/_T&)\ẃ26\U55Ge{N8ӶgN?ԫԇx"Ys`JHJn;T͐L>c"Vtw׾"v;<qf_Bڵ]1LwUU"]THr=|a1{Y|s}Tjl/OɴKOU ;ތA#?m59`6bΛ6xle"V&:AZ ϓ\-Su֯ZWrq**p+YgSOJ*aE?AG+2Ȑ6[Bjȁ,9M/TLԹ3[R F&8sϪ\(مNe1(hm1a貋QÙ)i3ZI#WI߬fF.q$?~Ws p'|e<^)q/LΙy1Ɂh.< ~-М'kmYoLW[SkA6P3q>z4J}X@ rъyn DY\B$IڅلL=~v}z$VWg)^iA.FMzt6'\eϿMy-3Ϝjway?AFjT!uܙMIԈVd"r@ 2AtСyDVCi,9)M{0+"kzxKJ0d0 @@̊|ߐ^5+9X!Y`=i@ r@]ъ*l˙eC}@ "Dt;$ߗ #ZrpB2+"r@ j%ej4 -Mf(SVD@ "ɚ LMSsL4+9 @SVUy:4Ȭ@ "93+J'ZQƐIs@@ "K9H$g.Pzm"IpaVE{<%e'{؇K ۱`*^4k6s?hk6^#yoG:cA "s9ГL s^Yi3rpmW*H./{Jnc[B77~6ݖf\[Q^0C "I69+M3%)eፒ2̄fgjYGZ|!a71ȯF"= )tur_ 7ʄIJayMd:К7@ r@yܱR>_J 7$̊`ЃOTk`eѧe5cށFr_RAa;ŵ|qL S!Ak-&ShG=WǠņ!>߂Iw|Ϧ8 9rsE"aӧ~ zAXI>ʓ0CM1ʹvpa~K#XD|h N3cSm`& {#a!WČ! {sDŽAĺp4ښ߾2`_/ ̃طs]S;Ѳb)ޔ|Wj)cwm{(2I CB ]wKAs\`r[<?φ@ P9Г!ym22+"3f^u)^F/a..p}C p˕Juxkg A@ժ_WYP)9苑r0%C^;\LZcV^x6Ѝս$ 3Lf20t¤NNB "$s%!![q?zwQ-^ܠUViW"r#caWĚXx4Q\LHt +#14ƚj׊iM ѹy!6 P1lhK6rY|^N)}d6z&{Zk1Ţa|_za[w|텀}YܞþK-A8p0` YH@X~3?-7JK${44ěa,P%$˕Gm36碚Oج6ijҞ_|)yɌs7`Lh(6V7rW??D~?_(j~4_u  sкZuUYQ6$Ü;FJȫ>)UJH֪vj kw\{(XiO5wZ'5P>e=kcRbA sp9p9@Y607 ],euڞ͆].eՊԓZ+t>5h&x9er)^qmToϲ\j1Հp gFY ?h~zRUӃ .}_zp6?˧iRil1+;R밳dNrq8y4ɸ  hH ]= ΧBf0JXてxm~a /)hRtZӌj%e[A+p&mzSH?lyvb|W>g4l<*k,63+yr 808-XM`M B*IyZX ࠋ\f`vѐs0Y%*OGbMfB2Οd@ˎKx 86G&sMMim5L0oOcۓF/8m9h~8p<Mr B#% A8t9P,MYQ6$Üi5lQE1䕑hS@?g=͞rOck8p,F2oFKJsiKh1[hi5=5Âz?44ķU3lIXԀM)Bρq148dbCjOQ( m2=-͜mՊ{4ny5i4&}R~:Kx4TluN=!*֞oD/`4ں B*z *-eW&4W{ }h:3XΆ}fBzfCj .2:mf.2jEVUs4Ƀֹ|EB|sI^{6ƳaebD0PYB8p}Bfp`&]޺Ϲx(IV#jٓDsŶWrA,B]HDŽ siX5A(` R+\Hm^v6 A8t98a.3WbYQ9ljT+5(_7M ۘJY~+s ֪'Xm*D yM:@Yɲ֗A]OHf  6,s ~tXS  ĺ!h{{vs0袬AA첬^sp   sz΁ץL7%sp<"  #wJyAAZaB~Yʴ]Y2 /+90!pAAp:sp] 6)j,E4Pi\Ui2|| MdžiZp# 8-dU\$ø|4I+qv~Sх|A.u^ :*-#4%ܷ w Ap 7usap˲]`Au$f;u7oumReqbwApeE8K9!٩`A3i^-VLhZ,yTk%95K- t.3рddA\lF#D^g3xśԱ؈{ÀOu1^>U+HrŎ}Cku<-{6Bs#y:ѹbs$AFe 88lSVԷ 0`MT:[mPsZqEhfB~t$/)POgF c) 3q!KPg Uy#k%pFNdjBPm(K5WQ-BB.;?>WLHվ;D)i2RWôPxl|֯&B?BH^?\}f9kůva]VߕΓJ!X-)6g"qSQ")m{eVbyiRsVr+t:(. 8Ϗv=`]i2fFpQ3ОFs;5<~]NO' _8 8R^l8RXp`)9g80A$zH#X7j]"pn礟:/9?5*t]Yg9CfSôJb~VkǠm{NmJf|| FNZy[s<^k92>uh*+Ptq~F&Z=ZDs#zPĩF Uv'9zj9Yh+z($ry67,3/'`)1Zi mg 835qo4X+i61D>N&%!c6IMrB#% p9`J[>/}|M8>Joob>\6秛{wKm!]B5]q89_v2~s`\dN=zB` x`;(M 6K1T иt^s*M i*ب_*S< a BJ6}2@? g%8zt>[|-[TӑFc/-i?Sy 6Lp0&4ӊ6uTm |+h5WR[QvD(NqUkFtzdĕbAZ,T:ZBjҠVx^1YKYDsiE|j\vN^Ũ$>ag̴A!{wy{5.C&k: o{-~9_qdё A:e A pÞAeEl >/õ=w_cnA'$wZQd2-7O@Fl#^F c: Q1xtx_^A!Xӂgcd"'PƂF51kڏ-WY2o26yf3Q5F ͳ%Z?a€M)r\sy6p.] rU T' 9ɸy3~Ԡ?דh =l~d>Υ=k/\/f)tZ صW/RtlGܟm?wϴ-e7 pp_22u32 x@#mVJFj)O3Q"[\<lkf ! '}`ւa:Qf(Oǣ ÍZ5keO՟<HXD5"snbfŧzFؕc<i65L $-`z)5u.T}0xeNk=+^(,l q`13O4D|ܟm=\+s'oA>9`o=.ʊ9FpPR&C557csT06l\:h2R\f MpJr<`jԗGٲ4c2Yh\UuShms?6A[~}U Gّ=Szv2B⹑>tws2H25~1-- g.G8py|õr:w+~A8m88e`eEʜ0=vZOYp`>.gx ΟR'Sݘ=faH[|?Wi2 /*]̥i.l~Oă40?%Yo<=oj;O*Տ bCjOQ!j;}Zofh)ʯI>r^Y|p0UW1ΤCM%W V;8>'Vӹ[~\7. su)M\..ppĦ=XpSd0?i 1B*f7%TF@eԈۙBv_}ih hM2k4K'5,v 5mɼLevƴLKe3T0΅mh2@sCUΆ}fcBzfCjk qm^ S7n7=owݳd,tј qbA]58ޣ0x¤XõrX}vdÏ}m; ! Ao(pWzuXdl'8`t$s02*ӌ|T!:? &z"*Y:j+i-8n Z:`&$O@ vfP2+88ȣ!ޤ[g~!hтP3%S/׉6[J@K!Zd= ?7G<Z/#`$f&*M AAK98.zi@ՊV+jQiXx`/Y_JJig,)#fpQM5H-Z{`]05L a RHgU^Tlyx|Οqڱ)Zt.Az!.z6 vtYVt"l!h)T~@y`o zñJ.h"Ha!iOf4B+P)¯ |b_1L&}n̓=-$VI2ϝv! pCeE8K9!٩`j2hVv1ZaXwZWf}Pӱ+UuߕJv! 2)+ۆcs8S6ӑakA89:췀C6sfPVM3Ab)SCl6r)SV8  p8hYh+z(  !Պj΁r98"AAdʊYg2  pp_22u32 ۬.sf^ (+BpAA`.3,+W܀9 su)M\..ppĦ= 9h?!dž]spjE^'$pAA`;Vx_2mWV  2ˊb>LH@A8tA=4mjE pAAeEW+:+CvtYVt"  l\f`vѐs0  2V+?t*+ۆcs8  p8pݐ|AO~ 88d3meE [v!h{6vv)V+@A8fN,ezXVtsAАjE}5@Y  2MeEݬV3ppAAed88/Kv AAms`9p3mo/{]!s8  pT]]+snAAd\9d.`G88bSVAA|cC^9a"s8  pspC)zghqqQ^^x:<---yYH0!y,eڮAA'|g?ѳ>KrYjj3s=uu>LH@A8.p}Oӟfggi~~^VZIJ ?///ST3'`E@? AhZ8@A8W8wKLO='?B`AR 1X`?c 7usap˲]pAAࠧ|;~}eXc20`׀ӧݔ2s/E`dAdAޔJ&''UŢ^^2 ?׳ O?́bmʊ!ԨȈ\W&3Brddh$[%m>53  eoQ:b ˿ޘz 0 `3 :spAO~ 88d3meE*S|F6Mf>)1*{pLSi luGzGu@`,{zXy,8erz  v!h{6vv)V+38Hp~}5}RAo}࠱*e+b  pՍe P*2ˋXs2?!eX`O<9'l2=e^,+:9[ :cAyT @ s]"U; lucl?LdR"|ֳ,CJ7 =NHW+9vppEʊ |͔8D[`~$IAbW4/ApDm T(*3)J {1PFM(q[A1`- z衇ooMc/׿u DpvBr7 @`{?DpOZ3"=Z+pg NnX2d lOcY }ROկ~/?yFggzFkkk"t9@d:,ef)dz @"PT*{{ B0 '#B%jP>(P\(] QͥLp2! *pdm}}@Am9`ΘFc ynnN_ȵZMv=v7C`EY29&(pv 4%X2h؁DZ.(eKBV"PUXH:M*G(4eb=& Q)[nJ}@2A8=,cZŋylJ26eT]]+sn>-/擖6|PEQL}w@C(e~oGS>#YTb1RʏUx  p}M@> -..eE6='`E@? AhZ8Ӟ@( P?gݨ $CPS>DDX*[ S<ʔz"#m[ '(5Am4p u΁ZwCR^Vq)SՊl責peE9pнj2K|VOܫ{R̷5)Wn4mRmuA8~ bY >:y {jײ\f`vѐs0 [u~Xpp=P,&sXamHZpAA'@^ʔ5 sՊ,2^ sp9p9@Y  V],euڞ͆].eՊA6K2spE/ŜAAmjE}5@Y  2MeEݬV3ppAAldq}7MzG_:}_}kcOᜃcdz[!9n3(+@A8YZXXgXS2 v!h{6vv)V+XZp#aJ#4IЅB@w&"E|uy =0M44a}$g=YVi!o|fq%?2t;8Yk>f=Νk~Μ9CRIA1C Lv9ˊbQ,|n -?83?-U2Oh0UȖ7{z圏ШoqNiu2~Gx=_{</-ipeLm>`?[_*}#S3[ZFGG,#ʉXCr>o,//듔LHf ?~ܴN`4AU}pPͦhBidfљ>ô\Q91aUkvͱ`\0;_f)Kj~v,rIpH_fs@*YHc'Z- Ѹ⧉P&ӳSaR*M(-l64RMƾKxosc9yq4=[4.bnB?fv-Uӷ~BXªy ~J BSx;l>72Jy%EC4tPuk{͜u|Jd8[=jZ 5߭VZ}Q׾;`o|C+7JӔdhddD OG?zdd88/Kv-`0 mss`9p3mo/{]g`0 mpp.ˊ.97xs50 `0Vx]tS2Kp#)+js֑`0 ~ᖟy9P:(+`W\Z ;`0 `~˿|V+Rϱǹvp[1GCm`7=s[|;{aN:rsկ~`0 `^~t}??OThD?3Lk,2oq  U ZL/,8+Њ +66yfwppoxf `0 5??H ײwxm`/7ɿl\d^F~"N `0 z|Ч;X|qz^'ds9r@+6 ^,d+88OM~NPo7_2`0 `bԧ?};qZ4-<};{<~x0oF6z{Y2Ў*!X>w `0 X\$wﴀqڍ?|E/SYռɀW pp<%&>N\4\(`0 ŷJEoqtcBY\o;8y)* k$O|'c"jm??H0 `0X?>}뭷~ŷtr: u Hh{!G+qlb7g08xf8~~O%6i`0 c+cY<ŵǷx7 ~]ZԀ^H}6pp)۹ +a8+O[xsw>fCGSE5fsEf4ߚ=ŹN ?oĆF҈5?(GO'W_Yk~7~7˘RXeu; vHpSuq9AbCl,iEfj;Wh>5fsB?B6?ÿ?C1C18c<|ǡqiǩq<=%,a*NF JEG~vpR 6 Klj"q9l&%Eƴw~߯5^ 7v?.AqP P?:@y\1c~??q0x?Ww S.7xyPRt ˘JZeR3663Fu .gzwm04c´O=]'‡@FAO&‚x3G?7̈E(02r3#<KT#k.ǾSM7xE31Wl1 8cXKZeT G-ʌ~_d1 ''?%! >&e R-}BE3 .x?n}>xmKY bOxUuƘo:_cuF38VnӮE6pj", 8nJۄ[@O, !(P9X?[߈ xQ,!z@*#&c ``5Kg~ 8W*ڹp6%+-U"1QrQ^Fب|EA=?D 31c<^5ʉB#8yטo`WR4 +%m@w`,izM@\k^?0hR6ჾR̀nF?ÿ @a (|,g`,]j50&"EX()zu+,@nF$p)e|x D^(/-3ʋi »}ͫ_(5 X-^?sz?[#| 2"cUwK`pgr;^[-s0pSRy݇pp yj=0fnQ^tu8*FFJFL,;3 ? 0 bѽRDFٮHo 敊.ȩ@.-.Q(/[d&e^H, >g?}a.1#d 2" i'|ȃ gN%EWYppzl`Eك){!{pMw8UR?H|?[}Rwwd U@.'W({ ĬUI\io^Qpb vK}bi=܃[7[#1 Bj 2,W Aϲ9?Zd %d D(0wKZ-ʉnvFJn]ۦd1{`U֙FuY-ʋՋ @g }oJi -+@{T{:x?n-č2J.@Y MnXYRk8žKZezpJ:Ʃ(/'N Mo4fLR$`*\?-.!>T d(x-xyƲyQN$Ptkpg|>~#Vw7XW8ȫwFs MroIG*92ʎ!eDh`0 m_!BxR*"Sh>3D7H`k 7"S-apॴJR@{` @xFHx o A& o.j?[р1KpW`@(x_obZE3ˉĹbjm UH?^ (*F.en'$%G", =0 `morxC0`d l)8cۄ q08kx&6!D7o1J"蠯y"A F 3 K `0 `PB,f! jׅ2" i YPty"%E/ B"|sĥMZ7TfW3:4bɑMC hx0 `kb|0pPv n d(ט,`j &DD Y xYT,/J(/U , F?-(Kq#Dp`0 DטKF\ R/ -ؕìAps v >, FȀ[d^/d \\d;?%_KBǥ[)I*!^(#: ^泞i"H83VE /A\W@8*@XntE-, Zطp[N?g?VXt0`dn5W`p k]IѺ"7Er]`@En.BVA.aJDs?o Aa(s(?f)g)%zM9ےKji] B^-e@ dhx bl@{?-H7K8fOZHPpH 0"cගhC^ry]6qP Lܓ`Uv$ 7Z#!f&??o31~3jQ6tS-x ڕmH-85';z\@(`cҟ  ;?߻rLh\/ęXe Jqdy@\btEѵ6 ؛Xd^+qa ?+VR!D r@1[`{Xm9]IѺA'"e F(CRVA'pS@٘{?:b&@4Kq諄 .U1p*'t NYw N*1#ƒ=?a !*;J!>Xe 0Ңh`i =/u"\-`F"( d&‚ bAva<?&EV*; \>$6;YX5 [m =pAf]&Trt b#04"wj?Zr i2 \#e 2{ۀK Introduction ------------ If you want to run a clustered SimpleSAMLphp IdP service and you would like to have centralized storage for metadata, you can use the PDO metadata storage handler. The present document explains how to configure SimpleSAMLphp and your database. Preparations ------------ You will need to have the appropriate PDO drivers for your database and you will have to configure the database section within the config/config.php file. Configuring SimpleSAMLphp ----------------------------- You will first need to configure a PDO metadata source. [root@simplesamlphp simplesamlphp]# vi config/config.php Here is an example of flatfile plus PDO: 'metadata.sources' => array( array('type' => 'flatfile'), array('type' => 'pdo'), ), Initializing the Database ------------------------- Once you have configured your metadata sources to include a PDO source, you will need to initialize the database. This process will create tables in the database for each type of metadata set (saml20-idp-hosted, saml20-idp-remote, saml20-sp-remote, etc). [root@simplesamlphp simplesamlphp]# php bin/initMDSPdo.php If you connect to your database, you will see 11 new empty tables; one for each metadata set. Adding Metadata --------------- With the PDO metadata storage handler, metadata is stored in the table for the appropriate set and is stored in JSON format. As an example, here is the saml20_idp_hosted table: entity_id | entity_data ----------------|------------------------------------------------------------------------------------------------------------------------- `__DEFAULT:1__` | {"host":"\_\_DEFAULT\_\_","privatekey":"idp.key","certificate":"idp.crt","auth":"example-ldap","userid.attribute":"uid"} Another example is the saml20_idp_remote table: entity_id | entity_data -------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- https://openidp.feide.no | {"name":{"en":"Feide OpenIdP - guest users","no":"Feide Gjestebrukere"},"description":"Here you can login with your account on Feide RnD OpenID. If you do not already have an account on this identity provider, you can create a new one by following the create new account link and follow the instructions.","SingleSignOnService":"https:\/\/openidp.feide.no\/simplesaml\/saml2\/idp\/SSOService.php","SingleLogoutService":"https:\/\/openidp.feide.no\/simplesaml\/saml2\/idp\/SingleLogoutService.php","certFingerprint":"c9ed4dfb07caf13fc21e0fec1572047eb8a7a4cb"} There is an included script in the `bin` directory that will import all flatfile metadata files and store them in the database, but you can use an external tool to maintain the metadata in the database. This document will only cover adding metadata using the included utility, but the tables above should provide enough information if you would like to create a utility to manage your metadata externally. To import all flatfile metadata files into the PDO database, run the following script [root@simplesamlphp simplesamlphp]# php bin/importPdoMetadata.php In the event that you import a metadata for an entity id that already exists in the database, it will be overwritten. simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.10.md0000644000000000000000000000043113245225037022436 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.10 ==================================== * The default encryption key padding scheme has been changed to `http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p`. This may cause problems if the recipient of messages do not support this padding scheme. simplesamlphp-1.15.3/docs/simplesamlphp-authsource.md0000644000000000000000000001326113245225037021473 0ustar rootrootCreating authentication sources =============================== All authentication sources are located in the `lib/Auth/Source/` directory in a module, and the class name is `sspmod__Auth_Source_`. The authentication source must extend the `SimpleSAML_Auth_Source` class or one of its subclasses. The "entry point" of an authentication source is the `authenticate()`-function. Once that function is called, the authentication module can do whatever it wishes to do. There are only two requirements: - Never show any pages to the user directly from within the `authenticate()`-function. (This will lead to problems if the user decides to reload the page.) - Return control to SimpleSAMLphp after authenticating the user. If the module is able to authenticate the user without doing any redirects, it should just update the state-array and return. If the module does a redirect, it must call `SimpleSAML_Auth_Source::completeAuth()` with the updated state array. Everything else is up to the module. If the module needs to redirect the user, for example because it needs to show the user a page asking for credentials, it needs to save the state array. For that we have the `SimpleSAML_Auth_State` class. This is only a convenience class, and you are not required to use it (but its use is encouraged, since it handles some potential pitfalls). Saving state ------------ The `SimpleSAML_Auth_State` class has two functions that you should use: `saveState($state, $stage)`, and `loadState($id, $stage)`. The `$stage` parameter must be an unique identifier for the current position in the authentication. It is used to prevent a malicious user from taking a state you save in one location, and give it to a different location. The `saveState()`-function returns an id, which you should pass to the `loadState()`-function later. Username/password authentication -------------------------------- Since username/password authentication is quite a common operation, a base class has been created for this. This is the `sspmod_core_Auth_UserPassBase` class, which is can be found as `modules/core/lib/Auth/UserPassBase.php`. The only function you need to implement is the `login($username, $password)`-function. This function receives the username and password the user entered, and is expected to return the attributes of that user. If the username or password is incorrect, it should throw an error saying so: throw new SimpleSAML_Error_Error('WRONGUSERPASS'); "[Implementing custom username/password authentication](./simplesamlphp-customauth)" describes how to implement username/password authentication using that base class. Generic rules & requirements ---------------------------- - Must be derived from the `SimpleSAML_Auth_Source`-class. **Rationale**: - Deriving all authentication sources from a single base class allows us extend all authentication sources by extending the base class. - If a constructor is implemented, it must first call the parent constructor, passing along all parameters, before accessing any of the parameters. In general, only the $config parameter should be accessed when implementing the authentication source. **Rationale**: - PHP doesn't automatically call any parent constructor, so it needs to be done manually. - The `$info`-array is used to provide information to the `SimpleSAML_Auth_Source` base class, and therefore needs to be included. - Including the `$config`-array makes it possible to add generic configuration options that are valid for all authentication sources. - The `authenticate(&$state)`-function must be implemented. If this function completes, it is assumed that the user is authenticated, and that the `$state`-array has been updated with the user's attributes. **Rationale**: - Allowing the `authenticate()`-function to return after updating the `$state`-array enables us to do authentication without redirecting the user. This can be used if the authentication doesn't require user input, for example if the authentication can be done based on the IP-address of the user. - If the `authenticate`-function does not return, it must at a later time call `SimpleSAML_Auth_Source::completeAuth` with the new state array. The state array must be an update of the array passed to the `authenticate`-function. **Rationale**: - Preserving the same state array allows us to save information in that array before the authentication starts, and restoring it when authentication completes. - No pages may be shown to the user from the `authenticate()`-function. Instead, the state should be saved, and the user should be redirected to a new page. **Rationale**: - The `authenticate()`-function is called in the context of a different PHP page. If the user reloads that page, unpredictable results may occur. - No state information about any authentication should be stored in the authentication source object. It must instead be stored in the state array. Any changes to variables in the authentication source object may be lost. **Rationale**: - This saves us from having to save the entire authentication object between requests. Instead, we can recreate it from the configuration. - The authentication source object must be serializable. It may be serialized between being constructed and the call to the `authenticate()`-function. This means that, for example, no database connections should be created in the constructor and later used in the `authenticate()`-function. **Rationale**: - If parsing the configuration and creating the authentication object is shown to be a bottleneck, we can cache an initialized authentication source. simplesamlphp-1.15.3/docs/simplesamlphp-changelog.md0000644000000000000000000020712413245225037021243 0ustar rootrootSimpleSAMLphp changelog ======================= This document lists the changes between versions of SimpleSAMLphp. See the upgrade notes for specific information about upgrading. ## Version 1.15.3 Released 2018-02-27 * Resolved a security issue related to signature validation in the SAML2 library. See [SSPSA 201802-01](https://simplesamlphp.org/security/201802-01). * Fixed edge-case scenario where an application uses one of the known LoggingHandlers' name as a defined class * Fixed issue #793 in the PHP logging handler. ## Version 1.15.2 Released 2018-01-31 * Resolved a Denial of Service security issue when validating timestamps in the SAML2 library. See [SSPSA 201801-01](https://simplesamlphp.org/security/201801-01). * Resolved a security issue with the open redirect protection mechanism. See [SSPSA 201801-02](https://simplesamlphp.org/security/201801-02). * Fix _undefined method_ error when using memcacheD. ### `authfacebook` * Fix compatibility with Facebook strict URI match. ### `consent` * Fix statistics not being gathered. ### `sqlauth` * Prevented a security issue with the connection charset used for MySQL backends. See [SSPSA 201801-03](https://simplesamlphp.org/security/201801-03). ## Version 1.15.1 Released 2018-01-12 ### Bug fixes * AuthX509 error messages were broken. * Properly calculate supported protocols based on config. * NameIDAttribute filter: update to use SAML2\XML\saml\NameID. * Replace remaining uses of SimpleSAML_Logger with namespace version. * Statistics: prevent mixed content errors. * Add 'no-store' to the cache-control header to avoid Chrome caching redirects. ## Version 1.15.0 Released 2017-11-20 ### New features * Added support for authenticated web proxies with the `proxy.auth` setting. * Added new `AttributeValueMap` authproc filter. * Added attributemaps for OIDs from SIS (Swedish Standards Institute) and for eduPersonUniqueId, eduPersonOrcid and sshPublicKey. * Added an option to specify metadata signing and digest algorithm `metadata.sign.algorithm`. * Added an option for regular expression matching of trusted.url.domains via new `trusted.url.regex` setting. * The `debug` option is more finegrained and allows one to specify whether to log full SAML messages, backtraces or schema validations separately. * Added a check for the latest SimpleSAMLphp version on the front page. It can be disabled via the new setting `admin.checkforupdates`. * Added a warning when there's a probable misconfiguration of PHP sessions. * Added ability to define additional attributes on ContactPerson elements in metatada, e.g. for use in Sirtfi contacts. * Added option to set a secure flag also on the language cookie. * Added option to specify the base URL for the application protected. * Added support for PHP Memcached extension next to Memcache extension. * Added Redis as possible session storage mechanism. * Added support to specify custom metadata storage handlers. * Invalidate opcache after writing a file, so simpleSAMLphp works when `opcache.validate_timestamps` is disabled. * Metadata converter will deal properly with XML with leading whitespace. * Update `ldapwhoami()` call for PHP 7.3. * Made response POST page compatible with strict Content Security Policy on calling webpage. * Updated Greek, Polish, Traditional Chinese and Spanish translations and added Afrikaans. ### Bug fixes * The deprecated OpenIdP has been removed from the metadata template. * Trailing slash is no longer required in `baseurlpath`. * Make redirections more resilient. * Fixed empty protocolSupportEnumeration in AttributeAuthorityDescriptor. * Other bug fixes and numerous documentation enhancements. * Fixed a bug in the Redis store that could lead to incorrect _duplicate assertion_ errors. ### API and user interface * Updated to Xmlseclibs 3.0. Minimum PHP version is now 5.4, mcrypt requirement dropped. * Added a PSR-4 autoloader for modules. Now modules can declare their classes under the SimpleSAML\Module namespace. * Added new hook for module loader exception handling `exception_handler`. * Expose RegistrationInfo in parsed SAML metadata. * The AuthnInstant is now available in the state array. * Introduced Twig templating for user interface. * Lots of refactoring, code cleanup and added many unit tests. ### `adfs` * Fixed POST response form parameter encoding. ### `authYubiKey` * Fixed PHP 7 support. ### `authfacebook` * Updated to work with latest Facebook API. ### `authlinkedin` * Added setting `attributes` to specify which attributes to request from LinkedIn. ### `authtwitter` * Added support for fetching the user's email address as attribute. ### `consent` * Added support for regular expressions in `consent.disable`. ### `core` * Added logging of `REMOTE_ADDR` on successful login. * `AttributeMap`: allow fetching mapping files from modules. * `ScopeAttribute`: added option `onlyIfEmpty` to add a scope only if none was present. * `AttributeCopy`: added option to copy to multiple destination attributes. ### `cron` * Allow invocation via PHP command line interface. ### `discopower` * Added South Africa tab. ### `ldap` * Added `search.filter` setting to limit LDAP queries to a custom search filter. * Added OpenLDAP support in AttributeAddUsersGroups. * Fixed for using non standard LDAP port numbers. * Fixed configuration option of whether to follow LDAP referrals. ### `memcacheMonitor` * Fixed several missing strings. ### `metarefresh` * Fixed several spurious PHP notices. ### `multiauth` * Fixed selected source timeout. ### `negotiate` * Fixed authentication failure on empty attributes-array. * Fixed PHP notices concerning missing arguments. ### `oauth` * Updated library to improve support for OAuth 1.0 Revision A. ### `radius` * Improved error messages. * Added parameter `realm` that will be suffixed to the username entered. ### `saml` * Handle instead of reject assertions that do not contain a NameID. * Added options to configure `AllowCreate` and `SPNameQualifier`. * Added option `saml:NameID` to set the Subject NameID in a SAML AuthnRequest. * Added filter `FilterScopes` to remove values which are not properly scoped. * Make sure we log the user out before reauthenticating. * More robust handling of IDPList support in proxy mode. * Increased `_authSource` field length in Logout Store. ### `smartattributes` * Fix SmartName authproc that failed to load. ### `sqlauth` * Fixed SQL schema for usergroups table. ## Version 1.14.17 Released 2017-10-25 * Resolved a security issue with the SAML 1.1 Service Provider. See [SSPSA 201710-01](https://simplesamlphp.org/security/201710-01). ## Version 1.14.16 Released 2017-09-04 * Resolved a security issue in the consentAdmin module. See [SSPSA 201709-01](https://simplesamlphp.org/security/201709-01). ## Version 1.14.15 Released 2017-08-08 * Resolved a security issue with the creation and validation of time-limited tokens. See [SSPSA 201708-01](https://simplesamlphp.org/security/201708-01). * Fixed an issue with session handling that could lead to crashes after upgrading from earlier 1.14.x versions. * Fixed issue #557 with instances of SimpleSAMLphp installed from the repository as well as custom modules. * Fixed issue #648 to properly handle SAML responses being sent to reply the same request, but using different response IDs. * Fixed issues #612 and #618 with the mobile view of the web interface. * Fixed issue #639 related to IdP names containing special characters not being properly displayed by discopower. * Fixed issue #571 causing timeouts when using Active Directory as a backend. * Other minor fixes. ## Version 1.14.14 Released 2017-05-05 * Resolved a security issue with in the authcrypt module (Htpasswd authentication source) and in SimpleSAMLphp's session validation. See [SSPSA 201705-01](https://simplesamlphp.org/security/201705-01). * Resolved a security issue with in the multiauth module. See [SSPSA 201704-02](https://simplesamlphp.org/security/201704-02). ## Version 1.14.13 Released 2017-04-27 * Resolved a security issue with unauthenticated encryption in the SimpleSAML\Utils\Crypto class. See [SSPSA 201704-01](https://simplesamlphp.org/security/201704-01). * Added requirement for the Multibyte String PHP extension and the corresponding checks. * Set a default name for SimpleSAMLphp sessions in the configuration template for the PHP session handler. ## Version 1.14.12 Released 2017-03-30 * Resolved a security issue in the authcrypt module (Htpasswd authentication source) and in SimpleSAMLphp's session validation. See [SSPSA 201703-01](https://simplesamlphp.org/security/201703-01). * Resolved a security issue with IV generation in the `SimpleSAML\Utils\Crypto::_aesEncrypt()` method. See [SSPSA 201703-02](https://simplesamlphp.org/security/201703-02). * Fixed an issue with the authfacebook module, broken after a change in Facebook's API. * Fixed an issue in the discopower module that ignored the `hide.from.discovery` metadata option. * Fixed an issue with trusted URLs validation that prevented a URL from being accepted if a standard port was explicitly included but not specified in the configuration. * Fixed an issue that prevented detecting a Memcache server being down when fetching Memcache statistics. * Fixed an issue with operating system detection that made SimpleSAMLphp identify OSX as Windows. ## Version 1.14.11 Released 2016-12-12 * Resolved a security issue involving signature validation of SAML 1.1 messages. See [SSPSA 201612-02](https://simplesamlphp.org/security/201612-02). * Fixed an issue when the user identifier used to generate a persistent NameID was missing due to a misconfiguration, causing SimpleSAMLphp to generate the nameID based on the null data type. * Fixed an issue when persistent NameIDs were generated out of attributes with empty strings or multiple values. * Fixed issue #530. An empty SubjectConfirmation element was causing SimpleSAMLphp to crash. On the other hand, invalid SubjectConfirmation elements were ignored in PHP 7.0. ## Version 1.14.10 Released 2016-12-02 * Resolved a security issue involving signature validation. See [SSPSA 201612-01](https://simplesamlphp.org/security/201612-01). * Fixed issue #517. A misconfigured session when acting as a service provider was leading to a PHP fatal error. * Fixed issue #519. Prevent persistent NameIDs from being generated from empty strings. * Fixed issue #520. It was impossible to verify Apache's custom MD5 passwords when using the Htpasswd authentication source. * Fixed issue #523. Avoid problems caused by different line-ending strategies in the project files. * Other minor fixes and enhancements. ## Version 1.14.9 Released 2016-11-10 * Fixed an issue that resulted in PHP 7 errors being masked. * Fixed the smartattributes:SmartName authentication processing filter. * Fixed issue #500. When parsing metadata, two 'attributes.required' options were generated. * Fixed the list of requirements in composer, the documentation, and the configuration page. * Fixed issue #479. There were several minor issues with XHTML compliance. * Other minor fixes. ## Version 1.14.8 Released 2016-08-23 * Fixed an issue in AuthMemCookie causing it to crash when an attribute received contains XML as its value. * Fixed an issue in AuthMemCookie that made it impossible to set its own cookie. * Fixed an issue when acting as a proxy and receiving attributes that contain XML as their values. * Fixed an issue that led to incorrect URL guessing when a script is invoked with a URI that doesn't include its name. ## Version 1.14.7 Released 2016-08-01 * Fixed issue #424. Attributes containing XML as their values (like eduPersonTargetedID) were empty. ## Version 1.14.6 Released 2016-07-18 * Fixed issue #418. SimpleSAMLphp was unable to obtain the current URL correctly when invoked from third-party applications. ## Version 1.14.5 Released 2016-07-12 * Fixed several issues with session handling when cookies couldn't be set for some reason. * Fixed an issue that caused wrong URLs to be generated in the web interface under certain circumstances. * Fixed the exception handler to be compatible with PHP 7. * Fixed an issue in the dropdown IdP selection page that prevented it to work with PHP 5.3. * Fixed compatibility with Windows machines. * Fixed an issue with the PDO and Serialize metadata storage handlers. * Fixed the authwindowslive module. It stopped working after the former API was discontinued. * Other minor issues and fixes. ## Version 1.14.4 Released 2016-06-08 * Fixed two minor security issues that allowed malicious URLs to be presented to the user in a link. Reported by John Page. * Fixed issue #366. The LDAP class was trying to authenticate even when no password was provided (using the CAS module). * Fixed issue #401. The authenticate.php script was printing exceptions instead of throwing them for the exception handler to capture them. * Fixed issue #399. The size limitation of the TEXT type in MySQL was creating problems in certain setups. * Fixed issue #5. Incoherent population of the $_SERVER variable was creating broken links when running PHP with FastCGI. * Other typos and minor bugs: #389, #392. ## Version 1.14.3 Released 2016-04-19 * Fixed a bug in the login form that prevented the login button to be displayed in mobile devices. * Resolved an issue in the PHP session handler that made it impossible to use PHP sessions simultaneously with other applications. ## Version 1.14.2 Released 2016-03-11 * Use stable versions of the externalized modules to prevent possible issues when further developing them. ## Version 1.14.1 Released 2016-03-08 * Resolved an information leakage security issue in the sanitycheck module. See [SSPSA 201603-01](/security/201603-01). ## Version 1.14.0 Released 2016-02-15 ### Security * Resolved a security issue with multiple modules that were not validating the URLs they were redirecting to. * Added a security check to disable loading external entities in XML documents. * Enforced admin access to the metadata converter tool. * Changed `xmlseclibs` dependency to point to `robrichards/xmlseclibs` version 1.4.1. ### New features * Allow setting the location of the configuration directory with an environment variable. * Added support for the Metadata Query Protocol by means of the new MDX metadata storage handler. * Added support for the Sender-Vouches method. * Added support for WantAssertionsSigned and AuthnRequestsSigned in SAML 2.0 SP metadata. * Added support for file uploads in the metadata converter. * Added support for setting the prefix for Memcache keys. * Added support for the Hide From Discovery REFEDS Entity Category. * Added support for the eduPersonAssurance attribute. * Added support for the full SCHAC 1.5.0 schema. * Added support for UNIX sockets when configuring memcache servers. * Added the SAML NameID to the attributes status page, when available. * Added attribute definitions for schacGender (schac), sisSchoolGrade and sisLegalGuardianFor (skolfederation.se). * Attributes required in metadata are now taken into account when parsing. ### Bug fixes * Fixed an issue with friendly names in the attributes released. * Fixed an issue with memcache that would result in a push for every fetch, when several servers configured. * Fixed an issue with memcache that would result in an endless loop if all servers are down. * Fixed an issue with HTML escaping in error reports. * Fixed an issue with the 'admin.protectmetadata' option not being enforced for SP metadata. * Fixed an issue with SAML 1.X SSO authentications that removed the NameID of the subject from available data. * Fixed an issue with the login form that resulted in a `NOSTATE` error if the user clicked the login button twice. * Fixed an issue with replay detection in IdP-initiated flows. * Fixed an issue with SessionNotOnOrAfter that kept moving forward in the future with every SSO authentication. * Fixed an issue with the session cookie being set twice for the first time. * Fixed an issue with the XXE attack prevention mechanism conflicting with other applications running in the same server. * Fixed an issue that prevented the SAML 1.X IdP to restart when the session is lost. * Fixed an issue that prevented classes using namespaces to be loaded automatically. * Fixed an issue that prevented certain metadata signatures to be verified (fixed upstream in `xmlseclibs`). * Other bug fixes and numerous documentation enhancements. ### API and user interface * Added a new and simple database class to serve as PDO interface for all the database needs. * Added the possibility to copy metadata and other elements by clicking a button in the web interface. * Removed the old, unused `pack` installer tool. * Improved usability by telling users the endpoints are not to be accessed directly. * Moved the hostname, port and protocol diagnostics tool to the admin directory. * Several classes and functions deprecated. * Changed the signature of several functions. * Deleted old and deprecated code, interfaces and endpoints. * Deleted old jQuery remnants. * Deleted the undocumented dynamic XML metadata storage handler. * Deleted the backwards-compatible authentication source. * Updated jQuery to the latest 1.8.X version. * Updated translations. ### `authcrypt` * Added whitehat101/apr1-md5 as a dependency for Apache htpasswd. ### `authX509` * Added an authentication processing filter to warn about certificate expiration. ### `ldap` * Added a new `port` configuration option. * Better error reporting. ### `metaedit` * Removed the `admins` configuration option. ### `metarefresh` * Added the possibility to specify which types of entities to load. * Added the possibility to verify metadata signatures by using the public key present in a certificate. * Fix `certificate` precedence over `fingerprint` in the configuration options when verifying metadata signatures. ### `smartnameattribute` * This module was deprecated long time ago and has now been removed. Use the `smartattributes` module instead. ## Version 1.13.2 Released 2014-11-04 * Solved performance issues when processing large metadata sets. * Fix an issue in the web interface when only one language is enabled. ## Version 1.13.1 Released 2014-10-27 * Solved an issue with empty fields in metadata to cause SimpleSAMLphp to fail with a translation error. Issues #97 and #114. * Added Basque language to the list of known languages. Issue #117. * Optimized the execution of redirections by removing an additional, unnecessary function call. * Solved an issue that caused SimpleSAMLphp to fail when the RelayState parameter was empty or missing on an IdP-initiated authentication. Issues #99 and # 104. * Fixed a certificate check for SubjectConfirmations with Holder of Key methods. ## Version 1.13 Released 2014-09-25. * Added the 'remember me' option to the default login page. * Improved error reporting. * Added a new 'logging.format' option to control the formatting of the logs. * Added support for the 'objectguid' binary attribute in LDAP modules. * Added support for custom search and private attributes read credentials in all LDAP modules. * Added support for the WantAuthnRequestsSigned option in generated SAML metadata. * Tracking identifiers are no longer generated based on MD5. * Several functions, classes and interfaces marked as deprecated. * Bug fixes and documentation enhancements. * Updated translations. * New language: Basque. ### `adfs` * Honour the 'wreply' parameter when redirecting. ### `aggregator` * Fixed an issue when regenerating metadata from certain metadata sources. ### `discopower` * Bug fix. ### `expirycheck` * Translations are now possible for this module. ### `metarefresh` * Use cached metadata if something goes wrong when refreshing feeds. ### `openidProvider` * Fix for compatibility with versions of PHP greater or equal to 5.4. ### `saml` * Make it possible to add friendly names to attributes in SP metadata. * The RSA_1.5 (RSA with PKCS#1 v1.5 padding) encryption algorithm is now blacklisted by default for security reasons. * Stop checking the 'IDPList' parameter in IdPs. * Solved an issue that allowed bypassing authentication status checks when presenting an 'IDPList' parameter. * The 'Destination' attribute is now always sent in logout responses issued by an SP. ### `sqlauth` * Updated documentation to remove bad practice with regard to password storage. ## Version 1.12 Released 2014-03-24. * Removed example authproc filters from configuration template. * Stopped using the 'target-densitydpi' option removed from WebKit. * The SimpleSAML_Utilities::generateRandomBytesMTrand() function is now deprecated. * Removed code for compatibility with PHP versions older than 5.3. * Removed the old interface of SimpleSAML_Session. * Fixed a memory leak in SimpleSAML_Session regarding serialization and unserialization. * Support for RegistrationInfo (MDRPI) elements in the metadata of identity and service providers. * Renamed SimpleSAML_Utilities::parseSAML2Time() function to xsDateTimeToTimestamp(). * New SimpleSAML_Utilities::redirectTrustedURL() and redirectUntrustedURL() functions. * Deprecated the SimpleSAML_Utilities::redirect() function. * Improved Russian translation. * Added Czech translation. * New 'errorreporting' option to enable or disable error reporting feature. * Example certificate removed. * New SimpleSAML_Configuration::getEndpointPrioritizedByBinding() function. * PHP 5.3 or newer required. * Started using Composer as dependency manager. * Detached the basic SAML2 library and moved to a standalone library in github. * Added support for exporting shibmd:Scope metadata with regular expressions. * Remember me option in the IdP. * New SimpleSAML_Utilities::setCookie wrapper. * Custom HTTP codes on error. * Added Romanian translation. * Bug fixes and documentation enhancements. ### `adfs` * Support for exporting metadata. ### `aggregator` * Support for RegistrationInfo (MDRPI) elements in the metadata. * Fix for HTTP header injection vulnerability. * Fix for directory traversal vulnerability. ### `aggregator2` * Support for RegistrationInfo (MDRPI) elements in the metadata. ### `aselect` * License changed to LGPL 2.1. ### `authfacebook` * Updated extlibinc to 3.2.2. ### `authtwitter` * Added 'force_login' configuration option. ### `cdc` * Bugfix related to request validation. ### `core` * The AttributeAlter filter no longer throws an exception if the attribute was not found. * Support for removal of values in the AttributeAlter filter, with '%remove' flag. * Support for empty strings and NULL values as a replacement in the AttributeAlter filter. * Bugfixes in the AttributeAlter filter. * Support for NULL attribute values. * Support for limiting values and not only attributes in the AttributeLimit filter. * Log a message when a user authenticates successfully. * Added %duplicate flag to AttributeMap, to leave original names in place when using map file. * Fix infinite loop when overwriting attributes with AttributeMap. ### `discopower` * Bugfix for incorrect handling of the 'idpdisco.extDiscoveryStorage' option. ### `ldap` * Support for configuring the duplicate attribute handling policy in AttributeAddFromLDAP, 'attribute.policy' option. * Support for binary attributes in the AttributeAddFromLDAP filter. * Support for multiple attributes in the AttributeAddFromLDAP filter. ### `metarefresh` * Support for specifying permissions of the resulting files. ### `negotiate` * Added support for "attributes"-parameter. ### `oauth` * Bugfix related to authorize URL building. ### `openidProvider` * Support for SReg and AX requests. ### `saml` * Send 'isPassive' in passive discovery requests. * Support for generating NameIDFormat in service providers with NameIDPolicy set. * Support for AttributeConsumingService and AssertionConsumingServiceIndex. * Support for the HTTP-POST binding in WebSSO profile. * Fix for entity ID validation problems when using the IDPList configuration option. ### `smartattributes` * New 'add_candidate' option to allow the user to decide whether to prepend or not the candidate attribute name to the resulting value. ### `statistics` * Bugfix in statistics aggregator. ## Version 1.11 Released 2013-06-05. * Support for RSA_SHA256, RSA_SHA384 and RSA_SHA512 in HTTP Redirect binding. * Support for RegistrationInfo element in SAML 2.0 metadata. * Support for AuthnRequestsSigned and WantAssertionsSigned when generating metadata. * Third party OpenID library updated with a bugfix. * Added the Name attribute to EntitiesDescriptor. * Removed deprecated option 'session.requestcache' from config-template. * Workaround for SSL SNI extension not being correctly set. * New language cookie and parameter config options. * Add 'module.enable' configuration option for enabling/disabling modules. * Check for existence of memcache extension. * Initial support for limiting redirects to trusted hosts. * Demo example now shows both friendly and canonical name of the attributes. * Other minor fixes for bugs and typos. * Several translations updated. * Added Latvian translation. ### `authorize` * Added a logout link to the 403 error page. ### `authtwitter` * Updated API endpoint for version 1.1. * Fix for oauth_verifier parameter. ### `authX509` * ldapusercert validation made optional. ### `consent` * Added support for SQLite databases. ### `core` * Fix error propagation in UserPass(Org)Base authentication sources. * MCrypt module marked as required. ### `discopower` * Get the name of an IdP from mdui:DisplayName. ### `expirycheck` * PHP 5.4 compatibility fixes. ### `InfoCard` * PHP 5.4 compatibility fixes. ### `ldap` * Added an option to disable following referrals. ### `metarefresh` * Improved help message. ### `oauth` * PHP 5.4 compatibility fixes. ### `saml` * Verify that the issuer of an AuthnResponse is the same entity ID we sent a request to. * Added separate option to enable Holder of Key support on SP. * Fix for HoK profile metadata. * New filter for storing persistent NameID in eduPersonTargetedID attribute. * Support for UIInfo elements. * Bugfix for SAML SP metadata signing. * Ignore default technical contact. * Support for MDUI elements in SP metadata. * Support for more contact types in SP metadata. * New information in statistics with the time it took for a login to happen. ### `sanitycheck` * Configuration file made optional. ### `smartattributes` * New filter: smartattributes:SmartID. * New filter: smartattributes:SmartName. ### `smartnameattribute` * Deprecated. ### `wsfed` * Support for SLO in WS-Fed. ## Version 1.10 Released 2012-09-25. * Add support for storing data without expiration timestamp in memcache. * Fix for reauthentication in old shib13 authentication handler. * Clean up executable-permissions on files. * Change encryption to use the rsa-oaep-mgf1p key padding instead of PKCS 1.5. * Update translations. * Added Serbian translation. ### `core` * `core:UserPass(Org)Base`: Add "remember username" option. ### `papi` * New authentication module supporting PAPI protocol. ### `radius` * New feature to configure multiple radius servers. ### `riak` * New module for storing sessions in a Riak database. ### `saml` * Add support for overriding SAML 2.0 SP authentication request generation. * Add support for blacklisting encryption algorithms. ## Version 1.9.2 Released 2012-08-29 * Fix related to the security issue addressed in version 1.9.1. ## Version 1.9.1 Released 2012-08-02. * Fix for a new attack against PKCS 1.5 in XML encryption. ## Version 1.9 Released 2012-06-13. * Restructure error templates to share a common base template. * Warnings about URL length limits from Suhosin PHP extension. * New base class for errors from authentication sources. * Support for overriding URL generation when behind a reverse proxy. * New languages: Russian, Estonian, Hebrew, Chinese, Indonesian * Add getAuthSource()-function to SimpleSAML_Auth_Simple. * Add reauthenticate()-function to SimpleSAML_Auth_Source. (Is called when the IdP receives a new authentication request.) * iframe logout: Make it possible to skip the "question-page" for code on the IdP. * RTL text support. * Make SimpleSAMLAuthToken cookie name configurable. * Block writing secure cookies when we are on http. * Fix state information being unavailable to UserPassOrgBase authentication templates. * Make it possible to send POST-messages to http-endpoints without triggering a warning when the IdP supports both http and https. * Add IPv6-support to the SimpleSAML_Utilities::ipCIDRcheck()-function. * Do not allow users to switch to a language that is not enabled. * iframe logout: Add a per-SP timeout option. * SimpleSAML_Auth_LDAP: Better logging of the cause of exceptions. * SimpleSAML_Auth_State: Add $allowMissing-parameter to loadState(). * module.php: More strict URL parsing. * Add support for hashed admin passwords. * Use openssl_random_pseudo_bytes() for better cross-platform random number generation. * Add the current hostname to the error reports. * Make the lifetime of SimpleSAML_Auth_State "state-arrays" configurable (via the `session.state.timeout`-option). * SimpleSAML_Auth_State: Add cloneState()-function. * Fix log levels used on Windows. * SimpleSAML_Auth_LDAP: Clean up some unused code. * core:UserPassOrgBase: Add selected organization to the authentication data that is stored in the session. * Do not warn about missing Radius and LDAP PHP extensions unless those modules are enabled. * Support for overriding the logic to determine the language. * Avoid crashes due to deprecation-warnings issued by PHP. * Use case-insensitive matching of language codes. * Add X-Frame-Options to prevent other sites from loading the SSP-pages in an iframe. * Add SimpleSAML_Utilities::isWindowsOS()-helper function. * chmod() generated files to only be accessible to the owner of the files. * Fix "re-posting" of POST data containing a key named "submit". * Do not attempt to read new sessions from the session handler. * Fix some pass-by-reference uses. (Support removed in PHP 5.4.) * Warn the user if the secretsalt-option isn't set. * A prototype for a new statistics logging core. Provides more structured logging of events, and support for multiple storage backends. * Support for arbitrary namespace-prefixed attributes in md:EndpointType-elements. * Fix invalid HTML for login pages where username is set. * Remove unecessary check for PHP version >= 5.2 when setting cookies. * Better error message when a module is missing a default-enable or default-disable file. * Support for validating RSA-SHA256 signatures. * Fixes for session exipration handling. ### `aselect` * New module that replaces the previous module. * Better error handling. * Support for request signing. * Loses support for A-Select Cross. ### `authcrypt` * `authcrypt:Hash`: New authentication source for checking username & password against a list of usernames and hashed passwords. * `authcrypt:Htpasswd`: New authentication source for checking username & password against a `.htpasswd`-file. ### `authfacebook` * Update to latest Facebook PHP SDK. ### `authorize` * `authorize:Authorize`: Add flag to change the behaviour from default-deny to default-allow. * `authorize:Authorize`: Add flag to do simple string matching instead of regex-matching. ### `authtwitter` * Update to use the correct API endpoint. * Propagate "user aborted" errors back to the caller. * Changes to error handling, throw more relevant exceptions. * Store state information directly in the state array, instead of the session. ### `authYubiKey` * Remove deprecated uses of split(). ### `cas` * Make it possible for subclasses to override finalState(). ### `core` * `core:AttributeCopy`: New filter to copy attributes. ### `consent` * Add a timeout option for the database connection. * Fix disabling of consent when the data store is down. * Simpler configuration for disabling consent for one SP or one IdP. * Do not connect to the database when consent is disabled for the current SP/IdP. ### `consentAdmin` * Fix for bridged IdP setup with `userid.attribute` set in `saml20-idp-hosted` metadata. ### `cron` * Set the From-address to be the technical contact email address. ### `expirycheck` * `expirycheck:ExpiryDate`: New module to check account expiration. ### `ldap` * Add a base class for authentication processing filters which fetch data from LDAP. * `ldap:AttributeAddUsersGroups`: Authentication processing filter that adds group information from LDAP. ### `metarefresh` * Support for blacklisting and whitelisting entities. * Support for conditional GET of metadata files. * Reuse old metadata when fetching metadata fails. ### `multiauth` * Add `multiauth:preselect`-parameter, to skip the page to select authentication source. * Make it possible to configure the names of the authentication sources. * Remember the last selected authentication source. ### `negotiate` * New module implementing "negotiate" authentication, which can be used for Kerberos authentication (including Windows SSO). ### `oauth` * Update to latest version of the OAuth library. * Remove support for older versions of OAuth than OAuth Rev A. ### `openid` * Separate linkback URL from page displaying OpenID URL field. * Throw more relevant exceptions. * Update to latest version of the OpenID library. * Support for sending authentication requests via GET requests (with the prefer_http_redirect option). * Prevent deprecation warnings from the OpenID library from causing deadlocks in the class loader. ### `openidProvider` * Prevent deprecation warnings from the OpenID library from causing deadlocks in the class loader. ### `radius` * Support for setting the "NAS-Identifier" attribute. ### `saml` * Preserve ID-attributes on elements during signing. (Makes it possible to change the binding for some messages.) * Allow SAML artifacts to be received through a POST request. * Log more debug information when we are unable to determine the binding a message was sent with. * Require HTTP-POST messages to be sent as POST data and HTTP-Redirect messages to be sent as query parameters. * Link to download certificates from metadata pages. * Fix canonicalization of <md:EntityDescriptor> and <md:EntitiesDescriptor>. * Support for receiving and sending extension in authentication request messages. * Reuse SimpleSAML_Utilities::postRedirect() to send HTTP-POST messages. * Allow ISO8601 durations with subsecond precision. * Add support for parsing and serializing the <mdrpi:PublicationInfo> metadata extension. * Ignore cacheDuration when validating metadata. * Add support for the Holder-of-Key profile, on both the [SP](./simplesamlphp-hok-sp) and [IdP](./simplesamlphp-hok-idp). * Better error handling when receiving a SAML 2.0 artifact from an unknown entity. * Fix parsing of <md:AssertionIDRequestService> metadata elements. * IdP: Do not always trigger reauthentication when the authentication request contains a IdPList-element. * IdP: Add `saml:AllowCreate` to the state array. This makes it possible to access this parameter from authentication processing filters. * IdP: Sign the artifact response message. * IdP: Allow the "host" metadata option to include more than one path element. * IdP: Support for generating metadata with MDUI extension elements. * SP: Use the discojuice-module as a discovery service if it is enabled. * SP: Add `saml:idp`-parameter to trigger login to a specific IdP to as_login.php. * SP: Do not display error on duplicate response when we have a valid session. * SP: Fix for logout after IdP initiated authentication. * SP: Fix handling of authentication response without a saml:Issuer element. * SP: Support for specifying required attributes in metadata. * SP: Support for limiting the AssertionConsumerService endpoints listed in metadata. * SP: Fix session expiration when the IdP limits the session lifetime. * `saml:PersistentNameID`: Fail when the user has more than one value in the user ID attribute. * `saml:SQLPersistentNameID`: Persistent NameID stored in a SQL database. * `saml:AuthnContextClassRef`: New filter to set the AuthnContextClassRef in responses. * `saml:ExpectedAuthnContextClassRef`: New filter to verify that the SP received the correct authentication class from the IdP. ## Version 1.8.2 Released 2012-01-10. * Fix for user-assisted cross site scripting on a couple of pages. ## Version 1.8.1 Released 2011-10-27. * Fix for key oracle attack against XML encryption on SP. * Fix for IdP initiated logout with IdP-initiated SSO. * Fix a PHP notice if we are unable to open /dev/urandom. * Fix a PHP notice during SAML 1.1 authentication. ## Version 1.8 * New authentication modules: * [`authmyspace`](./authmyspace:oauthmyspace) * [`authlinkedin`](./authlinkedin:oauthlinkedin) * [`authwindowslive`](./authwindowslive:windowsliveid) * Support for custom error handler, replacing the default display function. * Allow error codes to be defined in modules. * Better control of logout what we do after logout request. * This makes it possible for the SP to display a warning when receiving a PartialLogout response from the IdP. * New `cdc` module, for setting and reading common domain cookies. ### `consent` * Support for disabling consent for some attributes. ### `ldap` * `ldap:AttributeAddFromLDAP`: Extract values from multiple matching entries. ### `oauth` * Added support for: * RSASHA1 signatures * consent * callbackurl * verifier code * request parameters ### `openid` * Support for sending custom extension arguments (e.g. UI extensions). ### `saml` * Extract Extensions from AuthnRequest for use by custom modules when authenticating. * Allow signing of SP metadata. * Better control over NameIDPolicy when sending AuthnRequest. * Support encrypting/decrypting NameID in LogoutRequest. * Option to disable client certificate in SOAP client. * Better selection of AssertionConsumerService endpoint based on parameters in AuthnRequest. * Set NotOnOrAfter in IdP LogoutRequest. * Only return PartialLogout from the IdP. ## Version 1.7 * New authentication modules: * `aselect` * `authX509` * Unified cookie configuration settings. * Added protection against session fixation attacks. * Error logging when failing to initialize the Session class. * New session storage framework. * Add and use generic key/value store. * Support for storing sessions in SQL databases (MySQL, PostgreSQL & SQLite). * Support for implementing custom session storage handlers. * Allow loading of multiple sessions simultaneously. * Set headers allowing caching of static files. * More descriptive error pages: * Unable to load $state array because the session was lost. * Unable to find metadata for the given entityID. * Support for multiple keys in metadata. * Allow verification with any of the public keys in metadata. * Allow key rollower by defining new and old certificate in configuration. * Verify with signing keys, encrypt with encryption keys. * Change `debug`-option to log messages instead of displaying them in the browser. * Also logs data before encryption and after decryption. * Support for custom attribute dictionaries. * Add support for several authentication sessions within a single session. * Allows several SPs on a single host. * Allows for combining an SP and an IdP on a single host. * HTTP proxy support. ### Internal API changes & features removed * The `saml2` module has been removed. * The `saml2:SP` authsource has been removed. * The `sspmod_saml2_Error` class has been renamed to `sspmod_saml_Error`. * The `sspmod_saml2_Message` class has been renamed to `sspmod_saml_Message`. * Moved IdP functions from `sspmod_saml_Message` to `sspmod_saml_IdP_SAML2`. * Removed several functions and classes that are unused: * `SimpleSAML_Utilities::strleft` * `SimpleSAML_Utilities::array_values_equal` * `SimpleSAML_Utilities::getRequestURI` * `SimpleSAML_Utilities::getScriptName` * `SimpleSAML_Utilities::getSelfProtocol` * `SimpleSAML_Utilities::cert_fingerprint` * `SimpleSAML_Utilities::generateTrackID` * `SimpleSAML_Utilities::buildBacktrace` * `SimpleSAML_Utilities::formatBacktrace` * `SimpleSAML_Metadata_MetaDataStorageHandlerSAML2Meta` * `SimpleSAML_ModifiedInfo` * Moved function from Utilities-class to more appropriate locations. * `getAuthority` to `SimpleSAML_IdP` * `generateUserId` to `sspmod_saml_IdP_SAML2`. * Replaced calls to with throwing an `SimpleSAML_Error_Error` exception. * Removed metadata send functionality from old SP code. * Removed bin/test.php and www/admin/test.php. * Removed metashare. * Removed www/auth/login-auto.php. * Removed www/auth/login-feide.php. * Removed optional parameters from `SimpleSAML_XHTML_Template::getLanguage()`. * Removed functions from `SAML2_Assertion`: `get/setDestination`, `get/setInResponseTo`. Replaced with `setSubjectConfirmation`. * Removed several unused files & templates. ### SAML 2 IdP * Support for generation of NameID values via [processing filters](./saml:nameid) * Obey the NameIDPolicy Format in authentication request. * Allow AuthnContextClassRef to be set by processing filters. * Rework iframe logout page to not rely on cookies. ### SAML 2 SP * Support SOAP logout. * Various fixes to adhere more closely to the specification. * Allow multiple SessionIndex-elements in LogoutRequest. * Handle multiple Assertion-elements in Response. * Reject duplicate assertions. * Support for encrypted NameID in LogoutRequest. * Verify Destination-attribute in LogoutRequest messages. * Add specific options for signing and verifying authentication request and logout messages. * `saml:NameIDAttribute` filter for extracting NameID from authentication response. ### SAML 1 IdP * Add `urn:mace:shibboleth:1.0` as supported protocol in generated metadata. ### SAML 1 SP * Support for IdP initiated authentication. ### `aggregator` * Allow metadata generation from command line. ### `authfacebook` * Change attribute names. ### `casserver` * Support for proxying. * Add ttl for tickets. ### `core` * `core:AttributeLimit`: Make it possible to specify a default set of attributes. * Make the SP metadata available on the login pages. ### `discoPower` * Sort IdPs without a name (where we only have an entityID) last in the list. * CDC cookie support. ### `exampleAuth` * Add example of integration with external authentication page. ### `ldap` * Add `ldap:AttributeAddFromLDAP` filter for adding attributes from a LDAP directory. ### `metarefresh` * Don't stop updates on the first exception. ### `openid` * Don't require access to the PHP session. * Remove OpenID test page. (May as well use the normal test pages.) * Support for attribute exchange. * Add `target` option, for directing authentication to a specific OpenID provider. * Add `realm` option, for specifying the realm we should send to the OpenID provider. ### `portal` * Make it possible to register pages from modules, and not only from configuration. ### `statistics` * New y-axis scaling algorithm ### `twitter` * Change attribute names returned from twitter. ## Version 1.6.3 Released 2010-12-17. * Fix for cross site scripting in redirect page. ## Version 1.6.2 Released 2010-07-29. * Various security fixes. ## Version 1.6.1 Released 2010-06-25. * saml:SP: Fix SingleLogoutService endpoint in SSP-format metadata array. * Shib13:IdP: Add urn:mace:shibboleth:1.0 to supported protocols. * Fix SAMLParser::parseElement(). * SAML2:IdP: Fix persistent NameID generation. * Fix scoping on IdP discovery page. * metaedit: Fix endpoints parsed from XML. * Dictionary update. * Documentation fixes. ## Version 1.6 Released 2010-05-31. [Upgrade notes](./simplesamlphp-upgrade-notes-1.6) * Detection of cookies disabled on the IdP. * New IdP core, which makes it simpler to share code between different IdPs, e.g. between SAML 1.1 and SAML 2.0. * Dictionaries moved to JSON format. * New authentication module: [`cas:CAS`](./cas:cas). * All images that doesn't permit non-commercial use have been replaced. * Better support for OrganizationName, OrganizationDisplayName and OrganizationURL in metadata. * Cookie secure flag no longer automatically set. * Cross-protocol logout between ADFS and SAML 2. * New experimental module for aggregating metadata: [`aggregator2`](./aggregator2:aggregator2) * Metadata support for multiple endpoints with [multiple bindings](./simplesamlphp-metadata-endpoints). * The metadata generation is using a new set of classes. As a result, all generated metadata elements now have a `md:`-prefix. * The deprecated functions `init(...)` and `setAuthenticated(...) in the `SimpleSAML_Session` class have been removed. * Configuration check and metadata check was removed, as they were often wrong. ### SAML 2 SP * SAML 2.0 HTTP-Artifact support on the [SP](./simplesamlphp-artifact-sp). ### SAML 2 IdP * SAML 2.0 HTTP-Artifact support on the [IdP](./simplesamlphp-artifact-idp). * Support for sending PartialLogout status code in logout response. * Set AuthnInstant to the timestamp for authentication. * Combine normal and iframe versions of the logout handlers into a single endpoint. * The SessionIndex is now unique per SP. * Statistics for logout failures. * Better generation of persistent NameID when `nameid.attribute` isn't specified. ### The SP API * Support for handling errors from the IdP. * Support for passing parameters to the authentication module. This can be used to specify SAML 2 parameters, such as isPassive and ForceAuthn. ### `adfs` * Move to new IdP core. ### `casserver` * Collect all endpoints in a single file. * Fix prefix on the tickets. ### `consent` * Support for deactivating consent for specific services. ### `consentAdmin` * Support for the SAML SP module. ### `core` * New filter: [`core:PHP`](./core:authproc_php), which allows processing of attributes with arbitrary PHP code. * Support for multiple target attributes in [`core:AttributeMap`](./core:authproc_attributemap). * New filter: [`core:ScopeFromAttribute`](./core:authproc_scopefromattribute), which allows the creation an attribute based on the scope of another attribute. * Support for a target attribute in [`core:AttributeAlter`](./core:authproc_attributealter). ### `discoPower` * Support for new scoring algorithm. ### `ldap` * SASL support in LDAPMulti ### `ldapstatus` * This module was removed, as it was very specific for Feide. ### `multiauth` * Support for specifying the target authentication source through a request parameter. ### `oauth` * Configurable which authentication source should be used. ### `openidProvider` * OpenID 2.0 support. * XRDS generation support. ### `saml` * Support for specifying parameters for authentication request. * Add AttributeConsumingService to generated metadata. * The two SPSSODescriptor elements in the metadata has been merged. ## Version 1.5.1 Released 2010-01-08. * Fix security vulnerability due to insecure temp file creation: * statistics: The logcleaner script outputs to a file in /tmp. * InfoCard: Saves state directly in /tmp. Changed to the SimpleSAMLphp temp directory. * openidProvider: Default configuration saves state information in /tmp. Changed to '/var/lib/simplesamlphp-openid-provider'. * SAML 1 artifact support: Saves certificates temporarily in '/tmp/simplesaml', but directory creation was insecure. * statistics: Handle new year wraparound. * Dictionary updates. * Fix bridged logout. * Some documentation updates. * Fix all metadata to use assignments to arrays. * Fix $session->getIdP(). * Support AuthnContextClassRef in saml-module. * Do not attempt to send logout request to an IdP that does not support logout. * LDAP: Disallow bind with empty password. * LDAP: Assume that LDAP_NO_SUCH_OBJECT is an error due to invalid username/password. * statistics: Fix configuration template. * Handle missing authority in idp-hosted metadata better. ## Version 1.5 Released 2009-11-05. Revision 1937. * New API for SP authentication. * Make use of the portal module on the frontpage. * SQL datastore. * Support for setting timezone in config (instead of php.ini). * Logging of PHP errors and notices to SimpleSAMLphp log file. * Improve handling of unhandled errors and exceptions. * Admin authentication through authentication sources. * Various bugfixes & cleanups. * Translation updates. * Set the dropdown list as default for built in disco service. ### New modules: * `adfs` * [`authorize`](./authorize:authorize) * `authtwitter` * [`autotest`](./autotest:test) * `exampleattributeserver` * `metaedit` * [`multiauth`](./multiauth:multiauth) * `oauth` * [`openidProvider`](./openidProvider:provider) * [`radius`](./radius:radius) * [`saml`](./saml:sp) ### `aggregator`: * Add ARP + ARP signing functionality to the aggregator. * Improvements to the aggregator module. Added documentation, and re-written more OO-oriented. * Add support for reconstructing XML where XML for an entity is already cached. * Add support for excluding tags in metadata aggregator. ### `AuthMemCookie`: * Delete the session cookie when deleting the session. * Support for authentication sources. * Set expiry time of session data when saving to memcache. * Support multiple memcache servers. ### `cas`: * Added support for attributes in . ### `consent`: * Support for hiding some attribute values. ### `consentAdmin`: * Added config option to display description. ### `core`: * New WarnShortSSOInterval filter. ### `discopower`: * Live search in discopower-module. ### `ldap`: * Support for proxy authentication. * Add 'debug' and 'timeout' options. * Privilege separation for LDAP attribute retrieval. * Allow search.base to be an array. * (LDAPMulti) Add support for including the organization as part of the username. ### `ldapstatus`: * Do a connect-test to all ip-addresses for a hostname. * Check wheter hostname exists before attempting to connect. * hobbit output. * Check schema version. * Add command line tab to single LDAP status page for easier debugging. ### `logpeek`: * Blockwise reading of logfile for faster execution. ### `metarefresh`: * Adding support for generating Shibboleth ARP files. * Add 'serialize' metadata format. ### `preprodwarning`: * Don't show warning in passive request. * Focus on continue-button. ### SAML: * Support for multiple AssertionConsumerService endpoints. * SAML 1 artifact support on the SP side. * New SAML authentication module. * Deprecation of www/saml2/sp & www/shib13/sp. * Support for encrypted NameID. * NameIDPolicy replaces NameIDFormat. * Better support for IdP initiated SSO and bookmarked login pages. * Improvements to iframe logout page. * Scoping support. * New library for SAML 2 messages. * Support for transporting errors from the IdP to the SP. * Sign both the assertion and the response element by default. * Support for sending XML attribute values from the IdP. ### `statistics`: * Extended Google chart encoding... Add option of alternative compare plot in graph... * Added support for Ratio type reports in the statistics module.. * Changed default rule to sso. * Added incremental aggregation, independent time resolution from rule def, combined coldefs and more. * Add DST support in date handler. Added summary columns per delimiter. Added pie chart. +++ * Log first SSO to a service during a session. ## Version 1.4 Released 2009-03-12. Revision 1405. Updates to `config.php`. Please check for updates in your local modified configuration. * Language updates * Documentation update. New authencation source API now default and documented. * New authentication source (new API): * LDAP * LDAPMulti * YubiKey authentication source. (Separate module) * Facebook authentication source. (Separate module) * New Authentication Processing Filter: * AttributeAlter * AttributeFilter * AttributeMap * Smartname. does it best to guess the full name of the user based on several attributes. * Language adaptor: allow adopting UI by preferredLanguage SAML 2.0 Attribute both on the IdP and the SP. And if the user selects a lanauge, this can be sent to the SP as an attribute. * New module: portal, allows you to created tabbed interface for custom pages within SimpleSAMLphp. In example user consent management and attribute viewer. * New module: ldapstatus. Used by Feide to monitor connections to a large list of LDAP connections. Contact Feide on details on how to use. * ldapstatus also got certificate check capabilities. * New module: MemcacheMonitor: Show statistics for memcache servers. * New module: DiscoPower. A tabbed discovery service module with alot of functionality. * New module: SAML 2.0 Debugginer. An improved version of the one found on rnd.feide.no earlier is not included in SimpleSAMLphp allowing you to run it locally. * New module: Simple Consent Amdin module that have one button to remove all consent for one user. * New module: Consent Administration. Contribution from Wayf. * We also have a consent adminstration module that we use in Feide that is not checked in to subversion. * New module: logpeek. Lets administrator lookup loglines matching a TRackID. * New module: PreprodWarning: Adding a warning to users that access a preprod system. * New module: CAS Server * New module: Aggregator: Aggregates metadata. Used in Kalmar Union. * New module: Metarefresh, download, parses and consumes metadata. * New module: SanityCheck. Checks if things looks good and reports bad configuration etc. * New module: Cron. Will perform tasks regularly. * Module: SAML2.0. SAML 2.0 SP implemented as an module. Yet not documented how to use, but all SAML 2.0 SP functionality may be moved out to this module for better modularization. * New module: statistics. Parses STAT log files, and aggregates based on a generic rule system. Output is stored in aggregated text files, and a frontend is included to present statistics with tables and graphs. Used sanitycheck and cron. * Added support for IdP initiated SSO. * Added support for IdP-initiated SLO with iFrame type logout. * Major updates to iFrame AJAX SLO. Improved user experience. * iFrame AJAX SLO is not safe against simulanous update of the session. * Added support for bookmarking login pages. By adding enough information in the URL to be able to bootstrap a new IdP-initiated SSO and sending. * Major updates to the infocard module. * Added some handling of isPassive with authentication processing filters. * More localized UI. * New login as administrator link on frontpage. * Tabbed frontpage. Restructured. * Simplifications to the theming and updated documentation on theming SimpleSAMLphp. * Attribute presentation hook allows you to tweak attributes before presentation in the attribute viewers. Used by Feide to group orgUnit information in a hieararchy. * Verification of the Receipient attribute in the response. Will improve security if for some reason an IdP is not includeding sufficient Audience restrictions. * Added hook to let modules tell about themself moduleinfo hook. * Improved cron mails * Improved santity check exception handling * Preserver line breaks in stack trace UI * Improvements to WS-Federation support: dynamic realms, logout etc. * Better handling of presentation of JPEG photos as attributes. * Support limiting size of attribute retrieved from LDAP. * Added notes about how to aggregate and consume metadata. Just a start. * Large improvements to Configuration class, and config helper functions. * STAT logging is moved into separate authenticaion processing filter. * Fix for NoPassive responses to Google Apps with alternative NameIDFormats. * LDAP module allows to search multiple searchbases. * All documentation is converted from docbook to markdown format. * Added headers to not allow google to index pages. * Added check on frontpage for magic quotes * Added statistic loggging to Consent class. * Improvements to Exception handler in LDAP class, and better logging. * LDAP class supports turning on LDAP-debug logging. * Much improvements to SAML 2.0 Metadata generation and parsing. * Adding more recent jquery library. * Generic interface for including jquery dependencies in template headers. * Improved UI on default theme * Fix for session duration in the Conditions element in the Assertion (SAML 2.0). * Updated with new Feide IdP metadata in metadata-templates ## Version 1.3 Released 2008-11-04. Revision 973. Configuration file `config.php` should not include significant changes, except one language added. ### New features * Documentation update * Added new language. Now there are two different portugese dialects. * Consent "module" modified. Now added support for preselecting the checkbox by a configuration parameter. Consent module supports including attributs values (possible to configure). * CSS and look changed. Removed transparency to fix problem for some browsers. * The login-admin authentication module does not ask for username any more. * Added support for persistent NameID Format. (Added by Hans ZAndbelt) * Added experimental SAML 2.0 SP AuthSource module. * More readable XML output formatting. In example metadata. * Better support for choosing whether or not to sign authnrequest. Possible to specify both at SP hosted and IdP remote. * Adding more example metadata in metadata-templates. * Improved e-mails sent from SimpleSAMLphp. Now both plain text and html. * Configuration class may return information about what version. * iFrame AJAX SLO improved. Now with non-javascript failback handling. ### Bug fixes * Fixed warning with XML validator. * Improved loading of private/public keys in XML/Signer. * Improvements to CAS module. * Fixed memcache stats. ## Version 1.2 Released 2008-09-26. Revision 899. There are some changes in the configuration files from version 1.1 to 1.2. `/simplesaml/admin/config.php` should be used to check what options have changed. When you upgrade from an previous version you should copy `authsources.php` from `config-templates` into `config` directory. There are also some changes to the templates. If you have any custom templates, they should be updated to match the ones included. Of notable changes is that the `t(...)`-functtes, they should be updated to match the ones included. Of notable changes is that the `t(...)`-function has been simplified, and takes far fewer parameters. It is backwardscompatible, but will write a warning to the log until updated. The backwards compatibility will be removed in a future version. ### New features * Experimental support for modules. Currently modules can contain custom authentication sources, authentication processing filters and themes. * An generic SQL autentication module added for those who store their users in an SQL database. * Limited support for validating against a CA root certificate. The current implementation only supports cases where the certificate is directly signed by the CA. * Allow an IdP to have multiple valid certificate fingerprints, to allow for easier updating of certificates. * Shibboleth 1.3 authentication for Auth MemCookie. * Support for link to privacy policy on consent-pages. * Customizable initial focus on consent-page. * Almost all pages should be translateable. * Allow SAML 2.0 SP to handle error replies from IdP. * PostgreSQL support for consent storage. * Add support for encrypted private keys. * Proof-of-concept MetaShare service, for easy publishing and sharing of metadata. ### Bug fixes * Fixed generated SAML 2.0 metadata to be correct. * Fixed logout for Auth MemCookie. * Sign SAML 2.0 authentication response on failure (such as NoPassive). * Fixes for IsPassive in the SAML 2.0 IdP. * Fix default syslog configuration on Windows. * Fix order of signing and encryption of SAML 2.0 responses * Fix generated metadata for Shib 1.3 * Fix order of elements in encrypted assertions to be schema compliant. * Fix session index sent to SAML 2.0 SPs. * Remember SAML 2.0 NameID sent to SPs, and include it in logout requests. ## Version 1.1 Released 2008-06-19. Revision 673. When upgrading to version 1.1 from version 1.0, you should update the configuration files. Many options have been added, and some have moved or removed. The new configuration check page: `/simplesaml/admin/config.php` may be useful for determining what should be updated. Also note that the `language.available` option in `config.php` should be updated to reflect the new languages which have been added. There are also several changes to the template files. If you have done any customizations to these, you should test them to make sure that they still work. Some changes, such as allowing the users to save the IdP choice they make in the discovery service, will not work without updating the templates. New localizations in version 1.1: Sami, Svenska (swedish), Suomeksi (finnish), Nederlands, Luxembourgish, Slovenian, Hrvatski (Croatian), Magyar (Hungarian). ### New features * Add support for saving the users choice of IdP in the IdP discovery service. * Add a config option for whether the Response element or the Assertion element in the response should be signed. * Make it easier to add attribute alteration functions. * Added support for multiple languages in metadata name and description (for IdP discovery service). * Added configuration checker for checking if configuration files should be updated. * Add support for icons in IdP discovery service. * Add support for external IdP discovery services. * Support password encrypted private keys. * Added PHP autoloading as the preferred way of loading the SimpleSAMLphp library. * New error report script which will report errors to the `technicalcontact_email` address. * Support lookup of the DN of the user who is logging in by searching for an attribute when using the LDAP authentication module. * Add support for fetching name and description of entities from XML metadata files. * Support for setting custom AttributeNameFormats. * Support for signing generated metadata. * Support for signature validation of metadata. * Added consent support for Shib 1.3 logging. * Added errorlog logging handler for logging to the default Apache error log. * Added support for WS-Federation single signon. * Allow `session_save_path` to be overridden by setting the `session.phpsession.savepath` option in `config.php`. * Add support for overriding autogenerated metadata values, such as the `AssertionConsumerService` address. * Added IsPassive support in the SAML 2.0 IdP. * Add attribute filter for generating eduPersonTargetedID attribute. * Add support for validation of sent and received messages and metadata. * Add support for dynamic metadata loading with cache. * Add support for dynamic generation of entityid and metadata. * Added wayf.dk login module. * Add support for encrypting and decrypting assertions. * CAS authentication module: Add support for serviceValidate. * CAS authentication module: Add support for getting attributes from response by specifying XPath mappings. * Add support for specifying a certificate in the `saml20-idp-remote` metadata instead of a fingerprint. * Add an attribute alter function for dynamic group generation. * Add support for attribute processing in SAML 2 SP. * Added tlsclient authentication module. * Allow the templates to override the header and footer of pages. * Major improvements to the Feide authentication module. * Add support for ForceAuthn in the SAML 2.0 IdP. * Choose language based on the languages the user has selected in the web browser. * Added fallback to base language if translation isn't found. ### Bug fixes * Modified IdP discovery service to support Shibboleth 2.0 SP. * Fix setcookie warning for PHP version \< 5.2. * Fix logout not being performed for Auth MemCache sometimes. * Preserve case of attribute names during LDAP attribute retrival. * Fix IdP-initiated logout. * Ensure that changed sessions with changed SP associations are written to memcache. * Prevent infinite recursion during logging. * Don't send the relaystate from the SP which initiated the logout to other SPs during logout. * Prevent consent module from revealing DB password when an error occurs. * Fix logout with memcache session handler. * Allow new session to be created in login modules. * Removed the strict parameter from base64\_decode for PHP 5.1 compatibility. ## Version 1.0 Released 2008-03-28. Revision 470. ## Version 0.5 Released 2007-10-15. Revision 28. ### Warning Both `config.php` and metadata format are changed. Look at the templates to understand the new format. * Documentation is updated! * Metadata files made tidier. Unused entries removed. Look at the new templates on how to change your existing metadata. * Support for sending metadata by mail to Feide. Automatically detecting whether you have configured Feide as the default IdP or not. * Improved SAML 2.0 Metadata generation * Added support for Shibboleth 1.3 IdP functionality (beta, contact me if any problems) * Added RADIUS authentication backend * Added support for HTTP-Redirect debugging when enable `debug=true` * SAML 2.0 SP example now contains a logout page. * Added new authentication backend with support for multiple LDAP based on which organization the user selects. * Added SAML 2.0 Discovery Service * Initial 'proof of concept' implementation of "User consent on attribute release" * Fixed some minor bugs. ## Version 0.4 Released 2007-09-14. Revision X. * Improved documentation * Authentication plugin API. Only LDAP authenticaiton plugin is included, but it is now easier to implement your own plugin. * Added support for SAML 2.0 IdP to work with Google Apps for Education. Tested. * Initial implementation of SAML 2.0 Single Log Out functionality both for SP and IdP. Seems to work, but not yet well-tested. * Added support for bridging SAML 2.0 to SAML 2.0. * Added some time skew offset to the NotBefore timestamp on the assertion, to allow some time skew between the SP and IdP. * Fixed Browser/POST page to automaticly submit, and have fall back functionality for user agents with no javascript support. * Fixed some bug with warning traversing Shibboleth 1.3 Assertions. * Fixed tabindex on the login page of the LDAP authentication module to allow you to tab from username, to password and then to submit. * Fixed bug on autodiscovering hostname in multihost environments. * 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. * Several minor bugfixes. simplesamlphp-1.15.3/docs/simplesamlphp-idp-more.md0000644000000000000000000001063313245225037021025 0ustar rootrootSimpleSAMLphp Identity Provider Advanced Topics =============================================== AJAX iFrame Single Log-Out -------------------------- If you have read about the AJAX iFrame Single Log-Out approach at Andreas' blog and want to enable it, edit your saml20-idp-hosted.php metadata, and add this configuration line for the IdP: 'logouttype' => 'iframe', Attribute Release Consent ------------------------- The attribute release consent is documented in a [separate document](./consent:consent). Support for bookmarking the login page -------------------------------------- Most SAML software crash fatally when users bookmark the login page and return later on when the cached session information is lost. This is natural as the login page happens in the middle of a SAML transaction, and the SAML software needs some references to the original request in order to be able to produce the SAML Response. SimpleSAMLphp has implemented a graceful fallback to tackle this situation. When SimpleSAMLphp is not able to lookup a session during the login process, it falls back to the *IdP-first flow*, described in the next section, where the reference to the request is not needed. What happens in the IdP-first flow is that a *SAML unsolicited response* is sent directly to the SP. An *unsolicited response* is a SAML Response with no reference to a SAML Request (no `InReplyTo` field). When a SimpleSAMLphp IdP falls back to IdP-first flow, the `RelayState` parameter sent by the SP in the SAML request is also lost. The RelayState information contain a reference key for the SP to lookup where to send the user after successful authentication. The SimpleSAMLphp Service Provider supports configuring a static URL to redirect the user after a unsolicited response is received. See more information about the `RelayState` parameter in the next section: *IdP-first flow*. IdP-first flow -------------- If you do not want to start the SSO flow at the SP, you may use the IdP-first setup. To do this, redirect the user to the SSOService endpoint on the IdP with a `spentityid` parameter that matches the SP EntityID that the user should be authenticated for. Here is an example of such a URL: https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=urn:mace:feide.no:someservice You can also add a RelayState parameter to the IdP-first URL: https://idp.example.org/simplesaml/saml2/idp/SSOService.php?spentityid=urn:mace:feide.no:someservice&RelayState=https://sp.example.org/somepage The RelayState parameter is often uset do carry the URL the SP should redirect to after authentication. ### IdP first with SAML 1.1 A SAML 1.1 SP does not send an authentication request to the IdP, but instead triggers IdP initiated authentication directly. If you want to do it manually, you can access the following URL: https://idp.example.org/simplesaml/shib13/idp/SSOService.php?providerId=urn:mace:feide.no:someservice&shire=https://sp.example.org/acs-endpoint&target=https://sp.example.org/somepage The parameters are as follows: `providerID` : The entityID of the SP. This parameter is required. `shire` : The AssertionConsumerService endpoint of the SP. This parameter is required. `target` : The target parameter the SP should receive with the authentication response. This is often the page the user should be sent to after authentication. This parameter is optional for the IdP, but must be specified if the SP you are targeting is running SimpleSAMLphp. : *Note*: This parameter must be sent as `target` (with lowercase letters) when starting the authentication, while it is sent as `TARGET` (with uppercase letters) in the authentication response. IdP-initiated logout -------------------- IdP-initiated logout can be initiated by visiting the URL: https://idp.example.org/simplesaml/saml2/idp/SingleLogoutService.php?ReturnTo= It will send a logout request to each SP, and afterwards return the user to the URL specified in the `ReturnTo` parameter. Bear in mind that IdPs might disallow redirecting to URLs other than those of their own for security reasons, so in order to get the redirection to work, it might be necessary to ask the IdP to whitelist the URL we are planning to redirect to. simplesamlphp-1.15.3/docs/simplesamlphp-sp.md0000644000000000000000000002054413245225037017735 0ustar rootrootSimpleSAMLphp Service Provider QuickStart ========================================= This guide will describe how to configure SimpleSAMLphp as a service provider (SP). You should previously have installed SimpleSAMLphp as described in [the SimpleSAMLphp installation instructions](simplesamlphp-install). Configuring the SP ------------------ The SP is configured by an entry in `config/authsources.php`. This is a minimal `authsources.php` for a SP: array( 'saml:SP', ), ); For more information about additional options available for the SP, see the [`saml:SP` reference](./saml:sp). If you want multiple Service Providers in the same site and installation, you can add more entries in the `authsources.php` configuration. If so remember to set the EntityID explicitly. Here is an example: 'sp1' => array( 'saml:SP', 'entityID' => 'https://sp1.example.org/', ), 'sp2' => array( 'saml:SP', 'entityID' => 'https://sp2.example.org/', ), ### Enabling a certificate for your Service Provider Some Identity Providers / Federations may require that your Service Providers holds a certificate. If you enable a certificate for your Service Provider, it may be able to sign requests and response sent to the Identity Provider, as well as receiving encrypted responses. Create a self-signed certificate in the `cert/` directory. cd cert openssl req -newkey rsa:2048 -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.pem Then edit your `authsources.php` entry, and add references to your certificate: 'default-sp' => array( 'saml:SP', 'privatekey' => 'saml.pem', 'certificate' => 'saml.crt', ), Adding IdPs to the SP --------------------- The service provider you are configuring needs to know about the identity providers you are going to connect to it. This is configured by metadata stored in `metadata/saml20-idp-remote.php` and `metadata/shib13-idp-remote.php`. This is a minimal example of a `metadata/saml20-idp-remote.php` metadata file: 'https://example.com/simplesaml/saml2/idp/SSOService.php', 'SingleLogoutService' => 'https://example.com/simplesaml/saml2/idp/SingleLogoutService.php', 'certificate' => 'example.pem', ); `example.pem` under your `cert/` directory contains the certificate the identity provider uses for signing assertions. For more information about available options in the idp-remote metadata files, see the [IdP remote reference](simplesamlphp-reference-idp-remote). If you have the metadata of the remote IdP as an XML file, you can use the built-in XML to SimpleSAMLphp metadata converter, which by default is available as `/admin/metadata-converter.php` in your SimpleSAMLphp installation. Note that the idp-remote file lists all IdPs you trust. You should remove all IdPs that you don't use. Setting the default IdP ----------------------- An option in the authentication source allows you to configure which IdP should be used. This is the `idp` option. array( 'saml:SP', /* * The entity ID of the IdP this should SP should contact. * Can be NULL/unset, in which case the user will be shown a list of available IdPs. */ 'idp' => 'https://idp.example.com', ), ); Exchange metadata with the IdP ------------------------------ In order to complete the connection between your SP and an IdP, you must exchange the metadata of your SP with the IdP. The metadata of your SP can be found in the *Federation* tab of the web interface. Copy the SAML 2.0 XML Metadata document automatically generated by SimpleSAMLphp and send it to the administrator of the IdP. You can also send them the dedicated URL of your metadata, so that they can fetch it periodically and obtain automatically any changes that you may perform to your SP. You will also need to add the metadata of the IdP. Ask them to provide you with their metadata, and parse it using the *XML to SimpleSAMLphp metadata converter* tool available also in the *Federation* tab of the web interface. Copy the resulting parsed metadata and paste it with a text editor into the `metadata/saml20-idp-remote.php` file in your SimpleSAMLphp directory. If you intend to add your SP to a federation, the procedure for managing trust in federations differ, but the common part is that you would need to provide the *SAML 2.0 metadata of your SP*, and register that with the federation administration. You will probably be required too to consume the federation metadata periodically. Read more about [automated metadata management](simplesamlphp-automated_metadata) to learn more about that. Test the SP ----------------------------- After the metadata is configured on the IdP, you should be able to test the configuration. The installation page of SimpleSAMLphp has a link to test authentication sources. When you click the link, you should receive a list of authentication sources, including the one you have created for the SP. After you click the link for that authentication source, you will be redirected to the IdP. After entering your credentials, you should be redirected back to the test page. The test page should contain a list of your attributes: ![Screenshot of the status page after a user has succesfully authenticated](resources/simplesamlphp-sp/screenshot-example.png) For a better looking, more advanced Discovery Service with tabs and live search, you may want to use the `discopower` module contained in the SimpleSAMLphp distribution. Integrating authentication with your own application ---------------------------------------------------- The API is documented in [the SP API reference](simplesamlphp-sp-api). For those web resources you want to protect, you must add a few lines of PHP code: - Register the SimpleSAMLphp classes with the PHP autoloader. - Require authentication of the user for those places it is required. - Access the users attributes. Example code: We start off with loading a file which registers the SimpleSAMLphp classes with the autoloader. require_once('../../lib/_autoload.php'); We select our authentication source: $as = new \SimpleSAML\Auth\Simple('default-sp'); We then require authentication: $as->requireAuth(); And print the attributes: $attributes = $as->getAttributes(); print_r($attributes); Each attribute name can be used as an index into $attributes to obtain the value. Every attribute value is an array - a single-valued attribute is an array of a single element. We can also request authentication with a specific IdP: $as->login(array( 'saml:idp' => 'https://idp.example.org/', )); Other options are also available. Take a look in the documentation for the [SP module](./saml:sp) for a list of all parameters. If we are using PHP sessions in SimpleSAMLphp and in the application we are protecting, SimpleSAMLphp will close any existing session when invoked for the first time, and its own session will prevail afterwards. If you want to restore your own session after calling SimpleSAMLphp, you can do so by cleaning up the session like this: $session = SimpleSAML_Session::getSessionFromRequest(); $session->cleanup(); If you don't cleanup SimpleSAMLphp's session and try to use $_SESSION afterwards, you won't be using your own session and all your data is likely to get lost or inaccessible. Support ------- If you need help to make this work, or want to discuss SimpleSAMLphp with other users of the software, you are fortunate: Around SimpleSAMLphp there is a great Open source community, and you are welcome to join! The forums are open for you to ask questions, contribute answers other further questions, request improvements or contribute with code or plugins of your own. - [SimpleSAMLphp homepage](https://simplesamlphp.org) - [List of all available SimpleSAMLphp documentation](https://simplesamlphp.org/docs/) - [Join the SimpleSAMLphp user's mailing list](https://simplesamlphp.org/lists) simplesamlphp-1.15.3/docs/simplesamlphp-install-repo.md0000644000000000000000000000404613245225037021723 0ustar rootrootInstalling SimpleSAMLphp from the repository ============================================ These are some notes about running SimpleSAMLphp from the repository. Installing from github ---------------------- Go to the directory where you want to install SimpleSAMLphp: cd /var Then do a git clone: git clone git@github.com:simplesamlphp/simplesamlphp.git simplesamlphp Initialize configuration and metadata: cd /var/simplesamlphp cp -r config-templates/* config/ cp -r metadata-templates/* metadata/ Install the external dependencies with Composer (you can refer to [getcomposer.org](http://getcomposer.org/) to get detailed instructions on how to install Composer itself): php composer.phar install Upgrading --------- Go to the root directory of your SimpleSAMLphp installation: cd /var/simplesamlphp Ask git to update to the latest version: git fetch origin git pull origin master Install or upgrade the external dependencies with Composer ([get composer](http://getcomposer.org/)): php composer.phar install Migrating from Subversion ------------------------- If you installed SimpleSAMLphp from subversion, and want to keep updated on the development, you will have to migrate your installation to git. First, follow the steps to get a fresh install from github in a different directory. Skip the steps regarding configuration and metadata initialization, and copy all the files you might have modified instead (not only configuration and metadata, but also any custom modules or templates). Finally, proceed to install Composer and install all the dependencies with it. You may want to add all your custom files to the '.gitignore' file. If you really want to use subversion instead of git, or it is impossible for you to migrate (you cannot install git, for example), you might want to do a fresh install like the one described here, but using github's subversion interface. Refer to [github's documentation](https://help.github.com/articles/support-for-subversion-clients) for detailed instructions on how to do that. simplesamlphp-1.15.3/docs/simplesamlphp-upgrade-notes-1.8.md0000644000000000000000000000120113245225037022361 0ustar rootrootUpgrade notes for SimpleSAMLphp 1.8 =================================== * The IdP now sends the NotOnOrAfter attribute in LogoutRequest messages. * We now have full support for selecting the correct AssertionConsumerService endpoint based on parameters in the authentication request. As a side effect of this, an IdP may start sending responses to a new AssertionConsumerService endpoint after upgrade. (This should only happen in the case where it sent the response to the wrong endpoint before.) * The SP no longer incorrectly returns PartialLogout as a status code in a LogoutResponse after the local session has expired. simplesamlphp-1.15.3/docs/simplesamlphp-modules.md0000644000000000000000000002124213245225037020757 0ustar rootrootSimpleSAMLphp modules ================================================== This document describes how the module system in SimpleSAMLphp works. It descibes what types of modules there are, how they are configured, and how to write new modules. Overview -------- There are currently three parts of SimpleSAMLphp which can be stored in modules - authentication sources, authentication processing filters and themes. There is also support for defining hooks - functions run at specific times. More than one thing can be stored in a single module. There is also support for storing supporting files, such as templates and dictionaries, in modules. The different functionalities which can be created as modules will be described in more detail in the following sections; what follows is a short introduction to what you can do with them: - Authentication sources implement different methods for authenticating users, for example simple login forms which authenticate against a database backend, or login methods which use client-side certificates. - Authentication processing filters perform various tasks after the user is authenticated and has a set of attributes. They can add, remove and modify attributes, do additional authentication checks, ask questions of the user, +++. - Themes allow you to package custom templates for multiple modules into a single module. ## Module layout Each SimpleSAMLphp module is stored in a directory under the `modules`-directory. The module directory contains the following directories and files: default-disable : The presence of this file indicates that the module is disabled by default. It can be enabled using the `module.enable` option in `config.php`. default-enable : The presence of this file indicates that the module is enabled by default. It can be disabled using the `module.enable` option in `config.php`. dictionaries : This directory contains dictionaries which belong to this module. To use a dictionary stored in a module, the extended tag names can be used: `{::}` For example, `{example:login:hello}` will look up `hello` in `modules/example/dictionaries/login.php`. : It is also possible to specify `:` as the default dictionary when instantiating the `SimpleSAML_XHTML_Template` class. hooks : This directory contains hook functions for this module. Each file in this directory represents a single function. See the hook-section in the documentation for more information. lib : This directory contains classes which belong to this module. All classes must be named in the following pattern: `sspmod__` When looking up the filename of a class, SimpleSAMLphp will search for `` in the `lib` directory. Underscores in the class name will be translated into slashes. : Thus, if SimpleSAMLphp needs to load a class named `sspmod_example_Auth_Source_Example`, it will load the file named `modules/example/lib/Auth/Source/Example.php`. templates : These are module-specific templates. To use one of these templates, specify `: