pax_global_header00006660000000000000000000000064141010503660014506gustar00rootroot0000000000000052 comment=d5cfa396f69ba2ee0d1048fe59c2f0f4a6d077ef mod_auth_mellon-0.18.0/000077500000000000000000000000001410105036600147425ustar00rootroot00000000000000mod_auth_mellon-0.18.0/.dir-locals.el000066400000000000000000000001141410105036600173670ustar00rootroot00000000000000( (nil . ((indent-tabs-mode . nil))) (c-mode . ((c-basic-offset . 4))) ) mod_auth_mellon-0.18.0/.github/000077500000000000000000000000001410105036600163025ustar00rootroot00000000000000mod_auth_mellon-0.18.0/.github/workflows/000077500000000000000000000000001410105036600203375ustar00rootroot00000000000000mod_auth_mellon-0.18.0/.github/workflows/ccpp.yml000066400000000000000000000007271410105036600220150ustar00rootroot00000000000000name: C/C++ CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: update apt cache run: sudo apt-get update - name: install dependencies run: sudo apt-get install apache2-dev liblasso3-dev libcurl4-openssl-dev - name: autoreconf run: autoreconf -i -f - name: autoconf run: autoconf - name: configure run: ./configure - name: make run: make mod_auth_mellon-0.18.0/.gitignore000066400000000000000000000001131410105036600167250ustar00rootroot00000000000000*.lo *.la *.o *.slo aclocal.m4 config.* configure Makefile .libs/ .vscode/ mod_auth_mellon-0.18.0/.travis.yml000066400000000000000000000003361410105036600170550ustar00rootroot00000000000000dist: bionic language: c addons: apt: packages: - apache2-dev - libglib2.0-dev - liblasso3-dev - libssl-dev script: - ./autogen.sh - ./configure CFLAGS=-Werror - make - make distfile mod_auth_mellon-0.18.0/COPYING000066400000000000000000000613141410105036600160020ustar00rootroot00000000000000mod_auth_mellon is distributed under the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. In addition, as a special exception, permission is granted to link the code of this release of mod_mellon with the OpenSSL project's "OpenSSL" library (or with modified versions of it that use the same licence as the "OpenSSL" library), and distribute the linked executables. You must obey the GNU General Public License version 2 in all respects for all of the code used other than "OpenSSL". If you modify the code, you may extend this exception to your version of the code, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. The full text of the GNU General Public License: =============================================================================== GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 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. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, 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 or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's 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 give any other recipients of the Program a copy of this License along with the Program. 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 Program or any portion of it, thus forming a work based on the Program, 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) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, 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 Program, 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 Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) 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; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, 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 executable. However, as a special exception, the source code 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. If distribution of executable or 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 counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program 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. 5. 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 Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program 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 to this License. 7. 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 Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program 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 Program. 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. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program 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. 9. The Free Software Foundation may publish revised and/or new versions of the 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 Program 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 Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, 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 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. 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 PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. The full text of the OpenSSL License: =============================================================================== LICENSE ISSUES ============== The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the OpenSSL License and the original SSLeay license apply to the toolkit. See below for the actual license texts. Actually both licenses are BSD-style Open Source licenses. In case of any license issues related to OpenSSL please contact openssl-core@openssl.org. OpenSSL License --------------- /* ==================================================================== * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. All advertising materials mentioning features or use of this * software must display the following acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" * * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please contact * openssl-core@openssl.org. * * 5. Products derived from this software may not be called "OpenSSL" * nor may "OpenSSL" appear in their names without prior written * permission of the OpenSSL Project. * * 6. Redistributions of any form whatsoever must retain the following * acknowledgment: * "This product includes software developed by the OpenSSL Project * for use in the OpenSSL Toolkit (http://www.openssl.org/)" * * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * ==================================================================== * * This product includes cryptographic software written by Eric Young * (eay@cryptsoft.com). This product includes software written by Tim * Hudson (tjh@cryptsoft.com). * */ Original SSLeay License ----------------------- /* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) * All rights reserved. * * This package is an SSL implementation written * by Eric Young (eay@cryptsoft.com). * The implementation was written so as to conform with Netscapes SSL. * * This library is free for commercial and non-commercial use as long as * the following conditions are aheared to. The following conditions * apply to all code found in this distribution, be it the RC4, RSA, * lhash, DES, etc., code; not just the SSL code. The SSL documentation * included with this distribution is covered by the same copyright terms * except that the holder is Tim Hudson (tjh@cryptsoft.com). * * Copyright remains Eric Young's, and as such any Copyright notices in * the code are not to be removed. * If this package is used in a product, Eric Young should be given attribution * as the author of the parts of the library used. * This can be in the form of a textual message at program startup or * in documentation (online or textual) provided with the package. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. All advertising materials mentioning features or use of this software * must display the following acknowledgement: * "This product includes cryptographic software written by * Eric Young (eay@cryptsoft.com)" * The word 'cryptographic' can be left out if the rouines from the library * being used are not cryptographic related :-). * 4. If you include any Windows specific code (or a derivative thereof) from * the apps directory (application code) you must include an acknowledgement: * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" * * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * * The licence and distribution terms for any publically available version or * derivative of this code cannot be changed. i.e. this code cannot simply be * copied and put under another distribution licence * [including the GNU Public Licence.] */ mod_auth_mellon-0.18.0/ECP.rst000066400000000000000000000302251410105036600161050ustar00rootroot00000000000000Guide to using ECP ================== Introduction ------------ The **Enhanced Client or Proxy** (ECP) profile of SAML2 The Enhanced Client or Proxy (ECP) Profile supports several SSO use cases, in particular: * Clients with capabilities beyond those of a browser, allowing them to more actively participate in IdP discovery and message flow. * Using a proxy server, for example a WAP gateway in front of a mobile device which has limited functionality. * When other bindings are precluded (e.g. where the client does not support redirects, or when auto form post is not possible without Javascript, or when the artifact binding is ruled out because the identity provider and service provider cannot directly communicate. An enhanced client or proxy (ECP) is a system entity that knows how to contact an appropriate identity provider, possibly in a context-dependent fashion, and also supports the Reverse SOAP (PAOS) binding. An example scenario enabled by ECP profile is as follows: A principal, wielding an ECP, uses it to either access a resource at a service provider, or access an identity provider such that the service provider and desired resource are understood or implicit. The principal authenticates (or has already authenticated) with the identity provider [1]_, which then produces an authentication assertion (possibly with input from the service provider). The service provider then consumes the assertion and subsequently establishes a security context for the principal. During this process, a name identifier might also be established between the providers for the principal, subject to the parameters of the interaction and the consent of the principal. SAML2 Profile for ECP (Section 4.2) defines these steps for an ECP transaction: 1. ECP issues HTTP Request to SP 2. SP issues to ECP using PAOS 3. ECP determines IdP 4. ECP conveys to IdP using SOAP 5. IdP identifies principal 6. IdP issues to ECP, targeted at SP using SOAP 7. ECP conveys to SP using PAOS 8. SP grants or denies access to principal mod_auth_mellon and ECP ----------------------- mod_auth_mellon plays the role of the SP in an ECP transaction. mod_auth_mellon utilizes the Lasso library to provide it's SAML2 functionality. Fully functioning SAML2 ECP support in Lasso is relatively new. When mod_auth_mellon is built it detects the presence of SAML2 ECP in Lasso and only compiles in the ECP code in mod_auth_mellon if it's present in Lasso. How does mod_auth_mellon recognize a request is from an ECP client? ``````````````````````````````````````````````````````````````````` In Step 1. when the ECP client issues the HTTP Request to the SP it **MUST** include `application/vnd.paos+xml` as a mime type in the HTTP `Accept` header field and include an HTTP `PAOS` header specifying a PAOS version of `urn:liberty:paos:2003-08` and an ECP service declaration of `urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp` [2]_, for example:: Accept: text/html, application/vnd.paos+xml PAOS: ver="urn:liberty:paos:2003-08";"urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" If mod_auth_mellon sees this in the incoming request it knows the client is ECP aware and capable. If authentication is required mod_auth_mellon will initiate an ECP flow. The role of IdP's in ECP ```````````````````````` The SAML2 ECP profile states it is the ECP client which determines the IdP that will be used for authentication. This is in contrast to the Web SSO flow where the SP determines the IdP. However, the ECP protocol permits an SP to send the ECP client a list of IdP's it trusts. It is optional if the SP sends an IDPList, if it does the ECP client should select the IdP from the SP provided IDPList otherwise the ECP client is free to select any IdP it wishes. If the mellon configuration option `MellonECPSendIDPList` is true then mod_auth_mellon will include an IDPList when it returns a PAOS to the ECP client. To build the IDPList mod_auth_mellon scans it's list of loaded IdP's selecting those which are ECP capable. To support ECP an IdP must advertise the SingleSignOn service utilizing the SOAP binding. ECP specific mod_auth_mellon configuration directives ````````````````````````````````````````````````````` These configuration directives are specific to ECP: MellonECPSendIDPList If `On` mod_auth_mellon will send an IdP list to the ECP client containing only those IdP's capable of ECP flow. The ECP client should select an IdP only from this list. If this option is `Off` no IdP list will be sent and the ECP client is free to select any IdP. Example ECP client `````````````````` To illustrate a simple ECP client based on Lasso we'll use the Lasso Python binding (as opposed to pseudo code, Python is quite readable). All error checking and another necessary ancillary code has been eliminated in order to clearly illustrate only the ECP operations. .. code-block:: python import lasso import requests ecp = lasso.Ecp(server) session = requests.Session() MEDIA_TYPE_PAOS = 'application/vnd.paos+xml' PAOS_HEADER = 'ver="%s";"%s"' % (lasso.PAOS_HREF,lasso.ECP_HREF) # Step 1: Request protected resource, indicate ECP capable response = session.get(protected, headers={'Accept': MEDIA_TYPE_PAOS, 'PAOS': PAOS_HEADER}) # Process returned PAOS wrapped ecp.processAuthnRequestMsg(response.text) # Post SOAP wrapped to IdP, use Digest Auth to authenticate response = session.post(ecp.msgUrl, data=ecp.msgBody, auth=requests.auth.HTTPDigestAuth(user, password) headers={'Content-Type': 'text/xml'}) # Process returned SOAP wrapped from IdP ecp.processResponseMsg(response.text) # Post PASO wrapped to SP, response is protected resource response = session.post(ecp.msgUrl, data=ecp.msgBody, headers={'Content-Type': 'application/vnd.paos+xml'}) mod_auth_mellon internal ECP implementation notes ------------------------------------------------- Notes on ECP vs. Web SSO flow ````````````````````````````` Web SSO (Single Sign-On) flow is by far the most common and what most people are familiar with when they think of SAML. The Web SSO profile is designed so that browsers ignorant of SAML can perform SAML authentication without modification. This is accomplished with existing HTTP paradigms such as redirects, form posts, etc. which a browser will process normally yielding the desired result. ECP (Enhanced Client or Proxy) is a different SAML profile that also accomplishes SSO (Single Sign-On). The distinction is an ECP client is fully SAML aware and actively participates in the SAML conversation. Web SSO and ECP have very different flows, mod_auth_mellon must support both flows. mod_auth_mellon is a SP (Service Provider). IdP Selection Differences ````````````````````````` With Web SSO the SP determines the IdP and redirects there. With ECP the ECP client determines the IdP, the SP has no a prori knowledge of the target IdP, although the SP may provide a suggested list of IdP's when responding to the ECP client. Since with ECP it is the ECP client which selects the IdP the set of IdP's loaded into mod_auth_mellon are not relevant **except** if `MellonECPSendIDPList` is enabled. In this case mod_auth_mellon will filter the set of loaded IdP's and forward those IdP's supporting SingleSignOn with the SOAP binding. Apache request processing pipeline `````````````````````````````````` Apache implements a request processing pipeline composed of stages. An Apache extension module can participate in the pipeline by asking to be called at specific stages (steps) by registering a hook function for that stage. Final content returned to the HTTP client in the HTTP response is generated in the "handler", one of the final stages in the request processing pipeline. One of the stages in the request pipeline is determining authentication and authorization for protected resources. If a resource is protected and the authentication and authorization pipeline stages deny access or fail the request processing pipeline is aborted early, a non-success HTTP response is returned, the content handler is never reached. With Web SSO if authentication needs to be performed a redirect will be returned that redirects to a SAML endpoint (login) on our SP. This in turn generates the SAML with a redirect to the IdP. All of this is very vanilla standard HTTP easily accommodated by Apache's request processing pipeline which is designed to handle these types of flows. ECP requires special handling ````````````````````````````` However ECP has a very different flow. When an ECP client sends a request to the SP it includes a special HTTP headers indicating it is ECP capable. If the SP determines the resource is protected and authentication is needed and the client has signaled it is ECP capable then the SP responds successfully (200) with a SAML wrapped in PAOS. *This is very different than conventional HTTP request processing.* Here we have a case where there is a protected resource that has **not** been authenticated yet the web server will responds with an HTTP 200 success and content! One might normally expect a HTTP 401 or redirect response for a protected resource when there is no authenticated user. *This is clearly contrary to the expectations of Apache's request processing pipeline.* Reaching the Apache content handler ``````````````````````````````````` In order to be able to return a successful (HTTP 200) PAOS response when doing the ECP we have to reach the part of Apache's request processing pipeline that generates the response. In Apache terminology this is called a (content) handler. At an early stage we detect if authentication is required. For the normal Web SSO profile we would redirect the client back to our login endpoint which will be handled by our handler in a different request. But for ECP the current request must proceed. We set a flag on the request indicating ECP authentication is required. The pipeline continues. When the pipeline reaches the authentication and authorization stages we check the ECP flag on the request, if ECP authentication is indicated we lie and tell the pipeline the user is authenticated and authorized. We do this only so we can reach the handler stage (otherwise because the request is for a protected resource the pipeline would terminate with an error). Despite our having forced authentication and authorization to be valid for the protected resource the request processing pipeline *will not return the protected resource* because we will subsequently intercept the request in our handler before the pipeline reaches the point of returning the protected resource. At the handler stage ```````````````````` Once our handler is invoked it has 3 possible actions to perform: 1. The request is for one of our SAML endpoints (e.g. login, logout, metadata, etc.) We dispatch to the handler for the specific action. We detect this case by matching the request URI to our SAML endpoints. We signal to the pipeline that our hook handled the request. 2. The request is for a protected resource and needs ECP authentication performed. We detect this case by examining the ECP flag set on the request by an earlier hook function. The request URI is for the protected resource and has nothing to do with our SAML endpoints. We generate the PAOS and respond with success (200) and signal to the pipeline that our hook handled the request. Note, we have not returned the protected resource, instead we've returned the PAOS request. 3. The request has nothing to do with us, we decline to handle it. The pipeline proceeds to the next handler. .. [1] The means by which a principal authenticates with an identity provider is outside of the scope of SAML. Typically an ECP client will utilize an HTTP authentication method when posting the SOAP message to the IdP. .. [2] Contrary to most HTTP headers the values in the PAOS header must be enclosed in double quotes. A semicolon is used to separate the values. mod_auth_mellon-0.18.0/Makefile.in000066400000000000000000000037531410105036600170170ustar00rootroot00000000000000# Source files. mod_auth_mellon.c must be the first file. SRC=mod_auth_mellon.c \ auth_mellon_cache.c \ auth_mellon_config.c \ auth_mellon_cookie.c \ auth_mellon_diagnostics.c \ auth_mellon_handler.c \ auth_mellon_util.c \ auth_mellon_session.c \ auth_mellon_httpclient.c # Documentation files USER_GUIDE_FILES=\ doc/user_guide/mellon_user_guide.adoc \ doc/user_guide/Guardfile \ doc/user_guide/README \ doc/user_guide/images/chrome_SAML_Chrome_Panel.png \ doc/user_guide/images/chrome_SAML_Chrome_Panel.svg \ doc/user_guide/images/saml-tracer.png \ doc/user_guide/images/saml-tracer.svg \ doc/user_guide/images/saml-web-sso.svg # Files to include when making a .tar.gz-file for distribution DISTFILES=$(SRC) \ auth_mellon.h \ auth_mellon_compat.h \ lasso_compat.h \ config.h.in \ configure \ configure.ac \ Makefile.in \ autogen.sh \ README.md \ ECP.rst \ COPYING \ NEWS \ mellon_create_metadata.sh \ doc/mellon_create_metadata.8 \ $(USER_GUIDE_FILES) all: mod_auth_mellon.la mod_auth_mellon.la: $(SRC) auth_mellon.h auth_mellon_compat.h @APXS2@ -Wc,"-std=c99 @MELLON_CFLAGS@ @OPENSSL_CFLAGS@ @LASSO_CFLAGS@ @CURL_CFLAGS@ @GLIB_CFLAGS@ @CFLAGS@ @LIBXML2_CFLAGS@ @XMLSEC_CFLAGS@" -Wl,"@OPENSSL_LIBS@ @LASSO_LIBS@ @CURL_LIBS@ @GLIB_LIBS@ @LIBXML2_LIBS@ @XMLSEC_LIBS@" -Wc,-Wall -Wc,-g -c $(SRC) # Building configure (for distribution) configure: configure.ac ./autogen.sh @NAMEVER@.tar.gz: $(DISTFILES) tar -c --transform="s#^#@NAMEVER@/#" -vzf $@ $(DISTFILES) .PHONY: install install: mod_auth_mellon.la @APXS2@ -i -n auth_mellon mod_auth_mellon.la .PHONY: distfile distfile: @NAMEVER@.tar.gz .PHONY: clean clean: rm -f mod_auth_mellon.la rm -f $(SRC:%.c=%.o) rm -f $(SRC:%.c=%.lo) rm -f $(SRC:%.c=%.slo) rm -rf .libs/ .PHONY: distclean distclean: clean rm -f Makefile config.log config.status @NAMEVER@.tar.gz *~ \ build-stamp config.guess config.sub rm -rf debian/mod-auth-mellon rm -f debian/files .PHONY: fullclean fullclean: distclean rm -f configure aclocal.m4 mod_auth_mellon-0.18.0/NEWS000066400000000000000000000524371410105036600154540ustar00rootroot00000000000000Version 0.18.0 --------------------------------------------------------------------------- Security fixes: * [CVE-2019-13038] Redirect URL validation bypass Version 0.17.0 and older of mod_auth_mellon allows the redirect URL validation to be bypassed by specifying an URL formatted as "///fishing-site.example.com/logout.html". In this case, the browser would interpret the URL differently than the APR parsing utility mellon uses and redirect to fishing-site.example.com. This could be reproduced with: https://rp.example.co.jp/mellon/logout?ReturnTo=///fishing-site.example.com/logout.html This version fixes that issue by rejecting all URLs that start with "///". Enhancements: * A new option MellonSessionIdleTimeout that represents the amount of time a user can be inactive before the user's session times out in seconds. Bug fixes: * Several build-time fixes * The CookieTest SameSite attribute was only set to None if mellon configure option MellonCookieSameSite was set to something other than default. This is now fixed. Version 0.17.0 --------------------------------------------------------------------------- Enhancements: * New option MellonSendExpectHeader (default On) which allows to disable sending the Expect header in the HTTP-Artifact binding to improve performance when the remote party does not support this header. * Set SameSite attribute to None on on the cookietest cookie. * Bump default generated keysize to 3072 bits in mellon_create_metadata. Bug fixes: * Validate if the assertion ID has not been used earlier before creating a new session. * Release session cache after calling invalidate endpoint. * In MellonCond directives, fix a bug that setting the NC option would also activate substring match and that REG would activate REF. * Fix MellonCond substring match to actually match the substring on the attribute value. Version 0.16.0 --------------------------------------------------------------------------- Enhancements: * The MellonCookieSameSite option accepts a new valid "None". This is intended to be used together with "MellonSecureCookie On". With some newer browsers, only cookies with "SameSite=None; Secure" would be available for cross-site access. * A new option MellonEnabledInvalidateSessionEndpoint was added. When this option is enabled, then a user can invalidate their session locally by calling the "/invalidate" endpoint. Version 0.15.0 --------------------------------------------------------------------------- Security fixes: * [CVE-2019-13038] Redirect URL validation bypass Version 0.14.1 and older of mod_auth_mellon allows the redirect URL validation to be bypassed by specifying an URL formatted as "http:www.hostname.com". In this case, the APR parsing utility would parse the scheme as http, host as NULL and path as www.hostname.com. Browsers, however, interpret the URL differently and redirect to www.hostname.com. This could be reproduced with: https://application.com/mellon/login?ReturnTo=http:www.hostname.com This version fixes that issue by rejecting all URLs with scheme, but no host name. Enhancements: * A XSLT script that allows converting attribute maps from Shibboleth to a set of MellonSetEnvNoPrefix entries was added. The script can be found at doc/mellon-attribute-map.xsl * A new configuration option MellonEnvPrefix was added. This option allows you to configure the variable prefix, which normally defaults to MELLON_ * A new configuration option MellonAuthnContextComparisonType was added. This option allows you to set the "Comparison" attribute within the AuthnRequest Notable bug fixes: * Compilation issues on Solaris were fixed Version 0.14.2 --------------------------------------------------------------------------- Security fixes: * [CVE-2019-3878] Authentication bypass when Apache is used as a reverse proxy If Apache is configured as a reverse proxy with mod_auth_mellon for authentication, the authentication can be bypassed by adding SAML 2.0 ECP headers to the request. This vulnerability affects mod_auth_mellon 0.11.0 and newer. This vulnerability is due to both mod_auth_mellon and mod_proxy registering as handlers for the requests, with the same priority. When mod_auth_mellon handles the request first, it will trigger a ECP authentication request. If mod_proxy handles it first, it will forward it to the backend server. Which module handles it first depends on the order modules are loaded by Apache. This vulnerability is fixes by specifically registering that the mod_auth_mellon handler should run before mod_proxy. Thanks to Jakub Hrozek and John Dennis at RedHat for fixing this vulnerability. * [CVE-2019-3877] Redirect URL validation bypass Version 0.14.1 and older of mod_auth_mellon allows the redirect URL validation to be bypassed by specifying an URL with backslashes instead of forward slashes. Browsers silently convert backslashes to forward slashes, which allows an attacker to bypass the redirect URL validation by using `%5c` in the ReturnTo-parameter. E.g.: https://sp.example.org/mellon/logout?ReturnTo=https:%5c%5cmalicious.example.org/ This version fixes that issue by rejecting all URLs with backslashes. Thanks to Eric Chamberland for discovering this vulnerability. Version 0.14.1 --------------------------------------------------------------------------- Bug fixes: * Fix environment variables in MellonCond * Fix detection of AJAX requests * Fix trailing semi-colon in Set-Cookie header Version 0.14.0 --------------------------------------------------------------------------- Backwards incompatible changes: This version switches the default signature algorithm used when signing messages from rsa-sha1 to rsa-sha256. If your IdP does not allow messages to be signed with that algorithm, you need to add a setting switching back to the old algorithm: MellonSignatureMethod rsa-sha1 Note that this only affects messages sent from mod_auth_mellon to your IdP. It does not affect authentication responses or other messages sent from your IdP to mod_auth_mellon. New features: * Many improvements in what is logged during various errors. * Diagnostics logging, which creates a detailed log during request processing. * Add support for selecting which signature algorithm is used when signing messages, and switch to rsa-sha256 by default. Bug fixes: * Fix segmentation fault in POST replay functionality on empty value. * Fix incorrect error check for many `lasso_*`-functions. * Fix case sensitive match on MellonUser attribute name. Version 0.13.1 --------------------------------------------------------------------------- Security fix: Fix a cross-site session transfer vulnerability. mod_auth_mellon version 0.13.0 and older failed to validate that the session specified in the user's session cookie was created for the web site the user actually accesses. If two different web sites are hosted on the same web server, and both web sites use mod_auth_mellon for authentication, this vulnerability makes it possible for an attacker with access to one of the web sites to copy their session cookie to the other web site, and then use the same session to get access to the other web site. Thanks to François Kooman for reporting this vulnerability. This vulnerability has been assigned CVE-2017-6807. Note: The fix for this vunlerability makes mod_auth_mellon validate that the cookie parameters used when creating the session match the cookie parameters that should be used when accessing the current page. If you currently use mod_auth_mellon across multiple subdomains, you must make sure that you set the `MellonCookie`-option to the same value on all domains. Bug fixes: * Fix segmentation fault if a (trusted) identity provider returns a SAML 2.0 attribute without a Name. * Fix segmentation fault if MellonPostReplay is enabled but MellonPostDirectory is not set. Version 0.13.0 --------------------------------------------------------------------------- Security fix: Fix a denial of service attack in the logout handler, which allows a remote attacker to crash the Apache worker process with a segmentation fault. This is caused by a null-pointer dereference when processing a malformed logout message. New features: * Allow MellonSecureCookie to be configured to enable just one of the "httponly" of "secure" flags, instead of always enabling both flags. * Support per-module log level with Apache 2.4. * Allow disabling the Cache-Control HTTP response header. * Add support for SameSite cookie parameter. Bug fixes: * Fix MellonProbeDiscoveryIdP redirecting to the wrong IdP if no IdPs respond to the probe request. * Fix mod_auth_mellon interfering with other Apache authentication modules even when it is disabled for a path. * Fix wrong HTTP status code being returned in some cases during user permission checks. * Fix default POST size limit to actually be 1 MB. * Fix error if authentication response is missing the optional Conditions-element. * Fix AJAX requests being redirected to the IdP. * Fix wrong content type for ECP authentication request responses. In addition there are various fixes for errors in the documentation, as well as internal code changes that do not have any user visible effects. Version 0.12.0 --------------------------------------------------------------------------- Security fixes: * [CVE-2016-2145] Fix DOS attack (Apache worker process crash) due to incorrect error handling when reading POST data from client. * [CVE-2016-2146] Fix DOS attack (Apache worker process crash / resource exhaustion) due to missing size checks when reading POST data. In addition this release contains the following new features and fixes: * Add MellonRedirectDomains option to limit the sites that mod_auth_mellon can redirect to. This option is enabled by default. * Add support for ECP service options in PAOS requests. * Fix AssertionConsumerService lookup for PAOS requests. Version 0.11.1 --------------------------------------------------------------------------- Security fixes: * [CVE-2016-2145] Fix DOS attack (Apache worker process crash) due to incorrect error handling when reading POST data from client. * [CVE-2016-2146] Fix DOS attack (Apache worker process crash / resource exhaustion) due to missing size checks when reading POST data. Version 0.11.0 --------------------------------------------------------------------------- * Add SAML 2.0 ECP support. * The MellonDecode option has been disabled. It was used to decode attributes in a Feide-specific encoding that is no longer used. * Set max-age=0 in Cache-Control header, to ensure that all browsers verifies the data on each request. * MellonMergeEnvVars On now accepts second optional parameter, the separator to be used instead of the default ';'. * Add option MellonEnvVarsSetCount to specify if the number of values for any attribute should also be stored in environment variable suffixed _N. * Add option MellonEnvVarsIndexStart to specify if environment variables for multi-valued attributes should start indexing with 0 (default) or with 1. * Bugfixes: * Fix error about missing authentication with DirectoryIndex in Apache 2.4. Version 0.10.0 --------------------------------------------------------------------------- * Make sure that we fail in the unlikely case where OpenSSL is not able to provide us with a secure session id. * Increase the number of key-value pairs in the session to 2048. * Add MellonMergeEnvVars-option to store multi-valued attributes in a single environment variable, separated with ';'. * Bugfixes: * Fix the [MAP] option for MellonCond. * Fix cookie deletion for the session cookie. (Logout is not dependent on the cookie being deleted, so this only fixes the cookie showing up after the session is deleted.) Version 0.9.1 --------------------------------------------------------------------------- * Bugfixes: * Fix session offset calculation that prevented us from having active sessions at once. * Run mod_auth_mellon request handler before most other handlers, so that other handlers cannot block it by accident. Version 0.9.0 --------------------------------------------------------------------------- * Set the AssertionConsumerServiceURL attribute in authentication requests. * Bugfixes: * Fix use of uninitialized data during logout. * Fix session entry overflow leading to segmentation faults. * Fix looking up sessions by NameID, which is used during logout. Version 0.8.1 --------------------------------------------------------------------------- This is a security release with fixes backported from version 0.9.1. It turned out that session overflow bugs fixes in version 0.9.0 and 0.9.1 can lead to information disclosure, where data from one session is leaked to another session. Depending on how this data is used by the web application, this may lead to data from one session being disclosed to an user in a different session. (CVE-2014-8566) In addition to the information disclosure, this release contains some fixes for logout processing, where logout requests would crash the Apache web server. (CVE-2014-8567) Version 0.8.0 --------------------------------------------------------------------------- * Add support for receiving HTTP-Artifact identifiers as POST data. * Simplify caching headers. * Map login errors into more appropriate HTTP error codes than 400 Bad Request. * Add MellonNoSuccessErrorPage option to redirect to a error page on login failure. * Turn session storage into a dynamic pool of memory, which means that attribute values (and other items) can have arbitrary sizes as long as they fit in the session as a whole. * Various bugfixes: * Fix for compatibility with recent versions of CURL. * Fix broken option MellonDoNotVerifyLogoutSignature. * Fix deadlock that could occur during logout processing. * Fix some compile warnings. * Fix some NULL derefernce bugs that may lead to segmentation faults. * Fix a minor memory leak during IdP metadata loading. Version 0.7.0 --------------------------------------------------------------------------- * Add MellonSPentityId to control entityId in autogenerated metadata * Fix compatibility with Apache 2.4. * Handle empty RelayState the same as missing RelayState. * Add MellonSetEvnNoPrefix directive to set environment variables without "MELLON_"-prefix. Version 0.6.1 --------------------------------------------------------------------------- * Fix the POST replay functionality when multiple users logging in at once. * Add a fallback for the case where the POST replay data has expired before the user logs in. Version 0.6.0 --------------------------------------------------------------------------- Backwards-incompatible changes: * The POST replay functionality has been disabled by default, and the automatic creation of the MellonPostDirectory target directory has been removed. If you want to use the POST replay functionality, take a look at the README file for instructions for how to enable this. * Start discovery service when accessing the login endpoint. We used to bypass the discovery service in this case, and just pick the first IdP. This has been changed to send a request to the discovery service instead, if one is configured. * The MellonLockFile default path has been changed to: /var/run/mod_auth_mellon.lock This only affects platforms where a lock file is required and where Apache doesn't have write access to that directory during startup. (Apache can normally create files in that directory during startup.) Other changes: * Fix support for SOAP logout. * Local logout when IdP does not support SAML 2.0 Single Logout. * MellonDoNotVerifyLogoutSignature option to disable logout signature validation. * Support for relative file paths in configuration. * The debian build-directory has been removed from the repository. * Various cleanups and bugfixes: * Fix cookie parsing header parsing for some HTTP libraries. * Fix inheritance of MellonAuthnContextClassRef option. * Use ap_set_content_type() instead of accessing request->content_type. * README indentation cleanups. * Support for even older versions of GLib. * Fixes for error handling during session initialization. * Directly link with GLib rather than relying on the Lasso library linking to it for us. * Some code cleanups. Version 0.5.0 --------------------------------------------------------------------------- * Honour MellonProbeDiscoveryIdP order when sending probes. * MellonAuthnContextClassRef configuration directive, to limit authentication to specific authentication methods. * Support for the HTTP-POST binding when sending authentication requests to the IdP. * MellonSubjectConfirmationDataAddressCheck option to disable received address checking. * Various cleanups and bugfixes: * Support for older versions of GLib and APR. * Send the correct SP entityID to the discovery service. * Do not set response headers twice. * Several cleanups in the code that starts authentication. Version 0.4.0 --------------------------------------------------------------------------- * Allow MellonUser variable to be translated through MellonSetEnv * A /mellon/probeDisco endpoint replaces the builtin:get-metadata IdP dicovery URL scheme * New MellonCond directive to enable attribute filtering beyond MellonRequire functionalities. * New MellonIdPMetadataGlob directive to load mulitple IdP metadata using a glob(3) pattern. * Support for running behind reverse proxy. * MellonCookieDomain and MellonCookiePath options to configure cookie settings. * Support for loading federation metadata files. * Several bugfixes. Version 0.3.0 --------------------------------------------------------------------------- * New login-endpoint, which allows easier manual initiation of login requests, and specifying parameters such as IsPassive. * Validation of Conditions and SubjectConfirmation data in the assertion we receive from the IdP. * Various bugfixes. Version 0.2.7 --------------------------------------------------------------------------- * Optionaly save the remote IdP entityId in the environment * Shibboleth 2 interoperability Version 0.2.6 --------------------------------------------------------------------------- * Fix XSS/DOS vulnerability in repost handler. Version 0.2.5 --------------------------------------------------------------------------- * Replay POST requests after been sent to the IdP * Fix HTTP response splitting vulnerability. Version 0.2.4 --------------------------------------------------------------------------- * Fix for downloads of files with Internet Explorer with SSL enabled. * Mark session as disabled as soon as logout starts, in case the IdP doesn't respond. Version 0.2.3 --------------------------------------------------------------------------- * Bugfix for session lifetime. Take the session lifetime from the SessionNotOnOrAfter attribute if it is present. Version 0.2.2 --------------------------------------------------------------------------- * Improve metadata autogeneration: cleanup certificate, allow Organizarion element data to be supplied from Apache configuration Version 0.2.1 --------------------------------------------------------------------------- * Make SAML authentication assertion and Lasso session available in the environment. Version 0.2.0 --------------------------------------------------------------------------- * Autogeneration of SP metadata. (Requires Lasso 2.2.2 or newer.) * Multiple IdP support, with discovery service. * Built in discovery service which tests the availability of each IdP, and uses the first available IdP. * Fix a mutex leak. Version 0.1.1 --------------------------------------------------------------------------- * MellonSecureCookie option, which enables Secure + HttpOnly flags on session cookies. * Better handling of logout request when the user is already logged out. Version 0.1.0 --------------------------------------------------------------------------- * Better support for BSD. * Support for setting a IdP CA certificate and SP certificate. * Support for loading the private key during web server initialization. With this, the private key only needs to be readable by root. This requires a recent version of Lasso to work. * Better DOS resistance, by only allocating a session when the user has authenticated with the IdP. * Support for IdP initiated login. The MellonDefaultLoginPath option can be to configure which page the user should land on after authentication. Version 0.0.7 --------------------------------------------------------------------------- * Renamed the logout endpoint from "logoutRequest" to "logout". "logoutRequest" is now an alias for "logout", and may be removed in the future. * Added SP initiated logout. To initiate a logout from the web site, link the user to the logout endpoint, with a ReturnTo parameter with the url the user should be redirected to after being logged out. Example url: "https://www.example.com/secret/endpoint/logout ?ReturnTo=http://www.example.com/". (Note that this should be on a single line.) * Fixed a memory leak on login. * Increased maximum Lasso session size to 8192 from 3074. This allows us to handle users with more attributes. * Fixed handling of multiple AttributeValue elements in response. mod_auth_mellon-0.18.0/README.md000066400000000000000000001207401410105036600162250ustar00rootroot00000000000000# mod_auth_mellon mod_auth_mellon is an authentication module for Apache. It authenticates the user against a SAML 2.0 IdP, and grants access to directories depending on attributes received from the IdP. ## Dependencies mod_auth_mellon has four dependencies: * pkg-config * Apache (>=2.0) * OpenSSL * lasso (>=2.1) You will also require development headers and tools for all of the dependencies. If OpenSSL or lasso are installed in a "strange" directory, then you may have to specify the directory containing "lasso.pc" and/or "openssl.pc" in the PKG_CONFIG_PATH environment variable. For example, if OpenSSL is installed in /usr/local/openssl (with openssl.pc in /usr/local/openssl/lib/pkgconfig/) and lasso is installed in /opt/lasso (lasso.pc in /opt/lasso/lib/pkgconfig/), then you can set PKG_CONFIG_PATH before running configure like this: ``` PKG_CONFIG_PATH=/usr/local/openssl/lib/pkgconfig:/opt/lasso/lib/pkgconfig export PKG_CONFIG_PATH ``` If Apache is installed in a "strange" directory, then you may have to specify the path to apxs2 using the `--with-apxs2=/full/path/to/apxs2` option to configure. If, for example, Apache is installed in /opt/apache, with apxs2 in /opt/apache/bin, then you run ``` ./configure --with-apxs2=/opt/apache2/bin/apxs2 ``` Note that, depending on your distribution, apxs2 may be named apxs. ## Installing mod_auth_mellon mod_auth_mellon uses autoconf, and can be installed by running the following commands: ``` ./configure make make install ``` ## Configuring mod_auth_mellon Here we are going to assume that your web server's hostname is 'example.com', and that the directory you are going to protect is 'https://example.com/secret/'. We are also going to assume that you have configured your web site to use SSL. You need to edit the configuration file for your web server. Depending on your distribution, it may be named '/etc/apache/httpd.conf' or something different. You need to add a LoadModule directive for mod_auth_mellon. This will look similar to this: ``` LoadModule auth_mellon_module /usr/lib/apache2/modules/mod_auth_mellon.so ``` To find the full path to mod_auth_mellon.so, you may run: ``` apxs2 -q LIBEXECDIR ``` This will print the path where Apache stores modules. mod_auth_mellon.so will be stored in that directory. You will also need to make sure that Apache's authn_core module is also enabled. Most likely you also want authz_user to be enabled. After you have added the LoadModule directive, you must add configuration for mod_auth_mellon. The following is an example configuration: ```ApacheConf ########################################################################### # Global configuration for mod_auth_mellon. This configuration is shared by # every virtual server and location in this instance of apache. ########################################################################### # MellonCacheSize sets the maximum number of sessions which can be active # at once. When mod_auth_mellon reaches this limit, it will begin removing # the least recently used sessions. The server must be restarted before any # changes to this option takes effect. # Default: MellonCacheSize 100 MellonCacheSize 100 # MellonCacheEntrySize sets the maximum size for a single session entry in # bytes. When mod_auth_mellon reaches this limit, it cannot store any more # data in the session and will return an error. The minimum entry size is # 65536 bytes, values lower than that will be ignored and the minimum will # be used. # Default: MellonCacheEntrySize 196608 # MellonLockFile is the full path to a file used for synchronizing access # to the session data. The path should only be used by one instance of # apache at a time. The server must be restarted before any changes to this # option takes effect. # Default: MellonLockFile "/var/run/mod_auth_mellon.lock" MellonLockFile "/var/run/mod_auth_mellon.lock" # MellonPostDirectory is the full path of a directory where POST requests # are saved during authentication. This directory must writable by the # Apache user. It should not be writable (or readable) by other users. # Default: None # Example: MellonPostDirectory "/var/cache/mod_auth_mellon_postdata" # MellonPostTTL is the delay in seconds before a saved POST request can # be flushed. # Default: MellonPostTTL 900 (15 mn) MellonPostTTL 900 # MellonPostSize is the maximum size for saved POST requests # Default: MellonPostSize 1048576 (1 MB) MellonPostSize 1048576 # MellonPostCount is the maximum amount of saved POST requests # Default: MellonPostCount 100 MellonPostCount 100 # MellonDiagnosticsFile If Mellon was built with diagnostic capability # then diagnostic is written here, it may be either a filename or a pipe. # If it's a filename then the resulting path is relative to the ServerRoot. # If the value is preceeded by the pipe character "|" it should be followed # by a path to a program to receive the log information on its standard input. # This is a server context directive, hence it may be specified in the # main server config area or within a directive. # Default: logs/mellon_diagnostics MellonDiagnosticsFile logs/mellon_diagnostics # MellonDiagnosticsEnable If Mellon was built with diagnostic capability # then this is a list of words controlling diagnostic output. # Currently only On and Off are supported. # This is a server context directive, hence it may be specified in the # main server config area or within a directive. # Default: Off MellonDiagnosticsEnable Off ########################################################################### # End of global configuration for mod_auth_mellon. ########################################################################### # This defines a directory where mod_auth_mellon should do access control. # These are standard Apache apache configuration directives. # You must have enabled Apache's authn_core and authz_user modules. # See http://httpd.apache.org/docs/2.4/mod/mod_authn_core.html and # http://httpd.apache.org/docs/2.4/mod/mod_authz_user.html # about them. Require valid-user AuthType "Mellon" # MellonEnable is used to enable auth_mellon on a location. # It has three possible values: "off", "info" and "auth". # They have the following meanings: # "off": mod_auth_mellon will not do anything in this location. # This is the default state. # "info": If the user is authorized to access the resource, then # we will populate the environment with information about # the user. If the user isn't authorized, then we won't # populate the environment, but we won't deny the user # access either. # # You can also use this to set up the Mellon SSO paramaters # transparently at the top level of your site, and then use # "auth" to protect individual paths elsewhere in the site. # "auth": We will populate the environment with information about # the user if he is authorized. If he is authenticated # (logged in), but not authorized (according to the # MellonRequire and MellonCond directives, then we will # return a 403 Forbidden error. If he isn't authenticated # then we will redirect him to the login page of the IdP. # # There is a special handling of AJAX requests, that are # identified by the "X-Requested-With: XMLHttpRequest" HTTP # header. Since no user interaction can happen there, # we always fail unauthenticated (not logged in) requests # with a 403 Forbidden error without redirecting to the IdP. # # Default: MellonEnable "off" MellonEnable "auth" # MellonDecoder is an obsolete option which is a no-op but is # still accepted for backwards compatibility. # MellonVariable is used to select the name of the cookie which # mod_auth_mellon should use to remember the session id. If you # want to have different sites running on the same host, then # you will have to choose a different name for the cookie for each # site. # Default: "cookie" MellonVariable "cookie" # Whether the cookie set by auth_mellon should have HttpOnly and # secure flags set. Once "On" - both flags will be set. Values # "httponly" or "secure" will respectively set only one flag. # Default: Off MellonSecureCookie On # MellonCookieDomain allows to specify of the cookie which auth_mellon # will set. # Default: the domain for the received request (the Host: header if # present, of the ServerName of the VirtualHost declaration, or if # absent a reverse resolution on the local IP) # MellonCookieDomain example.com # MellonCookiePath is the path of the cookie which auth_mellon will set. # Default: / MellonCookiePath / # MellonCookieSameSite allows control over the SameSite value used # for the authentication cookie. # The setting accepts values of "Strict", "Lax", or "None". # When using none, you should set "MellonSecureCookie On" to prevent # compatibility issues with newer browsers. # If not set, the SameSite attribute is not set on the cookie. In newer # browsers, this may cause SameSite to default to "Lax". # Note: Regardless of the value set here a fixed SameSite value of # None is used for the cookie test. The cookie test, which is performed # with a static value, should detect whether the user's client accepts # cookies or not before the auth_mellon session is established and thus # avoid a redirect loop. Using a fixed SameSite value of None ensures # that the cookie with the static value does not get lost in the # HTTP-POST binding request issued by the autosubmit form returned by # the IDP. # Default: not set # MellonCookieSameSite lax # Some browsers will reject cookies if SameSite is specified. # The MELLON_DISABLE_SAMESITE environment variable suppresses # setting of SameSite cookies. You can use the following directives # to set it. # BrowserMatch "\(iP.+; CPU .*OS 12[_\d]*.*\) AppleWebKit\/" MELLON_DISABLE_SAMESITE=1 # BrowserMatch "\(Macintosh; Intel Mac OS X 10_14_\d\) AppleWebKit\/[\.\d]+ \(KHTML, like Gecko\)$" MELLON_DISABLE_SAMESITE=1 # BrowserMatch "Outlook-iOS" MELLON_DISABLE_SAMESITE=1 # BrowserMatch "UCBrowser\/(8|9|10|11)\.(\d+)\.(\d+)[\.\d]* " MELLON_DISABLE_SAMESITE=1 # BrowserMatch "UCBrowser\/12\.13\.[0-1][\.\d]* " MELLON_DISABLE_SAMESITE=1 # BrowserMatch "UCBrowser\/12\.1[0-2]\.(\d+)[\.\d]* " MELLON_DISABLE_SAMESITE=1 # BrowserMatch "UCBrowser\/12\.\d\.(\d+)[\.\d]* " MELLON_DISABLE_SAMESITE=1 # BrowserMatch "Chrom[^ \/]+\/6[0-6][\.\d]* " MELLON_DISABLE_SAMESITE=1 # BrowserMatch "Chrom[^ \/]+\/5[1-9][\.\d]* " MELLON_DISABLE_SAMESITE=1 # MellonUser selects which attribute we should use for the username. # The username is passed on to other apache modules and to the web # page the user visits. NAME_ID is an attribute which we set to # the id we get from the IdP. # Note: If MellonUser refers to a multi-valued attribute, any single # value from that attribute may be used. Do not rely on it selecting a # specific value. # Default: MellonUser "NAME_ID" MellonUser "NAME_ID" # MellonIdP selects in which attribute we should dump the remote # IdP entityId. This is passed to other apache modules and to # the web pages the user visits. # Default: none # MellonIdP "IDP" # MellonSetEnv configuration directives allows you to map # attribute names received from the IdP to names you choose # yourself. The syntax is 'MellonSetEnv '. # You can list multiple MellonSetEnv directives. # Default. None set. MellonSetEnv "e-mail" "mail" # MellonSetEnvNoPrefix is identical to MellonSetEnv, except this # does not prepend 'MELLON_' to the constructed environment variable. # The syntax is 'MellonSetEnvNoPrefix '. # You can list multiple MellonSetEnvNoPrefix directives. # Default. None set. MellonSetEnvNoPrefix "DISPLAY_NAME" "displayName" # MellonEnvPrefix changes the string the variables passed from the # IdP are prefixed with. # Default: MELLON_ MellonEnvPrefix "NOLLEM_" # MellonMergeEnvVars merges multiple values of environment variables # set using MellonSetEnv into single variable: # ie: MYENV_VAR => val1;val2;val3 instead of default behaviour of: # MYENV_VAR_0 => val1, MYENV_VAR_1 => val2 ... etc. # Second optional parameter specifies the separator, to override the # default semicolon. # Default: MellonMergeEnvVars Off MellonMergeEnvVars On MellonMergeEnvVars On ":" # MellonEnvVarsIndexStart specifies if environment variables for # multi-valued attributes should start indexing from 0 or 1 # The syntax is 'MellonEnvVarsIndexStart <0|1>'. # Default: MellonEnvVarsIndexStart 0 MellonEnvVarsIndexStart 1 # MellonEnvVarsSetCount specifies if number of values for any given # attribute should also be stored in variable suffixed _N. # ie: if user is member of two groups, the following will be set: # MELLON__groups=group1 # MELLON__groups_0=group1 # MELLON__groups_1=group2 # MELLON__groups_N=2 # Default: MellonEnvVarsSetCount Off MellonEnvVarsSetCount On # MellonRequire allows you to limit access to those with specific # attributes. The syntax is # 'MellonRequire '. # Note that the attribute name is the name we received from the # IdP. # # If you don't list any MellonRequire directives (and any # MellonCond directives, see below), then any user authenticated # by the IdP will have access to this service. If you list several # MellonRequire directives, then all of them will have to match. # If you use multiple MellonRequire directive on the same # attribute, the last overrides the previous ones. # # Default: None set. MellonRequire "eduPersonAffiliation" "student" "employee" # MellonCond provides the same function as MellonRequire, with # extra functionality (MellonRequire is retained for backward # compatibility). The syntax is # 'MellonCond []' # # is an attribute value to match. Unlike with MellonRequire, # multiples values are not allowed. # # If the [REG] flag is specified (see below), is a regular # expression. The syntax for backslash escape is the same as in # Apache's 's directives. # # Format strings are substituted into prior evaluation. # Here are the supported syntaxes: # %n With n being a digit between 0 and 9. If [REG,REF] # flags (see below) were used in an earlier matching # MellonCond, then regular expression back references # are substituted. # %{num} Same as %n, with num being a number that may be # greater than 9. # %{ENV:x} Substitute Apache environment variable x. # %% Escape substitution to get a literal %. # # is an optional, comma-separated list of option # enclosed with brackets. Here is an example: [NOT,NC] # The valid options are: # OR If this MellonCond evaluated to false, then the # next one will be checked. If it evaluates to true, # then the overall check succeeds. # NOT This MellonCond evaluates to true if the attribute # does not match the value. # SUB Substring match, evaluates to true if value is # included in attribute. # REG Value to check is a regular expression. # NC Perform case insensitive match. # MAP Attempt to search an attribute with name remapped by # MellonSetEnv. Fallback to non remapped name if not # found. # REF Used with REG, track regular expression back references, # So that they can be substituted in an upcoming # MellonCond directive. # # It is allowed to have multiple MellonCond on the same # attribute, and to mix MellonCond and MellonRequire. # Note that this can create tricky situations, since the OR # option has effect on a following MellonRequire directive. # # Default: none set # MellonCond "mail" "@example\.net$" [OR,REG] # MellonCond "mail" "@example\.com$" [OR,REG] # MellonCond "uid" "superuser" # MellonEndpointPath selects which directory mod_auth_mellon # should assume contains the SAML 2.0 endpoints. Any request to # this directory will be handled by mod_auth_mellon. # # The path is the full path (from the root of the web server) to # the directory. The directory must be a sub-directory of this # . # Default: MellonEndpointPath "/mellon" MellonEndpointPath "/secret/endpoint" # MellonDefaultLoginPath is the location where one should be # redirected after an IdP-initiated login. Default is "/" # Default: MellonDefaultLoginPath "/" MellonDefaultLoginPath "/" # MellonSessionLength sets the maximum lifetime of a session, in # seconds. The actual lifetime may be shorter, depending on the # conditions received from the IdP. The default length is 86400 # seconds, which is one day. # Default: MellonSessionLength 86400 MellonSessionLength 86400 # MellonSessionIdleTimeout represents the amount of time a user # can be inactive before the user's session times out in seconds. # This parameter is disabled by default to maintain backward # compatibility. # Default: MellonSessionIdleTimeout -1 (disabled) MellonSessionIdleTimeout -1 # MellonNoCookieErrorPage is the full path to a page which # mod_auth_mellon will redirect the user to if he returns from the # IdP without a cookie with a session id. # Note that the user may also get this error if he for some reason # loses the cookie between being redirected to the IdP's login page # and returning from it. # If this option is unset, then mod_auth_mellon will return a # 400 Bad Request error if the cookie is missing. # Default: unset MellonNoCookieErrorPage "https://example.com/no_cookie.html" # MellonSPMetadataFile is the full path to the file containing # the metadata for this service provider. # If mod_auth_mellon was compiled against Lasso version 2.2.2 # or higher, this option is optional. Otherwise, it is mandatory. # Default: None set. MellonSPMetadataFile /etc/apache2/mellon/sp-metadata.xml # If you choose to autogenerate metadata, this option # can be used to control the SP entityId # MellonSPentityId "https://www.example.net/foo" # # If you choose to autogenerate metadata, these options # can be used to fill the element. They # all follow the syntax "option [lang] value": # MellonOrganizationName "random-service" # MellonOrganizationDisplayName "en" "Random service" # MellonOrganizationDisplayName "fr" "Service quelconque" # MellonOrganizationURL "http://www.espci.fr" # MellonSPPrivateKeyFile is a .pem file which contains the private # key of the service provider. The .pem-file cannot be encrypted # with a password. If built with lasso-2.2.2 or higher, the # private key only needs to be readable by root, otherwise it has # to be readable by the Apache pseudo user. # Default: None set. MellonSPPrivateKeyFile /etc/apache2/mellon/sp-private-key.pem # MellonSPCertFile is a .pem file with the certificate for the # service provider. This directive is optional. # Default: None set. MellonSPCertFile /etc/apache2/mellon/sp-cert.pem # MellonIdPMetadataFile is the full path to the file which contains # metadata for the IdP you are authenticating against. This # directive is required. Multiple IdP metadata can be configured # by using multiple MellonIdPMetadataFile directives. # # If your lasso library is recent enough (higher than 2.3.5), # then MellonIdPMetadataFile will accept an XML file containing # descriptors for multiple IdP. An optional validating chain can # be supplied as a second argument to MellonIdPMetadataFile. If # omitted, no metadata validation will take place. # # Default: None set. MellonIdPMetadataFile /etc/apache2/mellon/idp-metadata.xml # MellonIdPMetadataGlob is a glob(3) pattern enabled alternative # to MellonIdPMetadataFile. Like MellonIdPMetadataFile it will # accept an optional validating chain if lasso is recent enough. # # Default: None set. #MellonIdPMetadataGlob /etc/apache2/mellon/*-metadata.xml # MellonIdpPublicKeyFile is the full path of the public key of the # IdP. This parameter is optional if the public key is embedded # in the IdP's metadata file, or if a certificate authority is # used. This parameter cannot be used if multiple IdP are configured. # Default: None set. MellonIdPPublicKeyFile /etc/apache2/mellon/idp-public-key.pem # MellonIdPCAFile is the full path to the certificate of the # certificate authority. This can be used instead of an # certificate for the IdP. # Default: None set. MellonIdPCAFile /etc/apache2/mellon/ca.pem # MellonIdPIgnore lists IdP entityId that should not loaded # from XML federation metadata files. This is useful if an # IdP cause bugs. Multiple entityId may be specified through # single MellonIdPIgnore, and multiple MellonIdPIgnore are allowed. # Default: None set. #MellonIdPIgnore "https://bug.example.net/saml/idp" # MellonDiscoveryURL is the URL for IdP discovery service. # This is used for selecting among multiple configured IdP. # On initial user authentication, it is redirected to the # IdP discovery URL, with the following arguments set: # # entityID SP entityId URL, where our metadata # are published. # returnIDParam Argument that IdP discovery must send back. # return Return URL the IdP discovery should return to. # # The IdP discovery must redirect the user to the return URL, # with returnIDParam set to the selected IdP entityId. # # See also MellonProbeDiscovery options below for an alternative # way to deal with discovery. # # Default: None set. MellonDiscoveryURL "http://www.example.net/idp-discovery" # MellonProbeDiscoveryTimeout sets the timeout of the # IdP probe discovery service, which is available on the # probeDisco endpoint. # # This will cause the SP to send HTTP GET requests on the # configured IdP PorviderID URL. This URL should be used to # publish metadata, though this is not mandatory. If the IdP # returns an HTTP status 200, then the IdP is selected. # If the ProviderID URL requires SSL, MellonIdPCAFile is used # as a trusted CA bundle. # # Default: unset, which means the feature is disabled # MellonProbeDiscoveryTimeout 1 # MellonProbeDiscoveryIdP can be used to restrict the # list of IdPs queried by the IdP probe discovery service. # If probe discovery fails and this list is provided, an # HTTP error 500 is returned, instead of proceeding # with first available IdP. # # Default unset, which means that all configured IdPs are # queried. # MellonProbeDiscoveryIdP http://idp1.example.com/saml/metadata # MellonProbeDiscoveryIdP http://idp2.example.net/saml/metadata # This option will make the SAML authentication assertion # available in the MELLON_SAML_RESPONSE environment # variable. This assertion holds a verifiable signature # that can be checked again. Default is Off. MellonSamlResponseDump Off # This option will make the Lasso session available in # the MELLON_SESSION environment variable. Default is Off. MellonSessionDump Off # This option will request specific authentication security-level # through the AuthnContextClassRef element of the AuthnRequest It will # also request enforcement of this level when receiving an # authenticating Assertion. # If the assertion does not have the required security level, an HTTP # Forbidden status code is returned to the browser. # MellonAuthnContextClassRef "urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos" # MellonAuthnContextClassRef "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" # MellonAuthnContextClassRef "urn:oasis:names:tc:SAML:2.0:ac:classes:SoftwarePKI" # This option will set the "Comparsion" attribute within the AuthnRequest # It could be set to "exact", "minimum", "maximum" or "better" # MellonAuthnContextComparisonType "minimum" # MellonSubjectConfirmationDataAddressCheck is used to control # the checking of client IP address against the address returned by the # IdP in Address attribute of the SubjectConfirmationData node. Can be useful if your SP is # behind a reverse proxy or any kind of strange network topology making IP address of client # different for the IdP and the SP. Default is on. # MellonSubjectConfirmationDataAddressCheck On # Does not check signature on logout messages exchanges with idp1 # MellonDoNotVerifyLogoutSignature http://idp1.example.com/saml/metadata # Whether to enable replay of POST requests after authentication. When this option is # enabled, POST requests that trigger authentication will be saved until the # authentication is completed, and then replayed. If this option isn't enabled, # the requests will be turned into normal GET requests after authentication. # # Note that if this option is enabled, you must also # set the MellonPostDirectory option in the server configuration. # # The default is that it is "Off". # MellonPostReplay Off # Page to redirect to if the IdP sends an error in response to # the authentication request. # # Example: # MellonNoSuccessErrorPage https://sp.example.org/login_failed.html # # The default is to not redirect, but rather send a # 401 Unautorized error. # This option controls whether to include a list of IDP's when # sending an ECP PAOS message to an ECP client. MellonECPSendIDPList Off # This option controls whether the Cache-control header is sent # back in responses. # Default: On # MellonSendCacheControlHeader Off # List of domains that we allow redirects to. # The special name "[self]" means the domain of the current request. # The domain names can also use wildcards. # # Example: # * Allow redirects to example.com and all subdomains: # MellonRedirectDomains example.com *.example.com # * Allow redirects to the host running mod_auth_mellon, as well as the # web page at www.example.com: # MellonRedirectDomains [self] www.example.com # * Allow redirects to all domains: # MellonRedirectDomains * # # Default: # MellonRedirectDomains [self] MellonRedirectDomains [self] # This option controls the signature method used to sign SAML # messages generated by Mellon, it may be one of the following # (depending if feature was supported when Mellon was built): # # rsa-sha1 # rsa-sha256 # rsa-sha384 # rsa-sha512 # # Default: rsa-sha256 # MellonSignatureMethod ``` ## Service provider metadata The contents of the metadata will depend on your hostname and on what path you selected with the MellonEndpointPath configuration directive. You can supply the metadata using the MellonSPMetadataFile directive, or you can just let it be autogenerated. The following is an example of metadata for the example configuration: ```xml urn:oasis:names:tc:SAML:2.0:nameid-format:transient ``` You should update `entityID="https://example.com"` and the two Location attributes. Note that '/secret/endpoint' in the two Location attributes matches the path set in MellonEndpointPath. To use the HTTP-Artifact binding instead of the HTTP-POST binding, change the AssertionConsumerService-element to something like this: ```xml ``` The metadata are published at the 'endpoint/metadata' URL. ## Using mod_auth_mellon After you have set up mod_auth_mellon, you should be able to visit (in our example) https://example.com/secret/, and be redirected to the IdP's login page. After logging in you should be returned to https://example.com/secret/, and get the contents of that page. When authenticating a user, mod_auth_mellon will set some environment variables to the attributes it received from the IdP. The name of the variables will be `MELLON_`. If you have specified a different name with the MellonSetEnv or MellonSetEnvNoPrefix configuration directive, then that name will be used instead. In the case of MellonSetEnv, the name will still be prefixed by `MELLON_`. If an attribute has multiple values, then they will be stored as `MELLON__0`, `MELLON__1`, `MELLON__2`, ... Since mod_auth_mellon doesn't know which attributes may have multiple values, it will store every attribute at least twice: once named `MELLON_`, and once named `_0`. In the case of multivalued attributes, `MELLON_` will contain the first value. NOTE: If MellonMergeEnvVars is set to On, multiple values of attributes will be stored in single environment variable, separated by semicolons: ``` MELLON_="value1;value2;value3[;valueX]" ``` and variables `MELLON__0`, `MELLON__1`, `MELLON__2`, ... will not be created. The following code is a simple PHP script which prints out all the variables: ```php $value) { if(substr($key, 0, 7) == 'MELLON_') { echo($key . '=' . $value . "\r\n"); } } ?> ``` ## Manual login It is possible to manually trigger login operations. This can be done by accessing "/login". That endpoint accepts three parameters: - `ReturnTo`: A mandatory parameter which contains the URL we should return to after login. - `IdP`: The entity ID of the IdP we should send a login request to. This parameter is optional. - `IsPassive`: This parameter can be set to "true" to send a passive authentication request to the IdP. ## Logging out mod_auth_mellon supports both IdP-initiated and SP-initiated logout through the same endpoint. The endpoint is located at "/logout". "/logoutRequest" is an alias for this endpoint, provided for compatibility with version 0.0.6 and earlier of mod_auth_mellon. To initiate a logout from your web site, you should redirect or link to "/logout?ReturnTo=". Note that the ReturnTo parameter is mandatory. For example, if the web site is located at "https://www.example.com/secret", and the mellon endpoints are located under "https://www.example.com/secret/endpoint", then the web site could contain a link element like the following: ```html Log out ``` This will return the user to "https://www.example.org/logged_out.html" after the logout operation has completed. ## Invalidating session It is possible to invalidate the current mod_auth_mellon session, without calling SLO. The mod_auth_mellon cookie session will be invalidated and the session will be removed from the mod_auth_mellon cache. SLO will not be possible after the mod_auth_mellon session is invalidated. If this functionality is enabled, invalidate the session by calling the endpoint "/invalidate?ReturnTo=". The "ReturnTo" parameter is required. Here is a sample configuration to enable this feature: ```ApacheConf MellonEnabledInvalidateSessionEndpoint On ``` Default value is Off ## Send Expect Header The Expect Header saves an additional network round-trip and is thus a good idea when the request isn't extremely large and the probability for rejection is low. For some Apache server versions, the Expect Header is not properly managed and a curl command will wait for 1 second before sending the body of the request. If the Expect Header is not present, there won't be wait time in the HTTP-Artifact binding. Here is a sample configuration to not send the Expect header: ```ApacheConf MellonSendExpectHeader Off ``` Default value is On ## Probe IdP discovery mod_auth_mellon has an IdP probe discovery service that sends HTTP GET to IdP and picks the first that answers. This can be used as a poor man's failover setup that redirects to your organisation internal IdP. Here is a sample configuration: ```ApacheConf MellonEndpointPath "/saml" (...) MellonDiscoveryUrl "/saml/probeDisco" MellonProbeDiscoveryTimeout 1 ``` The SP will send an HTTP GET to each configured IdP entityId URL until it gets an HTTP 200 response within the 1 second timeout. It will then proceed with that IdP. If you are in a federation, then your IdP login page will need to provide an IdP selection feature aimed at users from other institutions (after such a choice, the user would be redirected to the SP's /saml/login endpoint, with ReturnTo and IdP set appropriately). In such a setup, you will want to configure external IdP in mod_auth_mellon, but not use them for IdP probe discovery. The MellonProbeDiscoveryIdP directive can be used to limit the usable IdP for probe discovery: ```ApacheConf MellonEndpointPath "/saml" (...) MellonDiscoveryUrl "/saml/probeDisco" MellonProbeDiscoveryTimeout 1 MellonProbeDiscoveryIdP "https://idp1.example.net/saml/metadata" MellonProbeDiscoveryIdP "https://idp2.example.net/saml/metadata" ``` ## Replaying POST requests By default, POST requests received when the user isn't logged in are turned into GET requests after authentication. mod_auth_mellon can instead save the received POST request and replay/repost it after authentication. To enable this: 1. Create a data directory where mod_auth_mellon can store the saved data: mkdir /var/cache/mod_auth_mellon_postdata 2. Set the appropriate permissions on this directory. It needs to be accessible for the web server, but nobody else. chown www-data /var/cache/mod_auth_mellon_postdata chgrp www-data /var/cache/mod_auth_mellon_postdata chmod 0700 /var/cache/mod_auth_mellon_postdata 3. Set the MellonPostDirectory option in your server configuration: MellonPostDirectory "/var/cache/mod_auth_mellon_postdata" 4. Enable POST replay functionality for the locations you want: MellonEnable auth [...] MellonPostReplay On After you restart Apache to activate the new configuration, any POST requests that trigger authentication should now be stored while the user logs in. ## Example to support both SAML and Basic Auth The below snippet will allow for preemptive basic auth (such as from a REST client) for the "/auth" path, but if accessed interactively will trigger SAML auth with mod_auth_mellon. ```ApacheConf MellonEnable "info" MellonVariable "cookie" MellonEndpointPath "/sso" Mellon... # Other parameters as needed AuthName "My Auth" AuthBasicProvider ldap AuthType basic AuthLDAP* # Other basic auth config parms as needed require ldap-group .... Require valid-user AuthType "Mellon" MellonEnable "auth" ``` ## Mellon & User Agent Caching behavior For each content within an Apache Location enabled with "info" or "auth", mod_auth_mellon sends by default the HTTP/1.1 Cache-Control header with value `private, must-revalidate`: - `private` protects content against caching by any proxy servers. - `must-revalidate` obligates the user agent to revalidate maybe locally cached or stored content each time on accessing location. This default behavior ensures that the user agent never shows cached static HTML pages after logout without revalidating. So the user can't be misled about a malfunction of the logout procedure. Revalidating content after logout leads to a new authentication procedure via mellon. But mod_auth_mellon will never prohibit specifically any user agent from caching or storing content locally, that has to be revalidated. So that during the session, a user agent only revalidates data by the server `304 Not Modified` response, and does not have to download content again. For special content types like images it could make sense to disable revalidation completely, so that user agents can provide cached and stored content directly to the user. This can be achieved by using other Apache modules mod_headers and mod_setenvif. E.g. for PNG images: * Using Apache 2.2 configuration options: SetEnvIf Request_URI "\.png$" DISABLE_REVALIDATION Header always unset Cache-Control env=DISABLE_REVALIDATION * In Apache 2.4, there's a shorter notation: Header always unset Cache-Control expr=%{CONTENT_TYPE}==image/png Editing, appending, and overwriting headers is possible in other cases. ## Reporting security vulnerabilities For reporting security vulnerabilities in mod_auth_mellon, please contact the maintainers directly at the following email address: jhrozek@redhat.com simo@redhat.com This allows us to coordinate the disclosure of the vulnerability with the fixes for the vulnerability. ## Contributors Thanks to [Emmanuel Dreyfus](mailto:manu@netbsd.org) for many new features, including: - Metadata autogeneration support. - Support for multiple IdPs. - IdP discovery service support. - SOAP logout support. [Benjamin Dauvergne](mailto:bdauvergne@entrouvert.com) has contributed many patches, both with bugfixes and new features: - Cookie settings, for specifying domain and path of cookie. - Support for SAML 2.0 authentication contexts. - Support for running behind a reverse proxy. - Logout improvements, including support for local logout. mod_auth_mellon-0.18.0/auth_mellon.h000066400000000000000000000535231410105036600174320ustar00rootroot00000000000000/* * * auth_mellon.h: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #ifndef MOD_AUTH_MELLON_H #define MOD_AUTH_MELLON_H #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "lasso_compat.h" /* The following are redefined in ap_config_auto.h */ #undef PACKAGE_BUGREPORT #undef PACKAGE_NAME #undef PACKAGE_STRING #undef PACKAGE_TARNAME #undef PACKAGE_VERSION #undef HAVE_TIMEGM /* is redefined again in ap_config.h */ #include "apr_base64.h" #include "apr_time.h" #include "apr_strings.h" #include "apr_shm.h" #include "apr_md5.h" #include "apr_file_info.h" #include "apr_file_io.h" #include "apr_xml.h" #include "apr_lib.h" #include "apr_fnmatch.h" #include "ap_config.h" #include "httpd.h" #include "http_config.h" #include "http_core.h" #include "http_log.h" #include "http_protocol.h" #include "http_request.h" #include "mod_ssl.h" /* Backwards-compatibility helpers. */ #include "auth_mellon_compat.h" /* Size definitions for the session cache. */ #define AM_CACHE_KEYSIZE 120 #define AM_CACHE_ENVSIZE 2048 #define AM_CACHE_USERSIZE 512 #define AM_CACHE_DEFAULT_ENTRY_SIZE 196608 #define AM_CACHE_MIN_ENTRY_SIZE 65536 /* Internal error codes */ #define AM_ERROR_INVALID_PAOS_HEADER 1 #define AM_ERROR_MISSING_PAOS_HEADER 2 #define AM_ERROR_MISSING_PAOS_MEDIA_TYPE 3 #ifdef ENABLE_DIAGNOSTICS typedef enum { AM_DIAG_FLAG_ENABLED = (1 << 0), AM_DIAG_FLAG_DISABLE = 0, AM_DIAG_FLAG_ENABLE_ALL = ~0, } am_diag_flags_t; #endif /* Disable SameSite Environment Value */ #define AM_DISABLE_SAMESITE_ENV_VAR "MELLON_DISABLE_SAMESITE" /* Force setting SameSite to None */ #define AM_FORCE_SAMESITE_NONE_NOTE "MELLON_FORCE_SAMESITE_NONE" /* This is the length of the id we use (for session IDs and * replaying POST data). */ #define AM_ID_LENGTH 32 #define MEDIA_TYPE_PAOS "application/vnd.paos+xml" #define am_get_srv_cfg(s) (am_srv_cfg_rec *)ap_get_module_config((s)->module_config, &auth_mellon_module) #define am_get_mod_cfg(s) (am_get_srv_cfg((s)))->mc #define am_get_dir_cfg(r) (am_dir_cfg_rec *)ap_get_module_config((r)->per_dir_config, &auth_mellon_module) #define am_get_req_cfg(r) (am_req_cfg_rec *)ap_get_module_config((r)->request_config, &auth_mellon_module) #ifdef ENABLE_DIAGNOSTICS #define am_get_diag_cfg(s) (&(am_get_srv_cfg((s)))->diag_cfg) #endif typedef struct am_mod_cfg_rec { int cache_size; const char *lock_file; const char *post_dir; apr_time_t post_ttl; int post_count; apr_size_t post_size; int entry_size; /* These variables can't be allowed to change after the session store * has been initialized. Therefore we copy them before initializing * the session store. */ int init_cache_size; const char *init_lock_file; apr_size_t init_entry_size; apr_shm_t *cache; apr_global_mutex_t *lock; } am_mod_cfg_rec; #ifdef ENABLE_DIAGNOSTICS typedef struct am_diag_cfg_rec { const char *filename; apr_file_t *fd; am_diag_flags_t flags; apr_table_t *dir_cfg_emitted; } am_diag_cfg_rec; #endif typedef struct am_srv_cfg_rec { am_mod_cfg_rec *mc; #ifdef ENABLE_DIAGNOSTICS am_diag_cfg_rec diag_cfg; #endif } am_srv_cfg_rec; typedef enum { am_enable_default, am_enable_off, am_enable_info, am_enable_auth } am_enable_t; typedef enum { am_samesite_default, am_samesite_lax, am_samesite_strict, am_samesite_none, } am_samesite_t; typedef enum { AM_COND_FLAG_NULL = 0x000, /* No flags */ AM_COND_FLAG_OR = 0x001, /* Or with next condition */ AM_COND_FLAG_NOT = 0x002, /* Negate this condition */ AM_COND_FLAG_REG = 0x004, /* Condition is regex */ AM_COND_FLAG_NC = 0x008, /* Case insensitive match */ AM_COND_FLAG_MAP = 0x010, /* Try to use attribute name from MellonSetEnv */ AM_COND_FLAG_REF = 0x020, /* Set regex backreferences */ AM_COND_FLAG_SUB = 0x040, /* Substring match */ /* The other options are internally used */ AM_COND_FLAG_IGN = 0x1000, /* Condition is to be ignored */ AM_COND_FLAG_REQ = 0x2000, /* Condition was set using MellonRequire */ AM_COND_FLAG_FSTR = 0x4000, /* Value contains a format string */ } am_cond_flag_t; extern const char *am_cond_options[]; /* * am_file_data_t is used to maintain information about a file: * * * The filesystem pathname * * Stat information about the file (e.g. type, size, times, etc.) * * If and when the file was stat'ed or read * * Error code of failed operation and error string description * * Contents of the file * * Flag indicating if contents were generated instead of being read * from a file. */ typedef struct am_file_data_t { apr_pool_t *pool; /* allocation pool */ const char *path; /* filesystem pathname, NULL for generated file */ apr_time_t stat_time; /* when stat was performed, zero indicates never */ apr_finfo_t finfo; /* stat data */ char *contents; /* file contents */ apr_time_t read_time; /* when contents was read, zero indicates never */ apr_status_t rv; /* most recent result value */ const char *strerror; /* if rv is error then this is error description */ bool generated; /* true if contents generated instead of being read from path */ } am_file_data_t; typedef struct { const char *varname; int flags; const char *str; ap_regex_t *regex; const char *directive; } am_cond_t; typedef struct am_metadata { am_file_data_t *metadata; /* Metadata file with one or many IdP */ am_file_data_t *chain; /* Validating chain */ } am_metadata_t; typedef struct am_dir_cfg_rec { /* enable_mellon is used to enable auth_mellon for a location. */ am_enable_t enable_mellon; const char *varname; int secure; int http_only; const char *merge_env_vars; int env_vars_index_start; int env_vars_count_in_n; const char *cookie_domain; const char *cookie_path; am_samesite_t cookie_samesite; apr_array_header_t *cond; apr_hash_t *envattr; const char *env_prefix; const char *userattr; const char *idpattr; LassoSignatureMethod signature_method; int dump_session; int dump_saml_response; /* The "root directory" of our SAML2 endpoints. This path is relative * to the root of the web server. * * This path will always end with '/'. */ const char *endpoint_path; /* Lasso configuration variables. */ am_file_data_t *sp_metadata_file; am_file_data_t *sp_private_key_file; am_file_data_t *sp_cert_file; apr_array_header_t *idp_metadata; am_file_data_t *idp_public_key_file; am_file_data_t *idp_ca_file; GList *idp_ignore; /* metadata autogeneration helper */ char *sp_entity_id; apr_hash_t *sp_org_name; apr_hash_t *sp_org_display_name; apr_hash_t *sp_org_url; /* Maximum number of seconds a session is valid for. */ int session_length; /* Maximum number of seconds a session idle timeout is valid for. */ int session_idle_timeout; /* No cookie error page. */ const char *no_cookie_error_page; /* Authorization error page. */ const char *no_success_error_page; /* Login path for IdP initiated logins */ const char *login_path; /* IdP discovery service */ const char *discovery_url; int probe_discovery_timeout; apr_table_t *probe_discovery_idp; /* The configuration record we "inherit" the lasso server object from. */ struct am_dir_cfg_rec *inherit_server_from; /* Mutex to prevent us from creating several lasso server objects. */ apr_thread_mutex_t *server_mutex; /* AuthnContextClassRef list */ apr_array_header_t *authn_context_class_ref; /* AuthnContextComparisonType */ const char *authn_context_comparison_type; /* Controls the checking of SubjectConfirmationData.Address attribute */ int subject_confirmation_data_address_check; /* MellonDoNotVerifyLogoutSignature idp set */ apr_hash_t *do_not_verify_logout_signature; /* Controls whether the cache control header is set */ int send_cache_control_header; /* Whether we should replay POST data after authentication. */ int post_replay; /* Cached lasso server object. */ LassoServer *server; /* Whether to send an ECP client a list of IdP's */ int ecp_send_idplist; /* List of domains we can redirect to. */ const char * const *redirect_domains; /* Enabled the session invalidate endpoint. */ int enabled_invalidation_session; /* Send Expect Header. */ int send_expect_header; } am_dir_cfg_rec; /* Bitmask for PAOS service options */ typedef enum { ECP_SERVICE_OPTION_CHANNEL_BINDING = 1, ECP_SERVICE_OPTION_HOLDER_OF_KEY = 2, ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED = 4, ECP_SERVICE_OPTION_DELEGATION = 8, } ECPServiceOptions; typedef struct am_req_cfg_rec { char *cookie_value; #ifdef HAVE_ECP bool ecp_authn_req; ECPServiceOptions ecp_service_options; #endif /* HAVE_ECP */ #ifdef ENABLE_DIAGNOSTICS bool diag_emitted; #endif } am_req_cfg_rec; typedef struct am_cache_storage_t { apr_size_t ptr; } am_cache_storage_t; typedef struct am_cache_env_t { am_cache_storage_t varname; am_cache_storage_t value; } am_cache_env_t; typedef struct am_cache_entry_t { char key[AM_CACHE_KEYSIZE]; am_cache_storage_t cookie_token; apr_time_t access; apr_time_t expires; apr_time_t idle_timeout; int logged_in; unsigned short size; am_cache_storage_t user; /* Variables used to store lasso state between login requests *and logout requests. */ am_cache_storage_t lasso_identity; am_cache_storage_t lasso_session; am_cache_storage_t lasso_saml_response; am_cache_env_t env[AM_CACHE_ENVSIZE]; apr_size_t pool_size; apr_size_t pool_used; char pool[]; } am_cache_entry_t; typedef enum { AM_CACHE_SESSION, AM_CACHE_NAMEID, AM_CACHE_ASSERTIONID } am_cache_key_t; /* Type for configuring environment variable names */ typedef struct am_envattr_conf_t { // Name of the variable const char *name; // Should a prefix be added int prefixed; } am_envattr_conf_t; extern const command_rec auth_mellon_commands[]; typedef struct am_error_map_t { int lasso_error; int http_error; } am_error_map_t; extern const am_error_map_t auth_mellon_errormap[]; /* When using a value from a directory configuration structure, a special value is used * to state "inherit" from parent, when reading a value and the value is still inherit from, it * means that no value has ever been set for this directive, in this case, we use the default * value. * * This macro expects that if your variable is called "name" there is a static const variable named * "default_name" which holds the default value for this variable. */ #define CFG_VALUE(container, name) \ (container->name == inherit_##name ? default_##name : container->name) #define CFG_MERGE(add_cfg, base_cfg, name) \ (add_cfg->name == inherit_##name ? base_cfg->name : add_cfg->name) /** Default and inherit value for SubjectConfirmationData Address check setting. */ static const int default_subject_confirmation_data_address_check = 1; static const int inherit_subject_confirmation_data_address_check = -1; /** Default values for seting the cache-control header */ static const int default_send_cache_control_header = 1; static const int inherit_send_cache_control_header = -1; /* Default and inherit values for MellonPostReplay option. */ static const int default_post_replay = 0; static const int inherit_post_replay = -1; /* Whether to send an ECP client a list of IdP's */ static const int default_ecp_send_idplist = 0; static const int inherit_ecp_send_idplist = -1; /* Algorithm to use when signing Mellon SAML messages */ static const LassoSignatureMethod default_signature_method = #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA256 LASSO_SIGNATURE_METHOD_RSA_SHA256; #else LASSO_SIGNATURE_METHOD_RSA_SHA1; #endif static const int inherit_signature_method = -1; void *auth_mellon_dir_config(apr_pool_t *p, char *d); void *auth_mellon_dir_merge(apr_pool_t *p, void *base, void *add); void *auth_mellon_server_config(apr_pool_t *p, server_rec *s); void *auth_mellon_srv_merge(apr_pool_t *p, void *base, void *add); const char *am_cookie_get(request_rec *r); void am_cookie_set(request_rec *r, const char *id); void am_cookie_delete(request_rec *r); const char *am_cookie_token(request_rec *r); void am_cache_init(am_mod_cfg_rec *mod_cfg); am_cache_entry_t *am_cache_lock(request_rec *r, am_cache_key_t type, const char *key); const char *am_cache_entry_get_string(am_cache_entry_t *e, am_cache_storage_t *slot); am_cache_entry_t *am_cache_new(request_rec *r, const char *key, const char *cookie_token); void am_cache_unlock(request_rec *r, am_cache_entry_t *entry); void am_cache_update_expires(request_rec *r, am_cache_entry_t *t, apr_time_t expires); void am_cache_update_idle_timeout(request_rec *r, am_cache_entry_t *t, int session_idle_timeout); void am_cache_env_populate(request_rec *r, am_cache_entry_t *session); int am_cache_env_append(am_cache_entry_t *session, const char *var, const char *val); const char *am_cache_env_fetch_first(am_cache_entry_t *t, const char *var); void am_cache_delete(request_rec *r, am_cache_entry_t *session); int am_cache_set_lasso_state(am_cache_entry_t *session, const char *lasso_identity, const char *lasso_session, const char *lasso_saml_response); const char *am_cache_get_lasso_identity(am_cache_entry_t *session); const char *am_cache_get_lasso_session(am_cache_entry_t *session); am_cache_entry_t *am_get_request_session(request_rec *r); am_cache_entry_t *am_get_request_session_by_nameid(request_rec *r, char *nameid); am_cache_entry_t *am_get_request_session_by_assertionid(request_rec *r, char *assertionid); am_cache_entry_t *am_new_request_session(request_rec *r); void am_release_request_session(request_rec *r, am_cache_entry_t *session); void am_delete_request_session(request_rec *r, am_cache_entry_t *session); char *am_reconstruct_url(request_rec *r); int am_validate_redirect_url(request_rec *r, const char *url); int am_check_permissions(request_rec *r, am_cache_entry_t *session); void am_set_cache_control_headers(request_rec *r); int am_read_post_data(request_rec *r, char **data, apr_size_t *length); char *am_extract_query_parameter(apr_pool_t *pool, const char *query_string, const char *name); char *am_urlencode(apr_pool_t *pool, const char *str); int am_urldecode(char *data); int am_check_url(request_rec *r, const char *url); char *am_generate_id(request_rec *r); am_file_data_t *am_file_data_new(apr_pool_t *pool, const char *path); am_file_data_t *am_file_data_copy(apr_pool_t *pool, am_file_data_t *src_file_data); apr_status_t am_file_read(am_file_data_t *file_data); apr_status_t am_file_stat(am_file_data_t *file_data); char *am_get_endpoint_url(request_rec *r); int am_postdir_cleanup(request_rec *s); char *am_htmlencode(request_rec *r, const char *str); int am_save_post(request_rec *r, const char **relay_state); const char *am_filepath_dirname(apr_pool_t *p, const char *path); const char *am_strip_cr(request_rec *r, const char *str); const char *am_add_cr(request_rec *r, const char *str); const char *am_xstrtok(request_rec *r, const char *str, const char *sep, char **last); void am_strip_blank(const char **s); const char *am_get_header_attr(request_rec *r, const char *h, const char *v, const char *a); int am_has_header(request_rec *r, const char *h, const char *v); const char *am_get_mime_header(request_rec *r, const char *m, const char *h); const char *am_get_mime_body(request_rec *r, const char *mime); char *am_get_service_url(request_rec *r, LassoProfile *profile, char *service_name); bool am_parse_paos_header(request_rec *r, const char *header, ECPServiceOptions *options_return); bool am_header_has_media_type(request_rec *r, const char *header, const char *media_type); const char *am_get_config_langstring(apr_hash_t *h, const char *lang); int am_get_boolean_query_parameter(request_rec *r, const char *name, int *return_value, int default_value); char *am_get_assertion_consumer_service_by_binding(LassoProvider *provider, const char *binding); #ifdef HAVE_ECP char *am_ecp_service_options_str(apr_pool_t *pool, ECPServiceOptions options); bool am_is_paos_request(request_rec *r, int *error_code); #endif /* HAVE_ECP */ char * am_saml_response_status_str(request_rec *r, LassoNode *node); int am_auth_mellon_user(request_rec *r); int am_check_uid(request_rec *r); int am_handler(request_rec *r); int am_httpclient_get(request_rec *r, const char *uri, void **buffer, apr_size_t *size, int timeout, long *status); int am_httpclient_post(request_rec *r, const char *uri, const void *post_data, apr_size_t post_length, const char *content_type, void **buffer, apr_size_t *size); int am_httpclient_post_str(request_rec *r, const char *uri, const char *post_data, const char *content_type, void **buffer, apr_size_t *size); extern module AP_MODULE_DECLARE_DATA auth_mellon_module; #ifdef ENABLE_DIAGNOSTICS #if AP_SERVER_MAJORVERSION_NUMBER < 2 || \ (AP_SERVER_MAJORVERSION_NUMBER == 2 && AP_SERVER_MINORVERSION_NUMBER < 4) #error "Diagnostics requires Apache version 2.4 or newer." #endif /* Initializing an apr_time_t to 0x7fffffffffffffffLL yields an * iso 8601 time with 1 second precision of "294247-01-10T04:00:54Z" * this is 22 characters, +1 for null terminator. */ #define ISO_8601_BUF_SIZE 23 typedef struct { bool req_headers_written; } am_diag_request_data; const char * am_diag_cache_key_type_str(am_cache_key_t key_type); const char * am_diag_cond_str(request_rec *r, const am_cond_t *cond); int am_diag_finalize_request(request_rec *r); const char * am_diag_lasso_http_method_str(LassoHttpMethod http_method); void am_diag_log_cache_entry(request_rec *r, int level, am_cache_entry_t *entry, const char *fmt, ...) __attribute__((format(printf,4,5))); void am_diag_log_file_data(request_rec *r, int level, am_file_data_t *file_data, const char *fmt, ...) __attribute__((format(printf,4,5))); int am_diag_log_init(apr_pool_t *pc, apr_pool_t *p, apr_pool_t *pt, server_rec *s); void am_diag_log_lasso_node(request_rec *r, int level, LassoNode *node, const char *fmt, ...) __attribute__((format(printf,4,5))); void am_diag_log_saml_status_response(request_rec *r, int level, LassoNode *node, const char *fmt, ...) __attribute__((format(printf,4,5))); void am_diag_log_profile(request_rec *r, int level, LassoProfile *profile, const char *fmt, ...) __attribute__((format(printf,4,5))); void am_diag_printf(request_rec *r, const char *fmt, ...) __attribute__((format(printf,2,3))); void am_diag_rerror(const char *file, int line, int module_index, int level, apr_status_t status, request_rec *r, const char *fmt, ...); char * am_diag_time_t_to_8601(request_rec *r, apr_time_t t); /* Define AM_LOG_RERROR log to both the Apache log and diagnostics log */ #define AM_LOG_RERROR(...) AM_LOG_RERROR__(__VA_ARGS__) /* need additional step to expand macros */ #define AM_LOG_RERROR__(file, line, mi, level, status, r, ...) \ { \ ap_log_rerror(file, line, mi, level, status, r, __VA_ARGS__); \ am_diag_rerror(file, line, mi, level, status, r, __VA_ARGS__); \ } #else /* ENABLE_DIAGNOSTICS */ #define am_diag_log_cache_entry(...) do {} while(0) #define am_diag_log_file_data(...) do {} while(0) #define am_diag_log_lasso_node(...) do {} while(0) #define am_diag_log_saml_status_response(...) do {} while(0) #define am_diag_log_profile(...) do {} while(0) #define am_diag_printf(...) do {} while(0) /* Define AM_LOG_RERROR log only to the Apache log */ #define AM_LOG_RERROR(...) ap_log_rerror(__VA_ARGS__) #endif /* ENABLE_DIAGNOSTICS */ #endif /* MOD_AUTH_MELLON_H */ mod_auth_mellon-0.18.0/auth_mellon_cache.c000066400000000000000000000644341410105036600205530ustar00rootroot00000000000000/* * * auth_mellon_cache.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* Calculate the pointer to a cache entry. * * Parameters: * am_mod_cfg_rec *mod_cfg The module configuration. * void *table The base pointer for the table. * apr_size_t index The index we are looking for. * * Returns: * The session entry with the given index. */ static inline am_cache_entry_t *am_cache_entry_ptr(am_mod_cfg_rec *mod_cfg, void *table, apr_size_t index) { uint8_t *table_calc; table_calc = table; return (am_cache_entry_t *)&table_calc[mod_cfg->init_entry_size * index]; } /* Initialize the session table. * * Parameters: * am_mod_cfg_rec *mod_cfg The module configuration. * * Returns: * Nothing. */ void am_cache_init(am_mod_cfg_rec *mod_cfg) { void *table; apr_size_t i; /* Initialize the session table. */ table = apr_shm_baseaddr_get(mod_cfg->cache); for (i = 0; i < mod_cfg->init_cache_size; i++) { am_cache_entry_t *e = am_cache_entry_ptr(mod_cfg, table, i); e->key[0] = '\0'; e->access = 0; } } /* This function locks the session table and locates a session entry. * Unlocks the table and returns NULL if the entry wasn't found. * If a entry was found, then you _must_ unlock it with am_cache_unlock * after you are done with it. * * Parameters: * request_rec *r The request we are processing. * am_cache_key_t type AM_CACHE_SESSION, AM_CACHE_NAMEID or AM_CACHE_ASSERTIONID * const char *key The session key or user * * Returns: * The session entry on success or NULL on failure. */ am_cache_entry_t *am_cache_lock(request_rec *r, am_cache_key_t type, const char *key) { am_mod_cfg_rec *mod_cfg; void *table; apr_size_t i; int rv; char buffer[512]; /* Check if we have a valid session key. We abort if we don't. */ if (key == NULL) return NULL; switch (type) { case AM_CACHE_SESSION: if (strlen(key) != AM_ID_LENGTH) return NULL; break; case AM_CACHE_NAMEID: case AM_CACHE_ASSERTIONID: break; default: return NULL; break; } mod_cfg = am_get_mod_cfg(r->server); /* Lock the table. */ if((rv = apr_global_mutex_lock(mod_cfg->lock)) != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "apr_global_mutex_lock() failed [%d]: %s", rv, apr_strerror(rv, buffer, sizeof(buffer))); return NULL; } table = apr_shm_baseaddr_get(mod_cfg->cache); for(i = 0; i < mod_cfg->init_cache_size; i++) { am_cache_entry_t *e = am_cache_entry_ptr(mod_cfg, table, i); const char *tablekey; if (e->key[0] == '\0') { /* This entry is empty. Skip it. */ continue; } switch (type) { case AM_CACHE_SESSION: tablekey = e->key; break; case AM_CACHE_NAMEID: /* tablekey may be NULL */ tablekey = am_cache_env_fetch_first(e, "NAME_ID"); break; case AM_CACHE_ASSERTIONID: /* tablekey may be NULL */ tablekey = am_cache_env_fetch_first(e, "ASSERTION_ID"); break; default: tablekey = NULL; break; } if (tablekey == NULL) continue; if(strcmp(tablekey, key) == 0) { apr_time_t now = apr_time_now(); /* We found the entry. */ if ((e->expires > now) && ((e->idle_timeout == -1) || (e->idle_timeout > now))) { /* And it hasn't expired. */ return e; } else { am_diag_log_cache_entry(r, 0, e, "found expired session, now %s\n", am_diag_time_t_to_8601(r, now)); } } } /* We didn't find a entry matching the key. Unlock the table and * return NULL; */ apr_global_mutex_unlock(mod_cfg->lock); return NULL; } static inline bool am_cache_entry_slot_is_empty(am_cache_storage_t *slot) { return (slot->ptr == 0); } static inline void am_cache_storage_null(am_cache_storage_t *slot) { slot->ptr = 0; } static inline void am_cache_entry_env_null(am_cache_entry_t *e) { for (int i = 0; i < AM_CACHE_ENVSIZE; i++) { am_cache_storage_null(&e->env[i].varname); am_cache_storage_null(&e->env[i].value); } } static inline apr_size_t am_cache_entry_pool_left(am_cache_entry_t *e) { return e->pool_size - e->pool_used; } static inline apr_size_t am_cache_entry_pool_size(am_mod_cfg_rec *cfg) { return cfg->init_entry_size - sizeof(am_cache_entry_t); } /* This function sets a string into the specified storage on the entry. * * NOTE: The string pointer may be NULL, in that case storage is freed * and set to NULL. * * Parametrs: * am_cache_entry_t *entry Pointer to an entry * am_cache_storage_t *slot Pointer to storage * const char *string Pointer to a replacement string * * Returns: * 0 on success, HTTP_INTERNAL_SERVER_ERROR on error. */ static int am_cache_entry_store_string(am_cache_entry_t *entry, am_cache_storage_t *slot, const char *string) { char *datastr = NULL; apr_size_t datalen = 0; apr_size_t str_len = 0; if (string == NULL) return 0; if (slot->ptr != 0) { datastr = &entry->pool[slot->ptr]; datalen = strlen(datastr) + 1; } str_len = strlen(string) + 1; if (str_len - datalen <= 0) { memcpy(datastr, string, str_len); return 0; } /* recover space if slot happens to point to the last allocated space */ if (slot->ptr + datalen == entry->pool_used) { entry->pool_used -= datalen; slot->ptr = 0; } if (am_cache_entry_pool_left(entry) < str_len) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "apr_cache_entry_store_string() asked %" APR_SIZE_T_FMT " available: %" APR_SIZE_T_FMT ". " "It may be a good idea to increase MellonCacheEntrySize.", str_len, am_cache_entry_pool_left(entry)); return HTTP_INTERNAL_SERVER_ERROR; } slot->ptr = entry->pool_used; datastr = &entry->pool[slot->ptr]; memcpy(datastr, string, str_len); entry->pool_used += str_len; return 0; } /* Returns a pointer to the string in the storage slot specified * * * Parametrs: * am_cache_entry_t *entry Pointer to an entry * am_cache_storage_t *slot Pointer to storage slot * * Returns: * A string or NULL if the slot is empty. */ const char *am_cache_entry_get_string(am_cache_entry_t *e, am_cache_storage_t *slot) { char *ret = NULL; if (slot->ptr != 0) { ret = &e->pool[slot->ptr]; } return ret; } /* This function locks the session table and creates a new session entry. * It will first attempt to locate a free session. If it doesn't find a * free session, then it will take the least recentry used session. * * Remember to unlock the table with am_cache_unlock(...) afterwards. * * Parameters: * request_rec *r The request we are processing. * const char *key The key of the session to allocate. * const char *cookie_token The cookie token to tie the session to. * * Returns: * The new session entry on success. NULL if key is a invalid session * key. */ am_cache_entry_t *am_cache_new(request_rec *r, const char *key, const char *cookie_token) { am_cache_entry_t *t; am_mod_cfg_rec *mod_cfg; void *table; apr_time_t current_time; int i; apr_time_t age; int rv; char buffer[512]; /* Check if we have a valid session key. We abort if we don't. */ if(key == NULL || strlen(key) != AM_ID_LENGTH) { return NULL; } mod_cfg = am_get_mod_cfg(r->server); /* Lock the table. */ if((rv = apr_global_mutex_lock(mod_cfg->lock)) != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "apr_global_mutex_lock() failed [%d]: %s", rv, apr_strerror(rv, buffer, sizeof(buffer))); return NULL; } table = apr_shm_baseaddr_get(mod_cfg->cache); /* Get current time. If we find a entry with expires <= the current * time, then we can use it. */ current_time = apr_time_now(); /* We will use 't' to remember the best/oldest entry. We * initialize it to the first entry in the table to simplify the * following code (saves test for t == NULL). */ t = am_cache_entry_ptr(mod_cfg, table, 0); /* Iterate over the session table. Update 't' to match the "best" * entry (the least recently used). 't' will point a free entry * if we find one. Otherwise, 't' will point to the least recently * used entry. */ for(i = 0; i < mod_cfg->init_cache_size; i++) { am_cache_entry_t *e = am_cache_entry_ptr(mod_cfg, table, i); if (e->key[0] == '\0') { /* This entry is free. Update 't' to this entry * and exit loop. */ t = e; break; } if (e->expires <= current_time) { /* This entry is expired, and is therefore free. * Update 't' and exit loop. */ t = e; am_diag_log_cache_entry(r, 0, e, "%s ejecting expired sessions, now %s\n", __func__, am_diag_time_t_to_8601(r, current_time)); break; } if (e->access < t->access) { /* This entry is older than 't' - update 't'. */ t = e; } } if(t->key[0] != '\0' && t->expires > current_time) { /* We dropped a LRU entry. Calculate the age in seconds. */ age = (current_time - t->access) / 1000000; if(age < 3600) { AM_LOG_RERROR(APLOG_MARK, APLOG_NOTICE, 0, r, "Dropping LRU entry entry with age = %" APR_TIME_T_FMT "s, which is less than one hour. It may be a good" " idea to increase MellonCacheSize.", age); } } /* Now 't' points to the entry we are going to use. We initialize * it and returns it. */ strcpy(t->key, key); /* Far far into the future. */ t->expires = 0x7fffffffffffffffLL; t->idle_timeout = -1; t->logged_in = 0; t->size = 0; am_cache_storage_null(&t->cookie_token); am_cache_storage_null(&t->user); am_cache_storage_null(&t->lasso_identity); am_cache_storage_null(&t->lasso_session); am_cache_storage_null(&t->lasso_saml_response); am_cache_entry_env_null(t); t->pool_size = am_cache_entry_pool_size(mod_cfg); t->pool[0] = '\0'; t->pool_used = 1; rv = am_cache_entry_store_string(t, &t->cookie_token, cookie_token); if (rv != 0) { /* For some strange reason our cookie token is too big to fit in the * session. This should never happen outside of absurd configurations. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to store cookie token in new session."); t->key[0] = '\0'; /* Mark the entry as free. */ apr_global_mutex_unlock(mod_cfg->lock); return NULL; } am_diag_printf(r, "%s created new session, id=%s at %s" " cookie_token=\"%s\"\n", __func__, t->key, am_diag_time_t_to_8601(r, current_time), cookie_token); return t; } /* This function unlocks a session entry. * * Parameters: * request_rec *r The request we are processing. * am_cache_entry_t *entry The session entry. * * Returns: * Nothing. */ void am_cache_unlock(request_rec *r, am_cache_entry_t *entry) { am_mod_cfg_rec *mod_cfg; /* Update access time. */ entry->access = apr_time_now(); mod_cfg = am_get_mod_cfg(r->server); apr_global_mutex_unlock(mod_cfg->lock); } /* This function updates the expire-timestamp of a session, if the new * timestamp is earlier than the previous. * * Parameters: * request_rec *r The request we are processing. * am_cache_entry_t *t The current session. * apr_time_t expires The new timestamp. * * Returns: * Nothing. */ void am_cache_update_expires(request_rec *r, am_cache_entry_t *t, apr_time_t expires) { /* Check if we should update the expires timestamp. */ if(t->expires == 0 || t->expires > expires) { t->expires = expires; } } /* This function updates the idle timeout timestamp of a session, based on the * session idle timeout. * * Parameters: * request_rec *r The request we are processing. * am_cache_entry_t *t The current session. * int session_idle_timeout The new timestamp. * * Returns: * Nothing. */ void am_cache_update_idle_timeout(request_rec *r, am_cache_entry_t *t, int session_idle_timeout) { if(session_idle_timeout > -1) { t->idle_timeout = apr_time_now() + apr_time_make(session_idle_timeout, 0); } } /* This function appends a name-value pair to a session. It is possible to * store several values with the same name. This is the method used to store * multivalued fields. * * Parameters: * am_cache_entry_t *t The current session. * const char *var The name of the value to be stored. * const char *val The value which should be stored in the session. * * Returns: * OK on success or HTTP_INTERNAL_SERVER_ERROR on failure. */ int am_cache_env_append(am_cache_entry_t *t, const char *var, const char *val) { int status; /* Make sure that the name and value will fit inside the * fixed size buffer. */ if(t->size >= AM_CACHE_ENVSIZE) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "Unable to store attribute value because we have" " reached the maximum number of name-value pairs for" " this session. The maximum number is %d.", AM_CACHE_ENVSIZE); return HTTP_INTERNAL_SERVER_ERROR; } status = am_cache_entry_store_string(t, &t->env[t->size].varname, var); if (status != 0) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "Unable to store session data because there is no more " "space in the session. Attribute Name = \"%s\".", var); return HTTP_INTERNAL_SERVER_ERROR; } status = am_cache_entry_store_string(t, &t->env[t->size].value, val); if (status != 0) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "Unable to store session data because there is no more " "space in the session. Attribute Value = \"%s\".", val); return HTTP_INTERNAL_SERVER_ERROR; } t->size++; return OK; } /* This function fetches a value from a session. * If multiple values are available, the first one is returned. * * Parameters: * am_cache_entry_t *t The current session. * const char *var The name of the value to be stored. * * Returns: * The first value, NULL if it does not exist. */ const char *am_cache_env_fetch_first(am_cache_entry_t *t, const char *var) { const char *str; int i; for (i = 0; i < t->size; i++) { str = am_cache_entry_get_string(t, &t->env[i].varname); if (str == NULL) break; if (strcmp(str, var) == 0) return am_cache_entry_get_string(t, &t->env[i].value); } return NULL; } /* This function populates the subprocess environment with data received * from the IdP. * * Parameters: * request_rec *r The request we should add the data to. * am_cache_entry_t *t The session data. * * Returns: * Nothing. */ void am_cache_env_populate(request_rec *r, am_cache_entry_t *t) { am_dir_cfg_rec *d; int i; apr_hash_t *counters; am_envattr_conf_t *env_varname_conf; const char *varname; const char *varname_prefix; const char *value; const char *prefixed_varname; int *count; int status; d = am_get_dir_cfg(r); /* Check if the user attribute has been set, and set it if it * hasn't been set. */ if (am_cache_entry_slot_is_empty(&t->user)) { for(i = 0; i < t->size; ++i) { varname = am_cache_entry_get_string(t, &t->env[i].varname); if (strcasecmp(varname, d->userattr) == 0) { value = am_cache_entry_get_string(t, &t->env[i].value); status = am_cache_entry_store_string(t, &t->user, value); if (status != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_NOTICE, 0, r, "Unable to store the user name because there" " is no more space in the session. " "Username = \"%s\".", value); } } } } /* Allocate a set of counters for duplicate variables in the list. */ counters = apr_hash_make(r->pool); /* Populate the subprocess environment with the attributes we * received from the IdP. */ for(i = 0; i < t->size; ++i) { varname = am_cache_entry_get_string(t, &t->env[i].varname); varname_prefix = d->env_prefix; /* Check if we should map this name into another name. */ env_varname_conf = (am_envattr_conf_t *)apr_hash_get( d->envattr, varname, APR_HASH_KEY_STRING); if(env_varname_conf != NULL) { varname = env_varname_conf->name; if (!env_varname_conf->prefixed) { varname_prefix = ""; } } value = am_cache_entry_get_string(t, &t->env[i].value); /* * If we find a variable remapping to MellonUser, use it. */ if (am_cache_entry_slot_is_empty(&t->user) && (strcasecmp(varname, d->userattr) == 0)) { status = am_cache_entry_store_string(t, &t->user, value); if (status != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_NOTICE, 0, r, "Unable to store the user name because there" " is no more space in the session. " "Username = \"%s\".", value); } } prefixed_varname = apr_pstrcat(r->pool, varname_prefix, varname, NULL); /* Find the number of times this variable has been set. */ count = apr_hash_get(counters, varname, APR_HASH_KEY_STRING); if(count == NULL) { /* This is the first time. Create a counter for this variable. */ count = apr_palloc(r->pool, sizeof(int)); *count = 0; apr_hash_set(counters, varname, APR_HASH_KEY_STRING, count); /* Add the variable without a suffix. */ apr_table_set(r->subprocess_env,prefixed_varname,value); } /* Check if merging of environment variables is disabled. * This is either if it is NULL (default value if not configured * by user) or an empty string (if specifically disabled by the user). */ if (d->merge_env_vars == NULL || *d->merge_env_vars == '\0') { /* Add the variable with a suffix indicating how many times it has * been added before. */ apr_table_set(r->subprocess_env, apr_psprintf(r->pool, "%s_%d", prefixed_varname, (d->env_vars_index_start > -1 ? *count + d->env_vars_index_start : *count)), value); } else if (*count > 0) { /* * Merge multiple values, separating by default with ";" * this makes auth_mellon work same way mod_shib is: * https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAttributeAccess */ apr_table_set(r->subprocess_env, prefixed_varname, apr_pstrcat(r->pool, apr_table_get(r->subprocess_env,prefixed_varname), d->merge_env_vars, value, NULL)); } /* Increase the count. */ ++(*count); if (d->env_vars_count_in_n > 0) { apr_table_set(r->subprocess_env, apr_pstrcat(r->pool, prefixed_varname, "_N", NULL), apr_itoa(r->pool, *count)); } } if (!am_cache_entry_slot_is_empty(&t->user)) { /* We have a user-"name". Set r->user and r->ap_auth_type. */ r->user = apr_pstrdup(r->pool, am_cache_entry_get_string(t, &t->user)); r->ap_auth_type = apr_pstrdup(r->pool, "Mellon"); } else { /* We don't have a user-"name". Log error. */ AM_LOG_RERROR(APLOG_MARK, APLOG_NOTICE, 0, r, "Didn't find the attribute \"%s\" in the attributes" " which were received from the IdP. Cannot set a user" " for this request without a valid user attribute.", d->userattr); } /* Populate with the session? */ if (d->dump_session) { char *session; const char *srcstr; int srclen, dstlen; srcstr = am_cache_entry_get_string(t, &t->lasso_session); srclen = strlen(srcstr); dstlen = apr_base64_encode_len(srclen); session = apr_palloc(r->pool, dstlen); (void)apr_base64_encode(session, srcstr, srclen); apr_table_set(r->subprocess_env, "MELLON_SESSION", session); } if (d->dump_saml_response) { const char *sr = am_cache_entry_get_string(t, &t->lasso_saml_response); if (sr) { apr_table_set(r->subprocess_env, "MELLON_SAML_RESPONSE", sr); } } } /* This function deletes a given key from the session store. * * Parameters: * request_rec *r The request we are processing. * am_cache_entry_t *cache The entry we are deleting. * * Returns: * Nothing. */ void am_cache_delete(request_rec *r, am_cache_entry_t *cache) { /* We write a null-byte at the beginning of the key to * mark this slot as unused. */ cache->key[0] = '\0'; /* Unlock the entry. */ am_cache_unlock(r, cache); } /* This function stores a lasso identity dump and a lasso session dump in * the given session object. * * Parameters: * am_cache_entry_t *session The session object. * const char *lasso_identity The identity dump. * const char *lasso_session The session dump. * * Returns: * OK on success or HTTP_INTERNAL_SERVER_ERROR if the lasso state information * is to big to fit in our session. */ int am_cache_set_lasso_state(am_cache_entry_t *session, const char *lasso_identity, const char *lasso_session, const char *lasso_saml_response) { int status; status = am_cache_entry_store_string(session, &session->lasso_identity, lasso_identity); if (status != 0) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "Lasso identity is too big for storage. Size of lasso" " identity is %" APR_SIZE_T_FMT ".", (apr_size_t)strlen(lasso_identity)); return HTTP_INTERNAL_SERVER_ERROR; } status = am_cache_entry_store_string(session, &session->lasso_session, lasso_session); if (status != 0) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "Lasso session is too big for storage. Size of lasso" " session is %" APR_SIZE_T_FMT ".", (apr_size_t)strlen(lasso_session)); return HTTP_INTERNAL_SERVER_ERROR; } status = am_cache_entry_store_string(session, &session->lasso_saml_response, lasso_saml_response); if (status != 0) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "Lasso SAML response is too big for storage. Size of " "lasso SAML Response is %" APR_SIZE_T_FMT ".", (apr_size_t)strlen(lasso_saml_response)); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } /* This function retrieves a lasso identity dump from the session object. * * Parameters: * am_cache_entry_t *session The session object. * * Returns: * The identity dump, or NULL if we don't have a session dump. */ const char *am_cache_get_lasso_identity(am_cache_entry_t *session) { return am_cache_entry_get_string(session, &session->lasso_identity); } /* This function retrieves a lasso session dump from the session object. * * Parameters: * am_cache_entry_t *session The session object. * * Returns: * The session dump, or NULL if we don't have a session dump. */ const char *am_cache_get_lasso_session(am_cache_entry_t *session) { return am_cache_entry_get_string(session, &session->lasso_session); } mod_auth_mellon-0.18.0/auth_mellon_compat.h000066400000000000000000000030341410105036600207650ustar00rootroot00000000000000#ifndef AUTH_MELLON_COMPAT_H #define AUTH_MELLON_COMPAT_H #include #include "ap_config.h" #include "ap_release.h" #ifdef AP_NEED_SET_MUTEX_PERMS #include "unixd.h" #endif /* Old glib compatibility */ #if (GLIB_MAJOR_VERSION == 2) && (GLIB_MINOR_VERSION < 14) static void g_hash_table_get_keys_helper(gpointer key, gpointer value, gpointer user_data) { GList **out = user_data; *out = g_list_prepend(*out, key); } static GList *g_hash_table_get_keys(GHashTable *ht) { GList *ret = NULL; g_hash_table_foreach(ht, g_hash_table_get_keys_helper, &ret); return g_list_reverse(ret); } #endif /* "remote_ip" in struct conn_rec changed name to "client_ip" in Apache 2.4. * This function retrieves the corrent member depending on the Apache version. */ static inline const char *am_compat_request_ip(request_rec *r) { #if (AP_SERVER_MAJORVERSION_NUMBER == 2) && (AP_SERVER_MINORVERSION_NUMBER < 4) return r->connection->remote_ip; #else return r->connection->client_ip; #endif } /* unixd_set_global_mutex_perms changed name to ap_unixd_set_global_mutex_perms * in Apache 2.4. This function provides a wrapper with the new name for old * versions. */ #ifdef AP_NEED_SET_MUTEX_PERMS #if (AP_SERVER_MAJORVERSION_NUMBER == 2) && (AP_SERVER_MINORVERSION_NUMBER < 4) static inline apr_status_t ap_unixd_set_global_mutex_perms(apr_global_mutex_t *gmutex) { return unixd_set_global_mutex_perms(gmutex); } #endif #endif /* AP_NEED_SET_MUTEX_PERMS */ #endif /* AUTH_MELLON_COMPAT_H */ mod_auth_mellon-0.18.0/auth_mellon_config.c000066400000000000000000002373431410105036600207560ustar00rootroot00000000000000/* * * auth_mellon_config.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* This is the default endpoint path. Remember to update the description of * the MellonEndpointPath configuration directive if you change this. */ static const char *default_endpoint_path = "/mellon/"; /* This is the default name of the attribute we use as a username. Remember * to update the description of the MellonUser configuration directive if * you change this. */ static const char *default_user_attribute = "NAME_ID"; /* This is the default prefix to use for attributes received from the * server. Customizable using the MellonEnvPrefix option */ static const char *default_env_prefix = "MELLON_"; /* This is the default name of the cookie which mod_auth_mellon will set. * If you change this, then you should also update the description of the * MellonVar configuration directive. */ static const char *default_cookie_name = "cookie"; /* The default setting for cookie is to not enforce secure flag */ static const int default_secure_cookie = 0; /* The default setting for cookie is to not enforce HttpOnly flag */ static const int default_http_only_cookie = 0; /* The default setting for setting MELLON_SESSION */ static const int default_dump_session = 0; /* The default setting for setting MELLON_SAML_RESPONSE */ static const int default_dump_saml_response = 0; /* This is the default IdP initiated login location * the MellonDefaultLoginPath configuration directive if you change this. */ static const char *default_login_path = "/"; /* saved POST session time to live * the MellonPostTTL configuration directive if you change this. */ static const apr_time_t post_ttl = 15 * 60; /* saved POST session maximum size * the MellonPostSize configuration directive if you change this. */ static const apr_size_t post_size = 1024 * 1024; /* maximum saved POST sessions * the MellonPostCount configuration directive if you change this. */ static const int post_count = 100; #ifdef ENABLE_DIAGNOSTICS /* Default filename for mellon diagnostics log file. * Relative pathname is relative to server root. */ static const char *default_diag_filename = "logs/mellon_diagnostics"; /* Default state for diagnostics is off */ static am_diag_flags_t default_diag_flags = AM_DIAG_FLAG_DISABLE; #endif /* whether to merge env. vars or not * the MellonMergeEnvVars configuration directive if you change this. */ static const char *default_merge_env_vars = NULL; /* for env. vars with multiple values, the index start * the MellonEnvVarsIndexStart configuration directive if you change this. */ static const int default_env_vars_index_start = -1; /* whether to also populate env. var _N with number of values * the MellonEnvVarsSetCount configuration directive if you change this. */ static const int default_env_vars_count_in_n = -1; /* The default list of trusted redirect domains. */ static const char * const default_redirect_domains[] = { "[self]", NULL }; /* The default setting to enabled the invalidation session endpoint */ static const int default_enabled_invalidation_session = 0; /* The default setting to send the Expect Header. */ static const int default_send_expect_header = 1; /* This function handles configuration directives which set a * multivalued string slot in the module configuration (the destination * strucure is a hash). * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *key The string argument following this configuration * directive in the configuraion file. * const char *value Optional value to be stored in the hash. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_hash_string_slot(cmd_parms *cmd, void *struct_ptr, const char *key, const char *value) { server_rec *s = cmd->server; apr_pool_t *pconf = s->process->pconf; am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; int offset; apr_hash_t **hash; /* * If no value is given, we just store the key in the hash. */ if (value == NULL || *value == '\0') value = key; offset = (int)(long)cmd->info; hash = (apr_hash_t **)((char *)cfg + offset); apr_hash_set(*hash, apr_pstrdup(pconf, key), APR_HASH_KEY_STRING, value); return NULL; } /* This function handles configuration directives which set a * multivalued string slot in the module configuration (the destination * strucure is a table). * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *key The string argument following this configuration * directive in the configuraion file. * const char *value Optional value to be stored in the hash. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_table_string_slot(cmd_parms *cmd, void *struct_ptr, const char *key, const char *value) { server_rec *s = cmd->server; apr_pool_t *pconf = s->process->pconf; am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; int offset; apr_table_t **table; /* * If no value is given, we just store the key in the hash. */ if (value == NULL || *value == '\0') value = key; offset = (int)(long)cmd->info; table = (apr_table_t **)((char *)cfg + offset); apr_table_set(*table, apr_pstrdup(pconf, key), value); return NULL; } /* This function handles configuration directives which set a file slot * in the module configuration. The file contents are immediately read. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * This value isn't used by this function. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_file_contents_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { const char *path; apr_status_t rv; am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; int offset; am_file_data_t **p_file_data, *file_data; path = ap_server_root_relative(cmd->pool, arg); if (!path) { return apr_pstrcat(cmd->pool, cmd->cmd->name, ": Invalid file path ", arg, NULL); } offset = (int)(long)cmd->info; p_file_data = (am_file_data_t **)((char *)cfg + offset); *p_file_data = am_file_data_new(cmd->pool, path); file_data = *p_file_data; rv = am_file_read(file_data); if (rv != APR_SUCCESS) { return file_data->strerror; } return NULL; } /* This function handles configuration directives which set a file * pathname in the module configuration. The file is checked for * existence. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * This value isn't used by this function. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_file_pathname_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { const char *path; apr_status_t rv; am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; int offset; am_file_data_t **p_file_data, *file_data; path = ap_server_root_relative(cmd->pool, arg); if (!path) { return apr_pstrcat(cmd->pool, cmd->cmd->name, ": Invalid file_data path ", arg, NULL); } offset = (int)(long)cmd->info; p_file_data = (am_file_data_t **)((char *)cfg + offset); *p_file_data = am_file_data_new(cmd->pool, path); file_data = *p_file_data; rv = am_file_stat(file_data); if (rv != APR_SUCCESS) { return file_data->strerror; } if (file_data->finfo.filetype != APR_REG) { return apr_psprintf(cmd->pool, "file \"%s\" is not a regular file", file_data->path); } return NULL; } /* This function handles configuration directives which use * a glob pattern, with a second optional argument * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *glob_pat glob(3) pattern * const char *option Optional argument * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_glob_fn12(cmd_parms *cmd, void *struct_ptr, const char *glob_pat, const char *option) { const char *(*take_argv)(cmd_parms *, void *, const char *, const char *); apr_array_header_t *files; const char *error; const char *directory; int i; take_argv = cmd->info; directory = am_filepath_dirname(cmd->pool, glob_pat); if (glob_pat == NULL || *glob_pat == '\0') return apr_psprintf(cmd->pool, "%s takes one or two arguments", cmd->cmd->name); if (apr_match_glob(glob_pat, &files, cmd->pool) != 0) return take_argv(cmd, struct_ptr, glob_pat, option); for (i = 0; i < files->nelts; i++) { const char *path; path = apr_pstrcat(cmd->pool, directory, "/", ((const char **)(files->elts))[i], NULL); error = take_argv(cmd, struct_ptr, path, option); if (error != NULL) return error; } return NULL; } /* This function handles configuration directives which set an * idp related slot in the module configuration. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *metadata Path to metadata file for one or multiple IdP * const char *chain Optional path to validating chain * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_idp_string_slot(cmd_parms *cmd, void *struct_ptr, const char *metadata, const char *chain) { server_rec *s = cmd->server; apr_pool_t *pconf = s->process->pconf; am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; am_file_data_t *idp_file_data = NULL; am_file_data_t *chain_file_data = NULL; #ifndef HAVE_lasso_server_load_metadata if (chain != NULL) return apr_psprintf(cmd->pool, "Cannot specify validating chain " "for %s since lasso library lacks " "lasso_server_load_metadata()", cmd->cmd->name); #endif /* HAVE_lasso_server_load_metadata */ idp_file_data = am_file_data_new(pconf, metadata); if (am_file_stat(idp_file_data) != APR_SUCCESS) { return idp_file_data->strerror; } if (chain) { chain_file_data = am_file_data_new(pconf, chain); if (am_file_stat(chain_file_data) != APR_SUCCESS) { return chain_file_data->strerror; } } else { chain_file_data = NULL; } am_metadata_t *idp_metadata = apr_array_push(cfg->idp_metadata); idp_metadata->metadata = idp_file_data; idp_metadata->chain = chain_file_data; return NULL; } /* This function handles configuration directives which set an * idp federation blacklist slot in the module configuration. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * int argc Number of blacklisted providerId. * char *const argv[] List of blacklisted providerId. * * Returns: * NULL on success, or errror string */ static const char *am_set_idp_ignore_slot(cmd_parms *cmd, void *struct_ptr, int argc, char *const argv[]) { #ifdef HAVE_lasso_server_load_metadata server_rec *s = cmd->server; apr_pool_t *pconf = s->process->pconf; am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; GList *new_idp_ignore; int i; if (argc < 1) return apr_psprintf(cmd->pool, "%s takes at least one arguments", cmd->cmd->name); for (i = 0; i < argc; i++) { new_idp_ignore = apr_palloc(pconf, sizeof(GList)); new_idp_ignore->data = apr_pstrdup(pconf, argv[i]); /* Prepend it to the list. */ new_idp_ignore->next = cfg->idp_ignore; if (cfg->idp_ignore != NULL) cfg->idp_ignore->prev = new_idp_ignore; cfg->idp_ignore = new_idp_ignore; } return NULL; #else /* HAVE_lasso_server_load_metadata */ return apr_psprintf(cmd->pool, "Cannot use %s since lasso library lacks " "lasso_server_load_metadata()", cmd->cmd->name); #endif /* HAVE_lasso_server_load_metadata */ } /* This function handles configuration directives which set a file path * slot in the module configuration. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * This value isn't used by this function. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_module_config_file_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { return ap_set_file_slot(cmd, am_get_mod_cfg(cmd->server), arg); } /* This function handles configuration directives which set an int * slot in the module configuration. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * This value isn't used by this function. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_module_config_int_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { return ap_set_int_slot(cmd, am_get_mod_cfg(cmd->server), arg); } /* This function handles the MellonDiagnosticsFile configuration directive. * It emits as warning in the log file if Mellon is not built with * diagnostics enabled. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_module_diag_file_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { #ifdef ENABLE_DIAGNOSTICS return ap_set_file_slot(cmd, am_get_diag_cfg(cmd->server), arg); #else ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, cmd->server, "%s has no effect because Mellon was not compiled with" " diagnostics enabled, use ./configure --enable-diagnostics" " at build time to turn this feature on.", cmd->directive->directive); return NULL; #endif } /* This function handles configuration directives which sets the * diagnostics flags in the module configuration. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_module_diag_flags_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { #ifdef ENABLE_DIAGNOSTICS am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(cmd->server); if (strcasecmp(arg, "on") == 0) { diag_cfg->flags = AM_DIAG_FLAG_ENABLE_ALL; } else if (strcasecmp(arg, "off") == 0) { diag_cfg->flags = AM_DIAG_FLAG_DISABLE; } else { return apr_psprintf(cmd->pool, "%s: must be one of: 'on', 'off'", cmd->cmd->name); } return NULL; #else ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, cmd->server, "%s has no effect because Mellon was not compiled with" " diagnostics enabled, use ./configure --enable-diagnostics" " at build time to turn this feature on.", cmd->directive->directive); return NULL; #endif } /* This function handles the MellonCookieSameSite configuration directive. * This directive can be set to "lax" or "strict" * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_samesite_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; if(!strcasecmp(arg, "lax")) { d->cookie_samesite = am_samesite_lax; } else if(!strcasecmp(arg, "strict")) { d->cookie_samesite = am_samesite_strict; } else if(!strcasecmp(arg, "none")) { d->cookie_samesite = am_samesite_none; } else { return "The MellonCookieSameSite parameter must be 'lax', 'none' or 'strict'"; } return NULL; } /* This function handles the MellonEnable configuration directive. * This directive can be set to "off", "info" or "auth". * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_enable_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; if(!strcasecmp(arg, "auth")) { d->enable_mellon = am_enable_auth; } else if(!strcasecmp(arg, "info")) { d->enable_mellon = am_enable_info; } else if(!strcasecmp(arg, "off")) { d->enable_mellon = am_enable_off; } else { return "parameter must be 'off', 'info' or 'auth'"; } return NULL; } /* This function handles the MellonSecureCookie configuration directive. * This directive can be set to "on", "off", "secure" or "httponly". * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_secure_slots(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; if(!strcasecmp(arg, "on")) { d->secure = 1; d->http_only = 1; } else if(!strcasecmp(arg, "secure")) { d->secure = 1; } else if(!strcasecmp(arg, "httponly")) { d->http_only = 1; } else if(strcasecmp(arg, "off")) { return "parameter must be 'on', 'off', 'secure' or 'httponly'"; } return NULL; } /* This function handles the obsolete MellonDecoder configuration directive. * It is a no-op. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL */ static const char *am_set_decoder_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { return NULL; } /* This function handles the MellonEndpointPath configuration directive. * If the path doesn't end with a '/', then we will append one. * * Parameters: * cmd_parms *cmd The command structure for the MellonEndpointPath * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *arg The string argument containing the path of the * endpoint directory. * * Returns: * This function will always return NULL. */ static const char *am_set_endpoint_path(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; /* Make sure that the path ends with '/'. */ if(strlen(arg) == 0 || arg[strlen(arg) - 1] != '/') { d->endpoint_path = apr_pstrcat(cmd->pool, arg, "/", NULL); } else { d->endpoint_path = arg; } return NULL; } /* This function handles the MellonSetEnv configuration directive. * This directive allows the user to change the name of attributes. * * Parameters: * cmd_parms *cmd The command structure for the MellonSetEnv * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * const char *newName The new name of the attribute. * const char *oldName The old name of the attribute. * * Returns: * This function will always return NULL. */ static const char *am_set_setenv_slot(cmd_parms *cmd, void *struct_ptr, const char *newName, const char *oldName) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; /* Configure as prefixed attribute name */ am_envattr_conf_t *envattr_conf = (am_envattr_conf_t *)apr_palloc(cmd->pool, sizeof(am_envattr_conf_t)); envattr_conf->name = newName; envattr_conf->prefixed = 1; apr_hash_set(d->envattr, oldName, APR_HASH_KEY_STRING, envattr_conf); return NULL; } /* This function handles the MellonSetEnvNoPrefix configuration directive. * This directive allows the user to change the name of attributes without prefixing them with MELLON_. * * Parameters: * cmd_parms *cmd The command structure for the MellonSetEnv * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * const char *newName The new name of the attribute. * const char *oldName The old name of the attribute. * * Returns: * This function will always return NULL. */ static const char *am_set_setenv_no_prefix_slot(cmd_parms *cmd, void *struct_ptr, const char *newName, const char *oldName) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; /* Configure as not prefixed attribute name */ am_envattr_conf_t *envattr_conf = (am_envattr_conf_t *)apr_palloc(cmd->pool, sizeof(am_envattr_conf_t)); envattr_conf->name = newName; envattr_conf->prefixed = 0; apr_hash_set(d->envattr, oldName, APR_HASH_KEY_STRING, envattr_conf); return NULL; } /* This function handles the MellonAuthnContextComparisonType option. * It could be set to "exact", "minimum", "maximum" or "better" * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_authn_context_comparison_type_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; if (!strcasecmp(arg, LASSO_LIB_AUTHN_CONTEXT_COMPARISON_EXACT)) { d->authn_context_comparison_type = LASSO_LIB_AUTHN_CONTEXT_COMPARISON_EXACT; } else if (!strcasecmp(arg, LASSO_LIB_AUTHN_CONTEXT_COMPARISON_MINIMUM)) { d->authn_context_comparison_type = LASSO_LIB_AUTHN_CONTEXT_COMPARISON_MINIMUM; } else if (!strcasecmp(arg, LASSO_LIB_AUTHN_CONTEXT_COMPARISON_MAXIMUM)) { d->authn_context_comparison_type = LASSO_LIB_AUTHN_CONTEXT_COMPARISON_MAXIMUM; } else if (!strcasecmp(arg, LASSO_LIB_AUTHN_CONTEXT_COMPARISON_BETTER)) { d->authn_context_comparison_type = LASSO_LIB_AUTHN_CONTEXT_COMPARISON_BETTER; } else { return "parameter must be 'exact', 'minimum', 'maximum' or 'better'"; } return NULL; } /* This function decodes MellonCond flags, such as [NOT,REG] * * Parameters: * const char *arg Pointer to the flags string * * Returns: * flags, or -1 on error */ static int am_cond_flags(const char *arg) { int flags = AM_COND_FLAG_NULL; static const char * const options[] = { "OR", /* AM_EXPIRE_FLAG_OR */ "NOT", /* AM_EXPIRE_FLAG_NOT */ "REG", /* AM_EXPIRE_FLAG_REG */ "NC", /* AM_EXPIRE_FLAG_NC */ "MAP", /* AM_EXPIRE_FLAG_MAP */ "REF", /* AM_EXPIRE_FLAG_REF */ "SUB", /* AM_EXPIRE_FLAG_SUB */ /* The other options (IGN, REQ, FSTR, ...) are only internally used */ }; apr_size_t options_count = sizeof(options) / sizeof(*options); /* Skip initial [ */ if (arg[0] == '[') arg++; else return -1; do { apr_size_t i; for (i = 0; i < options_count; i++) { apr_size_t optlen = strlen(options[i]); if (strncmp(arg, options[i], optlen) == 0) { /* Make sure we have a separator next */ if (arg[optlen] && !strchr("]\t ,", (int)arg[optlen])) return -1; flags |= (1 << i); arg += optlen; break; } /* no match */ if (i == options_count) return -1; /* skip spaces, tabs and commas */ arg += strspn(arg, " \t,"); /* * End of option, but we fire an error if * there is trailing garbage */ if (*arg == ']') { arg++; return (*arg == '\0') ? flags : -1; } } } while (*arg); /* Missing trailing ] */ return -1; } /* This function handles the MellonCond configuration directive, which * allows the user to restrict access based on attributes received from * the IdP. * * Parameters: * cmd_parms *cmd The command structure for the MellonCond * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * const char *attribute Pointer to the attribute name * const char *value Pointer to the attribute value or regex * const char *options Pointer to options * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_cond_slot(cmd_parms *cmd, void *struct_ptr, const char *attribute, const char *value, const char *options) { am_dir_cfg_rec *d = struct_ptr; int flags = AM_COND_FLAG_NULL; am_cond_t *element; if (attribute == NULL || *attribute == '\0' || value == NULL || *value == '\0') return apr_pstrcat(cmd->pool, cmd->cmd->name, " takes at least two arguments", NULL); if (options != NULL && *options != '\0') flags = am_cond_flags(options); if (flags == -1) return apr_psprintf(cmd->pool, "%s - invalid flags %s", cmd->cmd->name, options); element = (am_cond_t *)apr_array_push(d->cond); element->varname = attribute; element->flags = flags; element->str = NULL; element->regex = NULL; element->directive = apr_pstrcat(cmd->pool, cmd->directive->directive, " ", cmd->directive->args, NULL); if (element->flags & AM_COND_FLAG_REG) { int regex_flags = AP_REG_EXTENDED|AP_REG_NOSUB; if (element->flags & AM_COND_FLAG_NC) regex_flags |= AP_REG_ICASE; element->regex = ap_pregcomp(cmd->pool, value, regex_flags); if (element->regex == NULL) return apr_psprintf(cmd->pool, "%s - invalid regex %s", cmd->cmd->name, value); } /* * Flag values containing format strings to that we do * not have to process the others at runtime. */ if (strchr(value, '%') != NULL) element->flags |= AM_COND_FLAG_FSTR; /* * We keep the string also for regex, so that we can * print it for debug purpose and perform substitutions on it. */ element->str = value; return NULL; } /* This function handles the MellonRequire configuration directive, which * allows the user to restrict access based on attributes received from * the IdP. * * Parameters: * cmd_parms *cmd The command structure for the MellonRequire * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg Pointer to the configuration string. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_require_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = struct_ptr; char *attribute, *value; int i; am_cond_t *element; am_cond_t *first_element; attribute = ap_getword_conf(cmd->pool, &arg); value = ap_getword_conf(cmd->pool, &arg); if (*attribute == '\0' || *value == '\0') { return apr_pstrcat(cmd->pool, cmd->cmd->name, " takes at least two arguments", NULL); } /* * MellonRequire overwrites previous conditions on this attribute * We just tag the am_cond_t with the ignore flag, as it is * easier (and probably faster) than to really remove it. */ for (i = 0; i < d->cond->nelts; i++) { am_cond_t *ce = &((am_cond_t *)(d->cond->elts))[i]; if ((strcmp(ce->varname, attribute) == 0) && (ce->flags & AM_COND_FLAG_REQ)) ce->flags |= AM_COND_FLAG_IGN; } first_element = NULL; do { element = (am_cond_t *)apr_array_push(d->cond); element->varname = attribute; element->flags = AM_COND_FLAG_OR|AM_COND_FLAG_REQ; element->str = value; element->regex = NULL; /* * When multiple values are given, we track the first one * in order to retreive the directive */ if (first_element == NULL) { element->directive = apr_pstrcat(cmd->pool, cmd->directive->directive, " ", cmd->directive->args, NULL); first_element = element; } else { element->directive = first_element->directive; } } while (*(value = ap_getword_conf(cmd->pool, &arg)) != '\0'); /* * Remove OR flag on last element */ element->flags &= ~AM_COND_FLAG_OR; return NULL; } /* This function handles the MellonOrganization* directives, which * which specify language-qualified strings * * Parameters: * cmd_parms *cmd The command structure for the MellonOrganization* * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * const char *lang Pointer to the language string (optional) * const char *value Pointer to the data * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_langstring_slot(cmd_parms *cmd, void *struct_ptr, const char *lang, const char *value) { apr_hash_t *h = *(apr_hash_t **)(struct_ptr + (apr_size_t)cmd->info); if (value == NULL || *value == '\0') { value = lang; lang = ""; } apr_hash_set(h, lang, APR_HASH_KEY_STRING, apr_pstrdup(cmd->server->process->pconf, value)); return NULL; } /* This function handles the MellonAuthnContextClassRef directive. * * Parameters: * cmd_parms *cmd The command structure for the MellonAuthnContextClassRef * configuration directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *arg An URI for an SAMLv2 AuthnContextClassRef * * Returns: * This function will always return NULL. */ static const char *am_set_authn_context_class_ref(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; apr_pool_t *p= cmd->pool; char **context_class_ref_p; if(strlen(arg) == 0) { return NULL; } context_class_ref_p = apr_array_push(d->authn_context_class_ref); *context_class_ref_p = apr_pstrdup(p, arg); return NULL; } /* This function handles the MellonDoNotVerifyLogoutSignature configuration directive, * it is identical to the am_set_hash_string_slot function. You can refer to it. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *key The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_do_not_verify_logout_signature(cmd_parms *cmd, void *struct_ptr, const char *key) { #ifdef HAVE_lasso_profile_set_signature_verify_hint return am_set_hash_string_slot(cmd, struct_ptr, key, NULL); #else return apr_pstrcat(cmd->pool, cmd->cmd->name, " is not usable as modmellon was compiled against " "a version of the lasso library which miss the " "function lasso_profile_set_signature_verify_hint.", NULL); #endif } /* This function handles the MellonMergeEnvVars configuration directive, * it sets merge_env_vars to nonempty separator (default semicolon), * or empty string to denote no merging. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * const char *flag On/Off flag * const char *sep Optional separator, should be only present with On * * Returns: * NULL on success or an error string on failure. */ static const char *am_set_merge_env_vars(cmd_parms *cmd, void *struct_ptr, const char *flag, const char *sep) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; apr_pool_t *p= cmd->pool; if (strcasecmp(flag, "on") == 0) { if (sep && *sep) { /* * TAKE12 will not give us the second argument if it is * empty string so we cannot complain about it, we will just * silently use semicolon */ d->merge_env_vars = apr_pstrdup(p, sep); } else { d->merge_env_vars = ";"; } } else if (strcasecmp(flag, "off") == 0) { if (sep) { return apr_pstrcat(cmd->pool, cmd->cmd->name, " separator should not be used with Off", NULL); } d->merge_env_vars = ""; } else { return apr_pstrcat(cmd->pool, cmd->cmd->name, " first parameer must be On or Off", NULL); } return NULL; } /* Handle MellonRedirectDomains option. * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * NULL if we are not in a directory configuration. * int argc Number of redirect domains. * char *const argv[] List of redirect domains. * * Returns: * NULL on success, or errror string on failure. */ static const char *am_set_redirect_domains(cmd_parms *cmd, void *struct_ptr, int argc, char *const argv[]) { am_dir_cfg_rec *cfg = (am_dir_cfg_rec *)struct_ptr; const char **redirect_domains; int i; if (argc < 1) return apr_psprintf(cmd->pool, "%s takes at least one arguments", cmd->cmd->name); redirect_domains = apr_palloc(cmd->pool, sizeof(const char *) * (argc + 1)); for (i = 0; i < argc; i++) { redirect_domains[i] = argv[i]; } redirect_domains[argc] = NULL; cfg->redirect_domains = redirect_domains; return NULL; } /* This function handles the MellonSignatureMethod configuration directive. * This directive can be set to one of: * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_signature_method_slot(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; char *valid_methods = "rsa-sha1" #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA256 " rsa-sha256" #endif #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA384 " rsa-sha384" #endif #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA512 " rsa-sha512" #endif ; if (!strcasecmp(arg, "rsa-sha1")) { d->signature_method = LASSO_SIGNATURE_METHOD_RSA_SHA1; } #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA256 else if (!strcasecmp(arg, "rsa-sha256")) { d->signature_method = LASSO_SIGNATURE_METHOD_RSA_SHA256; } #endif #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA384 else if (!strcasecmp(arg, "rsa-sha384")) { d->signature_method = LASSO_SIGNATURE_METHOD_RSA_SHA384; } #endif #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA512 else if (!strcasecmp(arg, "rsa-sha512")) { d->signature_method = LASSO_SIGNATURE_METHOD_RSA_SHA512; } #endif else { return apr_psprintf(cmd->pool, "%s: Invalid method \"%s\", must be one of: %s", cmd->cmd->name, arg, valid_methods); } return NULL; } /* This function handles the MellonEnabledInvalidateSessionEndpoint configuration directive. * This directive can be set to "on" or "off" (default). * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_invalidate_session_slots(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; if (strcasecmp(arg, "on") == 0) { d->enabled_invalidation_session = 1; } else if (strcasecmp(arg, "off") == 0) { d->enabled_invalidation_session = 0; } else { return apr_psprintf(cmd->pool, "%s: must be one of: 'on', 'off'", cmd->cmd->name); } return NULL; } /* This function handles the MellonSendExpectHeader configuration directive. * This directive can be set to "on" (default) or "off". * * Parameters: * cmd_parms *cmd The command structure for this configuration * directive. * void *struct_ptr Pointer to the current directory configuration. * const char *arg The string argument following this configuration * directive in the configuraion file. * * Returns: * NULL on success or an error string if the argument is wrong. */ static const char *am_set_send_expect_header_slots(cmd_parms *cmd, void *struct_ptr, const char *arg) { am_dir_cfg_rec *d = (am_dir_cfg_rec *)struct_ptr; if (strcasecmp(arg, "on") == 0) { d->send_expect_header = 1; } else if (strcasecmp(arg, "off") == 0) { d->send_expect_header = 0; } else { return apr_psprintf(cmd->pool, "%s: must be one of: 'on', 'off'", cmd->cmd->name); } return NULL; } /* This array contains all the configuration directive which are handled * by auth_mellon. */ const command_rec auth_mellon_commands[] = { /* Global configuration directives. */ AP_INIT_TAKE1( "MellonCacheSize", am_set_module_config_int_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, cache_size), RSRC_CONF, "The number of sessions we can keep track of at once. You must" " restart the server before any changes to this directive will" " take effect. The default value is 100." ), AP_INIT_TAKE1( "MellonCacheEntrySize", am_set_module_config_int_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, entry_size), RSRC_CONF, "The maximum size for a single session entry. You must" " restart the server before any changes to this directive will" " take effect. The default value is 192KiB." ), AP_INIT_TAKE1( "MellonLockFile", am_set_module_config_file_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, lock_file), RSRC_CONF, "The lock file for session synchronization." " Default value is \"/var/run/mod_auth_mellon.lock\"." ), AP_INIT_TAKE1( "MellonPostDirectory", am_set_module_config_file_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, post_dir), RSRC_CONF, "The directory for saving POST requests." " Not set by default." ), AP_INIT_TAKE1( "MellonPostTTL", am_set_module_config_int_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, post_ttl), RSRC_CONF, "The time to live for saved POST requests in seconds." " Default value is 900 (15 minutes)." ), AP_INIT_TAKE1( "MellonPostCount", am_set_module_config_int_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, post_count), RSRC_CONF, "The maximum saved POST sessions at once." " Default value is 100." ), AP_INIT_TAKE1( "MellonPostSize", am_set_module_config_int_slot, (void *)APR_OFFSETOF(am_mod_cfg_rec, post_size), RSRC_CONF, "The maximum size of a saved POST, in bytes." " Default value is 1048576 (1 MB)." ), AP_INIT_TAKE1( "MellonDiagnosticsFile", am_set_module_diag_file_slot, #ifdef ENABLE_DIAGNOSTICS (void *)APR_OFFSETOF(am_diag_cfg_rec, filename), #else NULL, #endif RSRC_CONF, "Diagnostics log file. [file|pipe] " "If file then file is a filename, relative to the ServerRoot." "If pipe then the filename is a pipe character \"|\", " "followed by the path to a program to receive the log information " "on its standard input. " " Default value is \"logs/mellon_diagnostics\"." ), AP_INIT_ITERATE( "MellonDiagnosticsEnable", am_set_module_diag_flags_slot, NULL, RSRC_CONF, "Diagnostics flags. [on|off] " " Default value is \"off\"." ), /* Per-location configuration directives. */ AP_INIT_TAKE1( "MellonEnable", am_set_enable_slot, NULL, OR_AUTHCFG, "Enable auth_mellon on a location. This can be set to 'off', 'info'" " and 'auth'. 'off' disables auth_mellon for a location, 'info'" " will only populate the environment with attributes if the user" " has logged in already. 'auth' will redirect the user to the IdP" " if he hasn't logged in yet, but otherwise behaves like 'info'." ), AP_INIT_TAKE1( "MellonDecoder", am_set_decoder_slot, NULL, OR_AUTHCFG, "Obsolete option, now a no-op for backwards compatibility." ), AP_INIT_TAKE1( "MellonVariable", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, varname), OR_AUTHCFG, "The name of the cookie which auth_mellon will set. Defaults to" " 'cookie'. This string is appended to 'mellon-' to create the" " cookie name, and the default name of the cookie will therefore" " be 'mellon-cookie'." ), AP_INIT_TAKE1( "MellonSecureCookie", am_set_secure_slots, NULL, OR_AUTHCFG, "Whether the cookie set by auth_mellon should have HttpOnly and" " secure flags set. Default is 'off'. Once 'on' - both flags will" " be set. Values 'httponly' or 'secure' will respectively set only" " one flag." ), AP_INIT_TAKE1( "MellonCookieDomain", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, cookie_domain), OR_AUTHCFG, "The domain of the cookie which auth_mellon will set. Defaults to" " the domain of the current request." ), AP_INIT_TAKE1( "MellonCookiePath", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, cookie_path), OR_AUTHCFG, "The path of the cookie which auth_mellon will set. Defaults to" " '/'." ), AP_INIT_TAKE1( "MellonCookieSameSite", am_set_samesite_slot, NULL, OR_AUTHCFG, "The SameSite value for the auth_mellon cookie. Defaults to" " having no SameSite value. Accepts values of Lax or Strict." ), AP_INIT_TAKE1( "MellonUser", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, userattr), OR_AUTHCFG, "Attribute to set as r->user. Defaults to NAME_ID, which is the" " attribute we set to the identifier we receive from the IdP." ), AP_INIT_TAKE1( "MellonIdP", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, idpattr), OR_AUTHCFG, "Attribute we set to the IdP ProviderId." ), AP_INIT_TAKE2( "MellonSetEnv", am_set_setenv_slot, NULL, OR_AUTHCFG, "Renames attributes received from the server while retaining the" " prefix. The prefix defaults to MELLON_ but can be changed with" " MellonEnvPrefix." " The format is MellonSetEnv ." ), AP_INIT_TAKE2( "MellonSetEnvNoPrefix", am_set_setenv_no_prefix_slot, NULL, OR_AUTHCFG, "Renames attributes received from the server without adding prefix. The format is" " MellonSetEnvNoPrefix ." ), AP_INIT_TAKE1( "MellonEnvPrefix", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, env_prefix), OR_AUTHCFG, "The prefix to use for attributes received from the server." ), AP_INIT_FLAG( "MellonSessionDump", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, dump_session), OR_AUTHCFG, "Dump session in environment. Default is off" ), AP_INIT_FLAG( "MellonSamlResponseDump", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, dump_saml_response), OR_AUTHCFG, "Dump SAML authentication response in environment. Default is off" ), AP_INIT_RAW_ARGS( "MellonRequire", am_set_require_slot, NULL, OR_AUTHCFG, "Attribute requirements for authorization. Allows you to restrict" " access based on attributes received from the IdP. If you list" " several MellonRequire configuration directives, then all of them" " must match. Every MellonRequire can list several allowed values" " for the attribute. The syntax is:" " MellonRequire [value2....]." ), AP_INIT_TAKE23( "MellonCond", am_set_cond_slot, NULL, OR_AUTHCFG, "Attribute requirements for authorization. Allows you to restrict" " access based on attributes received from the IdP. The syntax is:" " MellonRequire []." ), AP_INIT_TAKE1( "MellonSessionLength", ap_set_int_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, session_length), OR_AUTHCFG, "Maximum number of seconds a session will be valid for. Defaults" " to 86400 seconds (1 day)." ), AP_INIT_TAKE1( "MellonSessionIdleTimeout", ap_set_int_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, session_idle_timeout), OR_AUTHCFG, "Amount of time a user can be inactive before the user's session times out. Defaults" " to -1 seconds (disabled)." ), AP_INIT_TAKE1( "MellonNoCookieErrorPage", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, no_cookie_error_page), OR_AUTHCFG, "Web page to display if the user has disabled cookies. We will" " return a 400 Bad Request error if this is unset and the user" " ha disabled cookies." ), AP_INIT_TAKE1( "MellonNoSuccessErrorPage", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, no_success_error_page), OR_AUTHCFG, "Web page to display if the idp posts with a failed" " authentication error. We will return a 401 Unauthorized error" " if this is unset and the idp posts such assertion." ), AP_INIT_TAKE1( "MellonSPMetadataFile", am_set_file_contents_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_metadata_file), OR_AUTHCFG, "Full path to xml file with metadata for the SP." ), AP_INIT_TAKE1( "MellonSPPrivateKeyFile", am_set_file_contents_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_private_key_file), OR_AUTHCFG, "Full path to pem file with the private key for the SP." ), AP_INIT_TAKE1( "MellonSPCertFile", am_set_file_contents_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_cert_file), OR_AUTHCFG, "Full path to pem file with certificate for the SP." ), AP_INIT_TAKE12( "MellonIdPMetadataFile", am_set_idp_string_slot, NULL, OR_AUTHCFG, "Full path to xml metadata file for IdP, " "with optional validating chain." ), AP_INIT_TAKE12( "MellonIdPMetadataGlob", am_set_glob_fn12, am_set_idp_string_slot, OR_AUTHCFG, "Full path to xml metadata files for IdP, with glob(3) patterns. " "An optional validating chain can be supplied." ), AP_INIT_TAKE1( "MellonIdPPublicKeyFile", am_set_file_pathname_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, idp_public_key_file), OR_AUTHCFG, "Full path to pem file with the public key for the IdP." ), AP_INIT_TAKE1( "MellonIdPCAFile", am_set_file_pathname_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, idp_ca_file), OR_AUTHCFG, "Full path to pem file with CA chain for the IdP." ), AP_INIT_TAKE_ARGV( "MellonIdPIgnore", am_set_idp_ignore_slot, NULL, OR_AUTHCFG, "List of IdP entityId to ignore." ), AP_INIT_TAKE1( "MellonSPentityId", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_entity_id), OR_AUTHCFG, "SP entity Id to be used for metadata auto generation." ), AP_INIT_TAKE12( "MellonOrganizationName", am_set_langstring_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_org_name), OR_AUTHCFG, "Language-qualified oranization name." ), AP_INIT_TAKE12( "MellonOrganizationDisplayName", am_set_langstring_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_org_display_name), OR_AUTHCFG, "Language-qualified oranization name, human redable." ), AP_INIT_TAKE12( "MellonOrganizationURL", am_set_langstring_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, sp_org_url), OR_AUTHCFG, "Language-qualified oranization URL." ), AP_INIT_TAKE1( "MellonDefaultLoginPath", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, login_path), OR_AUTHCFG, "The location where to redirect after IdP initiated login." " Default value is \"/\"." ), AP_INIT_TAKE1( "MellonDiscoveryURL", ap_set_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, discovery_url), OR_AUTHCFG, "The URL of IdP discovery service. Default is unset." ), AP_INIT_TAKE1( "MellonProbeDiscoveryTimeout", ap_set_int_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, probe_discovery_timeout), OR_AUTHCFG, "The timeout in seconds of IdP probe discovery service. " "The default is unset, which means that this feature is disabled." ), AP_INIT_TAKE12( "MellonProbeDiscoveryIdP", am_set_table_string_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, probe_discovery_idp), OR_AUTHCFG, "An IdP that can be used for IdP probe discovery." ), AP_INIT_TAKE1( "MellonEndpointPath", am_set_endpoint_path, NULL, OR_AUTHCFG, "The root directory of the SAML2 endpoints, relative to the root" " of the web server. Default value is \"/mellon/\", which will" " make mod_mellon to the handler for every request to" " \"http:///mellon/*\". The path you specify must" " be contained within the current Location directive." ), AP_INIT_TAKE1( "MellonAuthnContextClassRef", am_set_authn_context_class_ref, NULL, OR_AUTHCFG, "A list of AuthnContextClassRef to request in the AuthnRequest and " "to validate upon reception of an Assertion" ), AP_INIT_TAKE1( "MellonAuthnContextComparisonType", am_set_authn_context_comparison_type_slot, NULL, OR_AUTHCFG, "An AuthnContextComparisonType attribute as part of the AuthnRequest." ), AP_INIT_FLAG( "MellonSubjectConfirmationDataAddressCheck", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, subject_confirmation_data_address_check), OR_AUTHCFG, "Check address given in SubjectConfirmationData Address attribute. Default is on." ), AP_INIT_FLAG( "MellonSendCacheControlHeader", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, send_cache_control_header), OR_AUTHCFG, "Send the cache-control header on responses. Default is on." ), AP_INIT_TAKE1( "MellonDoNotVerifyLogoutSignature", am_set_do_not_verify_logout_signature, (void *)APR_OFFSETOF(am_dir_cfg_rec, do_not_verify_logout_signature), OR_AUTHCFG, "A list of entity of IdP whose logout requests signatures will not " "be valided" ), AP_INIT_FLAG( "MellonPostReplay", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, post_replay), OR_AUTHCFG, "Whether we should replay POST requests that trigger authentication. Default is off." ), AP_INIT_TAKE12( "MellonMergeEnvVars", am_set_merge_env_vars, NULL, OR_AUTHCFG, "Whether to merge environment variables multi-values or not. Default is off." "When first parameter is on, optional second parameter is the separator, " "defaulting to semicolon." ), AP_INIT_TAKE1( "MellonEnvVarsIndexStart", ap_set_int_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, env_vars_index_start), OR_AUTHCFG, "Start indexing environment variables for multivalues with 0 or 1. Default is 0." ), AP_INIT_FLAG( "MellonEnvVarsSetCount", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, env_vars_count_in_n), OR_AUTHCFG, "Whether to also populate environment variable suffixed _N with number of values. Default is off." ), AP_INIT_FLAG( "MellonECPSendIDPList", ap_set_flag_slot, (void *)APR_OFFSETOF(am_dir_cfg_rec, ecp_send_idplist), OR_AUTHCFG, "Whether to send an ECP client a list of IdP's. Default is off." ), AP_INIT_TAKE_ARGV( "MellonRedirectDomains", am_set_redirect_domains, NULL, OR_AUTHCFG, "List of domains we can redirect to." ), AP_INIT_TAKE1( "MellonSignatureMethod", am_set_signature_method_slot, NULL, OR_AUTHCFG, "Signature method used to sign SAML messages sent by Mellon" ), AP_INIT_TAKE1( "MellonEnabledInvalidateSessionEndpoint", am_set_invalidate_session_slots, NULL, OR_AUTHCFG, "Enabled the session invalidation endpoint. Default is 'off'." ), AP_INIT_TAKE1( "MellonSendExpectHeader", am_set_send_expect_header_slots, NULL, OR_AUTHCFG, "Send the Expect Header. Default is 'on'." ), {NULL} }; const am_error_map_t auth_mellon_errormap[] = { { LASSO_PROFILE_ERROR_STATUS_NOT_SUCCESS, HTTP_UNAUTHORIZED }, #ifdef LASSO_PROFILE_ERROR_REQUEST_DENIED { LASSO_PROFILE_ERROR_REQUEST_DENIED, HTTP_UNAUTHORIZED }, #endif { 0, 0 } }; /* Release a lasso_server object associated with this configuration. * * Parameters: * void *data The pointer to the configuration data. * * Returns: * Always APR_SUCCESS. */ static apr_status_t auth_mellon_free_server(void *data) { am_dir_cfg_rec *dir = data; if (dir->server != NULL) { lasso_server_destroy(dir->server); dir->server = NULL; } return APR_SUCCESS; } /* This function creates and initializes a directory configuration * object for auth_mellon. * * Parameters: * apr_pool_t *p The pool we should allocate memory from. * char *d Unused, always NULL. * * Returns: * The new directory configuration object. */ void *auth_mellon_dir_config(apr_pool_t *p, char *d) { am_dir_cfg_rec *dir = apr_palloc(p, sizeof(*dir)); apr_pool_cleanup_register(p, dir, auth_mellon_free_server, auth_mellon_free_server); dir->enable_mellon = am_enable_default; dir->varname = default_cookie_name; dir->secure = default_secure_cookie; dir->http_only = default_http_only_cookie; dir->merge_env_vars = default_merge_env_vars; dir->env_vars_index_start = default_env_vars_index_start; dir->env_vars_count_in_n = default_env_vars_count_in_n; dir->cond = apr_array_make(p, 0, sizeof(am_cond_t)); dir->cookie_domain = NULL; dir->cookie_path = NULL; dir->cookie_samesite = am_samesite_default; dir->envattr = apr_hash_make(p); dir->env_prefix = default_env_prefix; dir->userattr = default_user_attribute; dir->idpattr = NULL; dir->signature_method = inherit_signature_method; dir->dump_session = default_dump_session; dir->dump_saml_response = default_dump_saml_response; dir->endpoint_path = default_endpoint_path; dir->session_length = -1; /* -1 means use default. */ dir->session_idle_timeout = -1; /* -1 means disabled. */ dir->no_cookie_error_page = NULL; dir->no_success_error_page = NULL; dir->sp_metadata_file = NULL; dir->sp_private_key_file = NULL; dir->sp_cert_file = NULL; dir->idp_metadata = apr_array_make(p, 0, sizeof(am_metadata_t)); dir->idp_public_key_file = NULL; dir->idp_ca_file = NULL; dir->idp_ignore = NULL; dir->login_path = default_login_path; dir->discovery_url = NULL; dir->probe_discovery_timeout = -1; /* -1 means no probe discovery */ dir->probe_discovery_idp = apr_table_make(p, 0); dir->sp_entity_id = NULL; dir->sp_org_name = apr_hash_make(p); dir->sp_org_display_name = apr_hash_make(p); dir->sp_org_url = apr_hash_make(p); apr_thread_mutex_create(&dir->server_mutex, APR_THREAD_MUTEX_DEFAULT, p); dir->inherit_server_from = dir; dir->server = NULL; dir->authn_context_class_ref = apr_array_make(p, 0, sizeof(char *)); dir->authn_context_comparison_type = NULL; dir->subject_confirmation_data_address_check = inherit_subject_confirmation_data_address_check; dir->send_cache_control_header = inherit_send_cache_control_header; dir->do_not_verify_logout_signature = apr_hash_make(p); dir->post_replay = inherit_post_replay; dir->redirect_domains = default_redirect_domains; dir->ecp_send_idplist = inherit_ecp_send_idplist; dir->enabled_invalidation_session = default_enabled_invalidation_session; dir->send_expect_header = default_send_expect_header; return dir; } /* Determine whether this configuration changes anything relevant to the * lasso_server configuration. * * Parameters: * am_dir_cfg_rec *add_cfg The new configuration. * * Returns: * true if we can inherit the lasso_server object, false if not. */ static bool cfg_can_inherit_lasso_server(const am_dir_cfg_rec *add_cfg) { if (add_cfg->endpoint_path != default_endpoint_path) return false; if (add_cfg->sp_metadata_file != NULL || add_cfg->sp_private_key_file != NULL || add_cfg->sp_cert_file != NULL) return false; if (add_cfg->idp_metadata->nelts > 0 || add_cfg->idp_public_key_file != NULL || add_cfg->idp_ca_file != NULL || add_cfg->idp_ignore != NULL) return false; if (apr_hash_count(add_cfg->sp_org_name) > 0 || apr_hash_count(add_cfg->sp_org_display_name) > 0 || apr_hash_count(add_cfg->sp_org_url) > 0) return false; return true; } /* This function merges two am_dir_cfg_rec structures. * It will try to inherit from the base where possible. * * Parameters: * apr_pool_t *p The pool we should allocate memory from. * void *base The original structure. * void *add The structure we should add to base. * * Returns: * The merged structure. */ void *auth_mellon_dir_merge(apr_pool_t *p, void *base, void *add) { am_dir_cfg_rec *base_cfg = (am_dir_cfg_rec *)base; am_dir_cfg_rec *add_cfg = (am_dir_cfg_rec *)add; am_dir_cfg_rec *new_cfg; new_cfg = (am_dir_cfg_rec *)apr_palloc(p, sizeof(*new_cfg)); apr_pool_cleanup_register(p, new_cfg, auth_mellon_free_server, auth_mellon_free_server); new_cfg->enable_mellon = (add_cfg->enable_mellon != am_enable_default ? add_cfg->enable_mellon : base_cfg->enable_mellon); new_cfg->varname = (add_cfg->varname != default_cookie_name ? add_cfg->varname : base_cfg->varname); new_cfg->secure = (add_cfg->secure != default_secure_cookie ? add_cfg->secure : base_cfg->secure); new_cfg->http_only = (add_cfg->http_only != default_http_only_cookie ? add_cfg->http_only : base_cfg->http_only); new_cfg->merge_env_vars = (add_cfg->merge_env_vars != default_merge_env_vars ? add_cfg->merge_env_vars : base_cfg->merge_env_vars); new_cfg->env_vars_index_start = (add_cfg->env_vars_index_start != default_env_vars_index_start ? add_cfg->env_vars_index_start : base_cfg->env_vars_index_start); new_cfg->env_vars_count_in_n = (add_cfg->env_vars_count_in_n != default_env_vars_count_in_n ? add_cfg->env_vars_count_in_n : base_cfg->env_vars_count_in_n); new_cfg->cookie_domain = (add_cfg->cookie_domain != NULL ? add_cfg->cookie_domain : base_cfg->cookie_domain); new_cfg->cookie_path = (add_cfg->cookie_path != NULL ? add_cfg->cookie_path : base_cfg->cookie_path); new_cfg->cookie_samesite = (add_cfg->cookie_samesite != am_samesite_default ? add_cfg->cookie_samesite : base_cfg->cookie_samesite); new_cfg->cond = apr_array_copy(p, (!apr_is_empty_array(add_cfg->cond)) ? add_cfg->cond : base_cfg->cond); new_cfg->envattr = apr_hash_copy(p, (apr_hash_count(add_cfg->envattr) > 0) ? add_cfg->envattr : base_cfg->envattr); new_cfg->env_prefix = (add_cfg->env_prefix != default_env_prefix ? add_cfg->env_prefix : base_cfg->env_prefix); new_cfg->userattr = (add_cfg->userattr != default_user_attribute ? add_cfg->userattr : base_cfg->userattr); new_cfg->idpattr = (add_cfg->idpattr != NULL ? add_cfg->idpattr : base_cfg->idpattr); new_cfg->signature_method = CFG_MERGE(add_cfg, base_cfg, signature_method); new_cfg->dump_session = (add_cfg->dump_session != default_dump_session ? add_cfg->dump_session : base_cfg->dump_session); new_cfg->dump_saml_response = (add_cfg->dump_saml_response != default_dump_saml_response ? add_cfg->dump_saml_response : base_cfg->dump_saml_response); new_cfg->endpoint_path = ( add_cfg->endpoint_path != default_endpoint_path ? add_cfg->endpoint_path : base_cfg->endpoint_path ); new_cfg->session_length = (add_cfg->session_length != -1 ? add_cfg->session_length : base_cfg->session_length); new_cfg->session_idle_timeout = (add_cfg->session_idle_timeout != -1 ? add_cfg->session_idle_timeout : base_cfg->session_idle_timeout); new_cfg->no_cookie_error_page = (add_cfg->no_cookie_error_page != NULL ? add_cfg->no_cookie_error_page : base_cfg->no_cookie_error_page); new_cfg->no_success_error_page = (add_cfg->no_success_error_page != NULL ? add_cfg->no_success_error_page : base_cfg->no_success_error_page); new_cfg->sp_metadata_file = (add_cfg->sp_metadata_file ? add_cfg->sp_metadata_file : base_cfg->sp_metadata_file); new_cfg->sp_private_key_file = (add_cfg->sp_private_key_file ? add_cfg->sp_private_key_file : base_cfg->sp_private_key_file); new_cfg->sp_cert_file = (add_cfg->sp_cert_file ? add_cfg->sp_cert_file : base_cfg->sp_cert_file); new_cfg->idp_metadata = (add_cfg->idp_metadata->nelts ? add_cfg->idp_metadata : base_cfg->idp_metadata); new_cfg->idp_public_key_file = (add_cfg->idp_public_key_file ? add_cfg->idp_public_key_file : base_cfg->idp_public_key_file); new_cfg->idp_ca_file = (add_cfg->idp_ca_file ? add_cfg->idp_ca_file : base_cfg->idp_ca_file); new_cfg->idp_ignore = add_cfg->idp_ignore != NULL ? add_cfg->idp_ignore : base_cfg->idp_ignore; new_cfg->sp_entity_id = (add_cfg->sp_entity_id ? add_cfg->sp_entity_id : base_cfg->sp_entity_id); new_cfg->sp_org_name = apr_hash_copy(p, (apr_hash_count(add_cfg->sp_org_name) > 0) ? add_cfg->sp_org_name : base_cfg->sp_org_name); new_cfg->sp_org_display_name = apr_hash_copy(p, (apr_hash_count(add_cfg->sp_org_display_name) > 0) ? add_cfg->sp_org_display_name : base_cfg->sp_org_display_name); new_cfg->sp_org_url = apr_hash_copy(p, (apr_hash_count(add_cfg->sp_org_url) > 0) ? add_cfg->sp_org_url : base_cfg->sp_org_url); new_cfg->login_path = (add_cfg->login_path != default_login_path ? add_cfg->login_path : base_cfg->login_path); new_cfg->discovery_url = (add_cfg->discovery_url ? add_cfg->discovery_url : base_cfg->discovery_url); new_cfg->probe_discovery_timeout = (add_cfg->probe_discovery_timeout != -1 ? add_cfg->probe_discovery_timeout : base_cfg->probe_discovery_timeout); new_cfg->probe_discovery_idp = apr_table_copy(p, (!apr_is_empty_table(add_cfg->probe_discovery_idp)) ? add_cfg->probe_discovery_idp : base_cfg->probe_discovery_idp); if (cfg_can_inherit_lasso_server(add_cfg)) { new_cfg->inherit_server_from = base_cfg->inherit_server_from; } else { apr_thread_mutex_create(&new_cfg->server_mutex, APR_THREAD_MUTEX_DEFAULT, p); new_cfg->inherit_server_from = new_cfg; } new_cfg->server = NULL; new_cfg->authn_context_class_ref = (add_cfg->authn_context_class_ref->nelts ? add_cfg->authn_context_class_ref : base_cfg->authn_context_class_ref); new_cfg->authn_context_comparison_type = (add_cfg->authn_context_comparison_type != NULL ? add_cfg->authn_context_comparison_type : base_cfg->authn_context_comparison_type); new_cfg->do_not_verify_logout_signature = apr_hash_copy(p, (apr_hash_count(add_cfg->do_not_verify_logout_signature) > 0) ? add_cfg->do_not_verify_logout_signature : base_cfg->do_not_verify_logout_signature); new_cfg->subject_confirmation_data_address_check = CFG_MERGE(add_cfg, base_cfg, subject_confirmation_data_address_check); new_cfg->send_cache_control_header = CFG_MERGE(add_cfg, base_cfg, send_cache_control_header); new_cfg->post_replay = CFG_MERGE(add_cfg, base_cfg, post_replay); new_cfg->ecp_send_idplist = CFG_MERGE(add_cfg, base_cfg, ecp_send_idplist); new_cfg->redirect_domains = (add_cfg->redirect_domains != default_redirect_domains ? add_cfg->redirect_domains : base_cfg->redirect_domains); new_cfg->enabled_invalidation_session = (add_cfg->enabled_invalidation_session != default_enabled_invalidation_session ? add_cfg->enabled_invalidation_session : base_cfg->enabled_invalidation_session); new_cfg->send_expect_header = (add_cfg->send_expect_header != default_send_expect_header ? add_cfg->send_expect_header : base_cfg->send_expect_header); return new_cfg; } /* This function creates a new per-server configuration. * auth_mellon uses the server configuration to store a pointer * to the global module configuration. * * Parameters: * apr_pool_t *p The pool we should allocate memory from. * server_rec *s The server we should add our configuration to. * * Returns: * The new per-server configuration. */ void *auth_mellon_server_config(apr_pool_t *p, server_rec *s) { am_srv_cfg_rec *srv; am_mod_cfg_rec *mod; const char key[] = "auth_mellon_server_config"; srv = apr_palloc(p, sizeof(*srv)); #ifdef ENABLE_DIAGNOSTICS srv->diag_cfg.filename = default_diag_filename; srv->diag_cfg.fd = NULL; srv->diag_cfg.flags = default_diag_flags; srv->diag_cfg.dir_cfg_emitted = apr_table_make(p, 0); #endif /* we want to keeep our global configuration of shared memory and * mutexes, so we try to find it in the userdata before doing anything * else */ apr_pool_userdata_get((void **)&mod, key, p); if (mod) { srv->mc = mod; return srv; } /* the module has not been initiated at all */ mod = apr_palloc(p, sizeof(*mod)); mod->cache_size = 100; /* ought to be enough for everybody */ mod->lock_file = "/var/run/mod_auth_mellon.lock"; mod->post_dir = NULL; mod->post_ttl = post_ttl; mod->post_count = post_count; mod->post_size = post_size; mod->entry_size = AM_CACHE_DEFAULT_ENTRY_SIZE; mod->init_cache_size = 0; mod->init_lock_file = NULL; mod->init_entry_size = 0; mod->cache = NULL; mod->lock = NULL; apr_pool_userdata_set(mod, key, apr_pool_cleanup_null, p); srv->mc = mod; return srv; } /* This function merges two am_srv_cfg_rec structures. * It will try to inherit from the base where possible. * * Parameters: * apr_pool_t *p The pool we should allocate memory from. * void *base The original structure. * void *add The structure we should add to base. * * Returns: * The merged structure. */ void *auth_mellon_srv_merge(apr_pool_t *p, void *base, void *add) { am_srv_cfg_rec *base_cfg = (am_srv_cfg_rec *)base; am_srv_cfg_rec *new_cfg; new_cfg = (am_srv_cfg_rec *)apr_palloc(p, sizeof(*new_cfg)); new_cfg->mc = base_cfg->mc; #ifdef ENABLE_DIAGNOSTICS am_srv_cfg_rec *add_cfg = (am_srv_cfg_rec *)add; new_cfg->diag_cfg.filename = (add_cfg->diag_cfg.filename != default_diag_filename ? add_cfg->diag_cfg.filename : base_cfg->diag_cfg.filename); new_cfg->diag_cfg.fd = NULL; new_cfg->diag_cfg.flags = (add_cfg->diag_cfg.flags != default_diag_flags ? add_cfg->diag_cfg.flags : base_cfg->diag_cfg.flags); new_cfg->diag_cfg.dir_cfg_emitted = apr_table_make(p, 0); #endif return new_cfg; } mod_auth_mellon-0.18.0/auth_mellon_cookie.c000066400000000000000000000205341410105036600207520ustar00rootroot00000000000000/* * * auth_mellon_cookie.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* This function retrieves the name of our cookie. * * Parameters: * request_rec *r The current request. Used to find the identifier of * the cookie. We also allocate memory from r->pool. * * Returns: * The name of the cookie. */ static const char *am_cookie_name(request_rec *r) { am_dir_cfg_rec *dir_cfg; dir_cfg = am_get_dir_cfg(r); return apr_pstrcat(r->pool, "mellon-", dir_cfg->varname, NULL); } /* Calculate the cookie parameters. * * Parameters: * request_rec *r The request we should set the cookie in. * * Returns: * The cookie parameters as a string. */ static const char *am_cookie_params(request_rec *r) { int secure_cookie; int http_only_cookie; const char *cookie_domain = ap_get_server_name(r); const char *cookie_path = "/"; const char *cookie_samesite = ""; const char *env_var_value = NULL; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); if (cfg->cookie_domain) { cookie_domain = cfg->cookie_domain; } if (cfg->cookie_path) { cookie_path = cfg->cookie_path; } if (r->subprocess_env != NULL){ env_var_value = apr_table_get(r->subprocess_env, AM_DISABLE_SAMESITE_ENV_VAR); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "%s : %s", AM_DISABLE_SAMESITE_ENV_VAR, env_var_value); } if (env_var_value == NULL){ if (cfg->cookie_samesite == am_samesite_none || apr_table_get(r->notes, AM_FORCE_SAMESITE_NONE_NOTE) != NULL){ cookie_samesite = "; SameSite=None"; } else if (cfg->cookie_samesite == am_samesite_lax) { cookie_samesite = "; SameSite=Lax"; } else if (cfg->cookie_samesite == am_samesite_strict) { cookie_samesite = "; SameSite=Strict"; } } secure_cookie = cfg->secure; http_only_cookie = cfg->http_only; return apr_psprintf(r->pool, "Version=1; Path=%s; Domain=%s%s%s%s", cookie_path, cookie_domain, http_only_cookie ? "; HttpOnly" : "", secure_cookie ? "; secure" : "", cookie_samesite); } /* This functions finds the value of our cookie. * * Parameters: * request_rec *r The request we should find the cookie in. * * Returns: * The value of the cookie, or NULL if we don't find the cookie. */ const char *am_cookie_get(request_rec *r) { am_req_cfg_rec *req_cfg; const char *name; const char *value; const char *cookie; char *buffer, *end; /* don't run for subrequests */ if (r->main) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "cookie_get: Subrequest, so return NULL"); return NULL; } /* Check if we have added a note on the current request. */ req_cfg = am_get_req_cfg(r); value = req_cfg->cookie_value; if(value != NULL) { return value; } name = am_cookie_name(r); cookie = apr_table_get(r->headers_in, "Cookie"); if(cookie == NULL) { return NULL; } for(value = ap_strstr_c(cookie, name); value != NULL; value = ap_strstr_c(value + 1, name)) { if(value != cookie) { /* value isn't pointing to the start of the string. */ switch(value[-1]) { /* We allow the name in the cookie-string to be * preceeded by [\t; ]. Note that only ' ' should be used * by browsers. We test against the others just to be sure. */ case '\t': case ';': case ' ': break; default: /* value isn't preceeded by one of the listed characters, and * therefore we assume that it is part of another cookie. */ continue; /* Search for the next instance of the name. */ } } if(value[strlen(name)] != '=') { /* We don't have an equal-sign right after the name. Therefore we * assume that what we have matched is only part of a longer name. * We continue searching. */ continue; } /* Now we have something that matches /[^ ,\t]=/. The value * (following the equal-sign) can be found at value + strlen(name) + 1. */ value += strlen(name) + 1; /* The cookie value may be double-quoted. */ if(*value == '"') { value += 1; } buffer = apr_pstrdup(r->pool, value); end = strchr(buffer, '"'); if(end) { /* Double-quoted string. */ *end = '\0'; } end = strchr(buffer, ';'); if(end) { *end = '\0'; } return buffer; } /* We didn't find the cookie. */ return NULL; } /* This function sets the value of our cookie. * * Parameters: * request_rec *r The request we should set the cookie in. * const char *id The value ve should store in the cookie. * * Returns: * Nothing. */ void am_cookie_set(request_rec *r, const char *id) { am_req_cfg_rec *req_cfg; const char *name; const char *cookie_params; char *cookie; if (id == NULL) return; name = am_cookie_name(r); cookie_params = am_cookie_params(r); cookie = apr_psprintf(r->pool, "%s=%s; %s", name, id, cookie_params); ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, r->server, "cookie_set: %s", cookie); /* Setting the headers inn err_headers_out ensures that they will be * sent for all responses. */ apr_table_addn(r->err_headers_out, "Set-Cookie", cookie); /* Add a note on the current request, to allow us to retrieve this * cookie in the current request. */ req_cfg = am_get_req_cfg(r); req_cfg->cookie_value = apr_pstrdup(r->pool, id); } /* This function deletes the cookie. * * Parameters: * request_rec *r The request we should clear the cookie in. We will * allocate any neccesary memory from r->pool. * * Returns: * Nothing. */ void am_cookie_delete(request_rec *r) { const char *name; const char *cookie_params; char *cookie; name = am_cookie_name(r); cookie_params = am_cookie_params(r); /* Format a cookie. To delete a cookie we set the expires-timestamp * to the past. */ cookie = apr_psprintf(r->pool, "%s=NULL;" " expires=Thu, 01-Jan-1970 00:00:00 GMT;" " %s", name, cookie_params); apr_table_addn(r->err_headers_out, "Set-Cookie", cookie); } /* Get string that is used to tie a session to a specific cookie. * * request_rec *r The current request. * Returns: * The cookie token, as a fixed length byte buffer. */ const char *am_cookie_token(request_rec *r) { const char *cookie_name = am_cookie_name(r); const char *cookie_domain = ap_get_server_name(r); const char *cookie_path = "/"; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); if (cfg->cookie_domain) { cookie_domain = cfg->cookie_domain; } if (cfg->cookie_path) { cookie_path = cfg->cookie_path; } return apr_psprintf(r->pool, "Name='%s' Domain='%s' Path='%s'", cookie_name, cookie_domain, cookie_path ); } mod_auth_mellon-0.18.0/auth_mellon_diagnostics.c000066400000000000000000001167171410105036600220210ustar00rootroot00000000000000#include "auth_mellon.h" #ifdef ENABLE_DIAGNOSTICS #if APR_HAVE_UNISTD_H #include #endif #if APR_HAVE_PROCESS_H #include /* for getpid() on Win32 */ #endif /*============================= Internal Static ==============================*/ /*------------------ Defines ------------------*/ #define AM_DIAG_ENABLED(diag_cfg) \ (diag_cfg && diag_cfg->fd && (diag_cfg->flags & AM_DIAG_FLAG_ENABLED)) /*------------------ Typedefs ------------------*/ typedef struct iter_callback_data { apr_file_t *diag_fd; int level; } iter_callback_data; /*------------------ Prototypes ------------------*/ static const char * indent(int level); static void write_indented_text(apr_file_t *diag_fd, int level, const char* text); static void am_diag_format_line(apr_pool_t *pool, apr_file_t *diag_fd, int level, const char *fmt, va_list ap); static const char * am_diag_cond_flag_str(request_rec *r, am_cond_flag_t flags); static const char * am_diag_enable_str(request_rec *r, am_enable_t enable); static const char * am_diag_samesite_str(request_rec *r, am_samesite_t samesite); static const char * am_diag_httpd_error_level_str(request_rec *r, int level); static const char * am_diag_signature_method_str(request_rec *r, LassoSignatureMethod signature_method); static apr_size_t am_diag_time_t_to_8601_buf(char *buf, apr_size_t buf_size, apr_time_t t); static int am_diag_open_log(server_rec *s, apr_pool_t *p); static int am_table_count(void *rec, const char *key, const char *value); static int log_headers(void *rec, const char *key, const char *value); static int log_probe_discovery_idp(void *rec, const char *key, const char *value); static void am_diag_log_dir_cfg(request_rec *r, int level, am_dir_cfg_rec *cfg, const char *fmt, ...) __attribute__((format(printf,4,5))); static bool am_diag_initialize_req(request_rec *r, am_diag_cfg_rec *diag_cfg, am_req_cfg_rec *req_cfg); /*------------------ Functions ------------------*/ static const char * indent(int level) { static const char * const indents[] = { "", /* 0 */ " ", /* 1 */ " ", /* 2 */ " ", /* 3 */ " ", /* 4 */ " ", /* 5 */ " ", /* 6 */ " ", /* 7 */ " ", /* 8 */ " ", /* 9 */ }; int n_indents = sizeof(indents)/sizeof(indents[0]); if (level < 0) { return ""; } if (level < n_indents) { return indents[level]; } return indents[n_indents-1]; } static void write_indented_text(apr_file_t *diag_fd, int level, const char* text) { const char *start, *end, *prefix; size_t len, prefix_len; bool crlf = false; if (!text) return; prefix = indent(level); prefix_len = strlen(prefix); start = end = text; while (*end) { /* find end of line */ for (; *end && *end != '\n'; end++); if (*end == '\n') { /* was this a crlf sequence? */ if (end > text && end[-1] == '\r') crlf = true; /* advance past line ending */ end += 1; } /* length of line including line ending */ len = end - start; /* write indent prefix */ apr_file_write_full(diag_fd, prefix, prefix_len, NULL); /* write line including line ending */ apr_file_write_full(diag_fd, start, len, NULL); /* begin again where we left off */ start = end; } /* always write a trailing line ending */ if (end > text && end[-1] != '\n') { if (crlf) { apr_file_write_full(diag_fd, "\r\n", 2, NULL); } else { apr_file_write_full(diag_fd, "\n", 1, NULL); } } } static void am_diag_format_line(apr_pool_t *pool, apr_file_t *diag_fd, int level, const char *fmt, va_list ap) { char * buf = NULL; apr_size_t buf_len; if (fmt) { buf = apr_pvsprintf(pool, fmt, ap); buf_len = strlen(buf); if (buf_len > 0) { const char *prefix = indent(level); apr_size_t prefix_len = strlen(prefix); apr_file_write_full(diag_fd, prefix, prefix_len, NULL); apr_file_write_full(diag_fd, buf, buf_len, NULL); apr_file_putc('\n', diag_fd); } } } static const char * am_diag_cond_flag_str(request_rec *r, am_cond_flag_t flags) { char *str; char *comma; str = apr_pstrcat(r->pool, "[", flags & AM_COND_FLAG_OR ? "OR," : "", flags & AM_COND_FLAG_NOT ? "NOT," : "", flags & AM_COND_FLAG_REG ? "REG," : "", flags & AM_COND_FLAG_NC ? "NC," : "", flags & AM_COND_FLAG_MAP ? "MAP," : "", flags & AM_COND_FLAG_REF ? "REF," : "", flags & AM_COND_FLAG_SUB ? "SUB," : "", flags & AM_COND_FLAG_IGN ? "IGN," : "", flags & AM_COND_FLAG_REQ ? "REQ," : "", flags & AM_COND_FLAG_FSTR ? "FSTR," : "", "]", NULL); /* replace trailing ",]" with "]" */ comma = rindex(str, ','); if (comma) { *comma = ']'; *(comma+1) = 0; } return str; } static const char * am_diag_enable_str(request_rec *r, am_enable_t enable) { switch(enable) { case am_enable_default: return "default"; case am_enable_off: return "off"; case am_enable_info: return "info"; case am_enable_auth: return "auth"; default: return apr_psprintf(r->pool, "unknown (%d)", enable); } } static const char * am_diag_samesite_str(request_rec *r, am_samesite_t samesite) { switch(samesite) { case am_samesite_default: return "default"; case am_samesite_lax: return "lax"; case am_samesite_strict: return "strict"; case am_samesite_none: return "none"; default: return apr_psprintf(r->pool, "unknown (%d)", samesite); } } static const char * am_diag_httpd_error_level_str(request_rec *r, int level) { switch(level) { case APLOG_EMERG: return "APLOG_EMERG"; case APLOG_ALERT: return "APLOG_ALERT"; case APLOG_CRIT: return "APLOG_CRIT"; case APLOG_ERR: return "APLOG_ERR"; case APLOG_WARNING: return "APLOG_WARNING"; case APLOG_NOTICE: return "APLOG_NOTICE"; case APLOG_INFO: return "APLOG_INFO"; case APLOG_DEBUG: return "APLOG_DEBUG"; case APLOG_TRACE1: return "APLOG_TRACE1"; case APLOG_TRACE2: return "APLOG_TRACE2"; case APLOG_TRACE3: return "APLOG_TRACE3"; case APLOG_TRACE4: return "APLOG_TRACE4"; case APLOG_TRACE5: return "APLOG_TRACE5"; case APLOG_TRACE6: return "APLOG_TRACE6"; case APLOG_TRACE7: return "APLOG_TRACE7"; case APLOG_TRACE8: return "APLOG_TRACE8"; default: return apr_psprintf(r->pool, "APLOG_%d", level); } } static const char * am_diag_signature_method_str(request_rec *r, LassoSignatureMethod signature_method) { switch(signature_method) { case LASSO_SIGNATURE_METHOD_RSA_SHA1: return "rsa-sha1"; #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA256 case LASSO_SIGNATURE_METHOD_RSA_SHA256: return "rsa-sha256"; #endif #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA384 case LASSO_SIGNATURE_METHOD_RSA_SHA384: return "rsa-sha384"; #endif #if HAVE_DECL_LASSO_SIGNATURE_METHOD_RSA_SHA512 case LASSO_SIGNATURE_METHOD_RSA_SHA512: return "rsa-sha512"; #endif default: return apr_psprintf(r->pool, "unknown (%d)", signature_method); } } static apr_size_t am_diag_time_t_to_8601_buf(char *buf, apr_size_t buf_size, apr_time_t t) { apr_size_t ret_size; apr_time_exp_t tm; const char fmt[] = "%FT%TZ"; apr_time_exp_gmt(&tm, t); apr_strftime(buf, &ret_size, buf_size, fmt, &tm); /* on errror assure string is null terminated */ if (ret_size == 0) buf[0] = 0; return ret_size; } static int am_diag_open_log(server_rec *s, apr_pool_t *p) { const char *server_name = NULL; const char *server_desc = NULL; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(s); /* Build the ServerName as it would appear in the ServerName directive */ if (s->server_scheme) { server_name = apr_psprintf(p, "%s://%s", s->server_scheme, s->server_hostname); } else { server_name = apr_psprintf(p, "%s", s->server_hostname); } if (s->port) { server_name = apr_psprintf(p, "%s:%u", server_name, s->port); } if (s->is_virtual) { server_desc = apr_psprintf(p, "virtual server %s:%d (%s:%u)" " ServerName=%s", s->addrs->virthost, s->addrs->host_port, s->defn_name, s->defn_line_number, server_name); } else { server_desc = apr_psprintf(p, "main server, ServerName=%s", server_name); } if (!(diag_cfg->flags & AM_DIAG_FLAG_ENABLED)) { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, "mellon diagnostics disabled for %s", server_desc); return 1; } else { ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, s, "mellon diagnostics enabled for %s, " "diagnostics filename=%s", server_desc, diag_cfg->filename); } if (!diag_cfg->filename || diag_cfg->fd) return 1; if (*diag_cfg->filename == '|') { piped_log *pl; const char *pname = ap_server_root_relative(p, diag_cfg->filename + 1); pl = ap_open_piped_log(p, pname); if (pl == NULL) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "couldn't spawn mellon diagnostics log pipe %s", diag_cfg->filename); return 0; } diag_cfg->fd = ap_piped_log_write_fd(pl); } else { const char *fname = ap_server_root_relative(p, diag_cfg->filename); apr_status_t rv; if ((rv = apr_file_open(&diag_cfg->fd, fname, APR_WRITE | APR_APPEND | APR_CREATE, APR_OS_DEFAULT, p)) != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, "could not open mellon diagnostics log file %s.", fname); return 0; } } return 1; } static int am_table_count(void *rec, const char *key, const char *value) { int *n_items = (int *)rec; (*n_items)++; return 1; } static int log_headers(void *rec, const char *key, const char *value) { iter_callback_data *iter_data = (iter_callback_data *)rec; apr_file_printf(iter_data->diag_fd, "%s%s: %s\n", indent(iter_data->level), key, value); return 1; } static int log_probe_discovery_idp(void *rec, const char *key, const char *value) { iter_callback_data *iter_data = (iter_callback_data *)rec; apr_file_printf(iter_data->diag_fd, "%s%s: %s\n", indent(iter_data->level), key, value); return 1; } static void am_diag_log_dir_cfg(request_rec *r, int level, am_dir_cfg_rec *cfg, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); int i, n_items; apr_hash_index_t *hash_item; GList *list_item; iter_callback_data iter_data; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; iter_data.diag_fd = diag_cfg->fd; iter_data.level = level+1; va_start(ap, fmt); am_diag_format_line(r->pool, diag_cfg->fd, level, fmt, ap); va_end(ap); if (!cfg) { apr_file_flush(diag_cfg->fd); return; } apr_file_printf(diag_cfg->fd, "%sMellonEnable (enable): %s\n", indent(level+1), am_diag_enable_str(r, cfg->enable_mellon)); apr_file_printf(diag_cfg->fd, "%sMellonVariable (varname): %s\n", indent(level+1), cfg->varname); apr_file_printf(diag_cfg->fd, "%sMellonSecureCookie (secure): %s\n", indent(level+1), cfg->secure ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonSecureCookie (httpd_only): %s\n", indent(level+1), cfg->http_only ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonMergeEnvVars (merge_env_vars): %s\n", indent(level+1), cfg->merge_env_vars); apr_file_printf(diag_cfg->fd, "%sMellonEnvVarsIndexStart (env_vars_index_start): %d\n", indent(level+1), cfg->env_vars_index_start); apr_file_printf(diag_cfg->fd, "%sMellonEnvVarsSetCount (env_vars_count_in_n): %s\n", indent(level+1), cfg->env_vars_count_in_n ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonCookieDomain (cookie_domain): %s\n", indent(level+1), cfg->cookie_domain); apr_file_printf(diag_cfg->fd, "%sMellonCookiePath (cookie_path): %s\n", indent(level+1), cfg->cookie_path); apr_file_printf(diag_cfg->fd, "%sMellonCookieSameSite (cookie_samesite): %s\n", indent(level+1), am_diag_samesite_str(r, cfg->cookie_samesite)); apr_file_printf(diag_cfg->fd, "%sMellonEnvPrefix (env_prefix): %s\n", indent(level+1), cfg->env_prefix); apr_file_printf(diag_cfg->fd, "%sMellonCond (cond): %d items\n", indent(level+1), cfg->cond->nelts); for (i = 0; i < cfg->cond->nelts; i++) { const am_cond_t *cond = &((am_cond_t *)(cfg->cond->elts))[i]; apr_file_printf(diag_cfg->fd, "%s[%2d]: %s\n", indent(level+2), i, am_diag_cond_str(r, cond)); } apr_file_printf(diag_cfg->fd, "%sMellonSetEnv (envattr): %u items\n", indent(level+1), apr_hash_count(cfg->envattr)); for (hash_item = apr_hash_first(r->pool, cfg->envattr); hash_item; hash_item = apr_hash_next(hash_item)) { const char *key; const am_envattr_conf_t *envattr_conf; const char *name; apr_hash_this(hash_item, (void *)&key, NULL, (void *)&envattr_conf); if (envattr_conf->prefixed) { name = apr_pstrcat(r->pool, cfg->env_prefix, envattr_conf->name, NULL); } else { name = envattr_conf->name; } apr_file_printf(diag_cfg->fd, "%s%s ==> %s\n", indent(level+2), key, name); } apr_file_printf(diag_cfg->fd, "%sMellonUser (userattr): %s\n", indent(level+1), cfg->userattr); apr_file_printf(diag_cfg->fd, "%sMellonIdP (idpattr): %s\n", indent(level+1), cfg->idpattr); apr_file_printf(diag_cfg->fd, "%sMellonSessionDump (dump_session): %s\n", indent(level+1), cfg->dump_session ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonSamlResponseDump (dump_saml_response): %s\n", indent(level+1), cfg->dump_saml_response ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonEndpointPath (endpoint_path): %s\n", indent(level+1), cfg->endpoint_path); am_diag_log_file_data(r, level+1, cfg->sp_metadata_file, "MellonSPMetadataFile (sp_metadata_file):"); am_diag_log_file_data(r, level+1, cfg->sp_private_key_file, "MellonSPPrivateKeyFile (sp_private_key_file):"); am_diag_log_file_data(r, level+1, cfg->sp_cert_file, "MellonSPCertFile (sp_cert_file):"); am_diag_log_file_data(r, level+1, cfg->idp_public_key_file, "MellonIdPPublicKeyFile (idp_public_key_file):"); am_diag_log_file_data(r, level+1, cfg->idp_ca_file, "MellonIdPCAFile (idp_ca_file):"); apr_file_printf(diag_cfg->fd, "%sMellonIdPMetadataFile (idp_metadata): %d items\n", indent(level+1), cfg->idp_metadata->nelts); for (i = 0; i < cfg->idp_metadata->nelts; i++) { const am_metadata_t *idp_metadata; idp_metadata = &(((const am_metadata_t*)cfg->idp_metadata->elts)[i]); am_diag_log_file_data(r, level+1, idp_metadata->metadata, "[%2d] Metadata", i); am_diag_log_file_data(r, level+1, idp_metadata->chain, "[%2d] Chain File", i); } apr_file_printf(diag_cfg->fd, "%sMellonIdPIgnore (idp_ignore):\n", indent(level+1)); for (list_item = cfg->idp_ignore, i = 0; list_item; list_item = g_list_next(list_item), i++) { apr_file_printf(diag_cfg->fd, "%s[%2d]: %s\n", indent(level+2), i, (char *)list_item->data); } apr_file_printf(diag_cfg->fd, "%sMellonSPentityId (sp_entity_id): %s\n", indent(level+1), cfg->sp_entity_id); apr_file_printf(diag_cfg->fd, "%sMellonOrganizationName (sp_org_name): %u items\n", indent(level+1), apr_hash_count(cfg->sp_org_name)); for (hash_item = apr_hash_first(r->pool, cfg->sp_org_name); hash_item; hash_item = apr_hash_next(hash_item)) { const char *lang; const char *value; apr_hash_this(hash_item, (void *)&lang, NULL, (void *)&value); apr_file_printf(diag_cfg->fd, "%s(lang=%s): %s\n", indent(level+2), lang, value); } apr_file_printf(diag_cfg->fd, "%sMellonOrganizationDisplayName (sp_org_display_name):" " %u items\n", indent(level+1), apr_hash_count(cfg->sp_org_display_name)); for (hash_item = apr_hash_first(r->pool, cfg->sp_org_display_name); hash_item; hash_item = apr_hash_next(hash_item)) { const char *lang; const char *value; apr_hash_this(hash_item, (void *)&lang, NULL, (void *)&value); apr_file_printf(diag_cfg->fd, "%s(lang=%s): %s\n", indent(level+2), lang, value); } apr_file_printf(diag_cfg->fd, "%sMellonOrganizationURL (sp_org_url): %u items\n", indent(level+1), apr_hash_count(cfg->sp_org_url)); for (hash_item = apr_hash_first(r->pool, cfg->sp_org_url); hash_item; hash_item = apr_hash_next(hash_item)) { const char *lang; const char *value; apr_hash_this(hash_item, (void *)&lang, NULL, (void *)&value); apr_file_printf(diag_cfg->fd, "%s(lang=%s): %s\n", indent(level+2), lang, value); } apr_file_printf(diag_cfg->fd, "%sMellonSessionLength (session_length): %d\n", indent(level+1), cfg->session_length); apr_file_printf(diag_cfg->fd, "%sMellonSessionIdleTimeout (session_idle_timeout): %d\n", indent(level+1), cfg->session_idle_timeout); apr_file_printf(diag_cfg->fd, "%sMellonNoCookieErrorPage (no_cookie_error_page): %s\n", indent(level+1), cfg->no_cookie_error_page); apr_file_printf(diag_cfg->fd, "%sMellonNoSuccessErrorPage (no_success_error_page): %s\n", indent(level+1), cfg->no_success_error_page); apr_file_printf(diag_cfg->fd, "%sMellonDefaultLoginPath (login_path): %s\n", indent(level+1), cfg->login_path); apr_file_printf(diag_cfg->fd, "%sMellonDiscoveryURL (discovery_url): %s\n", indent(level+1), cfg->discovery_url); apr_file_printf(diag_cfg->fd, "%sMellonProbeDiscoveryTimeout (probe_discovery_timeout):" " %d\n", indent(level+1), cfg->probe_discovery_timeout); n_items = 0; apr_table_do(am_table_count, &n_items, cfg->probe_discovery_idp, NULL); apr_file_printf(diag_cfg->fd, "%sMellonProbeDiscoveryIdP (probe_discovery_idp):" " %d items\n", indent(level+1), n_items); apr_table_do(log_probe_discovery_idp, &iter_data, cfg->probe_discovery_idp, NULL); apr_file_printf(diag_cfg->fd, "%sMellonAuthnContextClassRef (authn_context_class_ref):" " %d items\n", indent(level+1), cfg->authn_context_class_ref->nelts); for(i = 0; i < cfg->authn_context_class_ref->nelts; i++) { const char *context_class; context_class = APR_ARRAY_IDX(cfg->authn_context_class_ref, i, char *); apr_file_printf(diag_cfg->fd, "%s[%2d]: %s\n", indent(level+2), i, context_class); } apr_file_printf(diag_cfg->fd, "%sMellonAuthnContextComparisonType (authn_context_comparison_type): %s\n", indent(level+1), cfg->authn_context_comparison_type); apr_file_printf(diag_cfg->fd, "%sMellonSubjectConfirmationDataAddressCheck" " (subject_confirmation_data_address_check): %s\n", indent(level+1), CFG_VALUE(cfg, subject_confirmation_data_address_check) ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonDoNotVerifyLogoutSignature" " (do_not_verify_logout_signature): %u items\n", indent(level+1), apr_hash_count(cfg->do_not_verify_logout_signature)); for (hash_item = apr_hash_first(r->pool, cfg->do_not_verify_logout_signature); hash_item; hash_item = apr_hash_next(hash_item)) { const char *entity_id; apr_hash_this(hash_item, (void *)&entity_id, NULL, NULL); apr_file_printf(diag_cfg->fd, "%s%s\n", indent(level+2), entity_id); } apr_file_printf(diag_cfg->fd, "%sMellonSendCacheControlHeader" " (send_cache_control_header): %s\n", indent(level+1), CFG_VALUE(cfg, send_cache_control_header) ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonPostReplay (post_replay): %s\n", indent(level+1), CFG_VALUE(cfg, post_replay) ? "On":"Off"); apr_file_printf(diag_cfg->fd, "%sMellonECPSendIDPList (ecp_send_idplist): %s\n", indent(level+1), CFG_VALUE(cfg, ecp_send_idplist) ? "On":"Off"); for (n_items = 0; cfg->redirect_domains[n_items] != NULL; n_items++); apr_file_printf(diag_cfg->fd, "%sMellonRedirectDomains (redirect_domains): %d items\n", indent(level+1), n_items); for (i = 0; cfg->redirect_domains[i] != NULL; i++) { apr_file_printf(diag_cfg->fd, "%s%s\n", indent(level+2), cfg->redirect_domains[i]); } apr_file_printf(diag_cfg->fd, "%sMellonSignatureMethod (signature_method): %s\n", indent(level+1), am_diag_signature_method_str(r, CFG_VALUE(cfg, signature_method))); apr_file_flush(diag_cfg->fd); } static bool am_diag_initialize_req(request_rec *r, am_diag_cfg_rec *diag_cfg, am_req_cfg_rec *req_cfg) { server_rec *s = r->server; am_dir_cfg_rec *dir_cfg; apr_os_thread_t tid = apr_os_thread_current(); iter_callback_data iter_data; int level = 0; if (!diag_cfg) return false; if (!diag_cfg->fd) return false; if (!req_cfg) return false; if (req_cfg->diag_emitted) return true; iter_data.diag_fd = diag_cfg->fd; iter_data.level = level+1; apr_file_puts("---------------------------------- New Request" " ---------------------------------\n", diag_cfg->fd); apr_file_printf(diag_cfg->fd, "%s - %s\n", r->method, r->uri); apr_file_printf(diag_cfg->fd, "log_id: %s\n", r->log_id); apr_file_printf(diag_cfg->fd, "server: scheme=%s hostname=%s port=%d\n", s->server_scheme, s->server_hostname, s->port); apr_file_printf(diag_cfg->fd, "pid: %" APR_PID_T_FMT ", tid: %pT\n", getpid(), &tid); apr_file_printf(diag_cfg->fd, "unparsed_uri: %s\n", r->unparsed_uri); apr_file_printf(diag_cfg->fd, "uri: %s\n", r->uri); apr_file_printf(diag_cfg->fd, "path_info: %s\n", r->path_info); apr_file_printf(diag_cfg->fd, "filename: %s\n", r->filename); apr_file_printf(diag_cfg->fd, "query args: %s\n", r->args); apr_file_printf(diag_cfg->fd, "Request Headers:\n"); apr_table_do(log_headers, &iter_data, r->headers_in, NULL); req_cfg->diag_emitted = true; /* Only emit directory configuration once */ if (!apr_table_get(diag_cfg->dir_cfg_emitted, r->uri)) { dir_cfg = am_get_dir_cfg(r); am_diag_log_dir_cfg(r, level, dir_cfg, "Mellon Directory Configuration for URL: %s", r->uri); apr_table_set(diag_cfg->dir_cfg_emitted, r->uri, "1"); } return true; } /*=============================== Public API =================================*/ int am_diag_log_init(apr_pool_t *pc, apr_pool_t *p, apr_pool_t *pt, server_rec *s) { for ( ; s ; s = s->next) { if (!am_diag_open_log(s, p)) { return HTTP_INTERNAL_SERVER_ERROR; } } return OK; } int am_diag_finalize_request(request_rec *r) { am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); int level = 0; iter_callback_data iter_data; if (!AM_DIAG_ENABLED(diag_cfg)) return OK; if (!req_cfg) return OK; if (!req_cfg->diag_emitted) return OK; iter_data.diag_fd = diag_cfg->fd; iter_data.level = level+1; apr_file_puts("\n=== Response ===\n", diag_cfg->fd); apr_file_printf(diag_cfg->fd, "Status: %s(%d)\n", r->status_line, r->status); apr_file_printf(diag_cfg->fd, "user: %s auth_type=%s\n", r->user, r->ap_auth_type); apr_file_printf(diag_cfg->fd, "Response Headers:\n"); apr_table_do(log_headers, &iter_data, r->headers_out, NULL); apr_file_printf(diag_cfg->fd, "Response Error Headers:\n"); apr_table_do(log_headers, &iter_data, r->err_headers_out, NULL); apr_file_printf(diag_cfg->fd, "Environment:\n"); apr_table_do(log_headers, &iter_data, r->subprocess_env, NULL); return OK; } char * am_diag_time_t_to_8601(request_rec *r, apr_time_t t) { char *buf; buf = apr_palloc(r->pool, ISO_8601_BUF_SIZE); if (!buf) return NULL; am_diag_time_t_to_8601_buf(buf, ISO_8601_BUF_SIZE, t); return buf; } const char * am_diag_cond_str(request_rec *r, const am_cond_t *cond) { return apr_psprintf(r->pool, "varname=\"%s\" flags=%s str=\"%s\" directive=\"%s\"", cond->varname, am_diag_cond_flag_str(r, cond->flags), cond->str, cond->directive); } const char * am_diag_cache_key_type_str(am_cache_key_t key_type) { switch(key_type) { case AM_CACHE_SESSION: return "session"; case AM_CACHE_NAMEID: return "name id"; case AM_CACHE_ASSERTIONID: return "assertion id"; default: return "unknown"; } } const char * am_diag_lasso_http_method_str(LassoHttpMethod http_method) { switch(http_method) { case LASSO_HTTP_METHOD_NONE: return "LASSO_HTTP_METHOD_NONE"; case LASSO_HTTP_METHOD_ANY: return "LASSO_HTTP_METHOD_ANY"; case LASSO_HTTP_METHOD_IDP_INITIATED: return "LASSO_HTTP_METHOD_IDP_INITIATED"; case LASSO_HTTP_METHOD_GET: return "LASSO_HTTP_METHOD_GET"; case LASSO_HTTP_METHOD_POST: return "LASSO_HTTP_METHOD_POST"; case LASSO_HTTP_METHOD_REDIRECT: return "LASSO_HTTP_METHOD_REDIRECT"; case LASSO_HTTP_METHOD_SOAP: return "LASSO_HTTP_METHOD_SOAP"; case LASSO_HTTP_METHOD_ARTIFACT_GET: return "LASSO_HTTP_METHOD_ARTIFACT_GET"; case LASSO_HTTP_METHOD_ARTIFACT_POST: return "LASSO_HTTP_METHOD_ARTIFACT_POST"; case LASSO_HTTP_METHOD_PAOS: return "LASSO_HTTP_METHOD_PAOS"; default: return "unknown"; } } void am_diag_printf(request_rec *r, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); char *buf; apr_size_t buf_len; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; va_start(ap, fmt); buf = apr_pvsprintf(r->pool, fmt, ap); va_end(ap); buf_len = strlen(buf); if (buf_len > 0) { apr_file_write_full(diag_cfg->fd, buf, buf_len, NULL); } apr_file_flush(diag_cfg->fd); } void am_diag_rerror(const char *file, int line, int module_index, int level, apr_status_t status, request_rec *r, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); char *buf; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; buf = apr_psprintf(r->pool, "[%s %s:%d] ", am_diag_httpd_error_level_str(r, level), file, line); apr_file_puts(buf, diag_cfg->fd); va_start(ap, fmt); buf = apr_pvsprintf(r->pool, fmt, ap); va_end(ap); apr_file_puts(buf, diag_cfg->fd); apr_file_puts(APR_EOL_STR, diag_cfg->fd); apr_file_flush(diag_cfg->fd); } void am_diag_log_lasso_node(request_rec *r, int level, LassoNode *node, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); gchar *xml = NULL; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; va_start(ap, fmt); am_diag_format_line(r->pool, diag_cfg->fd, level, fmt, ap); va_end(ap); if (node) { xml = lasso_node_debug(node, 0); write_indented_text(diag_cfg->fd, level+1, xml); lasso_release_string(xml); } else { apr_file_printf(diag_cfg->fd, "%snode is NULL\n", indent(level+1)); } apr_file_flush(diag_cfg->fd); } void am_diag_log_file_data(request_rec *r, int level, am_file_data_t *file_data, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; va_start(ap, fmt); am_diag_format_line(r->pool, diag_cfg->fd, level, fmt, ap); va_end(ap); if (file_data) { if (file_data->generated) { apr_file_printf(diag_cfg->fd, "%sGenerated file contents:\n", indent(level+1)); write_indented_text(diag_cfg->fd, level+2, file_data->contents); } else { apr_file_printf(diag_cfg->fd, "%spathname: \"%s\"\n", indent(level+1), file_data->path); if (!file_data->read_time) { am_file_read(file_data); } if (file_data->rv == APR_SUCCESS) { write_indented_text(diag_cfg->fd, level+2, file_data->contents); } else { apr_file_printf(diag_cfg->fd, "%s%s\n", indent(level+1), file_data->strerror); } } } else { apr_file_printf(diag_cfg->fd, "%sfile_data: NULL\n", indent(level+1)); } apr_file_flush(diag_cfg->fd); } void am_diag_log_saml_status_response(request_rec *r, int level, LassoNode *node, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); LassoSamlp2StatusResponse *response = (LassoSamlp2StatusResponse*)node; LassoSamlp2Status *status = NULL; const char *status_code1 = NULL; const char *status_code2 = NULL; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; va_start(ap, fmt); am_diag_format_line(r->pool, diag_cfg->fd, level, fmt, ap); va_end(ap); if (response == NULL) { apr_file_printf(diag_cfg->fd, "%sresponse is NULL\n", indent(level+1)); return; } if (!LASSO_IS_SAMLP2_STATUS_RESPONSE(response)) { apr_file_printf(diag_cfg->fd, "%sERROR, expected LassoSamlp2StatusResponse " "but got %s\n", indent(level+1), lasso_node_get_name((LassoNode*)response)); return; } status = response->Status; if (status == NULL || !LASSO_IS_SAMLP2_STATUS(status) || status->StatusCode == NULL || status->StatusCode->Value == NULL) { apr_file_printf(diag_cfg->fd, "%sStatus missing\n", indent(level+1)); return; } status_code1 = status->StatusCode->Value; if (status->StatusCode->StatusCode) { status_code2 = status->StatusCode->StatusCode->Value; } apr_file_printf(diag_cfg->fd, "%sID: %s\n", indent(level+1), response->ID); apr_file_printf(diag_cfg->fd, "%sInResponseTo: %s\n", indent(level+1), response->InResponseTo); apr_file_printf(diag_cfg->fd, "%sVersion: %s\n", indent(level+1), response->Version); apr_file_printf(diag_cfg->fd, "%sIssueInstant: %s\n", indent(level+1), response->IssueInstant); apr_file_printf(diag_cfg->fd, "%sConsent: %s\n", indent(level+1), response->Consent); apr_file_printf(diag_cfg->fd, "%sIssuer: %s\n", indent(level+1), response->Issuer->content); apr_file_printf(diag_cfg->fd, "%sDestination: %s\n", indent(level+1), response->Destination); apr_file_printf(diag_cfg->fd, "%sStatus:\n", indent(level+1)); apr_file_printf(diag_cfg->fd, "%sTop Level Status code: %s\n", indent(level+2), status_code1); apr_file_printf(diag_cfg->fd, "%s2nd Level Status code: %s\n", indent(level+2), status_code2); apr_file_printf(diag_cfg->fd, "%sStatus Message: %s\n", indent(level+2), status->StatusMessage); am_diag_log_lasso_node(r, level+2, (LassoNode*)status->StatusDetail, "Status Detail:"); return; } void am_diag_log_profile(request_rec *r, int level, LassoProfile *profile, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); LassoSession *session = lasso_profile_get_session(profile); GList *assertions = lasso_session_get_assertions(session, NULL); GList *iter = NULL; int i; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; va_start(ap, fmt); am_diag_format_line(r->pool, diag_cfg->fd, level, fmt, ap); va_end(ap); if (profile) { apr_file_printf(diag_cfg->fd, "%sProfile Type: %s\n", indent(level+1), G_OBJECT_TYPE_NAME(profile)); for (iter = assertions, i=0; iter != NULL; iter = g_list_next(iter), i++) { LassoSaml2Assertion *assertion = NULL; assertion = LASSO_SAML2_ASSERTION(iter->data); if (!LASSO_IS_SAML2_ASSERTION(assertion)) { apr_file_printf(diag_cfg->fd, "%sObject at index %d in session assertion" " list is not LassoSaml2Assertion", indent(level+1), i); } else { am_diag_log_lasso_node(r, level+1, &assertion->parent, "Assertion %d", i); } } } else { apr_file_printf(diag_cfg->fd, "%sprofile is NULL\n", indent(level+1)); } apr_file_flush(diag_cfg->fd); } void am_diag_log_cache_entry(request_rec *r, int level, am_cache_entry_t *entry, const char *fmt, ...) { va_list ap; am_diag_cfg_rec *diag_cfg = am_get_diag_cfg(r->server); am_req_cfg_rec *req_cfg = am_get_req_cfg(r); const char *name_id = NULL; const char *assertion_id = NULL; if (!AM_DIAG_ENABLED(diag_cfg)) return; if (!am_diag_initialize_req(r, diag_cfg, req_cfg)) return; va_start(ap, fmt); am_diag_format_line(r->pool, diag_cfg->fd, level, fmt, ap); va_end(ap); if (entry) { name_id = am_cache_env_fetch_first(entry, "NAME_ID"); assertion_id = am_cache_env_fetch_first(entry, "ASSERTION_ID"); apr_file_printf(diag_cfg->fd, "%skey: %s\n", indent(level+1), entry->key); apr_file_printf(diag_cfg->fd, "%sname_id: %s\n", indent(level+1), name_id); apr_file_printf(diag_cfg->fd, "%sassertion_id: %s\n", indent(level+1), assertion_id); apr_file_printf(diag_cfg->fd, "%sexpires: %s\n", indent(level+1), am_diag_time_t_to_8601(r, entry->expires)); apr_file_printf(diag_cfg->fd, "%sidle_timeout: %s\n", indent(level+1), am_diag_time_t_to_8601(r, entry->idle_timeout)); apr_file_printf(diag_cfg->fd, "%saccess: %s\n", indent(level+1), am_diag_time_t_to_8601(r, entry->access)); apr_file_printf(diag_cfg->fd, "%slogged_in: %s\n", indent(level+1), entry->logged_in ? "True" : "False"); } else { apr_file_printf(diag_cfg->fd, "%sentry is NULL\n", indent(level+1)); } apr_file_flush(diag_cfg->fd); } #endif /* ENABLE_DIAGNOSTICS */ mod_auth_mellon-0.18.0/auth_mellon_handler.c000066400000000000000000004112271410105036600211210ustar00rootroot00000000000000/* * * auth_mellon_handler.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* * Note: * * Information on PAOS ECP vs. Web SSO flow processing can be found in * the ECP.rst file. */ #ifdef HAVE_lasso_server_new_from_buffers /* This function generates optional metadata for a given element * * Parameters: * apr_pool_t *p Pool to allocate memory from * apr_hash_t *t Hash of lang -> strings * const char *e Name of the element * * Returns: * the metadata, or NULL if an error occured */ static char *am_optional_metadata_element(apr_pool_t *p, apr_hash_t *h, const char *e) { apr_hash_index_t *index; char *data = ""; for (index = apr_hash_first(p, h); index; index = apr_hash_next(index)) { char *lang; char *value; apr_ssize_t slen; char *xmllang = ""; apr_hash_this(index, (const void **)&lang, &slen, (void *)&value); if (*lang != '\0') xmllang = apr_psprintf(p, " xml:lang=\"%s\"", lang); data = apr_psprintf(p, "%s<%s%s>%s", data, e, xmllang, value, e); } return data; } /* This function generates optinal metadata * * Parameters: * request_rec *r The request we received. * * Returns: * the metadata, or NULL if an error occured */ static char *am_optional_metadata(apr_pool_t *p, request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); int count = 0; char *org_data = NULL; char *org_name = NULL; char *org_display_name = NULL; char *org_url = NULL; count += apr_hash_count(cfg->sp_org_name); count += apr_hash_count(cfg->sp_org_display_name); count += apr_hash_count(cfg->sp_org_url); if (count == 0) return ""; org_name = am_optional_metadata_element(p, cfg->sp_org_name, "OrganizationName"); org_display_name = am_optional_metadata_element(p, cfg->sp_org_display_name, "OrganizationDisplayName"); org_url = am_optional_metadata_element(p, cfg->sp_org_url, "OrganizationURL"); org_data = apr_psprintf(p, "%s%s%s", org_name, org_display_name, org_url); return org_data; } /* This function generates metadata * * Parameters: * request_rec *r The request we received. * * Returns: * the metadata, or NULL if an error occured */ static char *am_generate_metadata(apr_pool_t *p, request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); char *url = am_get_endpoint_url(r); char *cert = ""; const char *sp_entity_id; am_diag_printf(r, "Generating SP metadata\n"); sp_entity_id = cfg->sp_entity_id ? cfg->sp_entity_id : url; if (cfg->sp_cert_file && cfg->sp_cert_file->contents) { char *sp_cert_file; char *cp; char *bp; const char *begin = "-----BEGIN CERTIFICATE-----"; const char *end = "-----END CERTIFICATE-----"; /* * Try to remove leading and trailing garbage, as it can * wreak havoc XML parser if it contains [<>&] */ sp_cert_file = apr_pstrdup(p, cfg->sp_cert_file->contents); cp = strstr(sp_cert_file, begin); if (cp != NULL) sp_cert_file = cp + strlen(begin); cp = strstr(sp_cert_file, end); if (cp != NULL) *cp = '\0'; /* * And remove any non printing char (CR, spaces...) */ bp = sp_cert_file; for (cp = sp_cert_file; *cp; cp++) { if (apr_isgraph(*cp)) *bp++ = *cp; } *bp = '\0'; cert = apr_psprintf(p, "" "" "" "%s" "" "" "" "" "" "" "%s" "" "" "", sp_cert_file, sp_cert_file); } return apr_psprintf(p, "\n\ \n\ \n\ %s\ \n\ \n\ urn:oasis:names:tc:SAML:2.0:nameid-format:transient\n\ \n\ \n\ \n\ \n\ %s\n\ ", sp_entity_id, cfg->sp_entity_id ? "" : "metadata", cert, url, url, url, url, url, am_optional_metadata(p, r)); } #endif /* HAVE_lasso_server_new_from_buffers */ /* * This function loads all IdP metadata in a lasso server * * Parameters: * am_dir_cfg_rec *cfg The server configuration. * request_rec *r The request we received. * * Returns: * number of loaded providers */ static guint am_server_add_providers(am_dir_cfg_rec *cfg, request_rec *r) { apr_size_t index; #ifndef HAVE_lasso_server_load_metadata const char *idp_public_key_file; if (cfg->idp_metadata->nelts == 1) idp_public_key_file = cfg->idp_public_key_file ? cfg->idp_public_key_file->path : NULL; else idp_public_key_file = NULL; #endif /* ! HAVE_lasso_server_load_metadata */ if (cfg->idp_metadata->nelts == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error, URI \"%s\" has no IdP's defined", r->uri); return 0; } for (index = 0; index < cfg->idp_metadata->nelts; index++) { const am_metadata_t *idp_metadata; int error; #ifdef HAVE_lasso_server_load_metadata GList *loaded_idp = NULL; #endif /* HAVE_lasso_server_load_metadata */ idp_metadata = &( ((const am_metadata_t*)cfg->idp_metadata->elts) [index] ); am_diag_log_file_data(r, 0, idp_metadata->metadata, "Loading IdP Metadata"); if (idp_metadata->chain) { am_diag_log_file_data(r, 0, idp_metadata->chain, "Loading IdP metadata chain"); } #ifdef HAVE_lasso_server_load_metadata error = lasso_server_load_metadata(cfg->server, LASSO_PROVIDER_ROLE_IDP, idp_metadata->metadata->path, idp_metadata->chain ? idp_metadata->chain->path : NULL, cfg->idp_ignore, &loaded_idp, LASSO_SERVER_LOAD_METADATA_FLAG_DEFAULT); if (error == 0) { GList *idx; for (idx = loaded_idp; idx != NULL; idx = idx->next) { AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "loaded IdP \"%s\" from \"%s\".", (char *)idx->data, idp_metadata->metadata->path); } } if (loaded_idp != NULL) { for (GList *idx = loaded_idp; idx != NULL; idx = idx->next) { g_free(idx->data); } g_list_free(loaded_idp); } #else /* HAVE_lasso_server_load_metadata */ error = lasso_server_add_provider(cfg->server, LASSO_PROVIDER_ROLE_IDP, idp_metadata->metadata->path, idp_public_key_file, cfg->idp_ca_file ? cfg->idp_ca_file->path : NULL); #endif /* HAVE_lasso_server_load_metadata */ if (error != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error adding metadata \"%s\" to " "lasso server objects. Lasso error: [%i] %s", idp_metadata->metadata->path, error, lasso_strerror(error)); } } return g_hash_table_size(cfg->server->providers); } static LassoServer *am_get_lasso_server(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); cfg = cfg->inherit_server_from; apr_thread_mutex_lock(cfg->server_mutex); if(cfg->server == NULL) { if(cfg->sp_metadata_file == NULL) { #ifdef HAVE_lasso_server_new_from_buffers /* * Try to generate missing metadata */ apr_pool_t *pool = r->server->process->pconf; cfg->sp_metadata_file = am_file_data_new(pool, NULL); cfg->sp_metadata_file->rv = APR_SUCCESS; cfg->sp_metadata_file->generated = true; cfg->sp_metadata_file->contents = am_generate_metadata(pool, r); #else AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing MellonSPMetadataFile option."); apr_thread_mutex_unlock(cfg->server_mutex); return NULL; #endif /* HAVE_lasso_server_new_from_buffers */ } #ifdef HAVE_lasso_server_new_from_buffers cfg->server = lasso_server_new_from_buffers(cfg->sp_metadata_file->contents, cfg->sp_private_key_file ? cfg->sp_private_key_file->contents : NULL, NULL, cfg->sp_cert_file ? cfg->sp_cert_file->contents : NULL); #else cfg->server = lasso_server_new(cfg->sp_metadata_file->path, cfg->sp_private_key_file ? cfg->sp_private_key_file->path : NULL, NULL, cfg->sp_cert_file ? cfg->sp_cert_file->path : NULL); #endif if (cfg->server == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error initializing lasso server object. Please" " verify the following configuration directives:" " MellonSPMetadataFile and MellonSPPrivateKeyFile."); apr_thread_mutex_unlock(cfg->server_mutex); return NULL; } if (am_server_add_providers(cfg, r) == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error adding IdP to lasso server object. Please" " verify the following configuration directives:" " MellonIdPMetadataFile and" " MellonIdPPublicKeyFile."); lasso_server_destroy(cfg->server); cfg->server = NULL; apr_thread_mutex_unlock(cfg->server_mutex); return NULL; } cfg->server->signature_method = CFG_VALUE(cfg, signature_method); } apr_thread_mutex_unlock(cfg->server_mutex); return cfg->server; } /* Redirect to discovery service. * * Parameters: * request_rec *r The request we received. * const char *return_to The URL the user should be returned to after login. * * Returns: * HTTP_SEE_OTHER on success, an error otherwise. */ static int am_start_disco(request_rec *r, const char *return_to) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *endpoint = am_get_endpoint_url(r); LassoServer *server; const char *sp_entity_id; const char *sep; const char *login_url; const char *discovery_url; server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } sp_entity_id = LASSO_PROVIDER(server)->ProviderID; login_url = apr_psprintf(r->pool, "%slogin?ReturnTo=%s", endpoint, am_urlencode(r->pool, return_to)); AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "login_url = %s", login_url); /* If discovery URL already has a ? we append a & */ sep = (strchr(cfg->discovery_url, '?')) ? "&" : "?"; discovery_url = apr_psprintf(r->pool, "%s%sentityID=%s&" "return=%s&returnIDParam=IdP", cfg->discovery_url, sep, am_urlencode(r->pool, sp_entity_id), am_urlencode(r->pool, login_url)); AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "discovery_url = %s", discovery_url); apr_table_setn(r->headers_out, "Location", discovery_url); return HTTP_SEE_OTHER; } /* This function returns the first configured IdP * * Parameters: * request_rec *r The request we received. * * Returns: * the providerID, or NULL if an error occured */ static const char *am_first_idp(request_rec *r) { LassoServer *server; GList *idp_list; const char *idp_providerid; server = am_get_lasso_server(r); if (server == NULL) return NULL; idp_list = g_hash_table_get_keys(server->providers); if (idp_list == NULL) return NULL; idp_providerid = idp_list->data; g_list_free(idp_list); return idp_providerid; } /* This function selects an IdP and returns its provider_id * * Parameters: * request_rec *r The request we received. * * Returns: * the provider_id, or NULL if an error occured */ static const char *am_get_idp(request_rec *r) { LassoServer *server; const char *idp_provider_id; server = am_get_lasso_server(r); if (server == NULL) return NULL; /* * If we have a single IdP, return that one. */ if (g_hash_table_size(server->providers) == 1) return am_first_idp(r); /* * If IdP discovery handed us an IdP, try to use it. */ idp_provider_id = am_extract_query_parameter(r->pool, r->args, "IdP"); if (idp_provider_id != NULL) { int rc; rc = am_urldecode((char *)idp_provider_id); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode IdP discovery value."); idp_provider_id = NULL; } else { if (g_hash_table_lookup(server->providers, idp_provider_id) == NULL) idp_provider_id = NULL; } /* * If we do not know about it, fall back to default. */ if (idp_provider_id == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "IdP discovery returned unknown or inexistant IdP"); idp_provider_id = am_first_idp(r); } return idp_provider_id; } /* * No IdP answered, use default * Perhaps we should redirect to an error page instead. */ return am_first_idp(r); } /* This function stores dumps of the LassoIdentity and LassoSession objects * for the given LassoProfile object. The dumps are stored in the session * belonging to the current request. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The session we are creating. * LassoProfile *profile The profile object. * char *saml_response The full SAML 2.0 response message. * * Returns: * OK on success or HTTP_INTERNAL_SERVER_ERROR on failure. */ static int am_save_lasso_profile_state(request_rec *r, am_cache_entry_t *session, LassoProfile *profile, char *saml_response) { LassoIdentity *lasso_identity; LassoSession *lasso_session; gchar *identity_dump; gchar *session_dump; int ret; lasso_identity = lasso_profile_get_identity(profile); if(lasso_identity == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "The current LassoProfile object doesn't contain a" " LassoIdentity object."); identity_dump = NULL; } else { identity_dump = lasso_identity_dump(lasso_identity); if(identity_dump == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not create a identity dump from the" " LassoIdentity object."); return HTTP_INTERNAL_SERVER_ERROR; } } lasso_session = lasso_profile_get_session(profile); if(lasso_session == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "The current LassoProfile object doesn't contain a" " LassoSession object."); session_dump = NULL; } else { session_dump = lasso_session_dump(lasso_session); if(session_dump == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not create a session dump from the" " LassoSession object."); if(identity_dump != NULL) { g_free(identity_dump); } return HTTP_INTERNAL_SERVER_ERROR; } } /* Save the profile state. */ ret = am_cache_set_lasso_state(session, identity_dump, session_dump, saml_response); if(identity_dump != NULL) { g_free(identity_dump); } if(session_dump != NULL) { g_free(session_dump); } return ret; } /* Returns a SAML response * * Parameters: * request_rec *r The current request. * LassoProfile *profile The profile object. * * Returns: * HTTP_INTERNAL_SERVER_ERROR if an error occurs, HTTP_SEE_OTHER for the * Redirect binding and OK for the SOAP binding. */ static int am_return_logout_response(request_rec *r, LassoProfile *profile) { if (profile->msg_url && profile->msg_body) { /* POST binding response */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error building logout response message." " POST binding is unsupported."); return HTTP_INTERNAL_SERVER_ERROR; } else if (profile->msg_url) { /* HTTP-Redirect binding response */ apr_table_setn(r->headers_out, "Location", apr_pstrdup(r->pool, profile->msg_url)); return HTTP_SEE_OTHER; } else if (profile->msg_body) { /* SOAP binding response */ ap_set_content_type(r, "text/xml"); ap_rputs(profile->msg_body, r); return OK; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error building logout response message." " There is no content to return."); return HTTP_INTERNAL_SERVER_ERROR; } } /* This function restores dumps of a LassoIdentity object and a LassoSession * object. The dumps are fetched from the session belonging to the current * request and restored to the given LassoProfile object. * * Parameters: * request_rec *r The current request. * LassoProfile *profile The profile object. * am_cache_entry_t *am_session The session structure. * * Returns: * OK on success or HTTP_INTERNAL_SERVER_ERROR on failure. */ static void am_restore_lasso_profile_state(request_rec *r, LassoProfile *profile, am_cache_entry_t *am_session) { const char *identity_dump; const char *session_dump; int rc; if(am_session == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not get auth_mellon session while attempting" " to restore the lasso profile state."); return; } identity_dump = am_cache_get_lasso_identity(am_session); if(identity_dump != NULL) { rc = lasso_profile_set_identity_from_dump(profile, identity_dump); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not restore identity from dump." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); am_release_request_session(r, am_session); } } session_dump = am_cache_get_lasso_session(am_session); if(session_dump != NULL) { rc = lasso_profile_set_session_from_dump(profile, session_dump); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not restore session from dump." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); am_release_request_session(r, am_session); } } am_diag_log_cache_entry(r, 0, am_session, "%s: Session Cache Entry", __func__); am_diag_log_profile(r, 0, profile, "%s: Restored Profile", __func__); } /* This function handles an IdP initiated logout request. * * Parameters: * request_rec *r The logout request. * LassoLogout *logout A LassoLogout object initiated with * the current session. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_logout_request(request_rec *r, LassoLogout *logout, char *msg) { gint res = 0, rc = HTTP_OK; am_cache_entry_t *session = NULL; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); am_diag_printf(r, "enter function %s\n", __func__); /* Process the logout message. Ignore missing signature. */ res = lasso_logout_process_request_msg(logout, msg); #ifdef HAVE_lasso_profile_set_signature_verify_hint if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND && logout->parent.remote_providerID != NULL) { if (apr_hash_get(cfg->do_not_verify_logout_signature, logout->parent.remote_providerID, APR_HASH_KEY_STRING)) { lasso_profile_set_signature_verify_hint(&logout->parent, LASSO_PROFILE_SIGNATURE_VERIFY_HINT_IGNORE); res = lasso_logout_process_request_msg(logout, msg); } } #endif if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " Lasso error: [%i] %s", res, lasso_strerror(res)); rc = HTTP_BAD_REQUEST; goto exit; } /* Search session using NameID */ if (! LASSO_IS_SAML2_NAME_ID(logout->parent.nameIdentifier)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " No NameID found"); rc = HTTP_BAD_REQUEST; goto exit; } am_diag_printf(r, "%s name id %s\n", __func__, ((LassoSaml2NameID*)logout->parent.nameIdentifier)->content); session = am_get_request_session_by_nameid(r, ((LassoSaml2NameID*)logout->parent.nameIdentifier)->content); if (session == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " No session found for NameID %s", ((LassoSaml2NameID*)logout->parent.nameIdentifier)->content); } am_diag_log_cache_entry(r, 0, session, "%s", __func__); if (session == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing logout request message." " No session found."); } else { am_restore_lasso_profile_state(r, &logout->parent, session); } /* Validate the logout message. Ignore missing signature. */ res = lasso_logout_validate_request(logout); if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND && res != LASSO_PROFILE_ERROR_SESSION_NOT_FOUND) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Error validating logout request." " Lasso error: [%i] %s", res, lasso_strerror(res)); rc = HTTP_INTERNAL_SERVER_ERROR; goto exit; } /* We continue with the logout despite those errors. They could be * caused by the IdP believing that we are logged in when we are not. */ if (session != NULL && res != LASSO_PROFILE_ERROR_SESSION_NOT_FOUND) { /* We found a matching session -- delete it. */ am_delete_request_session(r, session); session = NULL; } /* Create response message. */ res = lasso_logout_build_response_msg(logout); if(res != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error building logout response message." " Lasso error: [%i] %s", res, lasso_strerror(res)); rc = HTTP_INTERNAL_SERVER_ERROR; goto exit; } rc = am_return_logout_response(r, &logout->parent); exit: if (session != NULL) { am_release_request_session(r, session); } lasso_logout_destroy(logout); return rc; } /* This function handles an invalidate request. * * Parameters: * request_rec *r The logout request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_invalidate_request(request_rec *r) { gint res = 0, rc = HTTP_OK; char *return_to; am_cache_entry_t *session = am_get_request_session(r); am_dir_cfg_rec *cfg = am_get_dir_cfg(r); /* Check if the session invalidation endpoint is enabled. */ if (cfg->enabled_invalidation_session == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Session Invalidation Endpoint is not enabled."); rc = HTTP_BAD_REQUEST; goto exit; } am_diag_printf(r, "enter function %s\n", __func__); am_diag_log_cache_entry(r, 0, session, "%s\n", __func__); return_to = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); if (return_to == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No ReturnTo parameter provided for invalidate handler."); rc = HTTP_BAD_REQUEST; goto exit; } /* Check for bad characters in ReturnTo. */ res = am_check_url(r, return_to); if (res != OK) { rc = HTTP_BAD_REQUEST; goto exit; } res = am_urldecode(return_to); if (res != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode ReturnTo value in invalidate" " response."); rc = HTTP_BAD_REQUEST; goto exit; } /* Make sure that it is a valid redirect URL. */ res = am_validate_redirect_url(r, return_to); if (res != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in invalidate response ReturnTo parameter."); rc = HTTP_BAD_REQUEST; goto exit; } if (session == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing invalidate request message." " No session found."); rc = HTTP_BAD_REQUEST; goto exit; } am_delete_request_session(r, session); apr_table_setn(r->headers_out, "Location", return_to); rc = HTTP_SEE_OTHER; exit: if (session != NULL) { am_release_request_session(r, session); } return rc; } /* This function handles a logout response message from the IdP. We get * this message after we have sent a logout request to the IdP. * * Parameters: * request_rec *r The logout response request. * LassoLogout *logout A LassoLogout object initiated with * the current session. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_logout_response(request_rec *r, LassoLogout *logout) { gint res; int rc; am_cache_entry_t *session; char *return_to; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); res = lasso_logout_process_response_msg(logout, r->args); am_diag_log_lasso_node(r, 0, LASSO_PROFILE(logout)->response, "SAML Response (%s):", __func__); #ifdef HAVE_lasso_profile_set_signature_verify_hint if(res != 0 && res != LASSO_DS_ERROR_SIGNATURE_NOT_FOUND && logout->parent.remote_providerID != NULL) { if (apr_hash_get(cfg->do_not_verify_logout_signature, logout->parent.remote_providerID, APR_HASH_KEY_STRING)) { lasso_profile_set_signature_verify_hint(&logout->parent, LASSO_PROFILE_SIGNATURE_VERIFY_HINT_IGNORE); res = lasso_logout_process_response_msg(logout, r->args); } } #endif if(res != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to process logout response." " Lasso error: [%i] %s, SAML Response: %s", res, lasso_strerror(res), am_saml_response_status_str(r, LASSO_PROFILE(logout)->response)); lasso_logout_destroy(logout); return HTTP_BAD_REQUEST; } lasso_logout_destroy(logout); /* Delete the session. */ session = am_get_request_session(r); am_diag_log_cache_entry(r, 0, session, "%s\n", __func__); if(session != NULL) { am_delete_request_session(r, session); } return_to = am_extract_query_parameter(r->pool, r->args, "RelayState"); if(return_to == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No RelayState parameter to logout response handler." " It is possible that your IdP doesn't support the" " RelayState parameter."); return HTTP_BAD_REQUEST; } rc = am_urldecode(return_to); if(rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode RelayState value in logout" " response."); return HTTP_BAD_REQUEST; } /* Check for bad characters in RelayState. */ rc = am_check_url(r, return_to); if (rc != OK) { return rc; } /* Make sure that it is a valid redirect URL. */ rc = am_validate_redirect_url(r, return_to); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in logout response RelayState parameter."); return rc; } apr_table_setn(r->headers_out, "Location", return_to); return HTTP_SEE_OTHER; } /* This function initiates a logout request and sends it to the IdP. * * Parameters: * request_rec *r The logout response request. * LassoLogout *logout A LassoLogout object initiated with * the current session. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_init_logout_request(request_rec *r, LassoLogout *logout) { char *return_to; int rc; am_cache_entry_t *mellon_session; gint res; char *redirect_to; LassoProfile *profile; LassoSession *session; GList *assertion_list; LassoNode *assertion_n; LassoSaml2Assertion *assertion; LassoSaml2AuthnStatement *authnStatement; LassoSamlp2LogoutRequest *request; return_to = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); rc = am_urldecode(return_to); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode ReturnTo value."); return HTTP_BAD_REQUEST; } rc = am_validate_redirect_url(r, return_to); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in logout request ReturnTo parameter."); return rc; } /* Disable the the local session (in case the IdP doesn't respond). */ mellon_session = am_get_request_session(r); if(mellon_session != NULL) { am_restore_lasso_profile_state(r, &logout->parent, mellon_session); mellon_session->logged_in = 0; am_release_request_session(r, mellon_session); } /* Create the logout request message. */ res = lasso_logout_init_request(logout, NULL, LASSO_HTTP_METHOD_REDIRECT); /* Early non failing return. */ if (res != 0) { if(res == LASSO_PROFILE_ERROR_SESSION_NOT_FOUND) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "User attempted to initiate logout without being" " loggged in."); } else if (res == LASSO_LOGOUT_ERROR_UNSUPPORTED_PROFILE || res == LASSO_PROFILE_ERROR_UNSUPPORTED_PROFILE) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Current identity provider " "does not support single logout. Destroying local session only."); } else if(res != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to create logout request." " Lasso error: [%i] %s", res, lasso_strerror(res)); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } lasso_logout_destroy(logout); /* Check for bad characters in ReturnTo. */ rc = am_check_url(r, return_to); if (rc != OK) { return rc; } /* Redirect to the page the user should be sent to after logout. */ apr_table_setn(r->headers_out, "Location", return_to); return HTTP_SEE_OTHER; } profile = LASSO_PROFILE(logout); /* We need to set the SessionIndex in the LogoutRequest to the SessionIndex * we received during the login operation. This is not needed since release * 2.3.0. */ if (lasso_check_version(2, 3, 0, LASSO_CHECK_VERSION_NUMERIC) == 0) { session = lasso_profile_get_session(profile); assertion_list = lasso_session_get_assertions( session, profile->remote_providerID); if(! assertion_list || LASSO_IS_SAML2_ASSERTION(assertion_list->data) == FALSE) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No assertions found for the current session."); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } /* We currently only look at the first assertion in the list * lasso_session_get_assertions returns. */ assertion_n = assertion_list->data; assertion = LASSO_SAML2_ASSERTION(assertion_n); /* We assume that the first authnStatement contains the data we want. */ authnStatement = LASSO_SAML2_AUTHN_STATEMENT(assertion->AuthnStatement->data); if(!authnStatement) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No AuthnStatement found in the current assertion."); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } if(authnStatement->SessionIndex) { request = LASSO_SAMLP2_LOGOUT_REQUEST(profile->request); request->SessionIndex = g_strdup(authnStatement->SessionIndex); } } /* Set the RelayState parameter to the return url (if we have one). */ if(return_to) { profile->msg_relayState = g_strdup(return_to); } /* Serialize the request message into a url which we can redirect to. */ res = lasso_logout_build_request_msg(logout); if(res != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to serialize lasso logout message." " Lasso error: [%i] %s", res, lasso_strerror(res)); lasso_logout_destroy(logout); return HTTP_INTERNAL_SERVER_ERROR; } /* Set the redirect url. */ redirect_to = apr_pstrdup(r->pool, LASSO_PROFILE(logout)->msg_url); /* Check if the lasso library added the RelayState. If lasso didn't add * a RelayState parameter, then we add one ourself. This should hopefully * be removed in the future. */ if(return_to != NULL && strstr(redirect_to, "&RelayState=") == NULL && strstr(redirect_to, "?RelayState=") == NULL) { /* The url didn't contain the relaystate parameter. */ redirect_to = apr_pstrcat( r->pool, redirect_to, "&RelayState=", am_urlencode(r->pool, return_to), NULL ); } apr_table_setn(r->headers_out, "Location", redirect_to); lasso_logout_destroy(logout); /* Redirect (without including POST data if this was a POST request. */ return HTTP_SEE_OTHER; } /* This function handles requests to the logout handler. * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_logout(request_rec *r) { LassoServer *server; LassoLogout *logout; server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } logout = lasso_logout_new(server); if(logout == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error creating lasso logout object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Check which type of request to the logout handler this is. * We have three types: * - logout requests: The IdP sends a logout request to this service. * it can be either through HTTP-Redirect or SOAP. * - logout responses: We have sent a logout request to the IdP, and * are receiving a response. * - We want to initiate a logout request. */ /* First check for IdP-initiated SOAP logout request */ if ((r->args == NULL) && (r->method_number == M_POST)) { int rc; char *post_data; rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return HTTP_INTERNAL_SERVER_ERROR; } return am_handle_logout_request(r, logout, post_data); } else if(am_extract_query_parameter(r->pool, r->args, "SAMLRequest") != NULL) { /* SAMLRequest - logout request from the IdP. */ return am_handle_logout_request(r, logout, r->args); } else if(am_extract_query_parameter(r->pool, r->args, "SAMLResponse") != NULL) { /* SAMLResponse - logout response from the IdP. */ return am_handle_logout_response(r, logout); } else if(am_extract_query_parameter(r->pool, r->args, "ReturnTo") != NULL) { /* RedirectTo - SP initiated logout. */ return am_init_logout_request(r, logout); } else { /* Unknown request to the logout handler. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No known parameters passed to the logout" " handler. Query string was \"%s\". To initiate" " a logout, you need to pass a \"ReturnTo\"" " parameter with a url to the web page the user should" " be redirected to after a successful logout.", r->args); return HTTP_BAD_REQUEST; } } /* This function handles requests to the invalidate handler. * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_invalidate(request_rec *r) { LassoServer *server; server = am_get_lasso_server(r); if (server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } return am_handle_invalidate_request(r); } /* This function parses a timestamp for a SAML 2.0 condition. * * Parameters: * request_rec *r The current request. Used for logging of errors. * const char *timestamp The timestamp we should parse. Must be on * the following format: "YYYY-MM-DDThh:mm:ssZ" * * Returns: * An apr_time_t value with the timestamp, or 0 on error. */ static apr_time_t am_parse_timestamp(request_rec *r, const char *timestamp) { size_t len; int i; char c; const char *expected; apr_time_exp_t time_exp; apr_time_t res; apr_status_t rc; len = strlen(timestamp); /* Verify length of timestamp. */ if(len < 20){ AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Invalid length of timestamp: \"%s\".", timestamp); } /* Verify components of timestamp. */ for(i = 0; i < len - 1; i++) { c = timestamp[i]; expected = NULL; switch(i) { case 4: case 7: /* Matches " - - " */ if(c != '-') { expected = "'-'"; } break; case 10: /* Matches " T " */ if(c != 'T') { expected = "'T'"; } break; case 13: case 16: /* Matches " : : " */ if(c != ':') { expected = "':'"; } break; case 19: /* Matches " ." */ if (c != '.') { expected = "'.'"; } break; default: /* Matches "YYYY MM DD hh mm ss uuuuuu" */ if(c < '0' || c > '9') { expected = "a digit"; } break; } if(expected != NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid character in timestamp at position %i." " Expected %s, got '%c'. Full timestamp: \"%s\"", i, expected, c, timestamp); return 0; } } if (timestamp[len - 1] != 'Z') { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Timestamp wasn't in UTC (did not end with 'Z')." " Full timestamp: \"%s\"", timestamp); return 0; } time_exp.tm_usec = 0; if (len > 20) { /* Subsecond precision. */ if (len > 27) { /* Timestamp has more than microsecond precision. Just clip it to * microseconds. */ len = 27; } len -= 1; /* Drop the 'Z' off the end. */ for (i = 20; i < len; i++) { time_exp.tm_usec = time_exp.tm_usec * 10 + timestamp[i] - '0'; } for (i = len; i < 26; i++) { time_exp.tm_usec *= 10; } } time_exp.tm_sec = (timestamp[17] - '0') * 10 + (timestamp[18] - '0'); time_exp.tm_min = (timestamp[14] - '0') * 10 + (timestamp[15] - '0'); time_exp.tm_hour = (timestamp[11] - '0') * 10 + (timestamp[12] - '0'); time_exp.tm_mday = (timestamp[8] - '0') * 10 + (timestamp[9] - '0'); time_exp.tm_mon = (timestamp[5] - '0') * 10 + (timestamp[6] - '0') - 1; time_exp.tm_year = (timestamp[0] - '0') * 1000 + (timestamp[1] - '0') * 100 + (timestamp[2] - '0') * 10 + (timestamp[3] - '0') - 1900; time_exp.tm_wday = 0; /* Unknown. */ time_exp.tm_yday = 0; /* Unknown. */ time_exp.tm_isdst = 0; /* UTC, no daylight savings time. */ time_exp.tm_gmtoff = 0; /* UTC, no offset from UTC. */ rc = apr_time_exp_gmt_get(&res, &time_exp); if(rc != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Error converting timestamp \"%s\".", timestamp); return 0; } return res; } /* Validate the subject on an Assertion. * * request_rec *r The current request. Used to log * errors. * LassoSaml2Assertion *assertion The assertion we will validate. * const char *url The current URL. * * Returns: * OK on success, HTTP_BAD_REQUEST on failure. */ static int am_validate_subject(request_rec *r, LassoSaml2Assertion *assertion, const char *url) { apr_time_t now; apr_time_t t; LassoSaml2SubjectConfirmation *sc; LassoSaml2SubjectConfirmationData *scd; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); if (assertion->Subject == NULL) { /* No Subject to validate. */ return OK; } else if (!LASSO_IS_SAML2_SUBJECT(assertion->Subject)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Subject node."); return HTTP_BAD_REQUEST; } if (assertion->Subject->SubjectConfirmation == NULL) { /* No SubjectConfirmation. */ return OK; } else if (!LASSO_IS_SAML2_SUBJECT_CONFIRMATION(assertion->Subject->SubjectConfirmation)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of SubjectConfirmation node."); return HTTP_BAD_REQUEST; } sc = assertion->Subject->SubjectConfirmation; if (sc->Method == NULL || strcmp(sc->Method, "urn:oasis:names:tc:SAML:2.0:cm:bearer")) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid Method in SubjectConfirmation."); return HTTP_BAD_REQUEST; } scd = sc->SubjectConfirmationData; if (scd == NULL) { /* Nothing to verify. */ return OK; } else if (!LASSO_IS_SAML2_SUBJECT_CONFIRMATION_DATA(scd)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of SubjectConfirmationData node."); return HTTP_BAD_REQUEST; } now = apr_time_now(); if (scd->NotBefore) { t = am_parse_timestamp(r, scd->NotBefore); if (t == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotBefore in SubjectConfirmationData."); return HTTP_BAD_REQUEST; } if (t - 60000000 > now) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "NotBefore in SubjectConfirmationData was in the future."); return HTTP_BAD_REQUEST; } } if (scd->NotOnOrAfter) { t = am_parse_timestamp(r, scd->NotOnOrAfter); if (t == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotOnOrAfter in SubjectConfirmationData."); return HTTP_BAD_REQUEST; } if (now >= t + 60000000) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "NotOnOrAfter in SubjectConfirmationData was in the past."); return HTTP_BAD_REQUEST; } } if (scd->Recipient) { if (strcmp(scd->Recipient, url)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong Recipient in SubjectConfirmationData. Current URL is: %s, Recipient is %s", url, scd->Recipient); return HTTP_BAD_REQUEST; } } if (scd->Address && CFG_VALUE(cfg, subject_confirmation_data_address_check)) { if (strcasecmp(scd->Address, am_compat_request_ip(r))) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong Address in SubjectConfirmationData." "Current address is \"%s\", but should have been \"%s\".", am_compat_request_ip(r), scd->Address); return HTTP_BAD_REQUEST; } } return OK; } /* Validate the conditions on an Assertion. * * Parameters: * request_rec *r The current request. Used to log * errors. * LassoSaml2Assertion *assertion The assertion we will validate. * const char *providerID The providerID of the SP. * * Returns: * OK on success, HTTP_BAD_REQUEST on failure. */ static int am_validate_conditions(request_rec *r, LassoSaml2Assertion *assertion, const char *providerID) { LassoSaml2Conditions *conditions; apr_time_t now; apr_time_t t; GList *i; LassoSaml2AudienceRestriction *ar; conditions = assertion->Conditions; if (conditions == NULL) { /* An assertion without conditions -- nothing to validate. */ return OK; } if (!LASSO_IS_SAML2_CONDITIONS(conditions)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Conditions node."); return HTTP_BAD_REQUEST; } if (conditions->Condition != NULL) { /* This is a list of LassoSaml2ConditionAbstract - if it * isn't empty, we have an unsupported condition. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unsupported condition in Assertion."); return HTTP_BAD_REQUEST; } now = apr_time_now(); if (conditions->NotBefore) { t = am_parse_timestamp(r, conditions->NotBefore); if (t == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotBefore in Condition."); return HTTP_BAD_REQUEST; } if (t - 60000000 > now) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "NotBefore in Condition was in the future."); return HTTP_BAD_REQUEST; } } if (conditions->NotOnOrAfter) { t = am_parse_timestamp(r, conditions->NotOnOrAfter); if (t == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid timestamp in NotOnOrAfter in Condition."); return HTTP_BAD_REQUEST; } if (now >= t + 60000000) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "NotOnOrAfter in Condition was in the past."); return HTTP_BAD_REQUEST; } } for (i = g_list_first(conditions->AudienceRestriction); i != NULL; i = g_list_next(i)) { ar = i->data; if (!LASSO_IS_SAML2_AUDIENCE_RESTRICTION(ar)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AudienceRestriction node."); return HTTP_BAD_REQUEST; } if (ar->Audience == NULL || strcmp(ar->Audience, providerID)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid Audience in Conditions. Should be '%s', but was '%s'", providerID, ar->Audience ? ar->Audience : ""); return HTTP_BAD_REQUEST; } } return OK; } /* Validate that the ID of the Assertion has not been used. * * Parameters: * request_rec *r The current request. Used to log * errors. * LassoSaml2Assertion *assertion The assertion we will validate. * * Returns: * OK on success, HTTP_BAD_REQUEST on failure. */ static int am_validate_unique_assertion_id(request_rec *r, LassoSaml2Assertion *assertion) { am_cache_entry_t *session = NULL; if (assertion->ID == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Assertion ID is not present."); return HTTP_BAD_REQUEST; } // Check if there is a session associate with the Assertion ID session = am_get_request_session_by_assertionid(r, assertion->ID); if (session != NULL) { am_cache_unlock(r, session); AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Assertion ID %s has already been used.", assertion->ID); return HTTP_BAD_REQUEST; } return OK; } /* This function sets the session expire timestamp based on NotOnOrAfter * attribute of a condition element. * * Parameters: * request_rec *r The current request. Used to log * errors. * am_cache_entry_t *session The current session. * LassoSaml2Assertion *assertion The assertion which we will extract * the conditions from. * * Returns: * Nothing. */ static void am_handle_session_expire(request_rec *r, am_cache_entry_t *session, LassoSaml2Assertion *assertion) { GList *authn_itr; LassoSaml2AuthnStatement *authn; const char *not_on_or_after; apr_time_t t; for(authn_itr = g_list_first(assertion->AuthnStatement); authn_itr != NULL; authn_itr = g_list_next(authn_itr)) { authn = authn_itr->data; if (!LASSO_IS_SAML2_AUTHN_STATEMENT(authn)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AuthnStatement node."); continue; } /* Find timestamp. */ not_on_or_after = authn->SessionNotOnOrAfter; if(not_on_or_after == NULL) { am_diag_printf(r, "%s failed to find" " Assertion.AuthnStatement.SessionNotOnOrAfter\n", __func__); continue; } /* Parse timestamp. */ t = am_parse_timestamp(r, not_on_or_after); if(t == 0) { continue; } am_diag_printf(r, "%s Assertion.AuthnStatement.SessionNotOnOrAfter:" " %s\n", __func__, am_diag_time_t_to_8601(r, t)); /* Updates the expires timestamp if this one is earlier than the * previous timestamp. */ am_cache_update_expires(r, session, t); } } /* Add all the attributes from an assertion to the session data for the * current user. * * Parameters: * am_cache_entry_t *s The current session. * request_rec *r The current request. * const char *name_id The name identifier we received from * the IdP. * LassoSaml2Assertion *assertion The assertion. * * Returns: * HTTP_BAD_REQUEST if we couldn't find the session id of the user, or * OK if no error occured. */ static int add_attributes(am_cache_entry_t *session, request_rec *r, const char *name_id, LassoSaml2Assertion *assertion) { am_dir_cfg_rec *dir_cfg; GList *atr_stmt_itr; LassoSaml2AttributeStatement *atr_stmt; GList *atr_itr; LassoSaml2Attribute *attribute; GList *value_itr; LassoSaml2AttributeValue *value; GList *any_itr; char *content; char *dump; int ret; dir_cfg = am_get_dir_cfg(r); /* Set expires to whatever is set by MellonSessionLength. */ if(dir_cfg->session_length == -1) { /* -1 means "use default. The current default is 86400 seconds. */ am_cache_update_expires(r, session, apr_time_now() + apr_time_make(86400, 0)); } else { am_cache_update_expires(r, session, apr_time_now() + apr_time_make(dir_cfg->session_length, 0)); } /* Set the idle timeout to whatever is set by MellonSessionIdleTimeout. */ am_cache_update_idle_timeout(r, session, dir_cfg->session_idle_timeout); /* Save session NAME_ID information. */ ret = am_cache_env_append(session, "NAME_ID", name_id); if(ret != OK) { return ret; } /* Save session ASSERTION_ID information. */ ret = am_cache_env_append(session, "ASSERTION_ID", assertion->ID); if(ret != OK) { return ret; } /* Update expires timestamp of session. */ am_handle_session_expire(r, session, assertion); /* assertion->AttributeStatement is a list of * LassoSaml2AttributeStatement objects. */ for(atr_stmt_itr = g_list_first(assertion->AttributeStatement); atr_stmt_itr != NULL; atr_stmt_itr = g_list_next(atr_stmt_itr)) { atr_stmt = atr_stmt_itr->data; if (!LASSO_IS_SAML2_ATTRIBUTE_STATEMENT(atr_stmt)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AttributeStatement node."); continue; } /* atr_stmt->Attribute is list of LassoSaml2Attribute objects. */ for(atr_itr = g_list_first(atr_stmt->Attribute); atr_itr != NULL; atr_itr = g_list_next(atr_itr)) { attribute = atr_itr->data; if (!LASSO_IS_SAML2_ATTRIBUTE(attribute)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Attribute node."); continue; } if (attribute->Name == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "SAML 2.0 attribute without name."); continue; } /* attribute->AttributeValue is a list of * LassoSaml2AttributeValue objects. */ for(value_itr = g_list_first(attribute->AttributeValue); value_itr != NULL; value_itr = g_list_next(value_itr)) { value = value_itr->data; if (!LASSO_IS_SAML2_ATTRIBUTE_VALUE(value)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of AttributeValue node."); continue; } /* value->any is a list with the child nodes of the * AttributeValue element. * * We assume that the list contains a single text node. */ if(value->any == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "AttributeValue element was empty."); continue; } content = ""; for (any_itr = g_list_first(value->any); any_itr != NULL; any_itr = g_list_next(any_itr)) { /* Verify that this is a LassoNode object. */ if(!LASSO_NODE(any_itr->data)) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "AttributeValue element contained an " " element which wasn't a Node."); continue; } dump = lasso_node_dump(LASSO_NODE(any_itr->data)); if (!dump) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "AttributeValue content dump failed."); continue; } /* Use the request pool, no need to free results */ content = apr_pstrcat(r->pool, content, dump, NULL); g_free(dump); } /* Decode and save the attribute. */ am_diag_printf(r, "%s name=%s value=%s\n", __func__, attribute->Name, content); ret = am_cache_env_append(session, attribute->Name, content); if(ret != OK) { return ret; } } } } return OK; } /* This function validates that the received assertion verify the security level configured by * MellonAuthnContextClassRef directives */ static int am_validate_authn_context_class_ref(request_rec *r, LassoSaml2Assertion *assertion) { int i = 0; LassoSaml2AuthnStatement *authn_statement = NULL; LassoSaml2AuthnContext *authn_context = NULL; am_dir_cfg_rec *dir_cfg; apr_array_header_t *refs; dir_cfg = am_get_dir_cfg(r); refs = dir_cfg->authn_context_class_ref; if (! refs->nelts) return OK; if (! assertion->AuthnStatement) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing AuthnStatement in assertion, returning BadRequest."); return HTTP_BAD_REQUEST; } /* we only consider the first AuthnStatement, I do not know of any idp * sending more than one. */ authn_statement = g_list_first(assertion->AuthnStatement)->data; if (! authn_statement->AuthnContext) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing AuthnContext in assertion, returning BadRequest."); return HTTP_BAD_REQUEST; } authn_context = authn_statement->AuthnContext; if (! authn_context->AuthnContextClassRef) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing AuthnContextClassRef in assertion, returning Forbidden."); return HTTP_FORBIDDEN; } for (i = 0; i < refs->nelts; i++) { const char *ref = ((char **)refs->elts)[i]; if (strcmp(ref, authn_context->AuthnContextClassRef) == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "AuthnContextClassRef (%s) matches the " "MellonAuthnContextClassRef directive, " "access can be granted.", authn_context->AuthnContextClassRef); return OK; } } AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "AuthnContextClassRef (%s) does not match the " "MellonAuthnContextClassRef directive, returning " "Forbidden.", authn_context->AuthnContextClassRef); return HTTP_FORBIDDEN; } /* This function finishes handling of a login response after it has been parsed * by the HTTP-POST or HTTP-Artifact handler. * * Parameters: * request_rec *r The current request. * LassoLogin *login The login object which has been initialized with the * data we have received from the IdP. * char *relay_state The RelayState parameter from the POST data or from * the request url. This parameter is urlencoded, and * this function will urldecode it in-place. Therefore it * must be possible to overwrite the data. * is_paos If true then flow is PAOS ECP. * * Returns: * A HTTP status code which should be returned to the client. */ static int am_handle_reply_common(request_rec *r, LassoLogin *login, char *relay_state, char *saml_response, bool is_paos) { char *url; char *chr; const char *name_id; LassoSamlp2Response *response; LassoSaml2Assertion *assertion; const char *in_response_to; am_dir_cfg_rec *dir_cfg; am_cache_entry_t *session; int rc; const char *idp; url = am_reconstruct_url(r); chr = strchr(url, '?'); if (! chr) { chr = strchr(url, ';'); } if (chr) { *chr = '\0'; } dir_cfg = am_get_dir_cfg(r); if(LASSO_PROFILE(login)->nameIdentifier == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No acceptable name identifier found in" " SAML 2.0 response."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } name_id = LASSO_SAML2_NAME_ID(LASSO_PROFILE(login)->nameIdentifier) ->content; response = LASSO_SAMLP2_RESPONSE(LASSO_PROFILE(login)->response); if (response->parent.Destination) { if (strcmp(response->parent.Destination, url)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid Destination on Response. Should be '%s', but was '%s'", url, response->parent.Destination); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } if (g_list_length(response->Assertion) == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No Assertion in response."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } if (g_list_length(response->Assertion) > 1) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "More than one Assertion in response."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } assertion = g_list_first(response->Assertion)->data; if (!LASSO_IS_SAML2_ASSERTION(assertion)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Wrong type of Assertion node."); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } rc = am_validate_subject(r, assertion, url); if (rc != OK) { lasso_login_destroy(login); return rc; } rc = am_validate_conditions(r, assertion, LASSO_PROVIDER(LASSO_PROFILE(login)->server)->ProviderID); if (rc != OK) { lasso_login_destroy(login); return rc; } rc = am_validate_unique_assertion_id(r, assertion); if (rc != OK) { lasso_login_destroy(login); return rc; } in_response_to = response->parent.InResponseTo; if (!is_paos) { if(in_response_to != NULL) { /* This is SP-initiated login. Check that we have a cookie. */ if(am_cookie_get(r) == NULL) { /* Missing cookie. */ AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "User has disabled cookies, or has lost" " the cookie before returning from the SAML2" " login server."); if(dir_cfg->no_cookie_error_page != NULL) { apr_table_setn(r->headers_out, "Location", dir_cfg->no_cookie_error_page); lasso_login_destroy(login); return HTTP_SEE_OTHER; } else { /* Return 400 Bad Request when the user hasn't set a * no-cookie error page. */ lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } } } /* Check AuthnContextClassRef */ rc = am_validate_authn_context_class_ref(r, assertion); if (rc != OK) { lasso_login_destroy(login); return rc; } /* Create a new session. */ session = am_new_request_session(r); if(session == NULL) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, NULL, "am_new_request_session() failed"); return HTTP_INTERNAL_SERVER_ERROR; } rc = add_attributes(session, r, name_id, assertion); if(rc != OK) { am_release_request_session(r, session); lasso_login_destroy(login); return rc; } /* If requested, save the IdP ProviderId */ if(dir_cfg->idpattr != NULL) { idp = LASSO_PROFILE(login)->remote_providerID; if(idp != NULL) { rc = am_cache_env_append(session, dir_cfg->idpattr, idp); if(rc != OK) { am_release_request_session(r, session); lasso_login_destroy(login); return rc; } } } rc = lasso_login_accept_sso(login); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to accept SSO message." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); am_release_request_session(r, session); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Save the profile state. */ rc = am_save_lasso_profile_state(r, session, LASSO_PROFILE(login), saml_response); if(rc != OK) { am_release_request_session(r, session); lasso_login_destroy(login); return rc; } /* Mark user as logged in. */ session->logged_in = 1; am_release_request_session(r, session); lasso_login_destroy(login); /* No RelayState - we don't know what to do. Use default login path. */ if(relay_state == NULL || strlen(relay_state) == 0) { dir_cfg = am_get_dir_cfg(r); apr_table_setn(r->headers_out, "Location", dir_cfg->login_path); return HTTP_SEE_OTHER; } rc = am_urldecode(relay_state); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode RelayState value."); return HTTP_BAD_REQUEST; } /* Check for bad characters in RelayState. */ rc = am_check_url(r, relay_state); if (rc != OK) { return rc; } rc = am_validate_redirect_url(r, relay_state); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in logout response RelayState parameter."); return rc; } apr_table_setn(r->headers_out, "Location", relay_state); /* HTTP_SEE_OTHER should be a redirect where the browser doesn't repeat * the POST data to the new page. */ return HTTP_SEE_OTHER; } /* This function handles responses to login requests received with the * HTTP-POST binding. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_handle_post_reply(request_rec *r) { int rc; char *post_data; char *saml_response; LassoServer *server; LassoLogin *login; char *relay_state; am_dir_cfg_rec *dir_cfg = am_get_dir_cfg(r); int i, err; am_diag_printf(r, "enter function %s\n", __func__); /* Make sure that this is a POST request. */ if(r->method_number != M_POST) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Expected POST request for HTTP-POST endpoint." " Got a %s request instead.", r->method); /* According to the documentation for request_rec, a handler which * doesn't handle a request method, should set r->allowed to the * methods it handles, and return DECLINED. * However, the default handler handles GET-requests, so for GET * requests the handler should return HTTP_METHOD_NOT_ALLOWED. */ r->allowed = M_POST; if(r->method_number == M_GET) { return HTTP_METHOD_NOT_ALLOWED; } else { return DECLINED; } } /* Read POST-data. */ rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return rc; } /* Extract the SAMLResponse-field from the data. */ saml_response = am_extract_query_parameter(r->pool, post_data, "SAMLResponse"); if (saml_response == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not find SAMLResponse field in POST data."); return HTTP_BAD_REQUEST; } rc = am_urldecode(saml_response); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Could not urldecode SAMLResponse value."); return rc; } server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if (login == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to initialize LassoLogin object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Process login responce. */ rc = lasso_login_process_authn_response_msg(login, saml_response); am_diag_log_lasso_node(r, 0, LASSO_PROFILE(login)->response, "SAML Response (%s):", __func__); if (rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing authn response." " Lasso error: [%i] %s, SAML Response: %s", rc, lasso_strerror(rc), am_saml_response_status_str(r, LASSO_PROFILE(login)->response)); lasso_login_destroy(login); err = HTTP_BAD_REQUEST; for (i = 0; auth_mellon_errormap[i].lasso_error != 0; i++) { if (auth_mellon_errormap[i].lasso_error == rc) { err = auth_mellon_errormap[i].http_error; break; } } if (err == HTTP_UNAUTHORIZED) { if (dir_cfg->no_success_error_page != NULL) { apr_table_setn(r->headers_out, "Location", dir_cfg->no_success_error_page); return HTTP_SEE_OTHER; } } return err; } /* Extract RelayState parameter. */ relay_state = am_extract_query_parameter(r->pool, post_data, "RelayState"); /* Finish handling the reply with the common handler. */ return am_handle_reply_common(r, login, relay_state, saml_response, false); } /* This function handles responses to login requests received with the * PAOS binding. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_handle_paos_reply(request_rec *r) { int rc; char *post_data; LassoServer *server; LassoLogin *login; char *relay_state = NULL; int i, err; am_diag_printf(r, "enter function %s\n", __func__); /* Make sure that this is a POST request. */ if(r->method_number != M_POST) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Expected POST request for paosResponse endpoint." " Got a %s request instead.", r->method); /* According to the documentation for request_rec, a handler which * doesn't handle a request method, should set r->allowed to the * methods it handles, and return DECLINED. * However, the default handler handles GET-requests, so for GET * requests the handler should return HTTP_METHOD_NOT_ALLOWED. */ r->allowed = M_POST; if(r->method_number == M_GET) { return HTTP_METHOD_NOT_ALLOWED; } else { return DECLINED; } } /* Read POST-data. */ rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return rc; } server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if (login == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to initialize LassoLogin object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Process login response. */ rc = lasso_login_process_paos_response_msg(login, post_data); am_diag_log_lasso_node(r, 0, LASSO_PROFILE(login)->response, "SAML Response (%s):", __func__); if (rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error processing ECP authn response." " Lasso error: [%i] %s, SAML Response: %s", rc, lasso_strerror(rc), am_saml_response_status_str(r, LASSO_PROFILE(login)->response)); lasso_login_destroy(login); err = HTTP_BAD_REQUEST; for (i = 0; auth_mellon_errormap[i].lasso_error != 0; i++) { if (auth_mellon_errormap[i].lasso_error == rc) { err = auth_mellon_errormap[i].http_error; break; } } return err; } /* Extract RelayState parameter. */ if (LASSO_PROFILE(login)->msg_relayState) { relay_state = apr_pstrdup(r->pool, LASSO_PROFILE(login)->msg_relayState); } /* Finish handling the reply with the common handler. */ return am_handle_reply_common(r, login, relay_state, post_data, true); } /* This function handles responses to login requests which use the * HTTP-Artifact binding. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_handle_artifact_reply(request_rec *r) { int rc; LassoServer *server; LassoLogin *login; char *response; char *relay_state; char *saml_art; char *post_data; am_diag_printf(r, "enter function %s\n", __func__); /* Make sure that this is a GET request. */ if(r->method_number != M_GET && r->method_number != M_POST) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Expected GET or POST request for the HTTP-Artifact endpoint." " Got a %s request instead.", r->method); /* According to the documentation for request_rec, a handler which * doesn't handle a request method, should set r->allowed to the * methods it handles, and return DECLINED. * However, the default handler handles GET-requests, so for GET * requests the handler should return HTTP_METHOD_NOT_ALLOWED. * This endpoints handles GET requests, so it isn't necessary to * check for method_number == M_GET. */ r->allowed = M_GET; return DECLINED; } server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if (login == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to initialize LassoLogin object."); return HTTP_INTERNAL_SERVER_ERROR; } /* Parse artifact url. */ if (r->method_number == M_GET) { rc = lasso_login_init_request(login, r->args, LASSO_HTTP_METHOD_ARTIFACT_GET); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to handle login response." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } else { rc = am_read_post_data(r, &post_data, NULL); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data."); return HTTP_BAD_REQUEST; } saml_art = am_extract_query_parameter(r->pool, post_data, "SAMLart"); if (saml_art == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, rc, r, "Error reading POST data missing SAMLart form parameter."); return HTTP_BAD_REQUEST; } ap_unescape_url(saml_art); rc = lasso_login_init_request(login, saml_art, LASSO_HTTP_METHOD_ARTIFACT_POST); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to handle login response." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_BAD_REQUEST; } } /* Prepare SOAP request. */ rc = lasso_login_build_request_msg(login); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to prepare SOAP message for HTTP-Artifact" " resolution." " Lasso error: [%i] %s", rc, lasso_strerror(rc)); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Do the SOAP request. */ rc = am_httpclient_post_str( r, LASSO_PROFILE(login)->msg_url, LASSO_PROFILE(login)->msg_body, "text/xml", (void**)&response, NULL ); if(rc != OK) { lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } rc = lasso_login_process_response_msg(login, response); am_diag_log_lasso_node(r, 0, LASSO_PROFILE(login)->response, "SAML Response (%s):", __func__); if(rc != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to handle HTTP-Artifact response data." " Lasso error: [%i] %s, SAML Response: %s", rc, lasso_strerror(rc), am_saml_response_status_str(r, LASSO_PROFILE(login)->response)); lasso_login_destroy(login); return HTTP_INTERNAL_SERVER_ERROR; } /* Extract the RelayState parameter. */ if (r->method_number == M_GET) { relay_state = am_extract_query_parameter(r->pool, r->args, "RelayState"); } else { relay_state = am_extract_query_parameter(r->pool, post_data, "RelayState"); } /* Finish handling the reply with the common handler. */ return am_handle_reply_common(r, login, relay_state, "", false); } /* This function builds web form inputs for a saved POST request, * in multipart/form-data format. * * Parameters: * request_rec *r The request * const char *post_data The savec POST request * * Returns: * The web form fragment, or NULL on failure. */ const char *am_post_mkform_multipart(request_rec *r, const char *post_data) { const char *mime_part; const char *boundary; char *l1; char *post_form = ""; /* Replace CRLF by LF */ post_data = am_strip_cr(r, post_data); if ((boundary = am_xstrtok(r, post_data, "\n", &l1)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Cannot figure initial boundary"); return NULL; } for (mime_part = am_xstrtok(r, post_data, boundary, &l1); mime_part; mime_part = am_xstrtok(r, NULL, boundary, &l1)) { const char *hdr; const char *name = NULL; const char *value = NULL; const char *input_item; /* End of MIME data */ if (strcmp(mime_part, "--\n") == 0) break; /* Remove leading CRLF */ if (strstr(mime_part, "\n") == mime_part) mime_part += 1; /* Empty part */ if (*mime_part == '\0') continue; /* Find Content-Disposition header * Looking for * Content-Disposition: form-data; name="the_name"\n */ hdr = am_get_mime_header(r, mime_part, "Content-Disposition"); if (hdr == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No Content-Disposition header in MIME section,"); continue; } name = am_get_header_attr(r, hdr, "form-data", "name"); if (name == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unexpected Content-Disposition header: \"%s\"", hdr); continue; } if ((value = am_get_mime_body(r, mime_part)) == NULL) value = ""; input_item = apr_psprintf(r->pool, " \n", am_htmlencode(r, name), am_htmlencode(r, value)); post_form = apr_pstrcat(r->pool, post_form, input_item, NULL); } return post_form; } /* This function builds web form inputs for a saved POST request, * in application/x-www-form-urlencoded format * * Parameters: * request_rec *r The request * const char *post_data The savec POST request * * Returns: * The web form fragment, or NULL on failure. */ const char *am_post_mkform_urlencoded(request_rec *r, const char *post_data) { const char *item; char *last; char *post_form = ""; char empty_value[] = ""; for (item = am_xstrtok(r, post_data, "&", &last); item; item = am_xstrtok(r, NULL, "&", &last)) { char *l1; char *name; char *value; const char *input_item; name = (char *)am_xstrtok(r, item, "=", &l1); value = (char *)am_xstrtok(r, NULL, "=", &l1); if (name == NULL) continue; if (value == NULL) value = empty_value; if (am_urldecode(name) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "urldecode(\"%s\") failed", name); return NULL; } if (am_urldecode(value) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "urldecode(\"%s\") failed", value); return NULL; } input_item = apr_psprintf(r->pool, " \n", am_htmlencode(r, name), am_htmlencode(r, value)); post_form = apr_pstrcat(r->pool, post_form, input_item, NULL); } return post_form; } /* This function handles responses to repost request * * Parameters: * request_rec *r The request we received. * * Returns: * OK on success, or an error on failure. */ static int am_handle_repost(request_rec *r) { am_mod_cfg_rec *mod_cfg; const char *query; const char *enctype; char *charset; char *psf_id; char *cp; am_file_data_t *file_data; const char *post_data; const char *post_form; char *output; char *return_url; const char *(*post_mkform)(request_rec *, const char *); int rc; am_diag_printf(r, "enter function %s\n", __func__); if (am_cookie_get(r) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Repost query without a session"); return HTTP_FORBIDDEN; } mod_cfg = am_get_mod_cfg(r->server); if (!mod_cfg->post_dir) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Repost query without MellonPostDirectory."); return HTTP_NOT_FOUND; } query = r->parsed_uri.query; enctype = am_extract_query_parameter(r->pool, query, "enctype"); if (enctype == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: missing enctype"); return HTTP_BAD_REQUEST; } if (strcmp(enctype, "urlencoded") == 0) { enctype = "application/x-www-form-urlencoded"; post_mkform = am_post_mkform_urlencoded; } else if (strcmp(enctype, "multipart") == 0) { enctype = "multipart/form-data"; post_mkform = am_post_mkform_multipart; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid enctype \"%s\".", enctype); return HTTP_BAD_REQUEST; } charset = am_extract_query_parameter(r->pool, query, "charset"); if (charset != NULL) { if (am_urldecode(charset) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid charset \"%s\"", charset); return HTTP_BAD_REQUEST; } /* Check that charset is sane */ for (cp = charset; *cp; cp++) { if (!apr_isalnum(*cp) && (*cp != '-') && (*cp != '_')) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid charset \"%s\"", charset); return HTTP_BAD_REQUEST; } } } psf_id = am_extract_query_parameter(r->pool, query, "id"); if (psf_id == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: missing id"); return HTTP_BAD_REQUEST; } /* Check that Id is sane */ for (cp = psf_id; *cp; cp++) { if (!apr_isalnum(*cp)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: invalid id \"%s\"", psf_id); return HTTP_BAD_REQUEST; } } return_url = am_extract_query_parameter(r->pool, query, "ReturnTo"); if (return_url == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid or missing query ReturnTo parameter."); return HTTP_BAD_REQUEST; } if (am_urldecode(return_url) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Bad repost query: return"); return HTTP_BAD_REQUEST; } rc = am_validate_redirect_url(r, return_url); if (rc != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in repost request ReturnTo parameter."); return rc; } if ((file_data = am_file_data_new(r->pool, NULL)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Bad repost query: cannot allocate file_data"); apr_table_setn(r->headers_out, "Location", return_url); return HTTP_SEE_OTHER; } file_data->path = apr_psprintf(file_data->pool, "%s/%s", mod_cfg->post_dir, psf_id); rc = am_file_read(file_data); if (rc != APR_SUCCESS) { /* Unable to load repost data. Just redirect us instead. */ AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Bad repost query: %s", file_data->strerror); apr_table_setn(r->headers_out, "Location", return_url); return HTTP_SEE_OTHER; } else { post_data = file_data->contents; } if ((post_form = (*post_mkform)(r, post_data)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "am_post_mkform() failed"); return HTTP_INTERNAL_SERVER_ERROR; } if (charset != NULL) { ap_set_content_type(r, apr_psprintf(r->pool, "text/html; charset=\"%s\"", charset)); charset = apr_psprintf(r->pool, " accept-charset=\"%s\"", charset); } else { ap_set_content_type(r, "text/html"); charset = (char *)""; } output = apr_psprintf(r->pool, "\n" "\n" " \n" " SAML rePOST request\n" " \n" " \n" " \n" "
\n%s" " \n" "
\n" " \n" "\n", am_htmlencode(r, return_url), enctype, charset, post_form); ap_rputs(output, r); return OK; } /* This function handles responses to metadata request * * Parameters: * request_rec *r The request we received. * * Returns: * OK on success, or an error on failure. */ static int am_handle_metadata(request_rec *r) { #ifdef HAVE_lasso_server_new_from_buffers am_dir_cfg_rec *cfg = am_get_dir_cfg(r); LassoServer *server; const char *data; am_diag_printf(r, "enter function %s\n", __func__); server = am_get_lasso_server(r); if(server == NULL) return HTTP_INTERNAL_SERVER_ERROR; cfg = cfg->inherit_server_from; data = cfg->sp_metadata_file ? cfg->sp_metadata_file->contents : NULL; if (data == NULL) return HTTP_INTERNAL_SERVER_ERROR; ap_set_content_type(r, "application/samlmetadata+xml"); ap_rputs(data, r); return OK; #else /* ! HAVE_lasso_server_new_from_buffers */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "metadata publishing require lasso 2.2.2 or higher"); return HTTP_NOT_FOUND; #endif } /* Use Lasso Login to set the HTTP content & headers for HTTP-Redirect binding. * * Parameters: * request_rec *r * LassoLogin *login * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_set_authn_request_redirect_content(request_rec *r, LassoLogin *login) { char *redirect_to; /* The URL we should send the message to. */ redirect_to = apr_pstrdup(r->pool, LASSO_PROFILE(login)->msg_url); /* Check if the lasso library added the RelayState. If lasso didn't add * a RelayState parameter, then we add one ourself. This should hopefully * be removed in the future. */ if(strstr(redirect_to, "&RelayState=") == NULL && strstr(redirect_to, "?RelayState=") == NULL) { /* The url didn't contain the relaystate parameter. */ redirect_to = apr_pstrcat( r->pool, redirect_to, "&RelayState=", am_urlencode(r->pool, LASSO_PROFILE(login)->msg_relayState), NULL ); } apr_table_setn(r->headers_out, "Location", redirect_to); /* We don't want to include POST data (in case this was a POST request). */ return HTTP_SEE_OTHER; } /* Use Lasso Login to set the HTTP content & headers for HTTP-POST binding. * * Parameters: * request_rec *r The request we are processing. * LassoLogin *login The login message. * * Returns: * OK on success, or an error on failure. */ static int am_set_authn_request_post_content(request_rec *r, LassoLogin *login) { char *url; char *message; char *relay_state; char *output; url = am_htmlencode(r, LASSO_PROFILE(login)->msg_url); message = am_htmlencode(r, LASSO_PROFILE(login)->msg_body); relay_state = am_htmlencode(r, LASSO_PROFILE(login)->msg_relayState); output = apr_psprintf(r->pool, "\n" "\n" " \n" " \n" " POST data\n" " \n" " \n" " \n" "
\n" " \n" " \n" " \n" "
\n" " \n" "\n", url, message, relay_state); ap_set_content_type(r, "text/html"); ap_rputs(output, r); return OK; } /* Use Lasso Login to set the HTTP content & headers for PAOS binding. * * Parameters: * request_rec *r The request we are processing. * LassoLogin *login The login message. * * Returns: * OK on success, or an error on failure. */ static int am_set_authn_request_paos_content(request_rec *r, LassoLogin *login) { ap_set_content_type(r, MEDIA_TYPE_PAOS); ap_rputs(LASSO_PROFILE(login)->msg_body, r); return OK; } /* * Create and initialize LassoLogin object * * This function creates a LassoLogin object and initializes it to the * greatest extent possible to allow it to be shared by multiple * callers. There are two return values. The function return is an * error code, the login_return parameter is a pointer in which to * receive the LassoLogin object. The caller MUST free the returned * login object using lasso_login_destroy() in all cases (even when * this function returns an error), the only execption is if the * returned LassoLogin is NULL. * * Parameters: * r The request we are processing. * login_return The returned LassoLogin object (caller must free) * idp The provider id of remote Idp * [optional, may be NULL] * http_method Specifies the SAML profile to use * destination_url If the idp parameter is non-NULL this should be * the URL of the IdP endpoint the message is being sent to * [optional, may be NULL] * assertion_consumer_service_url * The URL of this SP's endpoint which will receive the * SAML assertion * return_to_url Used to initialize the RelayState value * is_passive The SAML IsPassive flag * * Returns: * OK on success, HTTP error code otherwise * */ static int am_init_authn_request_common(request_rec *r, LassoLogin **login_return, const char *idp, LassoHttpMethod http_method, const char *destination_url, const char *assertion_consumer_service_url, const char *return_to_url, int is_passive) { gint ret; am_dir_cfg_rec *dir_cfg; LassoServer *server; LassoLogin *login; LassoSamlp2AuthnRequest *request; const char *sp_name; *login_return = NULL; dir_cfg = am_get_dir_cfg(r); server = am_get_lasso_server(r); if (server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } login = lasso_login_new(server); if(login == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error creating LassoLogin object from LassoServer."); return HTTP_INTERNAL_SERVER_ERROR; } *login_return = login; ret = lasso_login_init_authn_request(login, idp, http_method); if(ret != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error creating login request." " Lasso error: [%i] %s", ret, lasso_strerror(ret)); return HTTP_INTERNAL_SERVER_ERROR; } request = LASSO_SAMLP2_AUTHN_REQUEST(LASSO_PROFILE(login)->request); if (request->NameIDPolicy == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error creating login request. Please verify the " "MellonSPMetadataFile directive."); return HTTP_INTERNAL_SERVER_ERROR; } /* * Make sure the Destination attribute is set to the IdP * SingleSignOnService endpoint. This is required for * Shibboleth 2 interoperability, and older versions of * lasso (at least up to 2.2.91) did not do it. */ if (destination_url && LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Destination == NULL) { lasso_assign_string(LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Destination, destination_url); } if (assertion_consumer_service_url) { lasso_assign_string(request->AssertionConsumerServiceURL, assertion_consumer_service_url); /* Can't set request->ProtocolBinding (which is usually set along side * AssertionConsumerServiceURL) as there is no immediate function * like lasso_provider_get_assertion_consumer_service_url to get them. * So leave that empty for now, it is not strictly required */ } request->ForceAuthn = FALSE; request->IsPassive = is_passive; request->NameIDPolicy->AllowCreate = TRUE; sp_name = am_get_config_langstring(dir_cfg->sp_org_display_name, NULL); if (sp_name) { lasso_assign_string(request->ProviderName, sp_name); } LASSO_SAMLP2_REQUEST_ABSTRACT(request)->Consent = g_strdup(LASSO_SAML2_CONSENT_IMPLICIT); /* Add AuthnContextClassRef */ if (dir_cfg->authn_context_class_ref->nelts) { apr_array_header_t *refs = dir_cfg->authn_context_class_ref; int i = 0; LassoSamlp2RequestedAuthnContext *req_authn_context; req_authn_context = (LassoSamlp2RequestedAuthnContext*) lasso_samlp2_requested_authn_context_new(); request->RequestedAuthnContext = req_authn_context; for (i = 0; i < refs->nelts; i++) { const char *ref = ((char **)refs->elts)[i]; req_authn_context->AuthnContextClassRef = g_list_append(req_authn_context->AuthnContextClassRef, g_strdup(ref)); AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "adding AuthnContextClassRef %s to the " "AuthnRequest", ref); } if (dir_cfg->authn_context_comparison_type != NULL) { lasso_assign_string(request->RequestedAuthnContext->Comparison, dir_cfg->authn_context_comparison_type); } } LASSO_PROFILE(login)->msg_relayState = g_strdup(return_to_url); #ifdef HAVE_ECP { am_req_cfg_rec *req_cfg; ECPServiceOptions unsupported_ecp_options; req_cfg = am_get_req_cfg(r); /* * Currently we only support the WANT_AUTHN_SIGNED ECP option, * if a client sends us anything else let them know it's not * implemented. * * We do test for CHANNEL_BINDING below but that's because if * and when we support it we don't want to forget channel * bindings require the authn request to be signed. */ unsupported_ecp_options = req_cfg->ecp_service_options & ~ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED; if (unsupported_ecp_options) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unsupported ECP service options [%s]", am_ecp_service_options_str(r->pool, unsupported_ecp_options)); return HTTP_NOT_IMPLEMENTED; } /* * The signature hint must be set prior to calling * lasso_login_build_authn_request_msg */ if (req_cfg->ecp_service_options & (ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED | ECP_SERVICE_OPTION_CHANNEL_BINDING)) { /* * authnRequest should be signed if the client requested it * or if channel bindings are enabled. */ lasso_profile_set_signature_hint(LASSO_PROFILE(login), LASSO_PROFILE_SIGNATURE_HINT_FORCE); } } #endif ret = lasso_login_build_authn_request_msg(login); if (ret != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error building login request." " Lasso error: [%i] %s", ret, lasso_strerror(ret)); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } /* Use Lasso Login to set the HTTP content & headers for selected binding. * * Parameters: * request_rec *r The request we are processing. * LassoLogin *login The login message. * * Returns: * HTTP response code */ static int am_set_authn_request_content(request_rec *r, LassoLogin *login) { am_diag_log_lasso_node(r, 0, LASSO_PROFILE(login)->request, "SAML AuthnRequest: http_method=%s URL=%s", am_diag_lasso_http_method_str(login->http_method), LASSO_PROFILE(login)->msg_url); switch (login->http_method) { case LASSO_HTTP_METHOD_REDIRECT: return am_set_authn_request_redirect_content(r, login); case LASSO_HTTP_METHOD_POST: return am_set_authn_request_post_content(r, login); case LASSO_HTTP_METHOD_PAOS: return am_set_authn_request_paos_content(r, login); default: /* We should never get here. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unsupported http_method."); return HTTP_INTERNAL_SERVER_ERROR; } } #ifdef HAVE_ECP /* Build an IDPList whose members have an endpoint supporing * the protocol_type and http_method. */ static LassoNode * am_get_idp_list(const LassoServer *server, LassoMdProtocolType protocol_type, LassoHttpMethod http_method) { GList *idp_entity_ids = NULL; GList *entity_id = NULL; GList *idp_entries = NULL; LassoSamlp2IDPList *idp_list; LassoSamlp2IDPEntry *idp_entry; idp_list = LASSO_SAMLP2_IDP_LIST(lasso_samlp2_idp_list_new()); idp_entity_ids = lasso_server_get_filtered_provider_list(server, LASSO_PROVIDER_ROLE_IDP, protocol_type, http_method); for (entity_id = g_list_first(idp_entity_ids); entity_id != NULL; entity_id = g_list_next(entity_id)) { idp_entry = LASSO_SAMLP2_IDP_ENTRY(lasso_samlp2_idp_entry_new()); idp_entry->ProviderID = g_strdup(entity_id->data); /* RFE: we should have a mechanism to obtain these values */ idp_entry->Name = NULL; idp_entry->Loc = NULL; idp_entries = g_list_append(idp_entries, idp_entry); } lasso_release_list_of_strings(idp_entity_ids); idp_list->IDPEntry = idp_entries; return LASSO_NODE(idp_list); } /* Send AuthnRequest using PAOS binding. * * Parameters: * request_rec *r * * Returns: * OK on success, or an error on failure. */ static int am_send_paos_authn_request(request_rec *r) { gint ret; am_dir_cfg_rec *dir_cfg; LassoServer *server; LassoLogin *login; const char *relay_state = NULL; char *assertion_consumer_service_url; int is_passive = FALSE; dir_cfg = am_get_dir_cfg(r); server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } relay_state = am_reconstruct_url(r); assertion_consumer_service_url = am_get_assertion_consumer_service_by_binding(LASSO_PROVIDER(server), "PAOS"); ret = am_init_authn_request_common(r, &login, NULL, LASSO_HTTP_METHOD_PAOS, NULL, assertion_consumer_service_url, relay_state, is_passive); g_free(assertion_consumer_service_url); if (ret != OK) { if (login) { lasso_login_destroy(login); } return ret; } if (CFG_VALUE(dir_cfg, ecp_send_idplist)) { lasso_profile_set_idp_list(LASSO_PROFILE(login), am_get_idp_list(LASSO_PROFILE(login)->server, LASSO_MD_PROTOCOL_TYPE_SINGLE_SIGN_ON, LASSO_HTTP_METHOD_SOAP)); } ret = am_set_authn_request_content(r, login); lasso_login_destroy(login); return ret; } #endif /* HAVE_ECP */ /* Create and send an authentication request. * * Parameters: * request_rec *r The request we are processing. * const char *idp The entityID of the IdP. * const char *return_to The URL we should redirect to when receiving the request. * int is_passive The value of the IsPassive flag in * * Returns: * HTTP response code indicating success or failure. */ static int am_send_login_authn_request(request_rec *r, const char *idp, const char *return_to_url, int is_passive) { int ret; LassoServer *server; LassoProvider *provider; LassoHttpMethod http_method; char *destination_url; char *assertion_consumer_service_url; LassoLogin *login; /* Add cookie for cookie test. We know that we should have * a valid cookie when we return from the IdP after SP-initiated * login. * Ensure that SameSite is set to None for this cookie if SameSite * is allowed to be set as the cookie otherwise gets lost on * HTTP-POST binding messages. */ apr_table_setn(r->notes, AM_FORCE_SAMESITE_NONE_NOTE, "1"); am_cookie_set(r, "cookietest"); apr_table_unset(r->notes, AM_FORCE_SAMESITE_NONE_NOTE); server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } /* Find our IdP. */ provider = lasso_server_get_provider(server, idp); if (provider == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not find metadata for the IdP \"%s\".", idp); return HTTP_INTERNAL_SERVER_ERROR; } /* Determine what binding and endpoint we should use when * sending the request. */ http_method = LASSO_HTTP_METHOD_REDIRECT; destination_url = lasso_provider_get_metadata_one( provider, "SingleSignOnService HTTP-Redirect"); if (destination_url == NULL) { /* HTTP-Redirect unsupported - try HTTP-POST. */ http_method = LASSO_HTTP_METHOD_POST; destination_url = lasso_provider_get_metadata_one( provider, "SingleSignOnService HTTP-POST"); } if (destination_url == NULL) { /* Both HTTP-Redirect and HTTP-POST unsupported - give up. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Could not find a supported SingleSignOnService endpoint" " for the IdP \"%s\".", idp); return HTTP_INTERNAL_SERVER_ERROR; } assertion_consumer_service_url = lasso_provider_get_assertion_consumer_service_url( LASSO_PROVIDER(server), NULL); ret = am_init_authn_request_common(r, &login, idp, http_method, destination_url, assertion_consumer_service_url, return_to_url, is_passive); g_free(destination_url); g_free(assertion_consumer_service_url); if (ret != OK) { if (login) { lasso_login_destroy(login); } return ret; } ret = am_set_authn_request_content(r, login); lasso_login_destroy(login); return ret; } /* Handle the "auth" endpoint. * * This endpoint is included for backwards-compatibility. * * Parameters: * request_rec *r The request we received. * * Returns: * OK or HTTP_SEE_OTHER on success, an error on failure. */ static int am_handle_auth(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *relay_state; am_diag_printf(r, "enter function %s\n", __func__); relay_state = am_reconstruct_url(r); /* Check if IdP discovery is in use and no IdP was selected yet */ if ((cfg->discovery_url != NULL) && (am_extract_query_parameter(r->pool, r->args, "IdP") == NULL)) { return am_start_disco(r, relay_state); } /* If IdP discovery is in use and we have an IdP selected, * set the relay_state */ if (cfg->discovery_url != NULL) { char *return_url; return_url = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); if ((return_url != NULL) && am_urldecode((char *)return_url) == 0) relay_state = return_url; } return am_send_login_authn_request(r, am_get_idp(r), relay_state, FALSE); } /* This function handles requests to the login handler. * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_login(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); char *idp_param; const char *idp; char *return_to; int is_passive; int ret; am_diag_printf(r, "enter function %s\n", __func__); return_to = am_extract_query_parameter(r->pool, r->args, "ReturnTo"); if(return_to == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing required ReturnTo parameter."); return HTTP_BAD_REQUEST; } ret = am_urldecode(return_to); if(ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding ReturnTo parameter."); return ret; } ret = am_validate_redirect_url(r, return_to); if(ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in login request ReturnTo parameter."); return ret; } idp_param = am_extract_query_parameter(r->pool, r->args, "IdP"); if(idp_param != NULL) { ret = am_urldecode(idp_param); if(ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding IdP parameter."); return ret; } } ret = am_get_boolean_query_parameter(r, "IsPassive", &is_passive, FALSE); if (ret != OK) { return ret; } if(idp_param != NULL) { idp = idp_param; } else if(cfg->discovery_url) { if(is_passive) { /* We cannot currently do discovery with passive authentication requests. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Discovery service with passive authentication request unsupported."); return HTTP_INTERNAL_SERVER_ERROR; } return am_start_disco(r, return_to); } else { /* No discovery service -- just use the default IdP. */ idp = am_get_idp(r); } return am_send_login_authn_request(r, idp, return_to, is_passive); } /* This function probes an URL (HTTP GET) * * Parameters: * request_rec *r The request. * const char *url The URL * int timeout Timeout in seconds * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_probe_url(request_rec *r, const char *url, int timeout) { void *dontcare; apr_size_t len; long status; int error; status = 0; if ((error = am_httpclient_get(r, url, &dontcare, &len, timeout, &status)) != OK) return error; if (status != HTTP_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Probe on \"%s\" returned HTTP %ld", url, status); return status; } return OK; } /* This function handles requests to the probe discovery handler * * Parameters: * request_rec *r The request. * * Returns: * OK on success, or an error if any of the steps fail. */ static int am_handle_probe_discovery(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); LassoServer *server; const char *disco_idp = NULL; int timeout; char *return_to; char *idp_param; char *redirect_url; int ret; am_diag_printf(r, "enter function %s\n", __func__); server = am_get_lasso_server(r); if(server == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } /* * If built-in IdP discovery is not configured, return error. * For now we only have the get-metadata metadata method, so this * information is not saved in configuration nor it is checked here. */ timeout = cfg->probe_discovery_timeout; if (timeout == -1) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "probe discovery handler invoked but not " "configured. Please set MellonProbeDiscoveryTimeout."); return HTTP_INTERNAL_SERVER_ERROR; } /* * Check for mandatory arguments early to avoid sending * probles for nothing. */ return_to = am_extract_query_parameter(r->pool, r->args, "return"); if(return_to == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing required return parameter."); return HTTP_BAD_REQUEST; } ret = am_urldecode(return_to); if (ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, ret, r, "Could not urldecode return value."); return HTTP_BAD_REQUEST; } ret = am_validate_redirect_url(r, return_to); if (ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid target domain in probe discovery return parameter."); return ret; } idp_param = am_extract_query_parameter(r->pool, r->args, "returnIDParam"); if(idp_param == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Missing required returnIDParam parameter."); return HTTP_BAD_REQUEST; } ret = am_urldecode(idp_param); if (ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, ret, r, "Could not urldecode returnIDParam value."); return HTTP_BAD_REQUEST; } /* * Proceed with built-in IdP discovery. * * First try sending probes to IdP configured for discovery. * Second send probes for all configured IdP * The first to answer is chosen. * If none answer, use the first configured IdP */ if (!apr_is_empty_table(cfg->probe_discovery_idp)) { const apr_array_header_t *header; apr_table_entry_t *elts; const char *url; const char *idp; int i; header = apr_table_elts(cfg->probe_discovery_idp); elts = (apr_table_entry_t *)header->elts; for (i = 0; i < header->nelts; i++) { idp = elts[i].key; url = elts[i].val; if (am_probe_url(r, url, timeout) == OK) { disco_idp = idp; break; } } } else { GList *iter; GList *idp_list; const char *idp; idp_list = g_hash_table_get_keys(server->providers); for (iter = idp_list; iter != NULL; iter = iter->next) { idp = iter->data; if (am_probe_url(r, idp, timeout) == OK) { disco_idp = idp; break; } } g_list_free(idp_list); } /* * On failure, fail if a MellonProbeDiscoveryIdP * list was provided, otherwise try first IdP. */ if (disco_idp == NULL) { if (!apr_is_empty_table(cfg->probe_discovery_idp)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "probeDiscovery failed and non empty " "MellonProbeDiscoveryIdP was provided."); return HTTP_INTERNAL_SERVER_ERROR; } disco_idp = am_first_idp(r); if (disco_idp == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "probeDiscovery found no usable IdP."); return HTTP_INTERNAL_SERVER_ERROR; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "probeDiscovery " "failed, trying default IdP %s", disco_idp); } } else { AM_LOG_RERROR(APLOG_MARK, APLOG_INFO, 0, r, "probeDiscovery using %s", disco_idp); } redirect_url = apr_psprintf(r->pool, "%s%s%s=%s", return_to, strchr(return_to, '?') ? "&" : "?", am_urlencode(r->pool, idp_param), am_urlencode(r->pool, disco_idp)); apr_table_setn(r->headers_out, "Location", redirect_url); return HTTP_SEE_OTHER; } /* This function handles responses to request on our endpoint * * Parameters: * request_rec *r The request we received. * * Returns: * OK on success, or an error on failure. */ int am_handler(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); #ifdef HAVE_ECP am_req_cfg_rec *req_cfg = am_get_req_cfg(r); #endif /* HAVE_ECP */ const char *endpoint; /* * Normally this content handler is used to dispatch to the SAML * endpoints implmented by mod_auth_mellon. SAML endpoint dispatch * occurs when the URI begins with the SAML endpoint path. * * However, this handler is also responsible for generating ECP * authn requests, in this case the URL will be a protected * resource we're doing authtentication for. Early in the request * processing pipeline we detected we were doing ECP authn and set * a flag on the request. Here we test for that flag and if true * respond with the ECP PAOS authn request. * * If the request is neither for a SAML endpoint nor one that * requires generating an ECP authn we decline handling the request. */ #ifdef HAVE_ECP if (req_cfg->ecp_authn_req) { /* Are we doing ECP? */ return am_send_paos_authn_request(r); } #endif /* HAVE_ECP */ /* Check if this is a request for one of our endpoints. We check if * the uri starts with the path set with the MellonEndpointPath * configuration directive. */ if(strstr(r->uri, cfg->endpoint_path) != r->uri) return DECLINED; endpoint = &r->uri[strlen(cfg->endpoint_path)]; if (!strcmp(endpoint, "metadata")) { return am_handle_metadata(r); } else if (!strcmp(endpoint, "repost")) { return am_handle_repost(r); } else if(!strcmp(endpoint, "postResponse")) { return am_handle_post_reply(r); } else if(!strcmp(endpoint, "artifactResponse")) { return am_handle_artifact_reply(r); } else if(!strcmp(endpoint, "paosResponse")) { return am_handle_paos_reply(r); } else if(!strcmp(endpoint, "auth")) { return am_handle_auth(r); } else if(!strcmp(endpoint, "logout") || !strcmp(endpoint, "logoutRequest")) { /* logoutRequest is included for backwards-compatibility * with version 0.0.6 and older. */ return am_handle_logout(r); } else if(!strcmp(endpoint, "invalidate")) { return am_handle_invalidate(r); } else if(!strcmp(endpoint, "login")) { return am_handle_login(r); } else if(!strcmp(endpoint, "probeDisco")) { return am_handle_probe_discovery(r); } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Endpoint \"%s\" not handled by mod_auth_mellon.", endpoint); return HTTP_NOT_FOUND; } } /** * Trigger a login operation from a "normal" request. * * Parameters: * request_rec *r The request we received. * * Returns: * HTTP_SEE_OTHER on success, or an error on failure. */ static int am_start_auth(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); const char *endpoint = am_get_endpoint_url(r); const char *return_to; const char *idp; const char *login_url; am_diag_printf(r, "enter function %s\n", __func__); return_to = am_reconstruct_url(r); /* If this is a POST request, attempt to save it */ if (r->method_number == M_POST) { if (CFG_VALUE(cfg, post_replay)) { if (am_save_post(r, &return_to) != OK) return HTTP_INTERNAL_SERVER_ERROR; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "POST data dropped because we do not have a" " MellonPostReplay is not enabled."); } } /* Check if IdP discovery is in use. */ if (cfg->discovery_url) { return am_start_disco(r, return_to); } idp = am_get_idp(r); login_url = apr_psprintf(r->pool, "%slogin?ReturnTo=%s&IdP=%s", endpoint, am_urlencode(r->pool, return_to), am_urlencode(r->pool, idp)); AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "Redirecting to login URL: %s", login_url); apr_table_setn(r->headers_out, "Location", login_url); return HTTP_SEE_OTHER; } int am_auth_mellon_user(request_rec *r) { am_dir_cfg_rec *dir = am_get_dir_cfg(r); int return_code = HTTP_UNAUTHORIZED; am_cache_entry_t *session; const char *ajax_header; if (r->main) { /* We are a subrequest. Trust the main request to have * performed the authentication. */ return OK; } /* Check that the user has enabled authentication for this directory. */ if(dir->enable_mellon == am_enable_off || dir->enable_mellon == am_enable_default) { return DECLINED; } am_diag_printf(r, "enter function %s\n", __func__); /* Set defaut Cache-Control headers within this location */ if (CFG_VALUE(dir, send_cache_control_header)) { am_set_cache_control_headers(r); } /* Check if this is a request for one of our endpoints. We check if * the uri starts with the path set with the MellonEndpointPath * configuration directive. */ if(strstr(r->uri, dir->endpoint_path) == r->uri) { /* No access control on our internal endpoints. */ return OK; } /* Get the session of this request. */ session = am_get_request_session(r); if(dir->enable_mellon == am_enable_auth) { /* This page requires the user to be authenticated and authorized. */ if(session == NULL || !session->logged_in) { /* We don't have a valid session. */ am_diag_printf(r, "%s am_enable_auth, no valid session\n", __func__); if(session) { /* Release the session. */ am_release_request_session(r, session); } /* * If this is an AJAX request, we cannot proceed to the IdP, * Just fail early to save our resources */ ajax_header = apr_table_get(r->headers_in, "X-Requested-With"); if (ajax_header != NULL && strcmp(ajax_header, "XMLHttpRequest") == 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_INFO, 0, r, "Deny unauthenticated X-Requested-With XMLHttpRequest " "(AJAX) request"); return HTTP_FORBIDDEN; } #ifdef HAVE_ECP /* * If PAOS set a flag on the request indicating we're * doing ECP and allow the request to proceed through the * request handlers until we reach am_handler which then * checks the flag and if True initiates an ECP transaction. * See am_check_uid for detailed explanation. */ bool is_paos; int error_code; is_paos = am_is_paos_request(r, &error_code); if (error_code) return HTTP_BAD_REQUEST; if (is_paos) { am_req_cfg_rec *req_cfg; req_cfg = am_get_req_cfg(r); req_cfg->ecp_authn_req = true; return OK; } else { /* Send the user to the authentication page on the IdP. */ return am_start_auth(r); } #else /* HAVE_ECP */ /* Send the user to the authentication page on the IdP. */ return am_start_auth(r); #endif /* HAVE_ECP */ } am_diag_printf(r, "%s am_enable_auth, have valid session\n", __func__); /* Verify that the user has access to this resource. */ return_code = am_check_permissions(r, session); if(return_code != OK) { am_diag_printf(r, "%s failed am_check_permissions, status=%d\n", __func__, return_code); am_release_request_session(r, session); return return_code; } /* Update the idle timeout to whatever is set by MellonSessionIdleTimeout. */ am_cache_update_idle_timeout(r, session, dir->session_idle_timeout); /* The user has been authenticated, and we can now populate r->user * and the r->subprocess_env with values from the session store. */ am_cache_env_populate(r, session); /* Release the session. */ am_release_request_session(r, session); return OK; } else { /* dir->enable_mellon == am_enable_info: * We should pass information about the user to the web application * if the user is authorized to access this resource. * However, we shouldn't attempt to do any access control. */ if(session != NULL && session->logged_in && am_check_permissions(r, session) == OK) { am_diag_printf(r, "%s am_enable_info, have valid session\n", __func__); /* Update the idle timeout to whatever is set by MellonSessionIdleTimeout. */ am_cache_update_idle_timeout(r, session, dir->session_idle_timeout); /* The user is authenticated and has access to the resource. * Now we populate the environment with information about * the user. */ am_cache_env_populate(r, session); } else { am_diag_printf(r, "%s am_enable_info, no valid session\n", __func__); } if(session != NULL) { /* Release the session. */ am_release_request_session(r, session); } /* We shouldn't really do any access control, so we always return * DECLINED. */ return DECLINED; } } int am_check_uid(request_rec *r) { am_dir_cfg_rec *dir = am_get_dir_cfg(r); am_cache_entry_t *session; int return_code = HTTP_UNAUTHORIZED; if (r->main) { /* We are a subrequest. Trust the main request to have * performed the authentication. */ if (r->main->user) { /* Make sure that the username from the main request is * available in the subrequest. */ r->user = apr_pstrdup(r->pool, r->main->user); } return OK; } /* Check that the user has enabled authentication for this directory. */ if(dir->enable_mellon == am_enable_off || dir->enable_mellon == am_enable_default) { return DECLINED; } am_diag_printf(r, "enter function %s\n", __func__); #ifdef HAVE_ECP am_req_cfg_rec *req_cfg = am_get_req_cfg(r); if (req_cfg->ecp_authn_req) { AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "am_check_uid is performing ECP authn request flow"); /* * Normally when a protected resource requires authentication * the request processing pipeline is exited early by * responding with either a 401 or a redirect. But the flow * for ECP is different, there will be a successful response * (200) but instead of the response body containing the * protected resource it will contain a SAML AuthnRequest * with the Content-Type indicating it's PAOS ECP. * * In order to return a 200 Success with a PAOS body we have * to reach the handler stage of the request processing * pipeline. But this is a protected resource and we won't * reach the handler stage unless authn and authz are * satisfied. Therefore we lie and return results which * indicate authn and authz are satisfied. This is OK because * we're not actually going to respond with the protected * resource, instead we'll be responsing with a SAML request. * * Apache's internal request logic * (ap_process_request_internal) requires that after a * successful return from the check_user_id authentication * hook the r->user value be non-NULL. This makes sense * because authentication establishes who the authenticated * principal is. But with ECP flow there is no authenticated * user at this point, we're just faking successful * authentication in order to reach the handler stage. To get * around this problem we set r-user to the empty string to * keep Apache happy, otherwise it would throw an * error. mod_shibboleth does the same thing. */ r->user = ""; return OK; } #endif /* HAVE_ECP */ /* Check if this is a request for one of our endpoints. We check if * the uri starts with the path set with the MellonEndpointPath * configuration directive. */ if(strstr(r->uri, dir->endpoint_path) == r->uri) { /* No access control on our internal endpoints. */ r->user = ""; /* see above explanation */ return OK; } /* Get the session of this request. */ session = am_get_request_session(r); /* If we don't have a session, then we can't authorize the user. */ if(session == NULL) { am_diag_printf(r, "%s no session, return HTTP_UNAUTHORIZED\n", __func__); return HTTP_UNAUTHORIZED; } /* If the user isn't logged in, then we can't authorize the user. */ if(!session->logged_in) { am_diag_printf(r, "%s session not logged in," " return HTTP_UNAUTHORIZED\n", __func__); am_release_request_session(r, session); return HTTP_UNAUTHORIZED; } /* Verify that the user has access to this resource. */ return_code = am_check_permissions(r, session); if(return_code != OK) { am_diag_printf(r, "%s failed am_check_permissions, status=%d\n", __func__, return_code); am_release_request_session(r, session); return return_code; } /* The user has been authenticated, and we can now populate r->user * and the r->subprocess_env with values from the session store. */ am_cache_env_populate(r, session); /* Release the session. */ am_release_request_session(r, session); return OK; } mod_auth_mellon-0.18.0/auth_mellon_httpclient.c000066400000000000000000000507271410105036600216660ustar00rootroot00000000000000/* * * mod_auth_mellon.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #include /* The size of the blocks we will allocate. */ #define AM_HC_BLOCK_SIZE 1000 #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* This structure describes a single-linked list of downloaded blocks. */ typedef struct am_hc_block_s { /* The next block we have allocated. */ struct am_hc_block_s *next; /* The number of bytes written to this block. */ apr_size_t used; /* The data stored in this block. */ uint8_t data[AM_HC_BLOCK_SIZE]; } am_hc_block_t; /* This structure describes a header for the block list. */ typedef struct { /* The pool we will allocate memory for new blocks from. */ apr_pool_t *pool; /* The first block in the linked list of blocks. */ am_hc_block_t *first; /* The last block in the linked list of blocks. */ am_hc_block_t *last; } am_hc_block_header_t; /* This function allocates and initializes a block for data copying. * * Parameters: * apr_pool_t *pool The pool we should allocate the block from. * * Returns: * The new block we allocated. */ static am_hc_block_t *am_hc_block_alloc(apr_pool_t *pool) { am_hc_block_t *blk; blk = (am_hc_block_t *)apr_palloc(pool, sizeof(am_hc_block_t)); blk->next = NULL; blk->used = 0; return blk; } /* This function adds data to the end of a block, and allocates new blocks * if the data doesn't fit in one block. * * Parameters: * am_hc_block_t *block The block we should begin by appending data to. * apr_pool_t *pool The pool we should allocate memory for new blocks * from. * const uint8_t *data The data we should append to the blocks. * apr_size_t size The length of the data we should append. * * Returns: * The last block written to (i.e. the next block we should write to). */ static am_hc_block_t *am_hc_block_write( am_hc_block_t *block, apr_pool_t *pool, const uint8_t *data, apr_size_t size ) { apr_size_t num_cpy; while(size > 0) { /* Find the number of bytes we should write to this block. */ num_cpy = AM_HC_BLOCK_SIZE - block->used; if(num_cpy == 0) { /* This block is full -- allocate a new block. */ block->next = am_hc_block_alloc(pool); block = block->next; num_cpy = AM_HC_BLOCK_SIZE; } if(num_cpy > size) { num_cpy = size; } /* Copy data to this block. */ memcpy(&block->data[block->used], data, num_cpy); block->used += num_cpy; size -= num_cpy; data += num_cpy; } /* The next write should be to this block. */ return block; } /* This function initializes a am_hc_block_header_t structure, which * contains information about the linked list of data blocks. * * Parameters: * am_hc_block_header_t *bh Pointer to the data header whcih we * should initialize. * apr_pool_t *pool The pool we should allocate data from. * * Returns: * Nothing. */ static void am_hc_block_header_init(am_hc_block_header_t *bh, apr_pool_t *pool) { bh->pool = pool; bh->first = am_hc_block_alloc(pool); bh->last = bh->first; } /* This function writes data to the linked list of blocks identified by * the stream-parameter. It matches the prototype required by curl. * * Parameters: * void *data The data that should be written. It is size*nmemb * bytes long. * size_t size The size of each block of data that should * be written. * size_t nmemb The number of blocks of data that should be written. * void *block_header A pointer to a am_hc_block_header_t structure which * identifies the linked list we should store data in. * * Returns: * The number of bytes that have been written. */ static size_t am_hc_data_write(void *data, size_t size, size_t nmemb, void *data_header) { am_hc_block_header_t *bh; bh = (am_hc_block_header_t *)data_header; bh->last = am_hc_block_write(bh->last, bh->pool, (const uint8_t *)data, size * nmemb); return size * nmemb; } /* This function fetches the data which was written to the databuffers * in the linked list which the am_hc_data_t structure keeps track of. * * Parameters: * am_hc_block_header_t *bh The header telling us which data buffers * we should extract data from. * apr_pool_t *pool The pool we should allocate the data * buffer from. * void **buffer A pointer to where we should store a pointer * to the data buffer we allocate. We will * always add a null-terminator to the end of * data buffer. This parameter can't be NULL. * apr_size_t *size This is a pointer to where we will store the * length of the data, not including the * null-terminator we add. This parameter can * be NULL. * * Returns: * Nothing. */ static void am_hc_data_extract(am_hc_block_header_t *bh, apr_pool_t *pool, void **buffer, apr_size_t *size) { am_hc_block_t *blk; apr_size_t length; uint8_t *buf; apr_size_t pos; /* First we find the length of the data. */ length = 0; for(blk = bh->first; blk != NULL; blk = blk->next) { length += blk->used; } /* Allocate memory for the data. Add one to the size in order to * have space for the null-terminator. */ buf = (uint8_t *)apr_palloc(pool, length + 1); /* Copy the data into the buffer. */ pos = 0; for(blk = bh->first; blk != NULL; blk = blk->next) { memcpy(&buf[pos], blk->data, blk->used); pos += blk->used; } /* Add the null-terminator. */ buf[length] = 0; /* Set up the return values. */ *buffer = (void *)buf; if(size != NULL) { *size = length; } } /* This function creates a curl object and performs generic initialization * of it. * * Parameters: * request_rec *r The request we should log errors against. * const char *uri The URI we should request. * am_hc_block_header_t *bh The buffer curl will write response data to. * char *curl_error A buffer of size CURL_ERROR_SIZE where curl * will store error messages. * * Returns: * A initialized curl object on succcess, or NULL on error. */ static CURL *am_httpclient_init_curl(request_rec *r, const char *uri, am_hc_block_header_t *bh, char *curl_error) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); CURL *curl; CURLcode res; /* Initialize the curl object. */ curl = curl_easy_init(); if(curl == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to initialize a curl object."); return NULL; } /* Set up error reporting. */ res = curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, curl_error); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set curl error buffer: [%u]\n", res); goto cleanup_fail; } /* Disable progress reporting. */ res = curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to disable curl progress reporting: [%u] %s", res, curl_error); goto cleanup_fail; } /* Disable use of signals. */ res = curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to disable signals in curl: [%u] %s", res, curl_error); goto cleanup_fail; } /* Set the timeout of the transfer. It is currently set to two minutes. */ res = curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set the timeout of the curl download:" " [%u] %s", res, curl_error); goto cleanup_fail; } /* If we have a CA configured, try to use it */ if (cfg->idp_ca_file != NULL) { res = curl_easy_setopt(curl, CURLOPT_CAINFO, cfg->idp_ca_file->path); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set SSL CA info %s:" " [%u] %s", cfg->idp_ca_file->path, res, curl_error); goto cleanup_fail; } } /* Enable fail on http error. */ res = curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to enable failure on http error: [%u] %s", res, curl_error); goto cleanup_fail; } /* Select which uri we should download. */ res = curl_easy_setopt(curl, CURLOPT_URL, uri); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set curl download uri to \"%s\": [%u] %s", uri, res, curl_error); goto cleanup_fail; } /* Set up data writing. */ /* Set curl write function. */ res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, am_hc_data_write); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set the curl write function: [%u] %s", res, curl_error); goto cleanup_fail; } /* Set the curl write function parameter. */ res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, bh); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set the curl write function data: [%u] %s", res, curl_error); goto cleanup_fail; } return curl; cleanup_fail: curl_easy_cleanup(curl); return NULL; } /* This function downloads data from a specified URI, with specified timeout * * Parameters: * request_rec *r The apache request this download is associated * with. It is used for memory allocation and logging. * const char *uri The URI we should download. * void **buffer A pointer to where we should store a pointer to the * downloaded data. We will always add a null-terminator * to the data. This parameter can't be NULL. * apr_size_t *size This is a pointer to where we will store the length * of the downloaded data, not including the * null-terminator we add. This parameter can be NULL. * int timeout Timeout in seconds, 0 for no timeout. * long *status Pointer to HTTP status code. * * Returns: * OK on success, or HTTP_INTERNAL_SERVER_ERROR on failure. On failure we * will write a log message describing the error. */ int am_httpclient_get(request_rec *r, const char *uri, void **buffer, apr_size_t *size, int timeout, long *status) { am_hc_block_header_t bh; CURL *curl; char curl_error[CURL_ERROR_SIZE]; CURLcode res; /* Initialize the data storage. */ am_hc_block_header_init(&bh, r->pool); /* Initialize the curl object. */ curl = am_httpclient_init_curl(r, uri, &bh, curl_error); if(curl == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } res = curl_easy_setopt(curl, CURLOPT_TIMEOUT, (long)timeout); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to download data from the uri \"%s\", " "cannot set timeout to %ld: [%u] %s", uri, (long)timeout, res, curl_error); goto cleanup_fail; } res = curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, (long)timeout); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to download data from the uri \"%s\", " "cannot set connect timeout to %ld: [%u] %s", uri, (long)timeout, res, curl_error); goto cleanup_fail; } /* Do the download. */ res = curl_easy_perform(curl); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to download data from the uri \"%s\", " "transaction aborted: [%u] %s", uri, res, curl_error); goto cleanup_fail; } if (status != NULL) { res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to download data from the uri \"%s\", " "no status report: [%u] %s", uri, res, curl_error); goto cleanup_fail; } } /* Free the curl object. */ curl_easy_cleanup(curl); /* Copy the data. */ am_hc_data_extract(&bh, r->pool, buffer, size); return OK; cleanup_fail: curl_easy_cleanup(curl); return HTTP_INTERNAL_SERVER_ERROR; } /* This function downloads data from a specified URI by issuing a POST * request. * * Parameters: * request_rec *r The apache request this download is * associated with. It is used for memory * allocation and logging. * const char *uri The URI we should post data to. * const void *post_data The POST data we should send. * apr_size_t post_length The length of the POST data. * const char *content_type The content type of the POST data. This * parameter can be NULL, in which case the * content type will be * "application/x-www-form-urlencoded". * void **buffer A pointer to where we should store a pointer * to the downloaded data. We will always add a * null-terminator to the data. This parameter * can't be NULL. * apr_size_t *size This is a pointer to where we will store the * length of the downloaded data, not including * the null-terminator we add. This parameter * can be NULL. * * Returns: * OK on success. On failure we will write a log message describing the * error, and return HTTP_INTERNAL_SERVER_ERROR. */ int am_httpclient_post(request_rec *r, const char *uri, const void *post_data, apr_size_t post_length, const char *content_type, void **buffer, apr_size_t *size) { am_hc_block_header_t bh; CURL *curl; char curl_error[CURL_ERROR_SIZE]; CURLcode res; struct curl_slist *ctheader; am_dir_cfg_rec *cfg = am_get_dir_cfg(r); /* Initialize the data storage. */ am_hc_block_header_init(&bh, r->pool); /* Initialize the curl object. */ curl = am_httpclient_init_curl(r, uri, &bh, curl_error); if(curl == NULL) { return HTTP_INTERNAL_SERVER_ERROR; } /* Enable POST request. */ res = curl_easy_setopt(curl, CURLOPT_POST, 1L); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to enable POST request: [%u] %s", res, curl_error); goto cleanup_fail; } /* Set POST data size. */ res = curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_length); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set the POST data length: [%u] %s", res, curl_error); goto cleanup_fail; } /* Set POST data. */ res = curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set the POST data: [%u] %s", res, curl_error); goto cleanup_fail; } /* Set the content-type header. */ /* Set default content type if content_type is NULL. */ if(content_type == NULL) { content_type = "application/x-www-form-urlencoded"; } /* Create header list. */ ctheader = NULL; ctheader = curl_slist_append(ctheader, apr_pstrcat( r->pool, "Content-Type: ", content_type, NULL )); /* Check if the send expect header is "off". */ if (cfg->send_expect_header == 0) { ctheader = curl_slist_append(ctheader, "Expect:"); } /* Set headers. */ res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, ctheader); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to set content-type header to \"%s\": [%u] %s", content_type, res, curl_error); goto cleanup_fail; } /* Do the download. */ res = curl_easy_perform(curl); if(res != CURLE_OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to download data from the uri \"%s\": [%u] %s", uri, res, curl_error); goto cleanup_fail; } /* Free the curl object. */ curl_easy_cleanup(curl); /* Free the content-type header. */ curl_slist_free_all(ctheader); /* Copy the data. */ am_hc_data_extract(&bh, r->pool, buffer, size); return OK; cleanup_fail: curl_easy_cleanup(curl); return HTTP_INTERNAL_SERVER_ERROR; } /* This function downloads data from a specified URI by issuing a POST * request. * * Parameters: * request_rec *r The apache request this download is * associated with. It is used for memory * allocation and logging. * const char *uri The URI we should post data to. * const char *post_data The POST data we should send. * const char *content_type The content type of the POST data. This * parameter can be NULL, in which case the * content type will be * "application/x-www-form-urlencoded". * void **buffer A pointer to where we should store a pointer * to the downloaded data. We will always add a * null-terminator to the data. This parameter * can't be NULL. * apr_size_t *size This is a pointer to where we will store the * length of the downloaded data, not including * the null-terminator we add. This parameter * can be NULL. * * Returns: * OK on success. On failure we will write a log message describing the * error, and return HTTP_INTERNAL_SERVER_ERROR. */ int am_httpclient_post_str(request_rec *r, const char *uri, const char *post_data, const char *content_type, void **buffer, apr_size_t *size) { return am_httpclient_post(r, uri, post_data, strlen(post_data), content_type, buffer, size); } mod_auth_mellon-0.18.0/auth_mellon_session.c000066400000000000000000000134531410105036600211660ustar00rootroot00000000000000/* * * auth_mellon_session.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* Retrieve a session from the cache and validate its cookie settings * * Parameters: * request_rec *r The request we received from the user. * am_cache_key_t type AM_CACHE_SESSION, AM_CACHE_NAMEID or AM_CACHE_ASSERTIONID * const char *key The session key or user * * Returns: * The session associated, or NULL if unable to retrieve the given session. */ am_cache_entry_t *am_lock_and_validate(request_rec *r, am_cache_key_t type, const char *key) { am_cache_entry_t *session = NULL; am_diag_printf(r, "searching for session with key %s (%s) ... ", key, am_diag_cache_key_type_str(type)); session = am_cache_lock(r, type, key); if (session == NULL) { am_diag_printf(r, "not found\n"); return NULL; } else { am_diag_printf(r, "found.\n"); am_diag_log_cache_entry(r, 0, session, "Session Cache Entry"); } const char *cookie_token_session = am_cache_entry_get_string( session, &session->cookie_token); const char *cookie_token_target = am_cookie_token(r); if (strcmp(cookie_token_session, cookie_token_target)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Session cookie parameter mismatch. " "Session created with {%s}, but current " "request has {%s}.", cookie_token_session, cookie_token_target); am_cache_unlock(r, session); return NULL; } return session; } /* This function gets the session associated with a user, using a cookie * * Parameters: * request_rec *r The request we received from the user. * * Returns: * The session associated with the user who places the request, or * NULL if we don't have a session yet. */ am_cache_entry_t *am_get_request_session(request_rec *r) { const char *session_id; /* Get session id from cookie. */ session_id = am_cookie_get(r); if(session_id == NULL) { /* Cookie is unset - we don't have a session. */ return NULL; } return am_lock_and_validate(r, AM_CACHE_SESSION, session_id); } /* This function gets the session associated with a user, using a NameID * * Parameters: * request_rec *r The request we received from the user. * char *nameid The NameID * * Returns: * The session associated with the user who places the request, or * NULL if we don't have a session yet. */ am_cache_entry_t *am_get_request_session_by_nameid(request_rec *r, char *nameid) { return am_lock_and_validate(r, AM_CACHE_NAMEID, nameid); } /* This function gets the session associated with a user, using the Assertion ID * * Parameters: * request_rec *r The request we received from the user. * char *assertionid The AssertionID * * Returns: * The session associated with the user who places the request, or * NULL if we don't have a session yet. */ am_cache_entry_t *am_get_request_session_by_assertionid(request_rec *r, char *assertionid) { return am_lock_and_validate(r, AM_CACHE_ASSERTIONID, assertionid); } /* This function creates a new session. * * Parameters: * request_rec *r The request we are processing. * * Returns: * The new session, or NULL if we have an internal error. */ am_cache_entry_t *am_new_request_session(request_rec *r) { const char *session_id; /* Generate session id. */ session_id = am_generate_id(r); if(session_id == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error creating session id."); return NULL; } /* Set session id. */ am_cookie_set(r, session_id); const char *cookie_token = am_cookie_token(r); am_diag_printf(r, "%s id=%s cookie_token=\"%s\"\n", __func__, session_id, cookie_token); return am_cache_new(r, session_id, cookie_token); } /* This function releases the session which was returned from * am_get_request_session. * * Parameters: * request_rec *r The request we are processing. * am_cache_entry_t *session The session we are releasing. * * Returns: * Nothing. */ void am_release_request_session(request_rec *r, am_cache_entry_t *session) { am_cache_unlock(r, session); } /* This function releases and deletes the session which was returned from * am_get_request_session. * * Parameters: * request_rec *r The request we are processing. * am_cache_entry_t *session The session we are deleting. * * Returns: * Nothing. */ void am_delete_request_session(request_rec *r, am_cache_entry_t *session) { am_diag_log_cache_entry(r, 0, session, "delete session"); /* Delete the cookie. */ am_cookie_delete(r); if(session == NULL) { return; } /* Delete session from the session store. */ am_cache_delete(r, session); } mod_auth_mellon-0.18.0/auth_mellon_util.c000066400000000000000000002412261410105036600204610ustar00rootroot00000000000000/* * * auth_mellon_util.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include #include #include #include "auth_mellon.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* This function is used to get the url of the current request. * * Parameters: * request_rec *r The current request. * * Returns: * A string containing the full url of the current request. * The string is allocated from r->pool. */ char *am_reconstruct_url(request_rec *r) { char *url; /* This function will construct an full url for a given path relative to * the root of the web site. To configure what hostname and port this * function will use, see the UseCanonicalName configuration directive. */ url = ap_construct_url(r->pool, r->unparsed_uri, r); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "reconstruct_url: url==\"%s\", unparsed_uri==\"%s\"", url, r->unparsed_uri); return url; } /* Get the hostname of the current request. * * Parameters: * request_rec *r The current request. * * Returns: * The hostname of the current request. */ static const char *am_request_hostname(request_rec *r) { const char *url; apr_uri_t uri; int ret; url = am_reconstruct_url(r); ret = apr_uri_parse(r->pool, url, &uri); if (ret != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to parse request URL: %s", url); return NULL; } if (uri.hostname == NULL) { /* This shouldn't happen, since the request URL is built with a hostname, * but log a message to make any debuggin around this code easier. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No hostname in request URL: %s", url); return NULL; } return uri.hostname; } /* Validate the redirect URL. * * Checks that the redirect URL is to a trusted domain & scheme. * * Parameters: * request_rec *r The current request. * const char *url The redirect URL to validate. * * Returns: * OK if the URL is valid, HTTP_BAD_REQUEST if not. */ int am_validate_redirect_url(request_rec *r, const char *url) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); apr_uri_t uri; int ret; ret = apr_uri_parse(r->pool, url, &uri); if (ret != APR_SUCCESS) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid redirect URL: %s", url); return HTTP_BAD_REQUEST; } /* Sanity check of the scheme of the domain. We only allow http and https. */ if (uri.scheme) { /* http and https schemes without hostname are invalid. */ if (!uri.hostname) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Preventing redirect with scheme but no hostname: %s", url); return HTTP_BAD_REQUEST; } if (strcasecmp(uri.scheme, "http") && strcasecmp(uri.scheme, "https")) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Only http or https scheme allowed in redirect URL: %s (%s)", url, uri.scheme); return HTTP_BAD_REQUEST; } } if (!uri.hostname) { return OK; /* No hostname to check. */ } for (int i = 0; cfg->redirect_domains[i] != NULL; i++) { const char *redirect_domain = cfg->redirect_domains[i]; if (!strcasecmp(redirect_domain, "[self]")) { if (!strcasecmp(uri.hostname, am_request_hostname(r))) { return OK; } } else if (apr_fnmatch(redirect_domain, uri.hostname, APR_FNM_PERIOD | APR_FNM_CASE_BLIND) == APR_SUCCESS) { return OK; } } AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Untrusted hostname (%s) in redirect URL: %s", uri.hostname, url); return HTTP_BAD_REQUEST; } /* This function builds an array of regexp backreferences * * Parameters: * request_rec *r The current request. * const am_cond_t *ce The condition * const char *value Attribute value * const ap_regmatch_t *regmatch regmatch_t from ap_regexec() * * Returns: * An array of collected backreference strings */ const apr_array_header_t *am_cond_backrefs(request_rec *r, const am_cond_t *ce, const char *value, const ap_regmatch_t *regmatch) { apr_array_header_t *backrefs; const char **ref; int nsub; int i; nsub = ce->regex->re_nsub + 1; /* +1 for %0 */ backrefs = apr_array_make(r->pool, nsub, sizeof(const char *)); backrefs->nelts = nsub; ref = (const char **)(backrefs->elts); for (i = 0; i < nsub; i++) { if ((regmatch[i].rm_so == -1) || (regmatch[i].rm_eo == -1)) { ref[i] = ""; } else { int len = regmatch[i].rm_eo - regmatch[i].rm_so; int off = regmatch[i].rm_so; ref[i] = apr_pstrndup(r->pool, value + off, len); } } return (const apr_array_header_t *)backrefs; } /* This function clones an am_cond_t and substitute value to * match (both regexp and string) with backreferences from * a previous regex match. * * Parameters: * request_rec *r The current request. * const am_cond_t *cond The am_cond_t to clone and substiture * const apr_array_header_t *backrefs Collected backreferences * * Returns: * The cloned am_cond_t */ const am_cond_t *am_cond_substitue(request_rec *r, const am_cond_t *ce, const apr_array_header_t *backrefs) { am_cond_t *c; const char *instr = ce->str; apr_size_t inlen = strlen(instr); const char *outstr = ""; size_t last; size_t i; c = (am_cond_t *)apr_pmemdup(r->pool, ce, sizeof(*ce)); last = 0; for (i = strcspn(instr, "%"); i < inlen; i += strcspn(instr + i, "%")) { const char *fstr; const char *ns; const char *name; const char *value; apr_size_t flen; apr_size_t pad; apr_size_t nslen; /* * Make sure we got a % */ assert(instr[i] == '%'); /* * Copy the format string in fstr. It can be a single * digit (e.g.: %1) , or a curly-brace enclosed text * (e.g.: %{12}) */ fstr = instr + i + 1; if (*fstr == '{') { /* Curly-brace enclosed text */ pad = 3; /* 3 for %{} */ fstr++; flen = strcspn(fstr, "}"); /* If there is no closing }, we do not substitute */ if (fstr[flen] == '\0') { pad = 2; /* 2 for %{ */ i += flen + pad; break; } } else if (*fstr == '\0') { /* String ending by a % */ break; } else { /* Single digit */ pad = 1; /* 1 for % */ flen = 1; } /* * Try to extract a namespace (ns) and a name, e.g: %{ENV:foo} */ fstr = apr_pstrndup(r->pool, fstr, flen); if ((nslen = strcspn(fstr, ":")) != flen) { ns = apr_pstrndup(r->pool, fstr, nslen); name = fstr + nslen + 1; /* +1 for : */ } else { nslen = 0; ns = ""; name = fstr; } value = NULL; if ((*ns == '\0') && (strspn(fstr, "0123456789") == flen) && (backrefs != NULL)) { /* * If fstr has only digits, this is a regexp backreference */ int d = (int)apr_atoi64(fstr); if ((d >= 0) && (d < backrefs->nelts)) value = ((const char **)(backrefs->elts))[d]; } else if ((*ns == '\0') && (strcmp(fstr, "%") == 0)) { /* * %-escape */ value = fstr; } else if (strcmp(ns, "ENV") == 0) { /* * ENV namespace. Get value from apache environment. * This is akin to how Apache itself does it during expression evaluation. */ value = apr_table_get(r->subprocess_env, name); if (value == NULL) { value = apr_table_get(r->notes, name); } if (value == NULL) { value = getenv(name); } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Resolving \"%s\" from ENV to \"%s\"", name, value == NULL ? "(nothing)" : value); } /* * If we did not find a value, substitue the * format string with an empty string. */ if (value == NULL) value = ""; /* * Concatenate the value with leading text, and * keep track * of the last location we copied in source string */ outstr = apr_pstrcat(r->pool, outstr, apr_pstrndup(r->pool, instr + last, i - last), value, NULL); last = i + flen + pad; /* * Move index to the end of the format string */ i += flen + pad; } /* * Copy text remaining after the last format string. */ outstr = apr_pstrcat(r->pool, outstr, apr_pstrndup(r->pool, instr + last, i - last), NULL); c->str = outstr; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Directive %s, \"%s\" substituted into \"%s\"", ce->directive, instr, outstr); /* * If this was a regexp, recompile it. */ if (ce->flags & AM_COND_FLAG_REG) { int regex_flags = AP_REG_EXTENDED|AP_REG_NOSUB; if (ce->flags & AM_COND_FLAG_NC) regex_flags |= AP_REG_ICASE; c->regex = ap_pregcomp(r->pool, outstr, regex_flags); if (c->regex == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Invalid regular expression \"%s\"", outstr); return ce; } } return (const am_cond_t *)c; } /* This function checks if the user has access according * to the MellonRequire and MellonCond directives. * * Parameters: * request_rec *r The current request. * am_cache_entry_t *session The current session. * * Returns: * OK if the user has access and HTTP_FORBIDDEN if he doesn't. */ int am_check_permissions(request_rec *r, am_cache_entry_t *session) { am_dir_cfg_rec *dir_cfg; int i, j; int skip_or = 0; const apr_array_header_t *backrefs = NULL; dir_cfg = am_get_dir_cfg(r); /* Iterate over all cond-directives */ for (i = 0; i < dir_cfg->cond->nelts; i++) { const am_cond_t *ce; const char *value = NULL; int match = 0; ce = &((am_cond_t *)(dir_cfg->cond->elts))[i]; am_diag_printf(r, "%s processing condition %d of %d: %s ", __func__, i, dir_cfg->cond->nelts, am_diag_cond_str(r, ce)); /* * Rule with ignore flag? */ if (ce->flags & AM_COND_FLAG_IGN) continue; /* * We matched a [OR] rule, skip the next rules * until we have one without [OR]. */ if (skip_or) { if (!(ce->flags & AM_COND_FLAG_OR)) skip_or = 0; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Skip %s, [OR] rule matched previously", ce->directive); am_diag_printf(r, "Skip, [OR] rule matched previously\n"); continue; } /* * look for a match on each value for this attribute, * stop on first match. */ for (j = 0; (j < session->size) && !match; j++) { const char *varname = NULL; am_envattr_conf_t *envattr_conf = NULL; /* * if MAP flag is set, check for remapped * attribute name with mellonSetEnv */ if (ce->flags & AM_COND_FLAG_MAP) { envattr_conf = (am_envattr_conf_t *)apr_hash_get(dir_cfg->envattr, am_cache_entry_get_string(session,&session->env[j].varname), APR_HASH_KEY_STRING); if (envattr_conf != NULL) varname = envattr_conf->name; } /* * Otherwise or if not found, use the attribute name * sent by the IdP. */ if (varname == NULL) varname = am_cache_entry_get_string(session, &session->env[j].varname); if (strcmp(varname, ce->varname) != 0) continue; value = am_cache_entry_get_string(session, &session->env[j].value); /* * Substitute backrefs if available */ if (ce->flags & AM_COND_FLAG_FSTR) ce = am_cond_substitue(r, ce, backrefs); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "Evaluate %s vs \"%s\"", ce->directive, value); am_diag_printf(r, "evaluate value \"%s\" ", value); if (value == NULL) { match = 0; /* can not happen */ } else if ((ce->flags & AM_COND_FLAG_REG) && (ce->flags & AM_COND_FLAG_REF)) { int nsub = ce->regex->re_nsub + 1; ap_regmatch_t *regmatch; regmatch = (ap_regmatch_t *)apr_palloc(r->pool, nsub * sizeof(*regmatch)); match = !ap_regexec(ce->regex, value, nsub, regmatch, 0); if (match) backrefs = am_cond_backrefs(r, ce, value, regmatch); } else if (ce->flags & AM_COND_FLAG_REG) { match = !ap_regexec(ce->regex, value, 0, NULL, 0); } else if ((ce->flags & AM_COND_FLAG_SUB) && (ce->flags & AM_COND_FLAG_NC)) { match = (ap_strcasestr(value, ce->str) != NULL); } else if (ce->flags & AM_COND_FLAG_SUB) { match = (strstr(value, ce->str) != NULL); } else if (ce->flags & AM_COND_FLAG_NC) { match = !strcasecmp(ce->str, value); } else { match = !strcmp(ce->str, value); } am_diag_printf(r, "match=%s, ", match ? "yes" : "no"); } if (ce->flags & AM_COND_FLAG_NOT) { match = !match; am_diag_printf(r, "negating now match=%s ", match ? "yes" : "no"); } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, "%s: %smatch", ce->directive, (match == 0) ? "no ": ""); /* * If no match, we stop here, except if it is an [OR] condition */ if (!match & !(ce->flags & AM_COND_FLAG_OR)) { ap_log_rerror(APLOG_MARK, APLOG_NOTICE, 0, r, "Client failed to match %s", ce->directive); am_diag_printf(r, "failed (no OR condition)" " returning HTTP_FORBIDDEN\n"); return HTTP_FORBIDDEN; } /* * Match on [OR] condition means we skip until a rule * without [OR], */ if (match && (ce->flags & AM_COND_FLAG_OR)) skip_or = 1; am_diag_printf(r, "\n"); } am_diag_printf(r, "%s succeeds\n", __func__); return OK; } /* This function sets default Cache-Control headers. * * Parameters: * request_rec *r The request we are handling. * * Returns: * Nothing. */ void am_set_cache_control_headers(request_rec *r) { /* Send Cache-Control header to ensure that: * - no proxy in the path caches content inside this location (private), * - user agent have to revalidate content on server (must-revalidate). * - content is always stale as the session login status can change at any * time synchronously (Redirect logout, session cookie is removed) or * asynchronously (SOAP logout, session cookie still exists but is * invalid), * * But never prohibit specifically any user agent to cache or store content * * Setting the headers in err_headers_out ensures that they will be * sent for all responses. */ apr_table_setn(r->err_headers_out, "Cache-Control", "private, max-age=0, must-revalidate"); } /* This function reads the post data for a request. * * The data is stored in a buffer allocated from the request pool. * After successful operation *data contains a pointer to the data and * *length contains the length of the data. * The data will always be null-terminated. * * Parameters: * request_rec *r The request we read the form data from. * char **data Pointer to where we will store the pointer * to the data we read. * apr_size_t *length Pointer to where we will store the length * of the data we read. Pass NULL if you don't * need to know the length of the data. * * Returns: * OK if we successfully read the POST data. * An error if we fail to read the data. */ int am_read_post_data(request_rec *r, char **data, apr_size_t *length) { apr_size_t bytes_read; apr_size_t bytes_left; apr_size_t len; long read_length; int rc; /* Prepare to receive data from the client. We request that apache * dechunks data if it is chunked. */ rc = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK); if (rc != OK) { return rc; } /* This function will send a 100 Continue response if the client is * waiting for that. If the client isn't going to send data, then this * function will return 0. */ if (!ap_should_client_block(r)) { len = 0; } else { len = r->remaining; } if (len >= 1024*1024) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Too large POST data payload (%lu bytes).", (unsigned long)len); return HTTP_BAD_REQUEST; } if (length != NULL) { *length = len; } *data = (char *)apr_palloc(r->pool, len + 1); if (*data == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to allocate memory for %lu bytes of POST data.", (unsigned long)len); return HTTP_INTERNAL_SERVER_ERROR; } /* Make sure that the data is null-terminated. */ (*data)[len] = '\0'; bytes_read = 0; bytes_left = len; while (bytes_left > 0) { /* Read data from the client. Returns 0 on EOF and -1 on * error, the number of bytes otherwise. */ read_length = ap_get_client_block(r, &(*data)[bytes_read], bytes_left); if (read_length == 0) { /* got the EOF */ (*data)[bytes_read] = '\0'; if (length != NULL) { *length = bytes_read; } break; } else if (read_length < 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Failed to read POST data from client."); return HTTP_INTERNAL_SERVER_ERROR; } bytes_read += read_length; bytes_left -= read_length; } am_diag_printf(r, "POST data: %s\n", *data); return OK; } /* extract_query_parameter is a function which extracts the value of * a given parameter in a query string. The query string can be the * query_string parameter of a GET request, or it can be the data * passed to the web server in a POST request. * * Parameters: * apr_pool_t *pool The memory pool which the memory for * the value will be allocated from. * const char *query_string Either the query_string from a GET * request, or the data from a POST * request. * const char *name The name of the parameter to extract. * Note that the search for this name is * case sensitive. * * Returns: * The value of the parameter or NULL if we don't find the parameter. */ char *am_extract_query_parameter(apr_pool_t *pool, const char *query_string, const char *name) { const char *ip; const char *value_end; apr_size_t namelen; if (query_string == NULL) { return NULL; } ip = query_string; namelen = strlen(name); /* Find parameter. Searches for /[^&][&=$]/. * Moves ip to the first character after the name (either '&', '=' * or '\0'). */ for (;;) { /* First we find the name of the parameter. */ ip = strstr(ip, name); if (ip == NULL) { /* Parameter not found. */ return NULL; } /* Then we check what is before the parameter name. */ if (ip != query_string && ip[-1] != '&') { /* Name not preceded by [^&]. */ ip++; continue; } /* And last we check what follows the parameter name. */ if (ip[namelen] != '=' && ip[namelen] != '&' && ip[namelen] != '\0') { /* Name not followed by [&=$]. */ ip++; continue; } /* We have found the pattern. */ ip += namelen; break; } /* Now ip points to the first character after the name. If this * character is '&' or '\0', then this field doesn't have a value. * If this character is '=', then this field has a value. */ if (ip[0] == '=') { ip += 1; } /* The value is from ip to '&' or to the end of the string, whichever * comes first. */ value_end = strchr(ip, '&'); if (value_end != NULL) { /* '&' comes first. */ return apr_pstrndup(pool, ip, value_end - ip); } else { /* Value continues until the end of the string. */ return apr_pstrdup(pool, ip); } } /* Convert a hexadecimal digit to an integer. * * Parameters: * char c The digit we should convert. * * Returns: * The digit as an integer, or -1 if it isn't a hex digit. */ static int am_unhex_digit(char c) { if (c >= '0' && c <= '9') { return c - '0'; } else if (c >= 'a' && c <= 'f') { return c - 'a' + 0xa; } else if (c >= 'A' && c <= 'F') { return c - 'A' + 0xa; } else { return -1; } } /* This function urldecodes a string in-place. * * Parameters: * char *data The string to urldecode. * * Returns: * OK if successful or HTTP_BAD_REQUEST if any escape sequence decodes to a * null-byte ('\0'), or if an invalid escape sequence is found. */ int am_urldecode(char *data) { char *ip; char *op; int c1, c2; if (data == NULL) { return HTTP_BAD_REQUEST; } ip = data; op = data; while (*ip) { switch (*ip) { case '+': *op = ' '; ip++; op++; break; case '%': /* Decode the hex digits. Note that we need to check the * result of the first conversion before attempting the * second conversion -- otherwise we may read past the end * of the string. */ c1 = am_unhex_digit(ip[1]); if (c1 < 0) { return HTTP_BAD_REQUEST; } c2 = am_unhex_digit(ip[2]); if (c2 < 0) { return HTTP_BAD_REQUEST; } *op = (c1 << 4) | c2; if (*op == '\0') { /* null-byte. */ return HTTP_BAD_REQUEST; } ip += 3; op++; break; default: *op = *ip; ip++; op++; } } *op = '\0'; return OK; } /* This function urlencodes a string. It will escape all characters * except a-z, A-Z, 0-9, '_' and '.'. * * Parameters: * apr_pool_t *pool The pool we should allocate memory from. * const char *str The string we should urlencode. * * Returns: * The urlencoded string, or NULL if str == NULL. */ char *am_urlencode(apr_pool_t *pool, const char *str) { const char *ip; apr_size_t length; char *ret; char *op; int hi, low; /* Return NULL if str is NULL. */ if(str == NULL) { return NULL; } /* Find the length of the output string. */ length = 0; for(ip = str; *ip; ip++) { if(*ip >= 'a' && *ip <= 'z') { length++; } else if(*ip >= 'A' && *ip <= 'Z') { length++; } else if(*ip >= '0' && *ip <= '9') { length++; } else if(*ip == '_' || *ip == '.') { length++; } else { length += 3; } } /* Add space for null-terminator. */ length++; /* Allocate memory for string. */ ret = (char *)apr_palloc(pool, length); /* Encode string. */ for(ip = str, op = ret; *ip; ip++, op++) { if(*ip >= 'a' && *ip <= 'z') { *op = *ip; } else if(*ip >= 'A' && *ip <= 'Z') { *op = *ip; } else if(*ip >= '0' && *ip <= '9') { *op = *ip; } else if(*ip == '_' || *ip == '.') { *op = *ip; } else { *op = '%'; op++; hi = (*ip & 0xf0) >> 4; if(hi < 0xa) { *op = '0' + hi; } else { *op = 'A' + hi - 0xa; } op++; low = *ip & 0x0f; if(low < 0xa) { *op = '0' + low; } else { *op = 'A' + low - 0xa; } } } /* Make output string null-terminated. */ *op = '\0'; return ret; } /* * Check that a URL is safe for redirect. * * Parameters: * request_rec *r The request we are processing. * const char *url The URL we should check. * * Returns: * OK on success, HTTP_BAD_REQUEST otherwise. */ int am_check_url(request_rec *r, const char *url) { const char *i; if (url == NULL) { return HTTP_BAD_REQUEST; } for (i = url; *i; i++) { if (*i >= 0 && *i < ' ') { /* Deny all control-characters. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r, "Control character detected in URL."); return HTTP_BAD_REQUEST; } if (*i == '\\') { /* Reject backslash character, as it can be used to bypass * redirect URL validation. */ AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r, "Backslash character detected in URL."); return HTTP_BAD_REQUEST; } } if (strstr(url, "///") == url) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, HTTP_BAD_REQUEST, r, "URL starts with '///'"); return HTTP_BAD_REQUEST; } return OK; } /* This function generates a given number of (pseudo)random bytes. * The current implementation uses OpenSSL's RAND_*-functions. * * Parameters: * request_rec *r The request we are generating random bytes for. * The request is used for configuration and * error/warning reporting. * void *dest The address if the buffer we should fill with data. * apr_size_t count The number of random bytes to create. * * Returns: * OK on success, or HTTP_INTERNAL_SERVER on failure. */ int am_generate_random_bytes(request_rec *r, void *dest, apr_size_t count) { int rc; rc = RAND_bytes((unsigned char *)dest, (int)count); if(rc != 1) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error generating random data: %lu", ERR_get_error()); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } /* This function generates an id which is AM_ID_LENGTH characters long. * The id will consist of hexadecimal characters. * * Parameters: * request_rec *r The request we associate allocated memory with. * * Returns: * The session id, made up of AM_ID_LENGTH hexadecimal characters, * terminated by a null-byte. */ char *am_generate_id(request_rec *r) { int rc; char *ret; int rand_data_len; unsigned char *rand_data; int i; unsigned char b; int hi, low; ret = (char *)apr_palloc(r->pool, AM_ID_LENGTH + 1); /* We need to round the length of the random data _up_, in case the * length of the session id isn't even. */ rand_data_len = (AM_ID_LENGTH + 1) / 2; /* Fill the last rand_data_len bytes of the string with * random bytes. This allows us to overwrite from the beginning of * the string. */ rand_data = (unsigned char *)&ret[AM_ID_LENGTH - rand_data_len]; /* Generate random numbers. */ rc = am_generate_random_bytes(r, rand_data, rand_data_len); if(rc != OK) { return NULL; } /* Convert the random bytes to hexadecimal. Note that we will write * AM_ID_LENGTH+1 characters if we have a non-even length of the * session id. This is OK - we will simply overwrite the last character * with the null-terminator afterwards. */ for(i = 0; i < AM_ID_LENGTH; i += 2) { b = rand_data[i / 2]; hi = (b >> 4) & 0xf; low = b & 0xf; if(hi >= 0xa) { ret[i] = 'a' + hi - 0xa; } else { ret[i] = '0' + hi; } if(low >= 0xa) { ret[i+1] = 'a' + low - 0xa; } else { ret[i+1] = '0' + low; } } /* Add null-terminator- */ ret[AM_ID_LENGTH] = '\0'; return ret; } /* This returns the directroy part of a path, a la dirname(3) * * Parameters: * apr_pool_t p Pool to allocate memory from * const char *path Path to extract directory from * * Returns: * The directory part of path */ const char *am_filepath_dirname(apr_pool_t *p, const char *path) { char *cp; /* * Try Unix and then Windows style. Borrowed from * apr_match_glob(), it seems it cannot be made more * portable. */ if (((cp = strrchr(path, (int)'/')) == NULL) && ((cp = strrchr(path, (int)'\\')) == NULL)) return "."; return apr_pstrndup(p, path, cp - path); } /* * Allocate and initialize a am_file_data_t * * Parameters: * apr_pool_t *pool Allocation pool. * const char *path If non-NULL initialize file_data->path to copy of path * * Returns: * Newly allocated & initialized file_data_t */ am_file_data_t *am_file_data_new(apr_pool_t *pool, const char *path) { am_file_data_t *file_data = NULL; if ((file_data = apr_pcalloc(pool, sizeof(am_file_data_t))) == NULL) { return NULL; } file_data->pool = pool; file_data->rv = APR_EINIT; if (path) { file_data->path = apr_pstrdup(file_data->pool, path); } return file_data; } /* * Allocate a new am_file_data_t and copy * * Parameters: * apr_pool_t *pool Allocation pool. * am_file_data_t *src_file_data The src being copied. * * Returns: * Newly allocated & initialized from src_file_data */ am_file_data_t *am_file_data_copy(apr_pool_t *pool, am_file_data_t *src_file_data) { am_file_data_t *dst_file_data = NULL; if ((dst_file_data = am_file_data_new(pool, src_file_data->path)) == NULL) { return NULL; } dst_file_data->path = apr_pstrdup(pool, src_file_data->path); dst_file_data->stat_time = src_file_data->stat_time; dst_file_data->finfo = src_file_data->finfo; dst_file_data->contents = apr_pstrdup(pool, src_file_data->contents); dst_file_data->read_time = src_file_data->read_time; dst_file_data->rv = src_file_data->rv; dst_file_data->strerror = apr_pstrdup(pool, src_file_data->strerror); dst_file_data->generated = src_file_data->generated; return dst_file_data; } /* * Peform a stat on a file to get it's properties * * A stat is performed on the file. If there was an error the * result value is left in file_data->rv and an error description * string is formatted and left in file_data->strerror and function * returns the rv value. If the stat was successful the stat * information is left in file_data->finfo and APR_SUCCESS * set set as file_data->rv and returned as the function result. * * The file_data->stat_time indicates if and when the stat was * performed, a zero time value indicates the operation has not yet * been performed. * * Parameters: * am_file_data_t *file_data Struct containing file information * * Returns: * APR status code, same value as file_data->rv */ apr_status_t am_file_stat(am_file_data_t *file_data) { char buffer[512]; if (file_data == NULL) { return APR_EINVAL; } file_data->strerror = NULL; file_data->stat_time = apr_time_now(); file_data->rv = apr_stat(&file_data->finfo, file_data->path, APR_FINFO_SIZE, file_data->pool); if (file_data->rv != APR_SUCCESS) { file_data->strerror = apr_psprintf(file_data->pool, "apr_stat: Error opening \"%s\" [%d] \"%s\"", file_data->path, file_data->rv, apr_strerror(file_data->rv, buffer, sizeof(buffer))); } return file_data->rv; } /* * Read file into dynamically allocated buffer * * First a stat is performed on the file. If there was an error the * result value is left in file_data->rv and an error description * string is formatted and left in file_data->strerror and function * returns the rv value. If the stat was successful the stat * information is left in file_data->finfo. * * A buffer is dynamically allocated and the contents of the file is * read into file_data->contents. If there was an error the result * value is left in file_data->rv and an error description string is * formatted and left in file_data->strerror and the function returns * the rv value. * * The file_data->stat_time and file_data->read_time indicate if and * when those operations were performed, a zero time value indicates * the operation has not yet been performed. * * Parameters: * am_file_data_t *file_data Struct containing file information * * Returns: * APR status code, same value as file_data->rv */ apr_status_t am_file_read(am_file_data_t *file_data) { char buffer[512]; apr_file_t *fd; apr_size_t nbytes; if (file_data == NULL) { return APR_EINVAL; } file_data->rv = APR_SUCCESS; file_data->strerror = NULL; am_file_stat(file_data); if (file_data->rv != APR_SUCCESS) { return file_data->rv; } if ((file_data->rv = apr_file_open(&fd, file_data->path, APR_READ, 0, file_data->pool)) != 0) { file_data->strerror = apr_psprintf(file_data->pool, "apr_file_open: Error opening \"%s\" [%d] \"%s\"", file_data->path, file_data->rv, apr_strerror(file_data->rv, buffer, sizeof(buffer))); return file_data->rv; } file_data->read_time = apr_time_now(); nbytes = file_data->finfo.size; file_data->contents = (char *)apr_palloc(file_data->pool, nbytes + 1); file_data->rv = apr_file_read_full(fd, file_data->contents, nbytes, NULL); if (file_data->rv != 0) { file_data->strerror = apr_psprintf(file_data->pool, "apr_file_read_full: Error reading \"%s\" [%d] \"%s\"", file_data->path, file_data->rv, apr_strerror(file_data->rv, buffer, sizeof(buffer))); (void)apr_file_close(fd); return file_data->rv; } file_data->contents[nbytes] = '\0'; (void)apr_file_close(fd); return file_data->rv; } /* * Purge outdated saved POST requests. * * Parameters: * request_rec *r The current request * * Returns: * OK on success, or HTTP_INTERNAL_SERVER on failure. */ int am_postdir_cleanup(request_rec *r) { am_mod_cfg_rec *mod_cfg; apr_dir_t *postdir; apr_status_t rv; char error_buffer[64]; apr_finfo_t afi; char *fname; int count; apr_time_t expire_before; mod_cfg = am_get_mod_cfg(r->server); /* The oldes file we should keep. Delete files that are older. */ expire_before = apr_time_now() - mod_cfg->post_ttl * APR_USEC_PER_SEC; /* * Open our POST directory or create it. */ rv = apr_dir_open(&postdir, mod_cfg->post_dir, r->pool); if (rv != 0) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unable to open MellonPostDirectory \"%s\": %s", mod_cfg->post_dir, apr_strerror(rv, error_buffer, sizeof(error_buffer))); return HTTP_INTERNAL_SERVER_ERROR; } /* * Purge outdated items */ count = 0; do { rv = apr_dir_read(&afi, APR_FINFO_NAME|APR_FINFO_CTIME, postdir); if (rv != OK) break; /* Skip dot_files */ if (afi.name[0] == '.') continue; if (afi.ctime < expire_before) { fname = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, afi.name); (void)apr_file_remove(fname , r->pool); } else { count++; } } while (1 /* CONSTCOND */); (void)apr_dir_close(postdir); if (count >= mod_cfg->post_count) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Too many saved POST sessions. " "Increase MellonPostCount directive."); return HTTP_INTERNAL_SERVER_ERROR; } return OK; } /* * HTML-encode a string * * Parameters: * request_rec *r The current request * const char *str The string to encode * * Returns: * The encoded string */ char *am_htmlencode(request_rec *r, const char *str) { const char *cp; char *output; apr_size_t outputlen; int i; outputlen = 0; for (cp = str; *cp; cp++) { switch (*cp) { case '&': outputlen += 5; break; case '"': outputlen += 6; break; default: outputlen += 1; break; } } i = 0; output = apr_palloc(r->pool, outputlen + 1); for (cp = str; *cp; cp++) { switch (*cp) { case '&': (void)strcpy(&output[i], "&"); i += 5; break; case '"': (void)strcpy(&output[i], """); i += 6; break; default: output[i] = *cp; i += 1; break; } } output[i] = '\0'; return output; } /* This function produces the endpoint URL * * Parameters: * request_rec *r The request we received. * * Returns: * the endpoint URL */ char *am_get_endpoint_url(request_rec *r) { am_dir_cfg_rec *cfg = am_get_dir_cfg(r); return ap_construct_url(r->pool, cfg->endpoint_path, r); } /* * This function saves a POST request for later replay and updates * the return URL. * * Parameters: * request_rec *r The current request. * const char **relay_state The returl URL * * Returns: * OK on success, HTTP_INTERNAL_SERVER_ERROR otherwise */ int am_save_post(request_rec *r, const char **relay_state) { am_mod_cfg_rec *mod_cfg; const char *content_type; const char *charset; const char *psf_id; char *psf_name; char *post_data; apr_size_t post_data_len; apr_size_t written; apr_file_t *psf; mod_cfg = am_get_mod_cfg(r->server); if (mod_cfg->post_dir == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "MellonPostReplay enabled but MellonPostDirectory not set " "-- cannot save post data"); return HTTP_INTERNAL_SERVER_ERROR; } if (am_postdir_cleanup(r) != OK) return HTTP_INTERNAL_SERVER_ERROR; /* Check Content-Type */ content_type = apr_table_get(r->headers_in, "Content-Type"); if (content_type == NULL) { content_type = "urlencoded"; charset = NULL; } else { if (am_has_header(r, content_type, "application/x-www-form-urlencoded")) { content_type = "urlencoded"; } else if (am_has_header(r, content_type, "multipart/form-data")) { content_type = "multipart"; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Unknown POST Content-Type \"%s\"", content_type); return HTTP_INTERNAL_SERVER_ERROR; } charset = am_get_header_attr(r, content_type, NULL, "charset"); } if ((psf_id = am_generate_id(r)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot generate id"); return HTTP_INTERNAL_SERVER_ERROR; } psf_name = apr_psprintf(r->pool, "%s/%s", mod_cfg->post_dir, psf_id); if (apr_file_open(&psf, psf_name, APR_WRITE|APR_CREATE|APR_BINARY, APR_FPROT_UREAD|APR_FPROT_UWRITE, r->pool) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot create POST session file"); return HTTP_INTERNAL_SERVER_ERROR; } if (am_read_post_data(r, &post_data, &post_data_len) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot read POST data"); (void)apr_file_close(psf); return HTTP_INTERNAL_SERVER_ERROR; } if (post_data_len > mod_cfg->post_size) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "POST data size %" APR_SIZE_T_FMT " exceeds maximum %" APR_SIZE_T_FMT ". " "Increase MellonPostSize directive.", post_data_len, mod_cfg->post_size); (void)apr_file_close(psf); return HTTP_INTERNAL_SERVER_ERROR; } written = post_data_len; if ((apr_file_write(psf, post_data, &written) != OK) || (written != post_data_len)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot write to POST session file"); (void)apr_file_close(psf); return HTTP_INTERNAL_SERVER_ERROR; } if (apr_file_close(psf) != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "cannot close POST session file"); return HTTP_INTERNAL_SERVER_ERROR; } if (charset != NULL) charset = apr_psprintf(r->pool, "&charset=%s", am_urlencode(r->pool, charset)); else charset = ""; *relay_state = apr_psprintf(r->pool, "%srepost?id=%s&ReturnTo=%s&enctype=%s%s", am_get_endpoint_url(r), psf_id, am_urlencode(r->pool, *relay_state), content_type, charset); return OK; } /* * This function replaces CRLF by LF in a string * * Parameters: * request_rec *r The current request * const char *str The string * * Returns: * Output string */ const char *am_strip_cr(request_rec *r, const char *str) { char *output; const char *cp; apr_size_t i; output = apr_palloc(r->pool, strlen(str) + 1); i = 0; for (cp = str; *cp; cp++) { if ((*cp == '\r') && (*(cp + 1) == '\n')) continue; output[i++] = *cp; } output[i++] = '\0'; return (const char *)output; } /* * This function replaces LF by CRLF in a string * * Parameters: * request_rec *r The current request * const char *str The string * * Returns: * Output string */ const char *am_add_cr(request_rec *r, const char *str) { char *output; const char *cp; apr_size_t xlen; apr_size_t i; xlen = 0; for (cp = str; *cp; cp++) if (*cp == '\n') xlen++; output = apr_palloc(r->pool, strlen(str) + xlen + 1); i = 0; for (cp = str; *cp; cp++) { if (*cp == '\n') output[i++] = '\r'; output[i++] = *cp; } output[i++] = '\0'; return (const char *)output; } /* * This function tokenize a string, just like strtok_r, except that * the separator is a string instead of a character set. * * Parameters: * const char *str The string to tokenize * const char *sep The separator string * char **last Pointer to state (char *) * * Returns: * OK on success, HTTP_INTERNAL_SERVER_ERROR otherwise */ const char *am_xstrtok(request_rec *r, const char *str, const char *sep, char **last) { char *s; char *np; /* Resume */ if (str != NULL) s = apr_pstrdup(r->pool, str); else s = *last; /* End of string */ if (*s == '\0') return NULL; /* Next sep exists? */ if ((np = strstr(s, sep)) == NULL) { *last = s + strlen(s); } else { *last = np + strlen(sep); memset(np, 0, strlen(sep)); } return s; } /* This function strips leading spaces and tabs from a string * * Parameters: * const char **s Pointer to the string * */ void am_strip_blank(const char **s) { while ((**s == ' ') || (**s == '\t')) (*s)++; return; } /* This function extracts a MIME header from a MIME section * * Parameters: * request_rec *r The request * const char *m The MIME section * const char *h The header to extract (case insensitive) * * Returns: * The header value, or NULL on failure. */ const char *am_get_mime_header(request_rec *r, const char *m, const char *h) { const char *line; char *l1; const char *value; char *l2; for (line = am_xstrtok(r, m, "\n", &l1); line && *line; line = am_xstrtok(r, NULL, "\n", &l1)) { am_strip_blank(&line); if (((value = am_xstrtok(r, line, ":", &l2)) != NULL) && (strcasecmp(value, h) == 0)) { if ((value = am_xstrtok(r, NULL, ":", &l2)) != NULL) am_strip_blank(&value); return value; } } return NULL; } /* This function extracts an attribute from a header * * Parameters: * request_rec *r The request * const char *h The header * const char *v Optional header value to check (case insensitive) * const char *a Optional attribute to extract (case insensitive) * * Returns: * if i was provided, item value, or NULL on failure. * if i is NULL, the whole header, or NULL on failure. This is * useful for testing v. */ const char *am_get_header_attr(request_rec *r, const char *h, const char *v, const char *a) { const char *value; const char *attr; char *l1; const char *attr_value = NULL; /* Looking for * header-value; item_name="item_value"\n */ if ((value = am_xstrtok(r, h, ";", &l1)) == NULL) return NULL; am_strip_blank(&value); /* If a header value was provided, check it */ if ((v != NULL) && (strcasecmp(value, v) != 0)) return NULL; /* If no attribute name is provided, return everything */ if (a == NULL) return h; while ((attr = am_xstrtok(r, NULL, ";", &l1)) != NULL) { const char *attr_name = NULL; char *l2; am_strip_blank(&attr); attr_name = am_xstrtok(r, attr, "=", &l2); if ((attr_name != NULL) && (strcasecmp(attr_name, a) == 0)) { if ((attr_value = am_xstrtok(r, NULL, "=", &l2)) != NULL) am_strip_blank(&attr_value); break; } } /* Remove leading and trailing quotes */ if (attr_value != NULL) { apr_size_t len; len = strlen(attr_value); if ((len > 1) && (attr_value[len - 1] == '\"')) attr_value = apr_pstrndup(r->pool, attr_value, len - 1); if (attr_value[0] == '\"') attr_value++; } return attr_value; } /* This function checks for a header name/value existence * * Parameters: * request_rec *r The request * const char *h The header (case insensitive) * const char *v Optional header value to check (case insensitive) * * Returns: * 0 if header does not exists or does not has the value, 1 otherwise */ int am_has_header(request_rec *r, const char *h, const char *v) { return (am_get_header_attr(r, h, v, NULL) != NULL); } /* This function extracts the body from a MIME section * * Parameters: * request_rec *r The request * const char *mime The MIME section * * Returns: * The MIME section body, or NULL on failure. */ const char *am_get_mime_body(request_rec *r, const char *mime) { const char lflf[] = "\n\n"; const char *body; apr_size_t body_len; if ((body = strstr(mime, lflf)) == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "No MIME body"); return NULL; } body += strlen(lflf); /* Strip tralling \n */ if ((body_len = strlen(body)) >= 1) { if (body[body_len - 1] == '\n') body = apr_pstrmemdup(r->pool, body, body_len - 1); } /* Turn back LF into CRLF */ return am_add_cr(r, body); } /* This function returns the URL for a given provider service (type + method) * * Parameters: * request_rec *r The request * LassoProfile *profile Login profile * char *endpoint_name Service and method as specified in metadata * e.g.: "SingleSignOnService HTTP-Redirect" * Returns: * The endpoint URL that must be freed by caller, or NULL on failure. */ char * am_get_service_url(request_rec *r, LassoProfile *profile, char *service_name) { LassoProvider *provider; gchar *url; provider = lasso_server_get_provider(profile->server, profile->remote_providerID); if (LASSO_IS_PROVIDER(provider) == FALSE) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Cannot find provider service %s, no provider.", service_name); return NULL; } url = lasso_provider_get_metadata_one(provider, service_name); if (url == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Cannot find provider service %s from metadata.", service_name); return NULL; } return url; } /*------------------------ Begin Token Parsing Code --------------------------*/ typedef enum { TOKEN_WHITESPACE = 1, TOKEN_SEMICOLON, TOKEN_COMMA, TOKEN_EQUAL, TOKEN_IDENTIFIER, TOKEN_DBL_QUOTE_STRING, } TokenType; typedef struct { TokenType type; /* The type of this token */ char *str; /* The string value of the token */ apr_size_t len; /* The number of characters in the token */ apr_size_t offset; /* The offset from the beginning of the string to the start of the token */ } Token; #ifdef DEBUG /* Return string representation of TokenType enumeration * * Parameters: * token_type A TokenType enumeration * Returns: String name of token_type */ static const char * token_type_str(TokenType token_type) { switch(token_type) { case TOKEN_WHITESPACE: return "WHITESPACE"; case TOKEN_SEMICOLON: return "SEMICOLON"; case TOKEN_COMMA: return "COMMA"; case TOKEN_EQUAL: return "EQUAL"; case TOKEN_IDENTIFIER: return "IDENTIFIER"; case TOKEN_DBL_QUOTE_STRING: return "DBL_QUOTE_STRING"; default: return "unknown"; } } static void dump_tokens(request_rec *r, apr_array_header_t *tokens) { apr_size_t i; for (i = 0; i < tokens->nelts; i++) { Token token = APR_ARRAY_IDX(tokens, i, Token); AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "token[%2zd] %s \"%s\" offset=%lu len=%lu ", i, token_type_str(token.type), token.str, token.offset, token.len); } } #endif /* Initialize token and add to list of tokens * * Utility to assist tokenize function. * * A token object is created and added to the end of the list of * tokens. It is initialized with the type of token, a copy of the * string, it's length, and it's offset from the beginning of the * string where it was found. * * Tokens with special processing needs are also handled here. * * A double quoted string will: * * * Have it's delimiting quotes removed. * * Will unescape escaped characters. * * Parameters: * tokens Array of Token objects. * type The type of the token (e.g. TokenType). * str The string the token was parsed from, used to compute * the position of the token in the original string. * start The first character in the token. * end the last character in the token. */ static inline void push_token(apr_array_header_t *tokens, TokenType type, const char *str, const char *start, const char *end) { apr_size_t offset = start - str; Token *token = apr_array_push(tokens); if (type == TOKEN_DBL_QUOTE_STRING) { /* do not include quotes in token value */ start++; end--; } token->type = type; token->len = end - start; token->offset = offset; token->str = apr_pstrmemdup(tokens->pool, start, token->len); if (type == TOKEN_DBL_QUOTE_STRING) { /* * The original HTTP 1.1 spec was ambiguous with respect to * backslash quoting inside double quoted strings. This has since * been resolved in this errata: * * http://greenbytes.de/tech/webdav/draft-ietf-httpbis-p1-messaging-16.html#rfc.section.3.2.3 * * Which states: * * Recipients that process the value of the quoted-string MUST * handle a quoted-pair as if it were replaced by the octet * following the backslash. * * Senders SHOULD NOT escape octets in quoted-strings that do not * require escaping (i.e., other than DQUOTE and the backslash * octet). */ char *p, *t; for (p = token->str; *p; p++) { if (p[0] == '\\' && p[1]) { /* * Found backslash with following character. * Move rest of string down 1 character. */ for (t = p; *t; t++) { t[0] = t[1]; } token->len--; } } } } /* Break a string into a series of tokens * * Given a string return an array of tokens. If the string cannot be * successfully parsed an error string is returned at the location * specified by the error parameter, if error is NULL then the parsing * was successful. If an error occured the returned array of tokens * will include all tokens parsed up until where the unrecognized * input occurred. The input str is never modified. * * Parameters: * pool memory allocation pool * str input string to be parsed. * ignore_whitespace if True whitespace tokens are not returned * error location where error string is returned * if NULL no error occurred * Returns: array of Token objects */ static apr_array_header_t * tokenize(apr_pool_t *pool, const char *str, bool ignore_whitespace, char **error) { apr_array_header_t *tokens = apr_array_make(pool, 10, sizeof(Token)); const char *p, *start; *error = NULL; p = start = str; while(*p) { if (apr_isspace(*p)) { /* whitespace */ p++; while(*p && apr_isspace(*p)) p++; if (!ignore_whitespace) { push_token(tokens, TOKEN_WHITESPACE, str, start, p); } start = p; } else if (apr_isalpha(*p)) { /* identifier: must begin with alpha then any alphanumeric or underscore */ p++; while(*p && (apr_isalnum(*p) || *p == '_')) p++; push_token(tokens, TOKEN_IDENTIFIER, str, start, p); start = p; } else if (*p == '"') { /* double quoted string */ p++; /* step over double quote */ while(*p) { if (*p == '\\') { /* backslash escape */ p++; /* step over backslash */ if (*p) { p++; /* step over escaped character */ } else { break; /* backslash at end of string, stop */ } } if (*p == '\"') break; /* terminating quote delimiter */ p++; /* keep scanning */ } if (*p != '\"') { *error = apr_psprintf(pool, "unterminated string beginning at " "position %" APR_SIZE_T_FMT " in \"%s\"", start-str, str); break; } p++; push_token(tokens, TOKEN_DBL_QUOTE_STRING, str, start, p); start = p; } else if (*p == '=') { /* equals */ p++; push_token(tokens, TOKEN_EQUAL, str, start, p); start = p; } else if (*p == ',') { /* comma */ p++; push_token(tokens, TOKEN_COMMA, str, start, p); start = p; } else if (*p == ';') { /* semicolon */ p++; push_token(tokens, TOKEN_SEMICOLON, str, start, p); start = p; } else { /* unrecognized token */ *error = apr_psprintf(pool, "unknown token at " "position %" APR_SIZE_T_FMT " in string \"%s\"", p-str, str); break; } } return tokens; } /* Test if the token is what we're looking for * * Given an index into the tokens array determine if the token type * matches. If the value parameter is non-NULL then the token's value * must also match. If the array index is beyond the last array item * false is returned. * * Parameters: * tokens Array of Token objects * index Index used to select the Token object from the Tokens array. * If the index is beyond the last array item False is returned. * type The token type which must match * value If non-NULL then the token string value must be equal to this. * Returns: True if the token matches, False otherwise. */ static bool is_token(apr_array_header_t *tokens, apr_size_t index, TokenType type, const char *value) { if (index >= tokens->nelts) { return false; } Token token = APR_ARRAY_IDX(tokens, index, Token); if (token.type != type) { return false; } if (value) { if (!g_str_equal(token.str, value)) { return false; } } return true; } /*------------------------- End Token Parsing Code ---------------------------*/ /* Return message describing position an error when parsing. * * When parsing we expect tokens to appear in a certain sequence. We * report the contents of the unexpected token and it's position in * the string. However if the parsing error is due to the fact we've * exhausted all tokens but are still expecting another token then our * error message indicates we reached the end of the string. * * Parameters: * tokens Array of Token objects. * index Index in tokens array where bad token was found */ static inline const char * parse_error_msg(apr_array_header_t *tokens, apr_size_t index) { if (index >= tokens->nelts) { return "end of string"; } return apr_psprintf(tokens->pool, "\"%s\" at position %" APR_SIZE_T_FMT, APR_ARRAY_IDX(tokens, index, Token).str, APR_ARRAY_IDX(tokens, index, Token).offset); } /* This function checks if an HTTP PAOS header is valid and * returns any service options which may have been specified. * * A PAOS header is composed of a mandatory PAOS version and service * values. A semicolon separates the version from the service values. * * Service values are delimited by semicolons, and options are * comma-delimited from the service value and each other. * * The PAOS version must be in the form ver="xxx" (note the version * string must be in double quotes). * * The ECP service must be specified, it MAY be followed by optional * comma seperated options, all values must be in double quotes. * * ECP Service * "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp" * * Recognized Options: * * Support for channel bindings * urn:oasis:names:tc:SAML:protocol:ext:channel-binding * * Support for Holder-of-Key subject confirmation * urn:oasis:names:tc:SAML:2.0:cm:holder-of-key * * Request for signed SAML request * urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp:2.0:WantAuthnRequestsSigned * * Request to delegate credentials to the service provider * urn:oasis:names:tc:SAML:2.0:conditions:delegation * * * Example PAOS HTTP header:: * * PAOS: ver="urn:liberty:paos:2003-08"; * "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp", * "urn:oasis:names:tc:SAML:protocol:ext:channel-binding", * "urn:oasis:names:tc:SAML:2.0:cm:holder-of-key" * * Parameters: * request_rec *r The request * const char *header The PAOS header value * ECPServiceOptions *options_return * Pointer to location to receive options, * may be NULL. Bitmask of option flags. * * Returns: * true if the PAOS header is valid, false otherwise. If options is non-NULL * then the set of option flags is returned there. * */ bool am_parse_paos_header(request_rec *r, const char *header, ECPServiceOptions *options_return) { bool result = false; ECPServiceOptions options = 0; apr_array_header_t *tokens; apr_size_t i; char *error; AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "PAOS header: \"%s\"", header); tokens = tokenize(r->pool, header, true, &error); #ifdef DEBUG dump_tokens(r, tokens); #endif if (error) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "%s", error); goto cleanup; } /* Header must begin with "ver=xxx" where xxx is paos version */ if (!is_token(tokens, 0, TOKEN_IDENTIFIER, "ver") || !is_token(tokens, 1, TOKEN_EQUAL, NULL) || !is_token(tokens, 2, TOKEN_DBL_QUOTE_STRING, LASSO_PAOS_HREF)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected header to begin with ver=\"%s\", " "actual header=\"%s\"", LASSO_PAOS_HREF, header); goto cleanup; } /* Next is the service value, separated from the version by a semicolon */ if (!is_token(tokens, 3, TOKEN_SEMICOLON, NULL)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected semicolon after PAOS version " "but found %s in header=\"%s\"", parse_error_msg(tokens, 3), header); goto cleanup; } if (!is_token(tokens, 4, TOKEN_DBL_QUOTE_STRING, LASSO_ECP_HREF)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected service token to be \"%s\", " "but found %s in header=\"%s\"", LASSO_ECP_HREF, parse_error_msg(tokens, 4), header); goto cleanup; } /* After the service value there may be optional flags separated by commas */ if (tokens->nelts == 5) { /* no options */ result = true; goto cleanup; } /* More tokens after the service value, must be options, iterate over them */ for (i = 5; i < tokens->nelts; i++) { if (!is_token(tokens, i, TOKEN_COMMA, NULL)) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected comma after PAOS service " "but found %s in header=\"%s\"", parse_error_msg(tokens, i), header); goto cleanup; } if (++i > tokens->nelts) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected option after comma " "in header=\"%s\"", header); goto cleanup; } Token token = APR_ARRAY_IDX(tokens, i, Token); if (token.type != TOKEN_DBL_QUOTE_STRING) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid PAOS header, " "expected quoted string after comma " "but found %s in header=\"%s\"", parse_error_msg(tokens, i), header); goto cleanup; } /* Have an option string, convert it to a bit flag */ const char *value = token.str; if (g_str_equal(value, LASSO_SAML_EXT_CHANNEL_BINDING)) { options |= ECP_SERVICE_OPTION_CHANNEL_BINDING; } else if (g_str_equal(value, LASSO_SAML2_CONFIRMATION_METHOD_HOLDER_OF_KEY)) { options |= ECP_SERVICE_OPTION_HOLDER_OF_KEY; } else if (g_str_equal(value, LASSO_SAML2_ECP_PROFILE_WANT_AUTHN_SIGNED)) { options |= ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED; } else if (g_str_equal(value, LASSO_SAML2_CONDITIONS_DELEGATION)) { options |= ECP_SERVICE_OPTION_DELEGATION; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_WARNING, 0, r, "Unknown PAOS service option = \"%s\"", value); goto cleanup; } } result = true; cleanup: if (options_return) { *options_return = options; } return result; } /* This function checks if Accept header has a media type * * Given an Accept header value like this: * * "text/html,application/xhtml+xml,application/xml;q=0.9" * * Parse the string and find name of each media type, ignore any parameters * bound to the name. Test to see if the name matches the input media_type. * * Parameters: * request_rec *r The request * const char *header The header value * const char *media_type media type header value to check (case insensitive) * * Returns: * true if media type is in header, false otherwise */ bool am_header_has_media_type(request_rec *r, const char *header, const char *media_type) { bool result = false; char **comma_tokens = NULL; char **media_ranges = NULL; char *media_range = NULL; if (header == NULL) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "invalid Accept header, NULL"); goto cleanup; } /* * Split the header into a list of media_range tokens separated by * a comma and iterate over the list. */ comma_tokens = g_strsplit(header, ",", 0); for (media_ranges = comma_tokens, media_range = *media_ranges; media_range; media_range = *(++media_ranges)) { char **semicolon_tokens = NULL; char *name = NULL; /* * Split the media_range into a name and parameters, each * separated by a semicolon. The first element in the list is * the media_type name, subsequent params are optional and ignored. */ media_range = g_strstrip(media_range); semicolon_tokens = g_strsplit(media_range, ";", 0); /* * Does the media_type match our required media_type? * If so clean up and return success. */ name = g_strstrip(semicolon_tokens[0]); if (name && g_str_equal(name, media_type)) { result = true; g_strfreev(semicolon_tokens); goto cleanup; } g_strfreev(semicolon_tokens); } cleanup: g_strfreev(comma_tokens); return result; } /* * Lookup a config string in a specific language. If lang is NULL and * the config string had been defined without a language qualifier * return the unqualified value. If not found NULL is returned. */ const char *am_get_config_langstring(apr_hash_t *h, const char *lang) { char *string; if (lang == NULL) { lang = ""; } string = (char *)apr_hash_get(h, lang, APR_HASH_KEY_STRING); return string; } /* * Get the value of boolean query parameter. * * Parameters: * request_rec *r The request * const char *name The name of the query parameter * int *return_value The address of the variable to receive * the boolean value * int default_value The value returned if parameter is absent or * in event of an error * * Returns: * OK on success, HTTP error otherwise * * Looks for the named parameter in the query parameters, if found * parses the value which must be one of: * * * true * * false * * If value cannot be parsed HTTP_BAD_REQUEST is returned. * * If not found, or if there is an error, the returned value is set to * default_value. */ int am_get_boolean_query_parameter(request_rec *r, const char *name, int *return_value, int default_value) { char *value_str; int ret = OK; *return_value = default_value; value_str = am_extract_query_parameter(r->pool, r->args, name); if (value_str != NULL) { ret = am_urldecode(value_str); if (ret != OK) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Error urldecoding \"%s\" boolean query parameter, " "value=\"%s\"", name, value_str); return ret; } if(!strcmp(value_str, "true")) { *return_value = TRUE; } else if(!strcmp(value_str, "false")) { *return_value = FALSE; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "Invalid value for \"%s\" boolean query parameter, " "value=\"%s\"", name, value_str); ret = HTTP_BAD_REQUEST; } } return ret; } /* * Get the URL of the AssertionConsumerServer having specific protocol * binding. * * Parameters: * LassoProvider *provider The provider whose endpoints will be scanned. * const char *binding The required binding short name. * * Returns: * The endpoint URL or NULL if not found. Must be freed with g_free(). * * Lasso does not provide a public API to select a provider endpoint * by binding. The best we can do is iterate over a list of endpoint * descriptors and select a matching descriptor. * * Lasso does not document the format of these descriptor names but * essentially a descriptor is a space separated concatenation of the * endpoint properties. For SAML2 one can assume it is the endpoint * type, optionally followed by the protocol binding name, optionally * followd by the index (if the endpoint type is indexed). If the * endpoint is a response location then "ResponseLocation" will be * appended as the final token. For example here is a list of * descriptors returned for a service provider (note they are * unordered). * * "AssertionConsumerService HTTP-POST 0" * "AuthnRequestsSigned" * "AssertionConsumerService PAOS 2" * "SingleLogoutService HTTP-Redirect" * "SingleLogoutService SOAP" * "AssertionConsumerService HTTP-Artifact 1" * "NameIDFormat" * "SingleLogoutService HTTP-POST ResponseLocation" * * The possible binding names are: * * "SOAP" * "HTTP-Redirect" * "HTTP-POST" * "HTTP-Artifact" * "PAOS" * "URI" * * We know the AssertionConsumerService is indexed. If there is more * than one endpoint with the required binding we select the one with * the lowest index assuming it is preferred. */ char *am_get_assertion_consumer_service_by_binding(LassoProvider *provider, const char *binding) { GList *descriptors; char *url; char *selected_descriptor; char *descriptor; char **tokens; guint n_tokens; GList *i; char *endptr; long descriptor_index, min_index; url = NULL; selected_descriptor = NULL; min_index = LONG_MAX; /* The descriptor list is unordered */ descriptors = lasso_provider_get_metadata_keys_for_role(provider, LASSO_PROVIDER_ROLE_SP); for (i = g_list_first(descriptors), tokens=NULL; i; i = g_list_next(i), g_strfreev(tokens)) { descriptor = i->data; descriptor_index = LONG_MAX; /* * Split the descriptor into tokens, only consider descriptors * which have at least 3 tokens and whose first token is * AssertionConsumerService */ tokens = g_strsplit(descriptor, " ", 0); n_tokens = g_strv_length(tokens); if (n_tokens < 3) continue; if (!g_str_equal(tokens[0], "AssertionConsumerService")) continue; if (!g_str_equal(tokens[1], binding)) continue; descriptor_index = strtol(tokens[2], &endptr, 10); if (tokens[2] == endptr) continue; /* could not parse int */ if (descriptor_index < min_index) { selected_descriptor = descriptor; min_index = descriptor_index; } } if (selected_descriptor) { url = lasso_provider_get_metadata_one_for_role(provider, LASSO_PROVIDER_ROLE_SP, selected_descriptor); } lasso_release_list_of_strings(descriptors); return url; } #ifdef HAVE_ECP /* String representation of ECPServiceOptions bitmask * * ECPServiceOptions is a bitmask of flags. Return a comma separated string * of all the flags. If any bit in the bitmask is unaccounted for an * extra string will be appended of the form "(unknown bits = x)". * * Parameters: * pool memory allocation pool * options bitmask of PAOS options */ char *am_ecp_service_options_str(apr_pool_t *pool, ECPServiceOptions options) { apr_array_header_t *names = apr_array_make(pool, 4, sizeof(const char *)); if (options & ECP_SERVICE_OPTION_CHANNEL_BINDING) { APR_ARRAY_PUSH(names, const char *) = "channel-binding"; options &= ~ECP_SERVICE_OPTION_CHANNEL_BINDING; } if (options & ECP_SERVICE_OPTION_HOLDER_OF_KEY) { APR_ARRAY_PUSH(names, const char *) = "holder-of-key"; options &= ~ECP_SERVICE_OPTION_HOLDER_OF_KEY; } if (options & ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED) { APR_ARRAY_PUSH(names, const char *) = "want-authn-signed"; options &= ~ECP_SERVICE_OPTION_WANT_AUTHN_SIGNED; } if (options & ECP_SERVICE_OPTION_DELEGATION) { APR_ARRAY_PUSH(names, const char *) = "delegation"; options &= ~ECP_SERVICE_OPTION_DELEGATION; } if (options) { APR_ARRAY_PUSH(names, const char *) = apr_psprintf(pool, "(unknown bits = %#x)", options); } return apr_array_pstrcat(pool, names, ','); } /* Determine if request is compatible with PAOS, decode headers * * To indicate support for the ECP profile, and the PAOS binding, the * request MUST include the following HTTP header fields: * * 1. An Accept header indicating acceptance of the MIME type * "application/vnd.paos+xml" * * 2. A PAOS header specifying the PAOS version with a value, at minimum, of * "urn:liberty:paos:2003-08" and a supported service value of * "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp". The service value MAY * contain option values. * * This function validates the Accept header the the PAOS header, if * all condidtions are met it returns true, false otherwise. If the * validation succeeds any ECP options specified along with the * ECP service are parsed and stored in req_cfg->ecp_service_options * * Any error discovered during processing are returned in the * error_code parameter, zero indicates success. This function never * returns true if an error occurred. * * Parameters: * request_rec *r The current request. * int * error_code Return error code here * */ bool am_is_paos_request(request_rec *r, int *error_code) { const char *accept_header = NULL; const char *paos_header = NULL; bool have_paos_media_type = false; bool valid_paos_header = false; bool is_paos = false; ECPServiceOptions ecp_service_options = 0; *error_code = 0; accept_header = apr_table_get(r->headers_in, "Accept"); paos_header = apr_table_get(r->headers_in, "PAOS"); if (accept_header) { if (am_header_has_media_type(r, accept_header, MEDIA_TYPE_PAOS)) { have_paos_media_type = true; } } if (paos_header) { if (am_parse_paos_header(r, paos_header, &ecp_service_options)) { valid_paos_header = true; } else { if (*error_code == 0) *error_code = AM_ERROR_INVALID_PAOS_HEADER; } } if (have_paos_media_type) { if (valid_paos_header) { is_paos = true; } else { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "request supplied PAOS media type in Accept header " "but omitted valid PAOS header"); if (*error_code == 0) *error_code = AM_ERROR_MISSING_PAOS_HEADER; } } else { if (valid_paos_header) { AM_LOG_RERROR(APLOG_MARK, APLOG_ERR, 0, r, "request supplied valid PAOS header " "but omitted PAOS media type in Accept header"); if (*error_code == 0) *error_code = AM_ERROR_MISSING_PAOS_MEDIA_TYPE; } } AM_LOG_RERROR(APLOG_MARK, APLOG_DEBUG, 0, r, "have_paos_media_type=%s valid_paos_header=%s is_paos=%s " "error_code=%d ecp options=[%s]", have_paos_media_type ? "True" : "False", valid_paos_header ? "True" : "False", is_paos ? "True" : "False", *error_code, am_ecp_service_options_str(r->pool, ecp_service_options)); if (is_paos) { am_req_cfg_rec *req_cfg; req_cfg = am_get_req_cfg(r); req_cfg->ecp_service_options = ecp_service_options; } return is_paos; } #endif /* HAVE_ECP */ char * am_saml_response_status_str(request_rec *r, LassoNode *node) { LassoSamlp2StatusResponse *response = (LassoSamlp2StatusResponse*)node; LassoSamlp2Status *status = NULL; const char *status_code1 = NULL; const char *status_code2 = NULL; if (!LASSO_IS_SAMLP2_STATUS_RESPONSE(response)) { return apr_psprintf(r->pool, "error, expected LassoSamlp2StatusResponse " "but got %s", lasso_node_get_name((LassoNode*)response)); } status = response->Status; if (status == NULL || !LASSO_IS_SAMLP2_STATUS(status) || status->StatusCode == NULL || status->StatusCode->Value == NULL) { return apr_psprintf(r->pool, "Status missing"); } status_code1 = status->StatusCode->Value; if (status->StatusCode->StatusCode) { status_code2 = status->StatusCode->StatusCode->Value; } return apr_psprintf(r->pool, "StatusCode1=\"%s\", StatusCode2=\"%s\", " "StatusMessage=\"%s\"", status_code1, status_code2, status->StatusMessage); } mod_auth_mellon-0.18.0/autogen.sh000077500000000000000000000000761410105036600167460ustar00rootroot00000000000000#!/bin/sh autoreconf --force --install rm -rf autom4te.cache/ mod_auth_mellon-0.18.0/configure.ac000066400000000000000000000076531410105036600172430ustar00rootroot00000000000000AC_INIT([mod_auth_mellon],[0.18.0],[https://github.com/latchset/mod_auth_mellon/issues]) AC_CONFIG_HEADERS([config.h]) # We require support for C99. AC_PROG_CC_C99 AC_SUBST(NAMEVER, AC_PACKAGE_TARNAME()-AC_PACKAGE_VERSION()) # This section defines the --with-apxs2 option. AC_ARG_WITH( [apxs2], [ --with-apxs2=PATH Full path to the apxs2 executable.], [ APXS2=${withval} ],) if test "x$APXS2" = "x"; then # The user didn't specify the --with-apxs2-option. # Search for apxs2 in the specified directories AC_PATH_PROG(APXS2, apxs2,, /usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin) if test "x$APXS2" = "x"; then # Didn't find apxs2 in any of the specified directories. # Search for apxs instead. AC_PATH_PROG(APXS2, apxs,, /usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin) fi fi # Test if $APXS2 exists and is an executable. if test ! -x "$APXS2"; then # $APXS2 isn't a executable file. AC_MSG_ERROR([ Could not find apxs2. Please spesify the path to apxs2 using the --with-apxs2=/full/path/to/apxs2 option. The executable may also be named 'apxs'. ]) fi AC_ARG_ENABLE( [diagnostics], [AS_HELP_STRING([--enable-diagnostics], [Build with diagnostic support])], [], [enable_diagnostics=no]) AS_IF([test "x$enable_diagnostics" != xno], [AC_DEFINE([ENABLE_DIAGNOSTICS],[],[build with diagnostics])]) # Replace any occurances of @APXS2@ with the value of $APXS2 in the Makefile. AC_SUBST(APXS2) # We need the lasso library for SAML2 communication. PKG_CHECK_MODULES(LASSO, lasso) saved_LIBS=$LIBS; LIBS="$LIBS $LASSO_LIBS"; AC_CHECK_LIB(lasso, lasso_server_new_from_buffers, [AC_DEFINE([HAVE_lasso_server_new_from_buffers],[], [lasso library exports lasso_server_new_from_buffers])]) AC_CHECK_LIB(lasso, lasso_server_load_metadata, [AC_DEFINE([HAVE_lasso_server_load_metadata],[], [lasso library exports lasso_server_load_metadata])]) AC_CHECK_LIB(lasso, lasso_profile_set_signature_verify_hint, [AC_DEFINE([HAVE_lasso_profile_set_signature_verify_hint],[], [lasso library exports lasso_profile_set_signature_verify_hint])]) AC_CHECK_LIB(lasso, lasso_ecp_request_new, [AC_DEFINE([HAVE_ECP],[], [lasso library supports ECP profile])]) LIBS=$saved_LIBS; AC_SUBST(LASSO_CFLAGS) AC_SUBST(LASSO_LIBS) # We need the curl library for HTTP-Artifact downloads. PKG_CHECK_MODULES(CURL, libcurl) AC_SUBST(CURL_CFLAGS) AC_SUBST(CURL_LIBS) # We also need openssl for its random number generator. PKG_CHECK_MODULES(OPENSSL, openssl) AC_SUBST(OPENSSL_CFLAGS) AC_SUBST(OPENSSL_LIBS) # We need at least version 2.12 of GLib. PKG_CHECK_MODULES([GLIB], [glib-2.0 >= 2.12]) AC_SUBST(GLIB_CFLAGS) AC_SUBST(GLIB_LIBS) AC_SUBST(MELLON_CFLAGS) #include PKG_CHECK_MODULES(LIBXML2, libxml-2.0) AC_SUBST(LIBXML2_CFLAGS) AC_SUBST(LIBXML2_LIBS) #include PKG_CHECK_MODULES(XMLSEC, xmlsec1-openssl) AC_SUBST(XMLSEC_CFLAGS) AC_SUBST(XMLSEC_LIBS) # Test to see if we can include lasso/utils.h # AC_CHECK_HEADER won't work correctly unless we specifiy the include directories # found in the LASSO_CFLAGS. Save and restore CFLAGS and CPPFLAGS. saved_CFLAGS=$CFLAGS saved_CPPFLAGS=$CPPFLAGS CFLAGS="$CFLAGS $pkg_cv_LASSO_CFLAGS $GLIB_CFLAGS $XMLSEC_CFLAGS" CPPFLAGS="$CPPFLAGS $pkg_cv_LASSO_CFLAGS $GLIB_CFLAGS $XMLSEC_CFLAGS" AC_CHECK_HEADER([lasso/utils.h], LASSO_CFLAGS="$LASSO_CFLAGS -DHAVE_LASSO_UTILS_H") CFLAGS=$saved_CFLAGS CPPFLAGS=$saved_CPPFLAGS # Determine what definitions exist in Lasso saved_CFLAGS=$CFLAGS CFLAGS="$CFLAGS $pkg_cv_LASSO_CFLAGS $GLIB_CFLAGS $LIBXML2_CFLAGS" AC_CHECK_DECLS([LASSO_SIGNATURE_METHOD_RSA_SHA256, LASSO_SIGNATURE_METHOD_RSA_SHA384, LASSO_SIGNATURE_METHOD_RSA_SHA512, ], [], [], [#include ]) CFLAGS=$saved_CFLAGS # Create Makefile from Makefile.in AC_CONFIG_FILES([Makefile]) AC_OUTPUT mod_auth_mellon-0.18.0/doc/000077500000000000000000000000001410105036600155075ustar00rootroot00000000000000mod_auth_mellon-0.18.0/doc/mellon-attribute-map.xsl000066400000000000000000000015501410105036600223020ustar00rootroot00000000000000 mod_auth_mellon-0.18.0/doc/mellon_create_metadata.8000066400000000000000000000022661410105036600222570ustar00rootroot00000000000000.TH man 8 "25 January 2020" "1.0" "mellon_create_metadata manual page" .SH NAME mellon_create_metadata \- Populate initial SP metadata for mod_auth_mellon .SH SYNOPSIS mellon_create_metadata ENTITY-ID ENDPOINT-URL .SH DESCRIPTION The Apache module mod_auth_mellon provides a SAML 2.0 service provider (SP). This service provider needs metadata to function. You can create the initial configuration for this with mellon_create_metadata. Three files will be created in the current directory. A public and private key pair, and a boilerplate metadata xml file with the public key and the URLs of this installation, that can be further edited at will. You can reference these files in the configuration options MellonSPPrivateKeyFile, MellonSPCertFile and MellonSPMetadataFile, respectively. .SH OPTIONS Specify the desired entity ID of the SP. This needs to be globally unique and is therefore an URL or URN, probably with your own domain in it. The endpoint URL is the full URL to your mellon installation. This normally ends with "/mellon" unless configured otherwise. .SH EXAMPLE mellon_create_metadata urn:someservice https://sp.example.org/mellon .SH AUTHOR Thijs Kinkhorst mod_auth_mellon-0.18.0/doc/user_guide/000077500000000000000000000000001410105036600176425ustar00rootroot00000000000000mod_auth_mellon-0.18.0/doc/user_guide/Guardfile000066400000000000000000000002731410105036600214710ustar00rootroot00000000000000Bundler.require :default guard 'shell' do watch(/^mellon_user_guide\.adoc$/) {|m| Asciidoctor.convert_file m[0] } end guard 'livereload' do watch(%r{^.+\.(css|js|html)$}) end mod_auth_mellon-0.18.0/doc/user_guide/README000066400000000000000000000016521410105036600205260ustar00rootroot00000000000000This is the mod_auth_mellon User Guide. It is written in AsciiDoc which is a popular plaintext markup language much like markdown or reStructuredText. You can find extensive documentation on AsciiDoc and AsciiDoctor on the web. AsciiDoc can be rendered into a variety of formats, but one of it's great advantages is the ability to produece DocBook. There are many ways to render AsciiDoc, but the simplest to produce html is this: % asciidoctor -a data-uri mellon_user_guide.adoc Note: the "-a data-uri" causes the images to be inlined in the HTML output so that everything is contained in one HTML file. If you want to edit the source it can be very useful to be able to see the rendered version as you work. This link is a guide on how to do that. Included in this directory is a Guardfile which can be used in conjuction with the live preview discussed in the link. http://asciidoctor.org/docs/editing-asciidoc-with-live-preview/ mod_auth_mellon-0.18.0/doc/user_guide/images/000077500000000000000000000000001410105036600211075ustar00rootroot00000000000000mod_auth_mellon-0.18.0/doc/user_guide/images/chrome_SAML_Chrome_Panel.png000066400000000000000000007453741410105036600263460ustar00rootroot00000000000000PNG  IHDRGd() pHYsodtEXtSoftwarewww.inkscape.org< IDATxy|Tיd&3}%+6pA\(UKJm>ԶZ[wo޶Vj(hED!!$dg_1!d&I|G3;gl~GkVu\!B!BN'k֬QAWQdѢE\~h4TU !B!BSOUz-}EpB ^/g(B!B6pM7zyQ/_K("#FB!B6lezݕ~[ưDrrQݎhh4y<.tvF3f|!B!b$?~<&)62_Q!\X,***֎XV*++mj5G;^!B!1L5"W u;F х!B!8ȇ~Ν;Y`K.{{"-Zęgr(H}}}ˤ8"B!B>7~#ر_<#R-8pGVVqqqjk䐝J ( VOGB!B \.ÁnmD6Z8@NN'x׫rXőݻwSXXHjj*7n ;;WYx1uuuܹN;-L)!B!PUUņ ؽ{7N͛GbboZHkk+7p3f( %%%tvvz1 ̛7;QEkmm0Ν;VNN;wd,^W .ܹ«B!B1D.իW*ojXV׿@ll,w}7&L*կhmm%33_>ʷjGEEE?a=fƍɾ}|7n3)!B!Cz;BnVbcclO+Vl2^uf3wq^뮻e˖;ő SY>G@+tnܸߵF#!B!"6 a|Wn~_?UU1x^N'pI'g.\Fs 'P\\ҥKiii i_GrԣGB!B466bZ)..h4p8j(+zE!!!OBjEUU4MH|&OLllkkkc޽GMq5'??߷LB!B!O??뮻r(..榛n⥗^gʔ)x^, fy=ƏP80] Iϳ"lŎ_<={+++++;r%S2G+vW=9' #pEE]v:ty̓O>=+kQX:;;QŗK\\MMM(bJVV{ xZPQWWG~~ {tJee%@W $ܹs#w0~|..uZRYYIy+++++;Fr%S2G3vf͚ݻ)//#/o@u:nW񢧹sꫯ?駟gj7=n)Œ3رc+WdٲeL4{u֑k{cc#ow^m\!wFdLVVwE_;= #= $ TX#.rS>,''gUDɕ\ɕ\ɕ\ɕ\ɕ\\ɔtTV=Mvv6L;`!))*rrrSOSO[oSL 7mVUUErr2x2RE';;;FvzUk/:]^J}}JJJJJ䎡\ɔTU7nܠ TZ8TAϧ*\.WEEELT~IJJ",'%%YYو3[ȋ#夤R]}Dr%Wr%Wr%Wr%Wr%WrG>W2%s$37.Ȁ2ɸq[JõJ;~<:/Lznwp=~ LnNv6ӧLd2R[WGm]}ő`rn7Md3e`P4 .Orb"-?,\hO`x$6WUU8sB444$7K1o(ӧNE?~ofbqOYyyH3W sVBBl-_Hg7žm3}T@Qݶ+mԔf͜! XʵlTVU1st+tWhhl[ &wƴ$'x{{`A{p쇎NjXti?˹ptp7f}%~hn{G@_I|k5 hoG[nJr<Kr;%/q0+K1~S-,9g~*̆F4̀ūSI,~d 63,Ev{Hgفt.L:շ-PɔQ^^F^^>)h:2hmmCQB0,11Ւ6&^!(pu"##^;WUAn &ygҊfc䶶*Q  [yW,7%9/Ʀ^hf:µܡ ~ FQP Пw~=ޗ*ؾ$'t!ʀpmXDEE4 `(7Zs(t<\.װ3RwqpjdgPYY+@W{NL谏q:].klaq\DD~V\\<^(2`rT4ȹ@ãW [ȈROjnRbogPZVoR^Qy\k,7\@k#3#K. ?o7FDDw(F37\hDZFl1)\K3L :7boQC`T 5 ~1FE Q=4^cdԩL:oyݍA礥vSS]FhIMMCUU\@`6GS__ a)-GrrJXl+ &'C?&nmv;n;, D0FLjC;[ u8:v uttt c ~(@9HznP?Fcqa2imkA,V+{C|69/ XbY9 ~8rbs8 )wN,ٸis41|P۞}mnnt;s(ḯLHi l4h3Y﾿VdZxKn`O=W.23JJJJ䎝\8/’sLNcc='O ,5Ν;8ēIHL@TUUGW/n7z}SN袋6ظF"#:;;$wsUUpci?HJJJJd%`ΐek,fvtc0D@tt4 ڮ($%%Z#iooYGXJ}}11 %9݆3G ]nCc#еzu(sKr%Wr%Wr%Wr%WrNdMwݍdXx8v22>222hhnӊ!2RO||<6 7xB!BKECvv6YY٣ݕ:TCyy9^NXUX8gaǎ,\xuN!B!mƥ3-̞=4`B!B!8HqD!B!5)!B!⸦.P__!BqL"!##b PWWn礓;B!Ʒq@G;B1LՌuu̚5[ #B!BPll,SLm"82\. B!ǜ8<hwC1IqD!B!5)!B!& +•W^9B!#b`2 6{PD0_w=嗄( ʹbXPH||(uNQrw@=_>ۻΐ#X>< 4x6 QOF^X#7d8裲hڰo?gY9s'0`\yf?|_q{X B!j~ak󅿃t|H#ŎVhmmCQEtjT0Mr  m^* hχpĹ[?nEkk; k^]{3˯ k[b|ZMwadYyWAq9VׄeoVW|e\3FV IDAT{ 6?|6};ջO30w[[G;}gj1jEoHgXxxG׿5{wlBf֬Y6TcH;9jkkٹsmmn7s9LΝ8toɰw:mxvT :Q=T aCzP &PiتBDc)څt/Mׇ- $pvjkk9t:}zi+,>B^ZJXC:rga$w{P(3#5I(Gl|=s"_p&Px6}N8===]G,&yY66̘Λ<YVehi466rMޱzٱcDFFw^T? v;/z#S!G"_qiӦ1Ukݣ2߄`䆢({z#.9s0}t^/vbcc)-ݏh$66v{d_G~.-TpWݿ˙NwEЗȈ2#C #,~BӧMޱaZhiiV{=> p}MMMRB1lBvv6lٲ%l= tm$jFnx^JJJ}UUvϴihjjbݺu[> ^OTT䋅#vTӎ7Pz֣zUTՋQ~\.;ˁrD>#VyEmnRCEai7ֲp8ho1( W(zn<'|999̞=SPP@||࿷|~vy$c9)31ns-hmm%//omB /~anRѳA`0fr{BJb>c~j*ƏONNFREu:Q^4CWfCEEXmk#(n_Er6p8\.t:ʹ0U׶갢(-8 OVg^rڙ밠x\-bڎ2 f=6UxZ6Y$cUh#?bf m[?$ar&R}t[E)Q'-IUv;JYY9I&knN9e;wd2b4 ш(:thfΜ֭[q\pUWeHaC+n.|X~^5Fz:@ZG|UftLL/sV{h7WELz5p1uݧkHΘ0g.%)u.:M׾^QR1y<~-ܻy f>*|~ڎ&N_ ͊u`c(_o^S$Ŭ8B6V{L:>b&TTiqxYdIEEEL4׷=nYbWfEk.fΜի?~<555|lݺ8M]]wy'7wuY\d__n^}UY` ˯~+{=>s"##ٴi_~9;w$33n 6I^^>,g?)ׯ%Kj*bbbsjkk;ؼy3:>}x9ػw/vI&l2z=^'xs=M6a0馛{ٰaIII,^z<\zL07xN' \p7W^yE믿NSOs  .d֬Y~ݏ}QNbgSWݵ'o<'Ox!1**w,ʝ?/=rƳ? 5|1#5V.6:YW7ۙk<;F"?ܗoWAky^Ry'(n\x/X==?zh$RIDDSL3oNp[kk+رjf͚ž}xIJJorEqgkw^N:$ {}P폪\r%xя~ĕW^{ˤIOj,\?O;<sz~ÕpXb+Wh4r뭷rw=?d|Gh4n6^xᅠ|z\tE̜9+W/^8zEaڵlذ˗]ꢢ"FcqF9EFmܹ,Y6lCJ֯_Ncҥ+}ygIKK#--߾=?n@Q3>[nUUYn^{oGƍgVc۶mޞ9fovyK0d˖-\H."o˃>HUU\r _|qs=@k׮%;;!9ůb||r7z=֢"&>< U/`3ՠhZQ4ڐ|koـ֗HHH ==T5p8zhZ:::zpNGWGCQ2Q>]%jK-I(PmTt:ToOO:8*v[K*6JwZgO6a5t\@k/:R]LFFt:MMMW;hnnrHNNd2cZ#jC;"G||<.\1 Q^P:Hn࿯;}S_ ܳDj#2yS++SW SɁ_ %;6SavBiɈI&Ǣ96U)#3&$^C^Ջ^JMG`1)j{0蚷D~~=\(,,dڴi,_\bbbXlٳ"~"##)((Og0{a/!66~i6mā|;餓HII!11}{^K/O>^ɓB3L<#viYU~gXv-vGK|'}~>(y^褦 ET6P/te4B炬02c<=21w|8jx|jz=Cn/w)_wHN{OLֹPQ4,v:˧/|#B:[d?ڻ:B3.:(~hNDGFha*Y)8=.ZL?/=~"x:RVp!zmD+**bĉstt'߇yIII3vTUUNtto[$=c&::Ke̙\q<$"\.WQGOvv6>#y]CӉsP@#c#66ǃntMƞ={HOO믿+vACCϋ]Ommm\.?=%(9 i[[ѽ򢣣=UWPԑ%^B7ߌje˖-\}վb?fܹ|!mSk2a۽Fx6Jo:~7߀6.kHp75TJTTXVoߎ` ==|fZ^%X^L5 ^k;( ^2i|%.%k j@Pݡ]wjR...DžZBnh{F>K.TkzUNObߩJ||70Acܸq? L:bɨcH ֗R֬YìY{xŮmڴ~ד϶m۸-++l6{۽өr)z_B`FKK DEEBTXXȚ5k|UG6e*W;l6c0뮻|oĂ{0bbbtKbF0L0m4Redܺշ PU4h##ihKt͊p8 nvn76 EQϧRF#7b{uT-j{38ndCxT460P]׿ӎK!iaiC?pAT O Y[4ѧa)oN۾sb6 Ei~vڍ&++ى l6xvHr'LHn.,`&6}77fˣFn}s%U|MCMݾ\~t:q8|!ٗGبAUU, |s xmNNSX6Z-kdtrp8HMM%::͛7rPUv{H#;6l ??7m0GޞtN=BZj*z!z!VZENNNE[CAUUMTZ pBk4v~gZ ^ODrMk^sz$F5Fӡ|#EZ-zw111!Y<\u9Q]]e0ǡ[" n7ʲ[Q杋%1n]$Êvn@i96vI츳Q 5dM%_:jhd34mf6Zt瑟xeu֙,Y=/^LLL 呖5yiCzٟ# VCRRIIIzكb7""X5dGhHQ]#{ʧ(¨;w+'_~i=֚ tYO. ӟuBFǿzo**r>BE!Wo{L{<8=.|Ö}2w2c>P;#3F<nw o58Nhhh߶`EDD~z!--ڷýu][uBnn.ӦMl6#;;;y(../O<6ロlz)VXAmm-W]use̙3'$Ok֬!''fΜ#<⻬>G>V˺uؽ{7L0{9}y"V\O?Mjj*gU(̙3ihh-]ŝtV\|Xroqޫʷ#k׮%$u+lUU;t8*hݏѪ({^^+tOh4;3XˁvڭiD;;Q3Q] FFNa+4kX[ĩwc@Ȝc'V0H*Jzp=;7p}EQ|S#""HLL$!!ۈ>ӂJUU|Ree%;vڵKQRSS9sfկ yiQá,]T׿EMM ;v`Ӈk &Xg eaO?V>ϼuOV :{m{v+pcly&'e9 @JJʀ)Bqq1SL c{[nFX"ǵBP/Yr%sk񝦷^z۷swJgÆ̝{€V|Oڻ/?;h `0( EEEl߾M(h4&MDjjJa~:75?\z+^|LUÿv-blXT,6)?<}T3}|} {JMJEP-;خG }kkk [>h g&--/xk$#=&\SLa ^c=~1L8‚_lYc[IU IDATvvJۄ8.blZq̛7dNːOuT z;]D[[---466b6蠬^ŒKtt4mm]g/Y{gCk$M*j"Jˇr3~\0*_rӴa) d$"ݺȀɩ4c~AfȌYll6}!B!DOxTEP0z:_|lAe|~VƷHRRh"!!BATPMU $>q(L8&,UQ,ܟGR#B!?bZ~騵A^^^Pʑ?jbce4hiѺ)G#)1oΨYG {G B!B{#!B!B1N#cNfv7B!8洵 BcGƀ$v9B!ʷ;]BqšC5l6n{#B!1A 66,YS10)ƥ3n\hwC!B!8ȴ!B!Bפ8"B!BGB!Bq\w͑O>2B!B!SO=϶~#\ry;#D^~e.B!BcgLB!B!qM#B!B!8IqD!B!5)!B!BtݩSrWw^&O<Ԯ !B!DGoR"V+re1 ֭cӦM<C[`k֭wA`45k^z)z>>_r)\r%Qy$99ٷ_,Y?E]x+9餓uVZŃ>ȟg͛7Hm}7ϽsrEE=> 3iҤٟP3ݯmmm~V^[oEGGr)?uV~anv>l{_~իWe]~nF{9뮻F O>O> 2ΐIqDnʽuSN9kv͛Yt鈾AmiiGe\veL&kq-7b} _|_|?ωUV~z.ވ0ͬYo1SUk׆82,X{^hkkcժUlذ˗sN/_inٶm8ّCD(-1K)HP,JVj E"b641bٿ?痛|AAA\H^FKKL?vǷxd;;;M(233HpD̛^Xr%1{lmå=zcǎ)G,--9q͓'Optt_~@MȁHOǑ֭[GժU>ǹv?whiiw^233QWWw4h@zq/a-[D___:>z;v=zQFhРx}R^= ,RR;w$** 5557oT¶EGGSF ) e``#FEwԨQ,X###l߾*U СCdffRvmfYZܹsܾ}ŽדȜ9sAnݚL¸q[.g۶m̚5K.q!144dX[[vZlllV `ԩS+dȈ:pz)Tܽ{ltB֭7]t᯿O?DFFKڵkW~w 0uTwNxx8111| ;v,ܹs]v+444/]vaRR۶m޽{ѧOիWzϟW\x'''sq۹$ۧԃǔ:uDӦMܠ֡C9s=))hFSV^M*Ux!0`3f ]v֭[Ѻukڵk0-[H5G또XXX=vǎ˜9s#Oru:uꄅC I&ٳ_Z!!հaClmm_~M6̛7!C믿$mGGGƏO=8|0@?p:::̛7SLXX²~֠A.\ݻw@zZ槮Γ'OhРǏٳg޽dzpBRSS9qD~'{ݻQW0h 4443g 4Ν;ܺu5jHo߾ϟ?~ˋyѴiSn*mΝKݺuİsNFYၑ묡~?$''s2}t 555XlݻwvڸZ~w:mmmdرt҅͛7ӹsg[չz*;vCtܙ;vH+8ܷo,]OOO6mDFFFjhjjux"d27oqܹsѣG3}tbbbt4.%%c2a<˗/ ]ΦMڵ+K,Aڵ (|{욚J)r L<{{{Ξ=MV9}P͚5Ņ^z5Laٶm}e޼y+_+W122QUjѢǏgÆ @G۶mdpqyTR---"""6~ .K֭6mZ5ׯvvvDEEm6RSS#vvv(SpdΜ91'gW `֭\/_r IKKSɼ s- Çeܸq )iՂ;MI{y݂5j_PPwbccDcbb۷ܜGGG455Ȉx¨Z*iӆ0NÆ QWWG&IL322… <}[[ k+ S7QHCGGGz֥򇅅I7A*UiӦJKqOj֬ɤIHNN_~aĉl۶ שY&_Ӿ}"u hڴ)ZZZ? ;j׮ݻw}6m#7nܠVZX[[7 Ey۠a ּ"@Æ tmڴAMM }}}6mjܸR#(~?]sx;7o><{L0,,-Z ɰDSSR/^|WOq穼ǣ-{JJ 999\z*K9A zuvvuQ8俵_ll,[:Hg R1 ]/gT9}PFaަ} OV"!!sŋcǎѱcG#۷ĉ8q۫C!@706IyYt3>|Xёٳ'*U>gee4Zj*K/x5jP ru0-TM6ajjJWA7 JffBuW^>:::p555*2l{.}4N[[[?nb K.RJK7>dll\ɤ$HJJBaVMM"˟жRJ<|PaEmoTZj~رcKTނTTIfI۬|]^}e 7EyYXX)S=8vܻwAqmJ9o+]yPPG}$ݨi&)akFFƒTayoO'' =\‰'ΖO ~XzZYx1{ŋ=sI+OqۧT{{{BCC\!Y<~h"iثW>ժUPmtttx9.))).GO]]]ڎE3ɿ?FKrRɅ+Em䝦0o>S$}I>}_믿ҤIWD%(%J}6l`,YH"""9r"""pttoߞQF1bN<… 9z*u$dH6Qe`ٵkWI.ׯYj0/Uܹ3+V>;99Qz Y,U 짟~ZBHH{Qʃ%:ccctuuY`ҸGxNNN 222//ʕ+x{{O?)UZjD… hhh`ggGrr2999 y8kPX INNoJR+Ppkkktºu,oއ 233ח.HKK+v=_ 6OA!ёk׮iBCC111AWW6 nti{@i׮&&&lݺSA, 3bĈBoPU~^X~lٲ3fPre?^ |ߓߏ̬R59{, \"Sw ¢jȓj  88Xz{ S1:wN.l_yiںꢼ F?TӋ]^ӦMiڴ)IIIlڴӧOөS'ԩSGڗVW_}%X/M{n;r:t===*Wr BE脬K@mll]FܱcG)xѣG+?ӴF"k ѣ%z˫'&&pQur]bbb܄dNAJJ iii,ZH!?$\\\a7n 6gڷo7ňu뒓: bΝ@ndJͨS~ضmӧOGMMիWȃۄ شi#G$%%===5k7|ÿKJ*U`ffm۶Y>e)4[.k֬Q: Binݺz@>,/6e%ҥKΞ={&44m`ee]DV_~)7KE)SN-[)S(|'>>dGz(Ό3 tRvڕ+Ua)lxIwMj׮]2 B~>>>ھ&֭YJ={hy ׻en߾c$oT]&''qY4i5z*[ccI-^X F[[xV\I۶m133ԔVZI *iiii\-[bdd899矗nݺ(}744WѢE  8gϞ4m 444dhkkcggGVXhRwfGׯOv8yd  O?~LxxH /gA(@-Z\\\\pppP"OKSf5˄ رcRUˠ ɡo߾/2o޼"?._9Çy&+Wf%*7[neE~oӦMxyyM !!cǎLpp0 ,(Uo.\`׮]ҭ[7ׯ&>>>\voFPPP&lْ9s搑2իWƍ MiN< EA{߿$gA(NjggWh]]R);2{l~H:uhڴi)Spe +ż?ÇK"#waԨQdggӱcG;0~̙j&wQl=|pW.v˖-ceZۦcǎRWyըQC!0m  ׳gOzQdo\+W|%3 ZnM```&d}މȌ3:$ #55T Xҽ{rZٟ)r(#F)8bbbW_}嗙 6THΙ?QQh IDATD sy$j֭[+*h":/  脬;ɯE 6lC_f͚Ұ~I dJޜ'{O>aСhh vvvܿ:C8.² {]Y߅dH   2%d} ,o3⨩agg'}>{,kŅ 6ʻ1^zz:^^^TV1c @ժUQWW/rRRRx 111++vAAA;QA)|ĉ|7T\iӦe*%Cf͚5hтƍ+w^oˋ5kVVVT\SN   ^`#3gΤK.{/M5-) +Whٲ%%gPP7fƍܽ{ Ν;/ubYAAAAм9GTA[[???oΊ+k:z(|*[nngܹRO2rYYY :7oh~^^^$''K]]]ٱcYdAA~Cq. >ܠA4hׯ_g׮]_gϞIO8&^fժUi&n}#""x!EÇ UZ7!AAA;{EP&tbkk4 t oooSc|'\zmb*;w$&&'PR%zM޽ٶmB(%YӧOoh $ MC%uy.{w;ޭ KllҰX)9r???ᆆ[࠲eFDDeR:::N^ϟ?/_!AAAGVZETT°lN< ʛDEEfDO&33޽{S*WLʕٳG?33S)    TҬ&==K.Idd$11UT/_N ^:7.ǽ{,y=z4wެ_///ȉ'3`ƍ?@Æ ,àAݻ7{e={+WpY:&666 <ѣGmϞ= ȬX۷3ϟ͛lڴ4h޼9{f%5"7x`Y|9cذatM~TT]t㸸x9  }$r»իl۶d2lmm߿RĉdggӮ];S"ݻw}nnn*_FFFqqqR!J#NNNJDÇ~,d%*WI~ёYflrݻw{Wo#  ׀ |}BACC[[[*W9cƌ!;;B:`aa tڕ7]wNvv6O>}6mnnnRjժVi۴i)7o&<[졤4/^ƾ}͟?oAç~Jjj*fbY|OdeeÃx!ׯ_͛}OzXr%SLaǎL0C͛7۷oeСӇΝ;3ydZ_+] _DEE.wyDEEajjJ>LÇ9|0HOOGKKK?uT̙/olݺG^iӆk׮2cLRsPWW/q"[{{{BBB ֭[9r]]]Ǝw/^jժeʟ"8"hժիWɓpuuuAwߗ_~Y`6mC6md$%%_AZx)8::rڵkoCeMSnܸ&&&!57[ޒ^㌍ J* #///,--yCCC.\Ȅ Xt)\rccc8@TTK.$[N!w,\@_VHn޼g}ƀ)))t ǏcllLTTRrkkbZDFFĉܷtЁy摐@޽Ԟ8Lz0`ٳ\~p>}2baa@^1biii1bׄ h޼|CCCd289937nǏ6mÆ .V&+++o=yfi*Uؿ?ׯe %WfMfϞ4\&W9ҨYTʊ5jVy]tcffFDDRҜKٺu+gMMM<<<2deܺuKNi/Rd2455˻G.Isf͚S``$##Cz?iki3rH6nX&##oo")MR/_^g7otݻC5%#[oѢESNR榐u9o5QA(^a=eKjiixXF vυ]ڶmKPP􎎎\v2_ίO>ӧq_]򯓫+JUPZՈ%((]˗ N:&LݻwQiiiaggGj8r={$99Y HJx9 aaa!55ŋs5kFfܹ3 zbʕl߾OOO:uĚ5kXb2gϞann^`7n̫Wĺw}hjjҩS'~gGJXv-yey2QF̰aØ9sfeREٻw/f׮]@qFժU+WVr 0a+Wdݺuq}Ze%accPk YYY_ȭU޺444=z4+Wd„ JyWX[2J*x9}>|3gFQu"8"Xz/< d* |پ};Ǐr7of͚8p)S(|ŅCdɒR-_~oΝ;wT077oeҤI:t7һwo`ٲetЁ)8ҨQ#ܨY&͚5rhhh عs'#Gddggӯ_?M9wzzzԮ]Ν;KW_쌿Jrnd2/_NϞ=ݻ7se4j---Vʾ} ,SLL TsO>ܼy-ZE:u} ]]] 7o@eRAӧ~)AvصkOUx3i$~w>|H˖-qww/5*XXX|̔J+V@[[CC.cIuڕzqq]FLL 2 {{{Zj\K.9{!::PڶmuS 0o<6oެ0#>>> ﶐t1T]^Ҕ]^T:S.&oߦ[nܸqMYx2dRb-[ѣPIعsgpfff2l05jT%{;*-)VNiiihii Sll gΜI&X[[ӫW/QsD(XժUER)AA䩂2w\n߾TS fH˖-_~{|ɻ}hkk|"8"  '009sp]MV`MJ'FIBQT;ҥK#Xlٛ.l  BȻ~988MjqqqaРAӇN4%|gFUhkoP6QTAAx_2{lݻW" |8>͛9.Fކiii  ByɃ"L:UEA(P ^^^lܸQiaHMMŋͦM "66Ϙ|2ׯxzzh"չz. oMaez]x1t]޽{qwwgԨQhkk~z._9~8{eϞ=hjj IDATׯ|z*z*ӧOӫW/^| @ݺuիǏ… \pիWsAڴi#McW0͛ӠAIß?N9wxzzrBCC eΝ;w333yӧOܦ*...r5ooڷoS˖ѣY~=}_~­[عs'-ZP}%)) uuu>s}c5W͉ ‡}:u[򺿏5k111L6 uu7],A"1J122k׮J9)΢E}ɉ=z !!.\Ϟ=VZG= 7o0/ɓ'+-C 9r>'OҾ}{nݺԩSw5:u$Q&k.,˗/WXfiO?I 0yd\2$%%7;ٓtiݻwӫW/ʕ+0a2+k_ fDDD(|W={]gϞ"BN*" JVcdd°+WQtϟ? ?;ww,X zꅞSŋ˴AAA9rD\zu>#Zn ڱc^>9R}U+o9K+55Qa_|7ߺuK6#... AAAxТE Fz* AJ»ۜӧV9dԩ<|ӫW/L={(LSvm&(re)rU&۫W|2$̙3 2Di5jԠ[nJqJ⯿"..N,ɨZ9//__a[/ Wdd$cǎo( ?q7߈ R7핆+(ԩSё>raj֬ɽ{<#r >}%q%)rRhfB \ơCJUʦPE[[[al/ ׯ_2 aKKKcݬ^ׯ0cƌ7\*}̙3IJJogAP * {y%O(4NWWWs &b-mpD/EO)J^SnddA]ڲlWgeeҲ ~~:˗/ԩSƒL&cĈB̘1dA*YMA'."%%\yp<˪VըQxPe.l-[ ZZZ*/*:u k֭cŊ+/\]]7o@!YoIy5::WWWh۶R}>ںĉSN~ MܤI,,,h۶-.\P7+W&55;v,~~~yѼyBK.-С&M*|@\xzzr2ͫU mj``ry0t邍 ժUcRrN>ҒjժѫW/`O˗+ >}* ;qRެݻ3j(c``jb T%**#GҲeKj׮Mؾ};WZj 0͛7U^s899ѱcG)OXNNW?O?e޽4ۗ 0tPZ!!!FѦM6oݻwvvv,Y!|XDŽ 5jW\gAP 9RMQYP"? Aʺ uRO77_Vʖ ;+.\@rrB: +$Yl4l޽DLxfϞYf ׯg̘1 9LBjjԭ˗ٰaԫƍʊ˗3fnKn O"_SSSXd ̛7>;ҧO:uy>T8vMJصk'NTκu={6K.-Cɓiٲ%aaaTRoVes>sjժɓʕ+J߱>ѵkW-[٩lS>sˋK.ʕ+3gL<ڵkS^=<==R .dL2E\|w2e pAP ?TP{{{֮][j?oJR|yoߎ'SNАTͥo߾ٓ#F( =|0s!$$Dڪwlڴt CL%ǔ)SWJwe{nC6mӦMSKӦMի+733L]__ezLĉر#cǎ%338Oڴi1c˗СC_DDD0k,uرc9{yܹs~:j# _/464jԈ)S0a|;LppwMMWJ*×|Շ} \JB2k޼JZ@@*^M":QGzT^^S6mB[[7*d޽|7XB;SGeŊlٲK*E3~xFɭ[᫯"==+WЧO Ɠ'O,_>1b/_͍˗+Rgee70fԩ$͛y!?qWV-9H\rkXw}ݻw>}:-"&&F">s!::-Zrˮٳgzj?0/((Jc̛7Kv,TT5kC6lȷ~V.\Ȱaؽ{75~:WJʥP;vh444Xh;w2zh?29n>HRR);wݽy>qEf̘ٳ9u,Xϟ3`*W_ŶmۨZjh%ߺu+7nϏ'O~z]ߨC"0ab1:tP"מ#G()rN}166ҥKܹsnݺWtU,___ܘ:u*ӦMS>#(E>ɓJ߻wGMϔ?|Pݻw_點ʍ6==]BO<ٳe˖T^])~*ᡲK֭[_[ʃ_@׮]iڴ)u%66======i%󻸸йsgTZZZ,Z___6lc 2o}|1MjpuuUhтmҹsRޞݻweի6 &4lؐΝ;ʹs֭[dڷoObb"Ғsbaa y3p@@YثW/틉 4y4iDZjѰaCH$RQ]z|>s *HϜ9Cj?~<իW燍 _| ???IIIaѢE888ЪU+LLL4GARzuRSS5k899^7DZc8x D"~N:1CSN ˽{½{puue߾}JbI&)o{˗/̛7ytxWԔ{ ү_?ŧ(5{U|LMM߹ѣGٷo?#*HL&S?-:mȑ#4iDebggG~ c$G&PL> ʍ]ʑazzzjʾ 3gT СC WD@x*oKuttTyݫxTL T1d߾}Jq:SDJ>٧NӓG)ҤR)JeM[[[a ^ҥKYtuVVׯ_ٳgE\pm۶)~teASSSm\7AKK*U@7o P9---C**\j]$X,Ϗ!Cw^5jDFFJZZZ2L|H-R4>}(RSSE -j㢱[\y\Eu҅(bbbȠZjjǽ:^(muY-m̾}YrAb1}Vze?QZ߼\D"!44UVXj6mڔ;wr9^ʕ+WHNN̙3_k. 0r)ʦM D[[}}}444J5z*yyyiӆ(vءtn5 L2 ФI*W{#H$ŚÇ3E&`BBBԩbcffƊ+Y7RdΜ9RD@@^#?3 4Gt֭҅[+q(YSpmݻR!!!/SbE:0덉qƌ3ƍ+fff],0 lmmuo޼YiQȀW񵰰cǎJ;iӆÇ+Ywٳjժ۫PVƍٳ?"YfaaazzzҨQ#߿(O>* ÇJÆ Rr(b* 揍>(0ԥ-whh(>>>lْY`_|iΝڵ wEڵkDGG+7Lƭ[=z4'!!pڶm/wϏ;wAbb"SN%)) WWW,--YnO<̙3η~ĉ aǎ!J166ӧoJHHlٲ?h}6$$$c"''GI;ƺux.]BWWf͚k.ٴi:::jJϴUׯXXL5Q$>ׯ_ǯ'''GXNSttt&44Tq 7C\Q'Wօ  %??sYIKq.s:JGzh׮Ϟ=cr۷oOd֬Y<}k׮Vjs= T666^zPj?sJ]ҦM9nb֬Y=zcǎi&D":t=== ##͛p]6lؠVooVʄ IJJRW+#G$66ワJl\%wW},BBB=z4͛7aÆŀc޽\zMƍT˖-ɓlܸ:|0<<==9}4.\`֭ hjh۶WJ3fpmA)" yԮ] \Bzz:899ѵkW&Ozǎ5JmYE6mbȑji۶-aaa^'Ocn݊ M6K.?^2Cܜ+Wr-nݺWȈǏî]v Oƍi۶-*ܺvJDD۷oԩSBAAʕYft]%8dYeXh_~ҥKĐ6kצe˖ 0@%Fɂ ԩ6lҥKq]pqqa*8l۶ZnM?C ŋk׎CxbrJ"##i޼9}cڴiDGGSjUEMQ.\ݻy{{SNOXXVVV1&OLՉ[n2{lz{aԮ]?o RbEΝ˖-[c޼yT\YHoܸq̙3GM shiin:Jk׎+V/_3gRZ5իGNNBFi&/_… Yba?i$A%xAd+3OOOoΔ)S3f lڴIg <<XSRZKGGGGGsٲe$&&֭[gWwޜ>}www\\\oȡCnݺJ>i$-ZDǎ_ӧɡgϞc``v,?3gFݺu{zz2fŖů*sE\vvv 8e˖c=z4 wuҴiS쌦R~Me|Vǔ)S#P;w.tܙ IDATsbll̾}9s"oVj"QFvZU?y$7o&55ƍ~* 5jP^ ([vv6/_f֭d2j֬ݻR cǎwJ.L6JҾ:m4ݫ*WȁXlsA*+)&NNoV\Izz:~~~;vO>İj*tuu駟]6۷ogƌL6F)\7sssΞ=xuuu%##k~z]d2=ʂ (,,dΜ9od+ uYEll,n kkDդw…̙3C*`ΝJ/ѣo%99kkkB*11Qa_WWW+"2XLMMn-IKK#''Gi\珁: U ^KRRWIK.iӆUV!n[lȑ#*VqX*WxTH$fϞ-(E^ɻ)zUqEի }y#%cpU2Y]"He\|HLLLʼ3!9)/ݻtڕ'*m\_o$=7?SM +'9E"(E>5ޫr]d |||}}/XnJ`"^v%~["==VZ^sgf$gu?y+C͢n+}ou]HR;d̚5KP79p~߼׀r8wm۶dddMɜ9sFpG g4hޥ*F'4iҤ̦V&N’c?V01d>R6lȒ%KuhU@@qZ@@@@@@@@e˖)!רQoJ*oJBf\~fJ"/Y޸m2_?[?<3Ʈď*T*E" ##7ﰪϯdꁯƒd2t4t!{IZs[D_K,|#Za|0)*6'3? 鲱%KJPDŽV߰jsԓO9|TBzn:{o9r"BRwJ >#ۻTKTqRilVq xyyvb+Wн{W9PEbfRp5zE N"t-^s%|2/_gϞQ9MZġ|258LL~>,f嫽 91M[(1uUFQy %ώXW.oC&[V]M]VLy JyR"i۹Qڙv{e)7MZWug@zz";?*qJeuvAINbPp @kz֎\:;`RHR:Jo*n^Q}?Y:7ghQ9m>Ν;˼%00֭[ammsN7bM[§p-q'j߈u-E~ԯ_9Rr忽7y;]Ï- ץ|]&JR3g̘1C}F@@@"==۷o9Oݺu066.q\pzaccC>}O*aGEIV6)ښbv^Lyeڻh-zL*2`͞) w~Xڪz"c<,hdȏ- bYl?deJ:vw/|_cIIΪtkUGC RdJc ^Q?+cc*_fJQ+}HϑilBFؙk$)߿)5+&ϳ$ٻ>v&RQs# |d|lQTcL#ϧMU[F 1~O( ws# ;[h3_uP<#ڙʌ~Ʉ'QQo`KUgjw+O~t<ϒ496&R\SjRH(6|džA#Xט-Xs8.<x"aX;sV*BZ3(Ny4t>z>,ٳŋciiɆ pww}F)rDbT!yw#ڻyK/ 1*i³ۚB%*1>#&Y  u5bXA&E5 8՘3Ady=R,q"0*6:8r~uh[35)X j 4䮖ؙk˲Cy=L6d/vMvL~>~6Ԩuj.y2>g_2M3 )T/$Ь˯c*#V]4D9Nzy2#8HURim%VmH\&d4D*csV́;Q9UO@X6aٴeDEsR~e=&t9}H\LjmɌ~Ha_2{.<Z_Q6+QB1CژxYނ?bSNB|\%rA[9<ϒ &chhHɏwcLǺƄ&7 1T&+OBjHaN!Ǣ["lF]~ls@EJEaee%(E^ V#Iqf:&=Lr636 >7t4EdI]G=zH#J*A9E/;1?4\l:[hq&T[[C?8.RR;s-Z;29v2gp7ǩnj+fgyDs$YyRQ^q\GK̒q`:Z"jkZ Q( 'c8a\wjiɩ,9G*hkn3 %2>kVNQİ|2CۘQky=[ K:C]1CD{'?U[kD"zN}g8Wԥ\I!v8Tz]> # Q> {]4)Giv.GidH}G}@XRז[j1p0ijv _͚S/+YjѬZQNRf%9y2`ϥ\Qe<obkEjojJ܏Ρ\9}5$K3QB2O,F>YEyϨLgCN ڄ[=S(5ͪž07=e˸c6TyԭoO|O<.A$I[Boƍ|[)FׯOo%K}QT/_1WW[YxG# 9rޞ?$o"(G>).gr'*<5+Q(2sԦqA0uxR@F7iu}G"Gn)iR݀%ۻ$gy<#r .gr|dnI=45D,\}Z0iWRM-4,2+tP:`flVZ}es^sp{H~dE5!s2AfnGp'*ss~kϑɠzy2S5 8A@X6OS zHiJ^IES, Bj!Yx HihF 0snq._M;]t4Ejo13Ԥ6J˖ףl# 厺-ݪE\<"Y)K 3WJv~֜Nl<[MTb.tΜ K2xy.xJXOVXjU?Flf sR*?n&bg+3NJgd6Zʊ" \)E@>;[c LkKȀ6E3X>ϋdi%3Kq#&bMll>w`T;)A'@n^* NuWͅǞoV#I"x'՞B) ZɠVfdIZLtfkgC 2.?n;nVr gKH˖ucݪ+UH'*^{e%ʽP"SQ3@֋j06=>0D*CC,zqmD,!Ut]WTdɋm_F܎!1]u\ZqDa1e/E;DK%R$*nrd24K`_s2ح4<ͥ Lr`܋ یדfүi9V /4Nʊ=u[Tmf5:"gY 9IRD \sX!ִRb^TP2آ;[oy?0q|Z!iҷ)5$)*%Umt+a!Ox|>b剖CK(WǸ1sSu{D@@ͿJ&hk0,\kkk~7ڶWH֓'rLKȑ BƍT3Fy! _F-QJKm4FUТ!s(g 9v+:o7K}6PsA&/.ؘjr {eafA7lM5p5BGSDRFy䒙+rXkң >Q@ozXajQj}*̈́$ҌXh"-mRJil]2 17¨ty*2fѡ1cmI :ʸC&Xhү)e)!946L (&MSBK$Ry|:>{*m,k˿eѴlupOu[CJù2EqBٔir{.UGW犺\ {8 %2KS{#FpzuW~~*wՍڷ3mpvmFPԨAjPF&`tTӦ2[[rcG98pC>,S>m8٨9ױ#91FYY\0UrՕGs~}.}>5kUƍS);#R_a2]Ȋ|rpt$^t-˗sC֮47K\x?wS͚q}(@fD~]pё#5j|<"oy"HhOb///5jsQ*#--ŋӬY3,,,ܜm۲eRJqqqL6 ggg֖uҿnrx!'Nvژ ;!s IDATvdƍ+o:u8ӧjiӦ{m'7o2fj֬!zzz/_ǚ5kTH$vA.]E[[###ׯς HKSSh`8{7-Wj6fl|6Lqc +Ӡ> 5hPE_1!yD%֎cp<ښkG<[aiIdb>=fg JdK*#:̍4ilM5˚ӷ̅ f=yMc+ѻ 7qWfèoZM-LgIX8|_FFhj)HeN?@F,G^=STϘ9{bIH+thxv\QS͊6:J-s>onF5[7ƒB 3X7"s۰W[1|5$ѹ<̎Y}lTLn-JϚc8{6J;cyia.y鱤E\!d7~b.9o]?Xw"6F,<-kh" MX6<{Y';>~Mc= ogR8Ueψ{^ȉtKfd{s:֑o,-Ln~6HR N&vTC``6? nt=gKKnUqK<ڵZeVG tysC֮Hd GxH_'3}S_6Jv=u9~}ŋߦ;tAC_Ed f߻̝;j4(77}bch":/i&uϣG:T9{6,弌qԘ4  Yxҵktt[rg|/]nݢ::޹UV=z%lmd؍)Sp]^=z{{%yd$t:kԱE˽KT~"Up]yy.&uTv^TRy^,>Ŀ CC [زKKrwKzgC$[`eIݺ/v_Y=+R%mv5QSsv6Zu] ?g\]舽rfB6&E?uKӦPXٳԩS=[[lql Q9hbU'7>ZL-ʾ_?MedQм9z*b^aO)=F̣Mػ7jğ;[20tp@-MH +2khw/bLi׎8__*|) 'NPW/ TQYCoQSo_,5"d2knk֠c!ԭwH27K Ϳ 0T/ jN̑5(DXLNl,9UhORhiiCpp0ƍѣGj>ݻ+#\rCCC*V… f׮] 4TIIǏk׮ahXCr:tR:u{Ϟ=9B[I{.hhhE۶m,U9"h֬666*GޥƎ˖-[ߛ4i©S06{dddP__~P;v r9ED0f̘͛K_ e*nY[% sʓ>X{۟$?Y!#zGS/vof d2)`'hX˨][\Y_wҧO;u&&(l,CKKDNݻ9̜Ã9HTH&ZټwD8P]vѸqcڵ U Z<7oDںLk+kk#yMMM(~Qd?Q*[Sʕ-r!tFKq$DbDbrI$#+,$o.e4r)W⻮RЕqdrCb2HC+b޸1)7n`Sr"+Wd~ I$J(DExmҟnfCÇJ=8qB0"ذadק_~Hٿmgg'yL&}nݺ5k֠rvd Z̍QQe;KP2@|R0hN%]͛oLVVHv5{1oiLzz)7Y>9sTTdO"=of S#FDW_9ذa_}͑˗cxҒ17lsvj9q۷Wz]5QU~+!N}{y Kzl߮rMHHSF,T/_&**J͛+liFa{OT5;-k41v,uWCCCTPP@XX?^ˋða'Ì_⫅ϻ._{#f2UԴlrap rXLZ)/F \V i^GƘ4Ʉ HN. \ɗr|qX6r44+>6iΕ+!o ܺUX-k}sCfݜ<@.Yr||2IL,gCu._LDDDW56y,,ȿw\j{p*Ipkz,+=IINZEC倫kr97׭wj?c[(>];; Z$bRJ)+*< U70@\QR}gjmKNt4YYW)AjД”ee<.LMGyZ:1[J8<@-ssˉwew/qqܿojkIVv" /-8+r倫+iv^^D"#Dn\yx7׭$;7wҺo?f9ں^bDڅ MSSܸz5e$II/P=BmY^őxe\_nݤ{0A * a2u,uOtڴi4δf(**"""BMRBMM*dffbggڵ兏Ř)Uٛ!UkU]x>!SYYGUhs6l[n+81V AC0k4}ZoFqe՞A xx{kNiXv48!JFFjW377];m:ua(&Ő[XXXw16!ggM&MRGfۗ^{}֑مsjTef]۔3%ҧ~c,dXdIRHLLaiiPZ-7oظϱ?…7l ;:Z,s}jtXScmͽ*9[ϛ]??XYqeb,w81t(\]stEQZ͝a뇯f͢4<1>vX;wF?ϘAqVMGB^89bD kOO<=Ձ=o'j͚J@:t옔8+XiGN-W-vZ͙ ;tCL{Pj7?$U^b;f *r?f͈7nDQ#꣮{j4q߰/`t2"=$>}%o u?o<<<$&&J޽07^٠&Μ9CϞ=LLUp>#VW ԤR^ǎ Pa=zlI{ĉ 37oW_6mR7jhlګge9a;QnDKM:tq3ȵ/efR>Ovڑ!BmCwߡR}ѿ&O`i׮>>>ժ׼ ܸcooܻwO޽{={'èQ})))xyy!ѥKoرR?uuuLLL8p EEE5/r㲏SNe͚5 -%СCL:+VTUQqJɩlXV\̕/6mkтo\w}*Z޽ɽs۷ceE'ЯU_nl,'<=H=+purtrq!zrssˍ"NNZԫW]\aq;wrsg|97ysk+8 ,ֶtRܘ>}z7oNDDk׮e4iMMM444066f_C5W^͘1c077gϞ=,Z 6`aa'|իWqww6ܹs cڴi8;;֭[co\tZ"SMri<==B&*:׮]s zAxx?֓>1k,ڵk1ѢE fΜɅ M8p}1vXA]]7|Ctttį@ P{9si IDAT~3f;ScÆ ,Z?W^yy(+,$nT45ik ޑ#õ￧?bbf jx4jْG8_30ruMZϟO`o9B͛Y:gNN;EV|x&_NY>)g`] Ϩ( Kݠys*t\Q ^fdrooo w^L肆a˖-L0jdgg3n8JKKٱcG5B#$$/k-re˖q16m RSSquuU؆䔘 9Gu*m6mcާiC~ooD3tWrcc9ܭݷlGT*U?̹};2<>5]]䥥챲b谯ysFGѸ1v_d@⫯uoˌ ~سAݺkn[CfDʊ6o 2E#@Uap SOyCk$N{?Gaggb ^p={6޽[uGHL4?s&]n OL$xT*ٖaв%vv}=~KĉXK5kPп:jՁdP.2550efF7%)oM.]ؿ?q#GRai:z49qqMJAR'fB$ ~~@L^R 3O(/%8"?n:ٹs'={lh/(L6 SSS6mڄfC¢Ѹ1-؎zFh[Zmf ]\jc;v,cR7 ff{(WRFƔRi'IIqy|lؾv-Ǐ |*,X˂d߸AqXYak8͜T$h@ ӧqFFŖ-[Z H||<Çu֬YFF-[bܹ37;//¿”ee<$%0;wH>ubTՑd)f eWеE#77^MVF_NΔPܿѣGYsuhj"%7?K_oR@ j@x@78p G_'""KJ|2SNgĉ -ο w!hdgܹ͢D.[F@~efgoO?^ oDUS&a8:: ={{['qY%^^l]CC6l{yCd.ɨ7jDO>A ^h`ASw.&&8Q@ jDG@ pEFqشiڕ*^U9ugfٲe߿ywvftz>W^av  }hW훓ڵkYb}!p.ZhgpHЊ֞X{z:1؎SL@ xz@ &}%))E<ܹ?_~EF^ rrrX|9svY0"a͛6l;w&44E~zy/_\NJ^^۶m眪"SDwޞҾmےy > yraEU\\5|R{@ 0]v… رiӦQ\\" 1gԨQ?Dٱc۷oԩS>Sj38R%,XUP ҥMrsg SR8;~+3=^ٷtΌԸwv"~n;k'%YlM[F-[*; CC<_ȑ4 yFȋr%bc1ڕ  #W5¬g4guPPOD.[ ݒ%TU SR$'cҵ+'(wO?UjMccAAA BQ#B?Djzwadu!?13^^؏kaat۲-33 AMWWafĈ'm-SBOO={FΝS%00///ϟ<Һuk ɉԧ:gVVQٳl޼'''kh$cHԔѲ[8GPP?YUKKz@!*4oN_~!3<4% Lycm.M1+qt_Wy3Ɲ;kk++'aZ~1(LO's8rcG8@n\'<== Z"xlJ eMH@UW>}`5d#Ye2ToolF@ESS)ɡΝge۶%OH9{mrUosv6m۹Sﴚ7&ijYϞJSZP@Ē%b0rs舮-'VӠ6yRΞٔ)jiaܥ ={xRD|5>MrgO2w'$`TTxUd**EccCcWWkuGД)өQȻ{Wssg|6'$ϑ=iڔnn>c#ݥ5hbڭZhejJ֍kzWԔ'OilQEB;1ݛfӧea :66r^KO>ft3K j_NL ve#?yYcߎ0@QUUeٲe?wޜxؿ?>?Cmhq^H022z*cxb5k" NN*Na5d4jْ3߁>z`ȑǓCr2285r$ͦNeht4o͹ɓtn3-!/)Aё𯾢(#=;;K!]NK?dܹS20q"C""DD;ҨeK9BYa!wI)+*";%gRΞ%lQQhڕ+_yW Jxo@K""(LOg۶h ~،Q͋*/Ӹm[䩆\NvLR\h켼:rp9x__ʊHΪigq͹0kvחkO CE(~-Cs'W/'%0;;r}ڼ;uƆWl21[`%mgFDijJ@>sv + ]{)gPEExx =w``` g$>^tqD gԩSٲe ƍcƍ - a~vA׮]ZbbbС7--ŋӢE n߾M``S5TFOOJ9<&&&^vvvұ۷qvh[Y,jC]h[ZbA77|Q~D<,DٙG#SUˋLrn_#9ʊ)͕)gp?? ]\q#'e`P*1[54e >>t\J2%$pI(#/)A^VLsej*43W7ѲzpO7'Oq:Cprn&p$_RtXK_V;9a=djzzұ62w.MG˺uR: SS23ر_۱cI ጗Fٙӧ@m(JOqhc7~|Uk+Wrg.Jrs*KH@ի\ Qqǎ@yXmssXm j^7hb/qvx<.]zHt놶OM[i\a n6eR To &hQ5QSGԔg8VqD gHf dȑ5?*?/yyy{k. *#\ر#M4=4]Ν;(dgqf! AB%DGcR/cၶ%cJ! VVA-[ް x ]Ӛ5Ⱦqöm)+.&7. /++#kʊp4::?.ڸ}{*.8~E{DFF~_ ^izU&'٣zdrdPzI ȈAAA;;ȩQhd f{+-%=wNҶ8N\ˊ)DPzejJNL O2&]D3;;q ,@MW5]],$1ƏwsϏ;v-ssDFrt@]ʊ;wj\Gдi8NDmې+ΉEHՑ@]B۔kk[?9 t|(ǕWFj#=4x+]?uŊZaeߗLF޽;wj1'x~IMMeرi&* +Phժ666RZZƪ>͛aHNAʹv nӾ]JDDDׅZ U)-(30lFmےxAj`.^$䥥ge};2'Oftz"o6r(O?nh(c3bɧN_KKɍ#ܹr]>x3::*$5lۖ˗y r9q;wO|mcmNx?̃WOH}J~=%]HڥK4߿\-[(,*TϨ(I"?)cFNJFFܾ>¬Yhjb޻7c;wq~L:9@zlE0kZffX@y0s&Pݝ[奐z'pˋ tiBBl ׇY^^)SȻ{M~[;rCT45zdq~]rbb8о=*؎mdGɊ"d\)DUGNk2oQQW'l:j cc̫x NQz:&QDV$#41AICȑd\+ި۷d\[oFEM .]pdzrkOOI1O?cֳ#R J Wǎ0%yxxȽILL$44޽{annr ^Rl„ Z @ x0m4~GLLLZ eڴi̞=|jj ʤZTdm4DPbb" E/^\@ xdU$)WSm0j(s $N}updgK۶qC|6dEEIm&M¯E XYql@Y奥ϙ ~ZuRs[hK9;~<{'h422 Ȩ+) ^$TsvsH21᠛oȑ챴d_].?Gzd~-ZpyBRuӧW[KĒ%R#G8ңGա ~~Y*1*/էy_ IDATusIOŋR{IN>_{{|61KJ=\\cm^#3"B<{v5Ϟ-G,YBe$pJa@ D||<ÇUV]MM͆ %%Qd͚5XZZ'$ٳ-wEK29Ů?ޜdzYᘛ_f̸g=#SUEښcм9۷s'{lt,,0tqAԠ J p9 CCZ| EYQ'$`TTxUd**z|zB̌FmeE@ö+:֨Ҥ5/!U]]AՐ! }ccFPBqf݋h[Zbֻ7F]sjilTՐ͍ᅬ#OHCo U--u#P0֥^ uY|hjbֳ'ZffZ]dK4MLб~5qvhݤ JT^Ls&N(ҤI'6ǩS6#25Gfs\nc)ȑ,Z,^7x]33s)cDSʼntKZZ{nlC߾ efr\ wbƌ8+`ɒ{N|=Μɑ=\֚&M4Ga#ӧDZ|9yR۸q13S#!w;2}zwիÇK" 7\,` gdt\ g߬RҸ];2\ 3"MSSޞ&H3ٷn߬t)Δ2N3gKYQil\_|t0x m:r$P0iԲ%{Ҥ!}_;;NKQШ5ʾyggn;i.L[8d\Lm/+*"qjWUm[{֯>RO05Êtq2Ɲ;KFd]ƾ'l+֢9Kv:*46g}:(JO?ӲRxCҁrgn04$3<-33tq Zh*5Ր!XzxШeKcoA@ ܹ˗n::vIIIaxyyD "4DWqww]/OBiO7arO_ Zbݷ2fn*YwP:=z)9aBy?[[ :uС,~ۘ{3Y U T51R777/7t˕+Ҙ**2 ykkun.2ڶfbt:uwo&cT]f|mބ#Grj}Kf͚2DKEMQ\ߢ8;ǎkn5#ht./\H~$75]]o"nv% fUm&T'˹OAh[ZbA77  K~|0dKi9gw%@I^jzzKIn.zzUUkNeIΝ=rZ[h4nqN;֠fy3dp$^^@d|SDoHΝÐ4Uۜ_>2bL 00~'^'''æM(,,@SS[[[tvz}||Eג? Exs6j@]]F~~',]j_&ݥ<$>ZxȰTNrr1Κ,[fg%ЩUTXܚ1c=4n絑{$'ca.YXsƣpdݱÁǒ%8;kvh_>U2ڶLN.穅:II/TchhX️CINPE4fSH"tu)Wx*Wtu)ر_۱cI ጗FYKI~>FwmJ.^dHDϙå>Gʙ3\yCnoɡCJE FPoԈײ%+Btt(ɑCGMO9!m'MRnlZHM:еȌ䮟q9׬QQ?}GG SRNeaYQQ\^V)uNlG'HciU|h֘H9AFh(!!\Yu>4MM񌊢' :OefV:J9;~oߦcÙ1c:uf9z(}o'O~d'''SXXME~Mܻw\aӇ? #::333lB޽>s] -ҿwj*m̌"ʢC\yŋ-QO}||kӤ:6)XxF?17d,]ukFqy 5F}}]s-BVgԩ8qZ%K^Ɠ ,ʓb'$Hm2$ɾu yY2o@ES\Finny ]wr"8 k /cvgر\5Kjkܾ=F@yɍQ0@yyI YQQؠ׬Y7n`ѿ?P^aҥKsG~b"F%Oa6TEECCJ,̜P~ ZI$;auť\^kh#Tx (wNtukz0tq!%(H9/Js֥PDFRJ= 9Cr9H ޶ =0euu~\"CQRSCeMMn }:97o@/{7v@PM GRAYQ !Ņdۻv,bcڲxӹbu<I̶m@swkL=WV\,w3?בULvEZx]c|١ܻGz@ѿ*/Tڐ>eR)eFEqt̾k KYi)St.mnc~ ("n|P]_8}q{.Jss %رzk NelfY.Rmi)s y11ۣcgհa\38WJJq%K"-,TحF&ʏK )+)AxHA+7n@WW(`ll\600u bܹ(WZ_[ս{Xv-R;駟bWiT*姟~LJ;w`ffȑ#={6u[̙3ݻ+nJRRL41c(İc6oL^^ _~5fϒ5'Ofɒ%dgg?i;wm۶\Q:Sի?:tƍݻ-x)aB ѭKM2{v׬/̙@f))ŏ?6 c\sP?剫Ӟ$QPsg<(_`F//FBz>f={%uun9sY=xmT};!Kru i}G~\+huY >y\rkWJJ03z…?,mm};*(x+WRVTvӦ77TttdM]}lJlpv'O`F;-?ژ5\c33ѰŬYcA"A Ci`kݺ@I]^wsˋCmڠÃ[I^ɨc5dm>ޯI}[/p} .ﯺlԦ1ڿP^sdJh5mJ1zPoZ} J1I(LI!a^Ir؝;ݹS~r@z<1Je>>>$''s5z1Qxx{{3/dA^M?3ӧOgݺu̜9#Ǐg7ʕ+9r$??-"t֍R̙[lƍ<<<ػw/| ۷'55oqΝ-vww+WrJ͛G|2ׯgw\|ӧOKRRRۗ :W _5s%77*+f}ܼy10`>}9###}]KqFrssիWXUxzz2vX͛DxIȠÃI }͌)* (,,$++b޽ W|,YU ^դӜ)" OH 555O`` Glܸ;vos]"""[VVL&#$$qrr"88Xḽglm HII FFF 1GNNU[fozuAm۶=RbӦMlڴ eee_{o/^O'R5)rڵZw[^ qN~-[郉9&# sSNtzP---Gosm>̙3gswwH\\Y9@[?ܾ}%%%b*ߟ\V*T. &99b}TӦM ,--QUmϋ[O?sNZVZs/;|7")RDQzx4v H =sssz->s222Y,Xov133ѣuT*L+1eeel߾ֺ!?SRRO; ^zUIR.]ʥKػw/ ۬_oooyBCC144lA[ ϕ ݻpL&4k֌v"}a*ܬ7ՖúÆ #//-[1 HMmѪU+vV//)((`İgy ofΜ9tMMMBCCYbIsQOO|}}iOgf>gpv}|}6 ia!>FF2+̡6mښCxO۴)[o -,ܔ)tvȈn>FF ML(˫7ֺu‚{#-(͂&L`{NkOƍݡ{,-9J;8=ʊ/^,Zߎ^JuRXŋ҅=; 8ѫ5i‰^Ⱥva[U=- ]W?ȱ O9" qL<<0֍ +#-*u >ݻc5t(&&DDpv8 \\o m;;:mڄ5YY\6mpx=؁E R(=]XYQ<=qCl'L E󞞴Ã-[87e C]CL;>=== '1sDAx,\~^JPPW^eΜ9 aȐ!p 駟۸*ܣ) IDAT7|CRR ..3i$u֭cݔpv={6{CCC:u*/^Ɔcǎq6m*/f 3S=JYYG_~pjV… k͍ lذasjo3~͙3}yی=Ν;n:1c:tАhV^QwuuuBSS j3Ǟ7jjyDGѫWFFAXX|y\Qc11 $C缽34M0n\˗ - v ',axMͬY uNSx-k֤bea:W,:y|Q>akצbo=ktonl(AioӦZee54>>>ؾ>u믴ZˁQ75ŬgOmӨcG>5nd^RT~()!QQyDRgDvL Ǎ;uB5CCU=ۼ9ڡnl%zzDD(d g*xTUOcI ]^^ 3.\@ZX̙<{6IQV\LnT%O*3gRIփg+ǒzY!![1 ϞAAx3qD&NX֬X]t3 xzzQU] !C0dȐj:vHǎl{ 0@իɩkwNkff*o{k2mJDGՎ뢬5&p70={$**hXX`ޯMxcd`ZĠM:=yQQՒ5 򢍗W˛T)ǼO {P]E[%uunFmhт \\PHY'41sDAL&cƍ>| +[$$$sa<<<8{,51~$ b,-Uٺ,}7c H,_ބ˛^HcHǎTKK&rr&6Uo_]uxNd̤IdfJ>[d'))Ik9iiL:i`dT&@vj9KJפsr|s:͛)/ҙ{&t+! 1D`x: Sa]DBq:Skl~r2oI+0R1xu($JJd߸͛oV?Ľ7h<PR'3yZ)>FF pr!QR"72%uu6KIjA۶@ʼy_ *;'1sDANqq1sߟ]vHň#033ٳ|ƍ!))x߿Onn.W^N[VII)ѡ*AGG%ۛl SSIط=m]nj|uX6uY 1;t\[7)&q~RǤS'b~ ("n#U3cPN޽RJТEhY[עB !t*c#"L!QRblf&-Z`ڵ+D3D|-VCzVp0ws1j^D(r&4̰z Dw_AAܻwӧchhF8v&M׷Q"I$BBBAII SSSzaթC0n(=;!!03ϗҬ:;w>Z;đZ2K4ɩ{];Mƌ1}04M$ 92o^qKpwc$zKQQ~Hxx!J> ̝kĉl~WW-n:ӹMpsӢiӆ bgԩ&n} ۷))$ @BeOՕӦ77Tttm|jEqVy2pvg$&rߟuI]}bc}{i4k@%Ir"":>W QbtbbcPƠ>+W knT~FQf&]D^ж$/KdT26}paZZu#J?2nnyB9;;&*\3%K0у6lO?s wwݻɹy4Qh7Ѩa9I,!99k׮ѻw/_BX‹ۻA;T /WbӍ2e ={d(=OSFF#-**???&MĴiխhFF:thPgrr2ϼ_A QTquu‚ѣG#  ywdƌsjRϯ֤  DrDAٳg={6˗/gذas֭[|3qDAG #  <|||Xz57oS}BH #AA[?;>>>D 3Ettt;WFa[gMGq[A$22y1~xlllc޼y\bO5*Zؗ_ѷo3gggW/Æ!-,H~\&r9Ԧ Y[szPCCn̯rٙ66\[Yu)3GAAxPVVvc\3E O?SV>f&o>~"JQRQ ̄aaAƍxx048$uoJgsn6al ^9#f ܸsoffflݺU$FL-ZpWrHUݻk#.Tƙ3y @IO?M&K`,IF&{xmw2~OӦ!XXsry[Gܣep۸QS?(SD~_ܴi5+"ԩq=Jjj] ff8}{oinh5m)c$AA nbԨQߟkעjOpHxxxȓ"fBS,xީH_ӧs ,@__mgsr9|>>Y m> .iS5O͚5Vdg;;5=sזV44qb,:iގe˚0zt99Ry\k΍9y2Zp=%UU6n4,,P75ܨ(tH?{GE rOAH \ ,`޼yN g޼yL0A$E1x<9rt|֭,[SSU1Ô}QwM00Pܼ|89rׯ7vHόH<}"uv\MMªUVZUsOO=Xk7n>Qӧ[xn˖f Pk; iezvFNaǎCe  LЭ[7:vȶmPUˠbԩSqssgA9$f ]t#GxbfΜ<3E]{dž PWywAAH T߿{[2lذ cӦM")" /F^O5nW١C8s3ܑ#G8}i9r䉴$a g˖-|9rO\~S憯/SNAAx#ZnMJJJl޼eeef̘IPQGw8::ҳg'j[LMM۷cm6|X 9s&l޼+WtRZjnbȐ!رܹsݛCr!FAzz:̞=SSSo.3,, PWW'66yںu+3g MMMTUUI =???GKKөS'0`_|3fgx{{3a,X{8x`̙3 >4iBJJ ݻkd2SLѣɢEXrcǎ1n8TUU155%""cDzcTUU9~8sݻKǎ144$55DBzz:;v?2g7oQQQrц}3СCݻ76lPHܾ jK'55b;Ar"sΑȍ78qGiAAK.e۶msyXdIsy8y$vvvSRRSpuuа8{Abb"?/1{l̙Cll,aaa̘1Cs%.]DHH 4oޜzݻ7'OSs1LƩSׯ_XK@@111?^pLMMIII!""kגc>n8';vݻwqFr%166&77ׯWMM=z XhWƍʷ~[kpuu놇_K|2SNeC^hU$F_YY5v( +E^VB[[>}`ooOPPPז JIJJ'6qpp}RZZٳgDܹsQVVŋcuiee… 埿;s-vڅD"aҥkf͚Eaԩ+Z8yݝӧOSVVF||<,^X #%%Θ !33S~E hjjj*C;Cii)8ql֯_/hrwwgȑnlllh޼:utBBBM!Hɇ(o۶|RSSi IDAT׮B(<App0'OՕѣG̭[8ufff mW%dÆ dgg+\ӴiS"CCC$5EGGc``@fͪ[r"]XY.JJJZcaaرcپ}<%mL0ݻw3nܸ B ٱc;v져Gء #]|rbccYr%L8:xP'''8}4jjjtTUU***H$jU\ @˖-iҤ 'O'ԩX߾}5U 0x~wYz5... 3I{M***Iwww.\@\\ׯ_'Gu ɧ9s搐 PSSc֬Y 4LVgJJJXhwQweΝ߿2?>:tC*fdffңGp޽:lܸQa˅ A 5p@lll5kiiiyfN8iU,]ڹs|,ӧ;ww򇲬,re ³ψ#ԩS7vHZȰaIF6n8MAh"9J;vӬY3IxJJJX|9FFFb``S?OGG^{2;RDCCoqI%""1c(\pB{=ƌge2C**jڵ+Y899>ӦM/THGCC%̙3%K0ydk+Ʋrceeelܸsssۛ?uu&OBjj*{ܜC!+?^$EAA 舎cǖBjj*:::ى8ᙻy&C aʔ) L=WbbbګK`` ׯ'11Yf[o=џgZ'+vz ^NN@;r֤b==]?XXX0zh_/;w?Mfff fffUbDxzn0j!eV:vTc122n,--|m BC?ѣG K^|W$%%=  />K֯_OJJ njooOLL3nwyb*M6m0A3gc #G|f gn 9\"#%2ZWS //Ti 4*P;y|/{'6yyylڴx$͚5{MA^_5֭رc6v8 Ⱥu{.fIŋ-Yؒ6mn4v(EOOޝŦMw꿡+`Fƍ9t)D|1WxXƅ \=A.=6Z`TۗN}'?/-(`ݺkq##Lv/^LM?yiQoGܑO&ruR8;vp]6mt̯rcJ ?_|D,ys: ~}ςff,Y^4oϢEسgYYY [V6`v#9"d2 .& ADRDژt!Td7s{7EwT%닺1R[QFwafƝ=V[[vu%Q 9ڹ3)ǏW'yvqᰋ w 0͛sɉ*kx22h>}:ZZh5mY %;FQб#,kaH:t+Wo %55xp]4p )LKkP{}`ڭZMb?e Uo_۳Ɔ{~\YQOg8 u^}8px&vNvu倣#Q[u]\شxZZPP_D^ؑ˗H?u;wgϞXYYJFF/^lADff&G&qa;:U$E-[ƤIDRcl/K~~3ff S:w.3('G*#8t9?\lڪdrZ+5gdKYwsV[SWR\,#"qoBJJ;>ؼRe .ƍB֮M]?gTΜqR?G lُ|]oo?8w޼~G6ukR!+$ MMm/7* ]''Ϟ%kтܨ(kjS>>4m#qxO0eܽrA.Fx8aА>c_ke--~ʊQ71a?fsee6nۼycɑW*FߟVZ!֐ax ϟXuF?hjuYF!O=zT$E^Q1x>={Rg=T==eiB[[uJ廗uMd JJIJ*AWW=ko\#)~$!;[Jdd**&U5?%7Έy{{u15U@AŋTkؾ=ڡie k}=l_(?d`TQĠMrvFzGtHtPӣ{&ܹ"QQ~D_()q?9ɨ6xـD"AVRBNDe%%m[andffan^~7@eKH,ĤkWTUqpW/y&&ckZ66hݹ)S0h%55?Ha $JJ޺EINVVj=c33i:fL^? BEPDae >4?1c8Ԫ| @Vp0wbڭ}rýe$9y߾ x*+"h"\I](-(@U[sH:ty>LAVoqq@׫=rQRQwo}u}%>n،DY)&Ҧ2|)JHy̭W&;+&$&O5*:{ш# ###ϟnpWÇc_ ԭ^mg%ѩSJYc[o F1~| ))%̚eʪUVdgKΜɣDFwqQU?3.l";;fc٣(-jYifO wQsEDDP\AE``&>5/{9˽8;g)QЫם˝6nSm!66vi N2W4 U?K45Q5YVyn.&]XZ]Tr/TT(Ծ G'06^ ~Ĉ&ޞKp><—^j/_VOj̈́.8) eu57nж[Q{%2u->xf+QVUF"sj.| _|ݗ/ _ѵF7&wro P*ɉϏ̝;iLO>}riv8q"7`0^QVƑq?=R4,-+4&@rtEeXea!Օ5K1c(rcSRӔ)xΟ4~핥S??q yy^Rm B4zQ3lI6vAVsN&Oʕ+:thkWA&< O.MKc, BC3:++{+%Ahx{iN5k(W3Ƥ9ҒfMX5ȨQ&tǼyJΞ@OOW_kWQmKC툉ipm}}}7TGBQs0ۚ|s阛Q&zd$WMб};y^;M5Ç79'@c0v,"?>6mN'ߑH8_ll ek۽;}׭9FuU'D`ll(@YU$c77/\;wRxjeUUMNp /Cue%G'Oˋ|NNLHP/tYCCsr8BHL}}࠺9ZZt;sR|"}a0}`޷/}֮O">Y櫯"&&Fŋ|j矫'ȼy1c}W\iD̙Ì3X~}˗STTg}FHHA-%%Yf1{l~Ӽ[_bͤ駟̞=_ի4ic9|0Æ #,,I&ψpۢEklY{O7]C-df|3)A__ ~gxnUȑΟ/Gmm)J%\˅ uӓRZZڵ>6M2%>>D3r65EgԴ)!C_Hmrm&JRR,*"y*k;7ociY3A$P3$}4&Oۑdܺuw}RYygLΝ;ٳ'pBN{ǭ[wv 4,d2}~~~*Rɂ Owޭ]%5fС|ᇄ&ZA'JY<ܹVU|yN9?Y@6g3|O?\άY2dH? >Tmy[-)0<##pPAx2.aмI];\ɰseggq!|||bȑ r qI>c";;xU ::2&NHAA2|pN8Au.zRF&MD6mьnuWV'88(ն6mڐ̆ Xr%vԔڮׯ_'!!ÖSLQ;qwwW՞J}Y;7Ψ <>6l%%%߿#PLݳg|GL>;vDFAAx YAP닱1{kkk뇇jikk'''S]]CbYY444$abgjBZnݺѮ];$ vLOONwG5o IDATۻ&''.u)'MFFAAAdɒVT*bΜ9@=AF&ѿكH`` {A*һwotuuZZZh7oެm6*4̢^7Y|9GW^}T?5477W{ȑ#I$/`VȂ bɌ;VEAAhUbX  0{nȾ}ػwj?r76YÇV}%33q ]uf^bݪ|iiiHR!;jKѾ}{~w6R>}&#O}ŋ[50T*[n̛7{X(#  :sDz5k7nPBHOOJ-8ɓ ׏ Μ9CBB}j_MMMFͼy(..歷ޢ[n <[oQQQAvXt)YYYt LoAHHjCq444㏙4it֍;vu{ڇ_~UV3ϰ`<<<ȑ#5 __w=) fΜѣG7 V7(… AQ2 LADpDǏ&QQQ|]5Ah TTT#;a… :_xEEE!H[ՋDAfll\o ܜW2w\prro5eee1tP:wʕ+eݠ.aaa"("4D"SSSnݺɓ'9uc ( )S0eʔ)ёkq̐!Cxي4̟?T=P3T׮]AAAx;v#FƴiELMM AHRJekWCAAAxBmܸ~&~P"("< yyyTUUabbByy9ڶvA A'вeرc8"LE%9~8hiiakk[oxAAxDpDA JH=C9A>LEgmm  A'DEE'O&;;#G`llǸ  # (((Ғm۶Ӣo8p`CAq%# KMMeȐ! 4%K J[컃"~--V  “>] N:E߾}9s&˖-kHee%?3,[oÇ9b8p)?YOj<ҷneC^KTˋvv_P  ucǢT(TOBô YwpmF>3$6wR|R+*8:y2'2ZZyxAAx(J,XE8t}y\RE Uoѣ50Vaclj\^YzL+s17?w6YO..?^JOI? SXmիrٶ %&bfY,AAɪT9\$8OBySпg??Ox{'P\\d[&LH.DNyt&5i/'OovrEF܂9Nڥ jN͚l̙ʎ %m޷{ޞC#GruUZҲelf+*8lvrbw}ˋ-.. ڽ=zޞm>>dء~uvkYz  c==EyF?<$.]Jdǎӣ= ;hET&^= 8lBY]͞@zyq/U銲2b%ӓM5eJSp4 mnۖcǢgk{كqNXLΝjym ږ-T;O˽Gѣѵ&;v Ey9.ӧelYGEv1*^yCW/. oD?<1KTlqsS^p [HZlb[(MKS]_%{~:[7P%}Uv]]'c/#9eKyDpDAUUU}v=.^Azk,m[ VgTJK BH9~~m8vrDB$*ʙUB&gx曖̟J[ ?6{s'˕$%UPTP۾vm>ii\Efff϶sjM35/:q|9Y>}YΑ,iN7WJL-ms!&ƃ;oqca6kRMM-]ڬ.^C-ilL;~Eհ+Sz ML7|&(.hߩkTTmԇ/U''SU\wqvVWTKc%&W|3f8i[2$&۩*.F^PQp:RRpze2euMŘv˗rTJyNڶ+WD*e޽ѱpQ141!CuL9x19(+W;=<(|m۰1Nn߸AYz:Ɲ;7Y^qr2ΔefhTWTpڵfY_w/QQj9јxy'еiz7!(.338p<$S__]DĉT5Acq _Ljiw/I|Cƍ >uD.._dg+O:?QEG{x)7˚  <JJJ:t(999߿ UHǎYr%VA֠A lD~dfVTiptU{{m+]q5c{nŋwAJ%\.'=)}svF /!J(,TpbUu7}})FF2^x[tGGm17Xv_m>}})F6a暇+++~ۮ B"K>oaҥ @ͰhiLWΝ)NIQcJ+`} m:tgji:DUII4 $R)6C"RX$R)7oRTC%R)ŗ.QYT FjqF{vb7z4h~H ϝSkcoO{{caAy^[3Lq )|YUײ 2200gzu >uz4I$Zmssd::؍ݬsbڭzΜ~mUDPw/VcOYz:E. $?aͯo#ۦSoK/adyUhiCqr2W֭C"76AAh#H߶M-]42B¢^*rs?y7@Mp0~ F"ydlNwrBiHߺDa=x0:VVjCHY~3oDbyp3Wn+AAheckk^7@C." k׮߿+,sΜ)c3j[$k^PUռttjkjJ(+3ebt9֯/hVǛ2r ]4sUi LI%{sSYywpMogSS UP{ytvMCCM{ܭ"/:cmssHT ңӪ!TWִnl96u*86\ܣGdπLHhv{MNΜ]O>Qj^YUU@HLdT(Tz"խӥ 6nlv]E^P^44+4!11 :tǹbpgJ=44ǧQ0tuQUV'4k6m*-ŲT=u4ڴiV~-c;?P>O޾^{)Ejj[]cy7eǻftnׅmHi}i4   b,Xr֭[… qtt_G-_QAx )0mZfYСFXYiskiIP(^ڵd{"" x4ƌ11➴$YfMbb9}$2j ]1o^:rg=ӓW9UhyuX;23+~ߥK=LGJehKFڃZEn.(蘛SGFb & jiy\:ϝKŋ{Yl ä6ޝQ]YI uԖ?Z.gPt42==.}7ID}vmXYѽvM89k 6 ebLWҫW1l^9 wrvPnعjMeU |S 1r/?y h41Xɉd5=.^D^_ZzCIc׻J/ssUU Tk-z66+n޺ggejiMRzr ."V/!󨉞# JπXh}F)o{nQ-Xe3}9/v_rPm˖BUASSLU9lݪ>!#%?_R RJׯ(^X]==)լ]_urs!!ʪ_:g_թS&訚CzdZχuv<5<ߟ+WرJ-üwo4 *-EaM6nswX͐ 6J_YUO en]6_zq6F~&*-g3d'OmJʢ"[ɐ};M͚6?d5XϫoH7P*rsڟrsp~bnĠ(+Emތ;wgbTV{7u.rW{'ejLV/4|L[U*.&믱 R1oL+ggs5"gm}]ZZRmssLv%q2++I<+ ^h?r$9RՐgڦMPYTDUjBrA8ZXF@AVYX~=1^.?_ w;',_Cll0s+s1¤5o)|y63g駶M9yӧ_TAZ\$%bJ^~9JeZyLxobh(_?RӦOUt/j6̘/WŔ)fUsnlll]<}ӦcO=֍AggMv ݶ-֬>oo Θѳ:m+̙idD9s0pqO?q/PQY\V//oP93g&]U-Swx]NLN=h`ޫM25ߺu~'^{ 6t]}FL gϦ-ڴoO!,u^ry6:xJ^~@0.dw5K P[^@"qlT22258PFKXYȤKllȬ])w,8Oh}Ҵ4";vTmZ_Woړjk9ąb޷/=,Qۧz`nj"? i*] ʕ@'c N&ѳ׏?id^wΆrб838s(˱<YԎI߶:`ܩ ukFyH ##3gUkKZv-&Lhj |o6lܸQEAAA,??aƪUԼjwE6mDnamA_vC; PX 'Z  lz a)nY7L[|C~wGR;CpK,ּ~rq*~+qv ::H%Rdu~mO&|Ղ4# wbDžHzL)̌T**Y=*RYMfדi}p<Ȉ#H×0ghk0o*KGR ^H%R2n^'(#[zU rRd悿eĪ#deh悮.4+8֙MWD"RQIRNxZ{KR.&ѧ&6F6_Oѫ1kc!l;I-8i8]lj:WA1FAPAA B.w^(--eٲegD`D" <<<())֠ܒs.G֫1 ?uk={STˤDz_U]boȒg! LyQT~8zOuw,STQg;~d'K`AxP"8"  -->}멮n0(UǎAý"Q8AAx‰  4S`` ь1>WW֮  DpDAiܸq,Y.]vUAA$VAf6mLϡÁMgxL)?YOjjjB½  2~;>h5yǎϯE,e!l!ÃZ-l׎CFm05eP05c}EPpzl:wf-(ov?@;83gsz{"O?M-V&#   O3nZ3UU~l KܹΝ>pm=r>5[HY]}αi85k~#Fۛ3g+@p0/=髯WWNϞβ^y-lvtܺ~]V!C77Ui'Oˋvv %H\H6۳; ҫWүo??*նo%!<չ3)k֨ҲvfW~lc{׮\;yާfW߾ c3AcMBe%MvfGlg;v#s׮zukdJRSV//rRwdx6;;͍˗7yJ^"/W_E^̘/cNڏōӧ1@Es5T>{ycaۿŭ)nVyc޻7z8iW7lhI˖ۛvvP5Q]QW_e;V-oc^3{6۳w UiW֖_~a[\\H{ ΜaI˗s5"BVܺ4}/ i2vkWn:y=yɁ=(z t֭@XX .l7L0:-AAAl۶;uƏ?`֭֭vq)((Pmݻ7+VzO?K/믿hVի<9jBBb_f>]\01P(rfժf]Q\3g[Hxxk>WMp0փcԱ#nfw+iC=Jky毿9t'򕤦z ܖw/I|Cƍ >uDtcOO9ra/bƉ^S_Ӯ]~2CN_ֶ{wp?^KRSd wfxj*ݗ/GCWWmʛ7)NNFYU`] nrfwpKI$|)G֔W\id~qB#ΝKի!T;[x-X4pjdկ!11G#(.Nu _xC77ϝico9еF̌wߥPJ2w2 zaøɵM}z1Nn+C ms&+NN͍ÇI7 ]])NNV^46_Dx"U5+Sz MLĭ5ص'/(ШQ8Os))82Mv?}gO`hb"V`CP\n3gb7fw-rx!\f OTinKql c5_t俇rGkAA9cƌQ8}4O?'A~~>ܹs|'ܹ/;VoRɒ%KHLLdiw;{,.\`]߬Z .pi|!33={6u ;O>A"5Y^խ[hiCѣGE p4uLI0pqA"b3t(R--JҀW͛ " @k/}V \]3Lq )|Y_Y]MCD&CѱY^ue ё駶>>.ww0MPDSEYYebRg50K;KJ.|%WR 7^#"kڏ1u·SKX @ߘI54T@…'% ԞLzn˭t$=2JUU(H\ _|A_`DH׭tthc~+VRINt4GƏLJ~~dI[??dzz郼OӶ{wU~ljLGP#0~|{hQYZWh(@MMfM"q2U`ʚݘ1\±S)i<)JLT@WPP!V!ZB5Ön$jCޚ̕p_$$LsDAx UUU?wwwh߾=QQQj8q777tBPP[1L2OӶm[6oެ*#..Ν;СC155%$$Bz]Ec0f|}}2e ;v:rpp ~I?~Yj+̙3SSSLMM9s }ҒÇ닕G2!00~t9֭Ǐ]P(`ڴ4fͲ 44{S=]KKB#BԮ&kؓمp[NMkΜhiI ٻ(oһ MTDbXbZcK [5X##KlWT iRs?N1&z{ۙ=ٙܺ՘˗ٸ ׮ oDLL|}--| ť«e V,ii!/.*y~F<)-ccyIIғ_%vOޭ@̌672suƌy:;F{ 0.]B:v2BYZ-KKg̠Gp0=y ݓ:T []QV Sn ,-ɍrJ }772oJ㍤̌ȋH7luѢ^sJ 񡮫+ ?ۓu珥ݓ:!Lĉ\)MaQՕ=.#0ǏsH/wkOmGp0QU'/SWrFdOi#7x}D B=dffҵkWLBgի^^^dddƙ3gؼy3ǏϏk׮Ozz:wٓFBjj*/_fڵK.hkkӽ{wK(,T);;F>4tܹ|njÏ?(?ѣ_Sd Xn]C\2uT>}Jrr2YYY71cƐPƍ5kiii$''ckk˘1cΛ7ggg҈%''G<MlYu몱v5&5(.v$$~鱱88aŀ.esfr9hjʐAG"#gg-jՒSb4hŭ[Ã7nTx?CMuuIS/L^XLaE<ڿh >%j&iLLMK4D?q(y1ZTKP*KNOPQAMGqT*mI:WÇsoɼUd=cMQv4DMW1ͳ8xxT H >{mLLT{!)qͺtidξG<<б  B]Oޜ0ZVV pcGpsrZ }e4`6o氫+-'bbs{{lW(Srbb ?ڬYۗ+V`Ҷm3>kk#ůΑCMpޞ7p}FrbbmcC#8šƍIFEU6;w42C..mѢBe@NΣGᇸ}$8zV8Ӈ[VLgba*RY{xoKKi禖)6iBO?ҨQļ[7?\Y:4y2'td\mJNҨQ쳱L>43GSյa`@]a9ڢOR?4:6jġƍjf-Mk= =STz)'>>Pڷ_?Ϗ#GfPζmÇb 4{IÆ ݝI&{nt颰;w$&& <==SNtޝK}lܸ=ň&&&Yn ,ߟ>}IݺuQՋ^zallg}Fll,666ܻw/Rn]iiideesO*|||ٳ'FFF|5jNj d\wyCCC444VX*YQQQAEEE2l0<==ٷoOfĈ4nܘ .PeAA+D J9;;szE*kii1p@HQQ]taggg~GvZ 55Q~0tlllX*> IDATS-C<Ǐg۶mJ;?ڵk9񕮀WU6{ym7nܠ o߾MAAB;'O_~/XAADA;v,r{li{LL OJGDܺnISFEڵ;vB8+ (fhܸ1l۶0x`א!CxFWQQ!<sё:HQoAA!:GAWښrLMMquuGGGr ...XZZҼysHOOgҥ@iÇ9M4ʊK,hhh_bnn!8::"ٻw/?~Ǐ3be1(.\4v5ߺ~z)l*}';v ---TTTz*#FI&ꊕ[ݖɓiРeԤ򍌌hذ!...tԉӧӺujE) |x9Z^W^Y W\\ZZZǏckk^?V# TikN _umvvvDGGcY.b{իallLƍӦMcȐ!ܸqLh޼B OOO޽˵kx!4nSSJ[oѷo_nݺEmllpqqItt4Jܹ3J:W^VQ tJ'e+(ӯ_?RRRHLL$//KKK455|2ꊽ=cǎUٳꢩ)((]v Snݺō7x!4ikkJ#o/2~x"##߿A6m2eҎُ?d6mȑ#Hod۶mcɒ%}:tuHXv-V=// "##dZ ___t… ٳ' 6sѧO222|2>iӦx{{cnn^e[ϝ;ǬY8q5bԩ̛78YlŅK.QXXҥKy$$$Ю];i۶-fŋ߿_%Ҍ j 9BΝWʚ5k d;uTVZ͛tܙ4jժ%u`fƍꜾ>Gёӯ_?\ "!!O?___~jSSNall̝;wꮽhiiqmLMMٳg͛7gΝxxxTy=?uTNZ鴚CҨQ#?~̝;wܹ3͚5^zU~?U,Woq0nۋkb58+N.uKOҨ sssn N7nя5jT!Ν;g*[=:,Y%KI6m8p ͚5`ԨQ54͛={6&&& ?擓)))6h ݻM*ͳ`Zj˗9q4O>@޳n:?^e}?#Q V\\p߿?3f̨ebKLLUuмysZh{ӥåڡtʍ7Xh߯Б|MLFDD 6T㒔ȑ#9{&8:|M3TRuYkOCC옪畝kkktuu}꾟*;'HH^ώB=bm:BQI6exnkΆugqoRZ焟j"?LEƨ>9" -Z݌?srAx#cRSS+M/C/B6l@ )~#%66>P\.gݺu̚5Ki}k֬a„ R܏ҥKܼy\&r\z"ӧ),,DCC)ENx+\+\Vq0|C%""Bp9~'6m֭[;vdddLZɑR_Mvv6/_ڵk #zzzWڵkظ8ݫRRR*W_AII .]^׏דŝ;wسg}|VswwڵkDDD CGGkkk&M$[|ڵ gggիWӹsgcdƍ\x4Yv{baaQaJNvv6R»wF/8::Jw=zD@@@LeNUeM=gggjAUP=₯/pIi:SuOU}'zX`ୄ|e37Lς6[INUujkQgfnM,ܐȸŹ{-zD Fںu+ ,Q5kHʖh,+W* &Bׯرciܸ1VVV:uJ!}ҥ?+++.]*m]6rرc+WЬY3ԩ/۷oW(hi%2%%%JNKK2_oM:uh߾= .b03w\ IZȈݻw3w\ ￯q.XMҢE ֭999dff2zh6mڄ>:t`РA{@_6m0hР KW;;;y6o\aKq%:v-YCK+B$::´!C0w\L:u ,;;;vʜ9sСC%11*lر#-_~~z[mdeeヅK. իNg֬YlْnݺѣݝO>o8+RY{Su 5F&uԡu5&̙39x  wg޽ S~;>S"xv$\j&U~Cq]3la _}:?5~H:Bw{}Tz)'>>PڷԬ=j Teڴi\vMӨխ[e˖1eʔeL>]& Na>IHHFFF/EJJ{,)))xxx(oM|+xUpssQ?GߟI&UH[baaaڵ_aeei޼nʿJPP  SvD{^ܛt_}A}9Uʂ+ȹsqww̌# _Fjj*[ߟ8<==ԩP<.00kkk :P:_~M6XXXHݸqǏ+}5ݻwԔRPP@XXt<]tnݺܿSSS9s cӧڥ9piii899ѣG~(_O֭. jl 233ٸq?#֞&O2jƿcujAl۳yf;FΝٰa}'N… L8~[/??!Cp~gfϞ-pwwСCܾ};v0p@y&III|w|w\| '' sWZEÆ ٺu+]VZ*t ϓ'O(..fȐ!ұ:uVj*>c8tlذWJMWy, V[SU^nݺѫW ?=/M;9vW# 2ׯ_g۶mЩS2g˭[PUUĉt֍Pk\ڵk?>- 2e oڵk8qF:uP())aСL6۷o7|þ}F?;2`:vT>OyKAʞ7o.]… ̝;˗3gid4SLQV1cưl2N<))((য়~| Çgڴi,[ rrrh߾=~!;v*>Fx^wA#GABaxnxgϖԩܾ}OApp0wޕt2Æ bd2͛'- RMf8{l룣CTTΝ899)D߾};?TطliКrttul۶MvRSS~#//O>D Yvm&LPq¿͐{dy= VĮ  # _feelDԪUOZv-#GA4jԈ^z1k,}Jvv6CF^ӧO'33:um6:w,p]+|6䐟 Cv{Ԝ?5S)ۧu+N[\A99" ee#j:JJJ*&&&`N>֭[ٱcoߦN:eK=CG.3j(Ν[a߲<ʔIJJBWW(]ykԨQrmVfȐ!̘1ݻwӿ>Ħ\.GWWW< A2mD\0~D-LA"[ ۉAoEݺuy ye2xzzү_?4iBxx8hkk_zz9TTTpqqˋ lll)A5qM~mc̙3 y\]]9p)-G[[jf۶m梭ӓKWC4C INƫ~; se͖%^x#n'`Gki!yRN㼦QgjӦ3;vD{.$$']Y3;J;cch 3bNKzrq^Sh`R7Az\Au҅kHtt4}Bɓ's9JqH,Xݻw1bᤦ+tݻ '11իW/(7nzzzߟ˗/\֭[<7fϞ=;v'OV9 LJK FϞ=i۶-Ç$%%q}˫|{q-837V{|\\Ϋ?0T0"1"#  ¿ʷ3age.7#%e%(BԜ?5[,Цu+Nc#$ ijmRު҆zxuż)4oNﶄq; ﶚTM;'EA@gϞr }{oLM/3(#G|A&!!!f)))o챤Qo^qk*VAݪZ:5ȹsqww̌i5    AAAAD    isDAA^xWo7m_au jAA֭[\zUa CE]]5q1oʔv￴Rr04N;esz\d)PsDAAx-6lH $&&';Fl ccP7{d4մ]N9JqI+SM%   ۿfXrv_^8vkݨNJޮ/1~|[!u-(e8/5ȇxrPHˈU0a<8@GQ0ҟD̍!bn b(ّD~@'9#n̏#2s{eָ\A#  ?@tt4666TY݄f־#s~Zzp79?4T5¾2yDEMF{5xaZٶMkjkfה y H[!g`ӡ ۿ#ͭ[1뗉8,1bJ;IJ܆д1***$=MsoC]U{#G;q=>FZy1s.F]UU*vĤ'({BSMtr;^J w#  kU\\ԩ^'$e%һq\̛+$cX5Y?MuPQ9`??hwy /*>RAA?ל&89iXZ2֯FUU奕ycRnXAɓ5qW/+MK X˖/J%9-@^03<{""׬ሇ{9ڼ9wzMRpO:c=҈gN@Az7/)so^ss~kӆ)M^\̵ٳgmͯcB=_Ws /)=AAd9&?%EJK 䄷7XXpۛJtu==zGD/TNeTq_U?_qn.6l@$8!ٓ'/x17Wx7#  ԩ&7oqD۶aڡ_C˪gܦ ~nbB%֭,x8M@^r2Ft>~gϒ@ؼyR>@n]EFRo@.H6o&9zo>d?x %/]873zWp! MYQQ]aDmތR}[Vwq0Nc+k..*sYJV]h#ԲBEM*,t4n~[[8îDomٙ (СW swo;3u4AQ͚`4tmcCѣIv (o45 :uh0u*iiwcǢmaI{ywfڡƭ[S1cjۓHq^&Q.ӧ!J Ȋ0+ 1c8iiiU(70~<2uun5ws . 9ۧliv(ʪ~:^AZt/FsˠGYï֖ۓ+ǡ&MN琋 :;ˑaO CiS 22Y%{~Ӗ]!?W/O":GAA7[󀜜&N|ĉƴlYO9~}}U:M)ՔZ*?_:9_x)mѢxZMj5S']} ܹӧO?0ڵe$'1veKi,[@PPCغ>j]숈h\ryiɘ1!,gO4lHJ (Φod$6CpcrAS7x0#"jZ Щ__J3Mt={Sx|B T{~KQP]BZhcت͛djEzX}*vYNKJ*t>bмymβ}o7m:TY&3&{:qe MI>Ũji]Ə'eu۸Gi(+9"/*Bޞ~JAz::dݻ@ֽ{99qI FIʊBA\HΝK=YQQ:8eur$?)T(MH ߰9ϜIQv60lNutVܲ;xPie}}QU~wRH}Kӧcڲ哠JuԦ]I癖ƥiz5>+Vu+i>fHVDDϻmҨjAxRP!B&I#kjbӣiϓ~2%yyDoެVq*==I e: 10=\ XAAxjU 64࣏IK+fժs<6{ {d``$~p#oR^g'wV UDG4XȎ7i&f%3f$`f5u3 kP(^: 5 JnܘY(2ttd堭]mߟ4]]9ԼZnƍiҮ7')̶woNZU&،jGްF]~2V(k??SR0-[LJd|}1ozj IDATۖlǽ1XoMTJƪ,,$76%5ũ8ش)a|ۜ9S{cfF6w.?99y:VmJ8DXo/M) ~nr8rwkk^[[;8H^KP''bwؑ}|8?~<^K`ܴ)P:c|Ύ/mbӣMF?ԙ9Dq^Fy>/mp];[sQ)@4ŬuktK.LTi.Pۥbt'uqOr|( ;E 9o఻;G;vJW:NNL vv,[ƹѣS//ZJJxWPwGΈeN41F޽{vEbb"!!!t ++ڏ-[1bij 7r uVOܞKZZ^NS={$&&booUblB~ZZsGE'M37Q-PYQVEYYZZvzWY+#zʽC0izzXuo<.k   <&N4gc:Wuu{z'$YwCxJ|VgŊgݍ%1FAA%?ú%8t {Tه̿ۋ![Z}ϯi%OL_NA#GAAg&::WRPP@i׮{ha/KM]gV'wSk:pm7cQV{a5G+Q0;u6ADpDAAx&rss9w]vƆΜ9C߾}u36y?7M]. ~DYO[;R:Z $qAN,JB0ߏ܋?rS)Q Ki5  3&d25jÇun$]Z޻ [h1 oAqa9I=P|* JGkzӄxZ±%D/şgt cf`Pؙ4by*h4۸ҴJɬb?׌vK1{<&`.O-rO8qؔdlk:1dc/LLլJqۆX椑[CaItΐ-͈w~3a3bY6rԄxw߾GqJv,-|X' (OU͈VUo|'ν}[?Whs aJ|~lT6n0 -={&m@FƹSսUcmtoa3b _. .#+GgQ|&wή}ވ9G'6N#_KGAAgB.@PP[n%>>gݭ &ƛ#lF3{.ؖȔgc|7fjwՎ<$:-M&֩ci@16`\+coHB,7QThBap3\L^Q˒bGEV۾'\IH^Q w[Z.o&%'x][1 S{Y+ʣQ ޮ a'I*UJ).)~:DoW֝[IvA6Soqnz5 10=(#!F  τ5RPP]tASS|E_f"l - mi$KJvL,Jsn|/?%Y/T`n`Aഋؙ4bua*T )%8}h նk5m807%Mǩ2#^Jz ˴[c[zE.[`+;[/lLQIv>,Oz/bl-5e!kۙz^;Jet=:;wX]#A9d{VڵDBBBܹVVuW(OӖ-[1bij 7r4yܞKZZ^^ MLLooW(u,0N.䠿GCWSz!++all\8u*OOO8pV#  cyTbe1[.nS’B<$%Q=x}_ަ Byb̢   j]Ǥ#ԣSwx᝿=Jgg3y(t 7)P  f okOGSSo^AHLAAۋ![üLLgȞ%dO7FΨc΂3Zu,vla𷽞J]raSK+DpDAA*gJ4dOcGu\:.WWja/9'km0\=6\&-m.< 3d>,?f\$g'=ti5  P6O}jJk[_۵?1uG56mBc&kmOoʳjܙv/ O9"  x{d&`.O-rO8q;eǚq;g=wMa\d]}UN+㣃F(>kۆJeO-"{6 /9Yu)by*h4۸۩he6dܶa8Ϸ~EBf<! p_`UЗ يB <*zTke %D" Km{WPPR]οspVZ\ ,z+)R٤#j؊۳R}.#HVe~~{;~fIJl*4D$83ǵTfW]LL+-̭Z5jK،X^mZ2'@G ND==y l=9kDJ+a3bxJ،XfĢ_v=kj3 (OɾrH"YT^!נUyG(L/?\L'޼̹s,0`ݰ-͈E. `EfGұb5۳S@ios"gj]cP6W}5'œJKAj'#  s̝tʍޡ/a3X9[<xp4:.}ySXLFQIRnP,Ͷ%ABf<;LCKC\f/T1cԜdVY [Pcz ƵP}, nܞȲ:Sou- -^l>KC+~ ꏁzZziItٱ5QTxy3s,]M]|E*zNMk1mMbDZo](UJ&m6`Y>XZ)dDq\ZC-u2NcBi߯OUcnyovW۾Fftfi3#N |G}6OEش C[C:@Pt99DDp?۽6S;GAO9GAANjN Im-l<01ʔ*%rӰ6YJ|5p-˨ā9dՐ֫d є~0D&۽F C+2WY0/o)*)"0VI:j)"(Oz4ZjAK#kDIKCckN^#WTY~(|O.ˠHYTkueMRgFna.%bK|{9)R0 Țr]uc/8boD!t&JEb=7E̺Edo&=ԑLIZ*{KN"-7Esied]ϥ O  2ݚfWw"%FPX`n`Li̼ zdH2w|ީM C+Q,FSIZn**Nma3j=R٧fQXRWg}:- -)V(] d'aidU>U)zZgR,%'IGppIZdS@KCe s.&Mp+?ݡ?!+#kjer>ԅZ )9; úwrM~ Gd2 ~ZxT{\&GEknhIJΓ椠R)0RYV:.AxBAA璑™a|5p-W]I<{'G$H|F,AсL&ðȓ8[`g҈ DYš3+퓐bGEV>ӻΐ-ddsU<L-]'0![tx7lS(*)P>ixNWG&1{4sOjN 칺܂4pl<x25ȆsAnaY!@0/+#QTlVF6JAmjD&+!ϭý0HIAR~?{amlCym~y"ZD&11ɢ)VRW~ Eb  kH[EG K "VXadDAqsttլF5m'yZ 41sW3y(äo6ʡh Ɲo_΂3m8l0oibzgtiRo {'8;Flzz&]z GdgP.0y]Vx[sOy S:n+[c[ώv>4װRd-WӳًL=SQd>JG.3d cq=Cb=m{1[#tZ9Uj_A قg ДkrHkl[^L8/Χ?ovyN{M:?Vd>ʠ#3{~=P1dNyfl|u'#iC6P6{'E|<FPw޽{vEbb"!!!t ++ڏ-[1bij 7r ϺOEZZs{.iiixyyվ#_NOYbb"7-̥c䣥#𯐕UN066~I:' 8PLAA:{|watA1FAA("~@S+WV {$ < "8"  u4Dƴ! \@@vAxx8>>>ܭ?ٳgV5_* ,`ʔ)OիW3lذw޽Ν;?n 9t( 8x0YwE2th}D?$?y*#׬i$ՍEcnܠժU̘7u !7>^ѽ{8M@`=1O֭( 87f #GMڄ|TוwE ocƠ*.@b0ۿ*U]k#GG.AmSޝêKV^fZ## U^jj*Ϻ j~Ȝ9璚Z\cJxBi<1cb?>N*?v,-11 Gܽ[vBqN66Wy-KAJj3dH4(P(-5*C:v -\ƏK[XY]eDi?~*.&xdػ7~=Ip+|aww >έUߵvw#.Lq8ѧG۵#lJ#KӦkU*\+f^b"fZav!$;*:bٙoMI<쵳#8ONNDm(My:^x}qm킍NeWj*ͦO~[,9J۷&p0aaiעE ?Pӓaa4:FC7,aahKf\ ;wIwZ911䧤TkTͨkdH4 0&B>;:MCC,_xJudҠ_?2(K ǧھ֦-P:rဣ#=<17ǪKH t޷ޗ.AUj(yp2T7FRU ݿOѣ8Sm20qs+=(ɻ}3ʂݽKn\b 6>*d}sjԟ~  ڴ) zjn[7v-2Ip2ѣw0}\!)ήXun~%i z;cǸ?.ӧSÝ͛ %YB:YdED=0~4OESPP~Jn6lgϞU+ߴi+W^ӯ_?|}}1bw﮶m۶_H& ӦYbaQcӦ!ݻ'OzDF>1`ohP萜\ҥK177xWcmzDS//{xiҢVhaӽ{G7ԦA߾h"[؋x?@Tt^03ݻq0AfCFh(ʢ"~rrd8L Fߒn(HO03[˗s~xؚc ɓZŰahPcğK/a1&pa}TJ%-f@LCC{{"#yĉȵhZͨi8`ڦU^&#FPQQmߎLCC^2Uq1q۷Źc5]ԟ\a5^KرDXA믿VeP`P`hoWϤbn蘛mj1߂L.ڵXt):T#sca.D俅3f dffKddT~ ~ѣٸq#~~~5 BcǪyQTT Xn ,~П>} 6`ee%>\ ˗Yn={Ą>}pquFIIߛo̙3i߾=ݺuc̙FDGGÙ3gٙ7xwIlٶmmӦMDDDT{ޛ6mbѸ0j(:vɓ'զ1ѣGd2/"/^cǎI̛7aÆaeeŀu[&lmzXjaaa@ii|Wܹs(矣Yc,Y^xQFvAo:t!ݺЭ1G>$EX[?_yJz&r9$%aogLSz##/I0FCC&=J_NÙ0a+݁Ir)ɓ듟Zc#׬)IPE>};;iYV~$8!*Lȍ(+4OFrL[$#4gy@af&5mZckj1 }}P*.^$Ķ姦[@ʪj RS13CVE$/;5qw0׬mȵ4087.]즌J::Y6ggK#zꢦk]Xm޼6m`kk7?s"¿ÙoB!%}Wlw04 r jJ~&4o~_o'ks=3f$`aqWiS:ypkܸψ1(غ&My5s7.?;;J33?UcSl֌F_6]]9ԼZ mޜ2cOcsJG7SA8(ob֪Fȵ0o ]]$/SooOI˫LJd|}1oۖ'?eeEMmfGEqj6mJ'6gn|''WJ$jݭSpxya˛oVyjd2~-1[pFiCJT؝'?9=kk^[[΍ Niy37rɉ<<˭2d iipr"vo,xx&{mm0q"9쵵%vϏ^[[n"xڒ}va B~}2e61cG4}zm/\G}سgq}]L7׮]ٯP디G4:֭17/0iEY\y̛ǻt᧦MI{Fkzڵkйs'k?R[laDY(ƆAB`ڴiCdd$Nm0iQxZ(͗o1|.^HBBUa׮]1[2}tk׮]cǒ!l8q"ȒsenWڮ˚5k3f 6mbĉI5FAXXٙ~_JϪU# @KK ]]]Xd ӧO'++ 뼊͛7ٴiK,ȑ#ݛo3fzF1uT}9B߾}Q_}1d2P(߿9رΞ=K*..ҥK,^#GpΝJ#r8.--=4~0֦ e^zzrY&N͍sdIJed^F fttù1c*W˾F\KiЧ`kTɣGrsk#eeeվS5_rrN鉵5#GAw+?WZKKN:1sL>|HJ R\\̼y hjjpBڵk'FmۢT*ٷo-;;҈oooKaevAII z<" nݺЭ[7~WN:UHUfXx1FFF<5iٲ%X#RYYҵSf͚vvvҡC>cq455i۶-~)׹o 8t!J%woDx6MLp}*ˊ^TFjvgggYb4iR0SPBCCy?p t;w[f9r$Ǐ'>> V^]i:ٳi۶-ݺu^ݻ,\Ç-׵kWM/:uך#_~퍡!?Ju> S1k,>| ;v ,, A~~~[7xC>C6ݨiӦ 6 WWWJJJX~=xTXR.T*=F`loXҩS# GUy"%/@VDʒ7mOيEr\Yve舑SWoAqFQnKKK&NHzԶCmZ/9eK1rHΝ;100G(;>}ݻK&N%*$Q{p)ۛ*7nMXv- ;w믿Nz)5PDp-bժU`J+4k֌7|KKKiT13g$33Z43}vZT*M6R]]]SW^yE-Hh"6mʏ?ѣGqrrٻ{3-!%{ VPjG(E5jZ5QIJ,+!BEdI"}sJ$WKz}ZߟZjU{M 5-~[0::šalhܸtsDG9GA[DΑgپ};SNGGGf͚EnTsub.߿E:jwWH BO[zUކXwh?onͰo3S|DAAe̘1XYY5j=K.U466~j'9*+,䐋 PFѣ(d2T,<ֳ'{}\ n͘i k/3?;G`% Ž2I&‹p֫\8s$ $|~tX5Xe[& 5IK}"8"  TI"Ю];yyy$%%w^|||puu'>>jR--lGugBqFssFMN]O~IY {_vw{T4\˥ o>22ӶiiOceh`+߂ 9GAA|e~\DOC.^nnQUQ(jM9XN&̙ >k֬a˖-XYY1uTMVc/~ vz SSB[U>NN89q*|9M/wܢn o|l܈q˖2WPY3JrrP3O֟AKm㇏0t5u{#5pl6z7{'xv Ųbfȱios–L*-K~]L.cX <@$1x}WcoKIl棕.&2qr 0SeY\!gqt ,nToܬ\Rk)%8_`LLtl_8PPLWЭtpvwbw&a[HNn|>>c1!aL'0U%_u6j@Lzm~:t9dMoۄ&A(5J5Ə9>xog5_[![ڥ Yח0r:4@V}˹ۧ`ߕ"߿  Q\\LAAA劊(,,\AA5WيYL\j,փvNNNmr$//ދbj_XX{dУ:zzz ♉ JkkhhXchD֚jTVUMMM444x w؆D"A*ScN@Qv #W}<]3J%%蘘inmLy>pYARmmbׯG^RB-]͍W2gNgMHT +<~Xq;.|Asy8Ʒ+e-#16#[K 9w;Lh;e?v@d4?2 ʜ#*5''77f o()+VfLY-Y'٭%bA ͪ?_87'Bֶ7%eŘrp6Cojeτ_7q$zom@G#[&a^_.̍oMdE\N>υJ$fϒsfM]ne07xMk{9gV+#goC9{g&p:K/d:6ʗqs<%e%<,#{|yl=mub#d􏃹2?C]Cͣ@< IDAT9s?bLkA_%,,=z{]N>Uk'ڵk0v JtihhF匌j|[neRi;A:ZZZsJ;P=I[[[BGG]qUZEڵ TuL 0k,N:U鸡!xyy1sL_0+߹S1rs}{='AN"7626nM̚5Xzy7"c=zi.ϙu~Xy?9aWU>Ul8Ƶ) l&Irv"?^€Mhlʚ7r(r;93fM_WvS2s1v DKC~X~C?M/~LuĹ~IGAwwwT=z}'N( gggfϞMnԚܽK ޕ q[3Rmm4*mMM*f%Lơƍ)#%8Si2~Nl0ͪɍIĬx M-=p1wSF2өg#c(9Լ{Lh;E?qyi=1k 15dq,̛ gV3۳s(tz܏wkk(iJ(*}<,9R40Ryc2`ݜd}xHAI>M,cjܗV ک}̩[q>(er[A_[_{0Muk+˚X\!~~5XZ}_"# 6Yv-QQQd2,--СCyդr.66["HXdIo ё'p5~'Νޝ;wXpaoCBB8~8gƌjߋיZ3ABCC}6*e]>II5|JVx8BBT16wh(O{qq͛I88pOw AilDxTu/naxo_lT"eYUL.o85V6S}sr/w-zB8G6x-fD{]4*f\LC{z^${G &*-Ʋf?C}?Lc94$ͭ[`XeID"EAY.:ȞH[UT IYzl1{Qf睹}R˗Xݾ8WK:6uaJwyMgYL͐K.RHR130G*bgʃ's n[Kn5 """pqq!00MҦM={6QQQ'$${_o߾JYr%_|*r9g/d111\,ǝʕ++… /پ}m ({\z3fc##Gn_G!J~bbI"䥥(`@L CESH ɩ>^'Y{x=VB2Jo6G Ar2Ȇz-'bA"cF6[3Ts؏*&%*:  (ӮyW㾄&r K v~c2wrh!A*W;),-$6#nU'y7am?LNNk_- -ʷV>O=[Xr] 7qF5K)y@>mbgGQXZHIY 'oFr|Ík}p_"U?XZr^ZcH 6|sj%e^?@j^ l;Ч]?ߋzm 9"kaXYYq%Um6h׮?c*ֈ?Gl۶osx)222T3iuDDDеkWa h_}Uͅ^!A(ܸqRIn(B:vToe͠A$ ^޽ծ{,r7zU:wuA5b0Dַ~f~?V[M+h'a*'=/:c1&_JaiyEDϔ3.bmԀՃ71!(P0h))+!!+bR |w$k};`މu,R)ҟ/ap\{v_bihWj-ٲgԎ鱦5VunI:6*e1}튦Tuö ,1:っ3'z3V3 iݒm^g|ൔ;Gp'' }3ƷRC弻 Ʒ?2kKM6jRYc龦V4S#X%E>}(CJJ tss ‹o! iӆz\cY\=#Fsۗ[nѨ9B޽ٹs'cǎ%99sssƌD"!==\Α#СCIo.]DVg̘1DEE)Ξ=7obddDtt6ׯ_g޼y?~---ZhUhYdYY};v %%<<<ػw/fff̬YΎ+V{nzɓ6l_͛7a|j%^oaaaMΑ5!EKIIQћ[zÄSj.+ r/Ȣyj[ 3];1--SN鉅jAx=jՊGj*-{ 3f ={‚~ʲO?[߿cǾҥKRe`itԉZjqI~w6lHϞ=[nnڴ7~z8y$>>>_>_~Ek333ڵ+\~ŋn:VX,mڴʕ+l۶+W½{V\ܹs9s&7n`ݺuB ?UֶѢYD!A 𿸕=jp$*P@Pާ “DpDׯ+++&NHDDu[nUVɨQؾ}{oƍǶmؽ{7t_øq!0`@r_}ϴmlق1;v 22www @Fh޼9sRyGtԉFѾ}{>ceݺucܹigggƎ˜9sT^KKKlBfܹ3?yyX|r>O cڵܹU t4uxE֪y+~f 1N ]Ri6Sho)$sDׂ 99GrY8?!!!t8_|;vXVZoWG c۶m3l]9dOαcضm#GDm&ׯ @WWW|ˋөS'J޽UxyydRRR(..*f###ر#)qwwWYVpM>|HBB+WTC.M]A^o̷on̰?A# Vlll0a[lƍԩSGe)Ȯ](,,СC 6aÆtR ooo>cΝ;—<СCYj7r)))pmWǎiݺ5ĉ8::j*ѣ2~Ν;a޼yX[[3qD˗3e7o΂ XbV5_eΐ'tkFF~~~]/Axx{ߤA${77gѻ͗8wo} ¿MsA?yYYXPXYw8ާ_ - <<d9CĒ%j==锏UlwjyCҵe˸UK;q_[WD^Mp쳴ז-IܵUI޽Iڳӧ9޻7 tu%T9ܶ-,-}P3385h,-9ҡYag<~SqƆ_v-{  9Y ݻ7"Çv i^JA^ ?C9@\\/^ (_r/^\!LΝ;lo߾=z[ر#{*>T孷"88'O/**"5餴[nݺT:/Jٳ'@'>/njÙ3g8x P>@&)4i҄f͚tR>|RĹ  FCr^PV*cNS'v'OozL;t? Z} ƍW=,OJwq4>.~w " shhGE!&|Be[asmdĀh ơ v\(_(JM%41ڕ66,Oװ!* M6akkK֭9r$޸addħ~ 144ѣGg.ҢG8G|=QDoAƍYfɼ[oooMVVVʭgϞ3cƌwߥud2ƌc֭wޡm۶xzz*sfʤIhѢmڴQG޽cҠA߿=ٹs')))L8:T{  zV\OF \sޡ^+^gܸ|}CBrqsn\Re~K˫V<Wck{ mJj":4blmak{'~8;FY~-tU-Jixx /$C!qa[[sO+d{< rwȑ@u;1}GҞ=NYA8i׎%K*8ٯ_XM1tqt.-.^Ķb<,2/\ɉÇS~j>ǸΟDC6eeڼS+(8=dѫW+?9`ܼ9ơƍ^C..\Ps6Gիߴ~~砽=?5$ݝ_\VgnnjܘV_# Z8vgΜܻw&NȀЪЮ];w^e ? .P\\+7n}Μ9Zhƍ,SJ-.2arr#*Jٱc'O~#''v1w\ڷo5k8v7oޤɓ'3bԩ@`` 'N >> /_Λo j̟?Mr)tuu vڕvٸq#SL!&&;;;Zh! 6TiҤ QQQݻiӦ aӌA-&>aÌt0vlsCd+rڵ#ߗ1dH;w6W/C. J9v SSMbb`g2c,3<<*uˍ3g@t[O?$(ȁү-> Aŋ&96Nѯ_e, Ш]dƹstgZ1;h<{6&0i||h4z46C(oDQVQBgի~\sTg混oڲedgc船ܥ;FڵtFԔ3#Fw̝,#/*pTJ~b"(NHcq4>S &JmՋ*FXn~#77̻u+/׳'#gM}}npJpϬv8 *~KrrHڻ3glI+ L`&t9x7nPeK[?pf,{~߾KKу,Jrr߾khy3WФ\*HáCx O(#jIWl||Яa$bV%8q[@9CQ#rcb(sG??$8OF7Tjc3hUbo GSO[Hܵ 犼f|2LF]Xyyi`,?|>KK _E *(o1~EZZ)sXXhToz933MRHM-qbF*oWԫcf44$Wp&Mİa*O{!Y62zsݺed]7vL2HwNdZ"bR.]B‚'(wz-[bԴ)RIɓԪ_z-["J1 Ϟ47320xbFHUڵ]6Fqv(A _edPY3T tgdS!SJ;v-0ʾqs#E,?]AI,VM=#r9ReKWmU^W筍1ijɺ|;w89K//ǬcGj=G|=]!~^qf}57WC01P__RS0pW(=]33*bc9\%T-Kj9GA?L0f͚1m4_·~%e=>n>>˓s<*eeu͵HM-Ug3L} IDAT3s2:OzxRmP/mtsszw^HTe kbVy7ou#FT:ɸys$u=<ȍ" ~1Αq˖HӤ q[bٳ'.]^Vl0i׎۩DZ#Ru}VE")EiiߋOUE SSW>gfVzownfUO&|:٣\ooOޭ[(偺X::ԶAa\*KKOLѱgNܖ-16f1V+tN'E~)-ZpNyOC6GcjJiB}CCy#$۶}JꚙŊ 8914+YY"0 9::2~xϟq"y pp.iPX؜Wu8t聲Z>RLjm2IKTq݀bz@i+Sҥ|_eiݻܿ^ؚLZ~ 3fPUUcGG_n舎)BBO %7*  ::( fXzyJ˔mAAur8Ј'4.[Ycȑ'0}(CJJ tsg /?oW$A^aaa9.33_{-4WsȢyШQywUKҥV5uMq1E^MεkݲҹK87n}\QwMρ TKoI}oo W*+( iS":Bt ~C\ *ʩS‚#  KI吙)clebW62ɼyU+ͥ_H52CC)r{vL;t@֭T^Y))}kBYA x:AA?EE2fL<;aU'gli/<͹1c0tvTZ%ս/Wn` 'OFV-̻v%Y'#  &z66!'57W*\G,AAХ3^UC_AGAA9rbP__R}#z 9;j #t$Jow|gO+_TmB;,,8ڥN6!",z5,;!/NIJAAAx9׮aԴ)jo5oQI۶4[&y+ ze;v`ѣ AA\_VkbɃhj/+bDžċ A5#  ¿Nڕ@77B'MqٙWL/NAݹT.;}0#ͭnx8P8ޫ"7:Zn+m֌ee\Y'?1`kˡ&M37B|ݻՋ66\>]>[y k1cJR--4tuE<47K//tLM1 Za?sWf'>'3?Ax"8"  +i;4`FiT ꫯOL;,q/ Uuw_d$}.\Vdgsz|}1a {!!r壏]@77 RR%5ew% ѸV{7􋌤t4Ӧ)ˋi4f "#Jnt41j&cb-Zs6ޗ.aֹ{!!{z?Ͻ#Gs}*uU͛G;Ǝ%?!AyAd$YYsqc.ZB.Ww5 ǪO*xt}\̈*gv"4ZFy  װ!蘘^p78G?? 1ptz@ԯmmhթw0prСH449޾}#"зe[X@mIHP.p4I9TJ!ĬKey''&hb3xJ:” SR40m[נ0nт3ev\̡?aN$ B&4/~㍐p_HܵKYײW/t-,0nޜ͚Ude2z:?4nźa۸:?YL:}!$ P"  (DCCRPyn׷fݻFGsuk1EYřՋ"274/`Nն`P&&KQj*&t"J<ʇKKSU+eݼXW=jYZu9ŋ|9R?Au?Kut(+,TOosn8nތ3>Z[ǁ ȍN&hԪZX`=h[oU'Upf5S:D"tPWf4p! 2jSA"8"  > tLMH<DǴcG:W:70.y0i})+ԮT[>ȋxX@]"?yI ^Fܸ8914+յwp' 3gbUTS))~MpTRSԶ }rߟRSIS'eÙՌm=ZUˍk|7Nθ6~"0" 'f  &GFZ4~]L;v1vʇra4::wBQQ=ѫWly(=ӧSV:ΟGd<.L½~Cv*-z0.ϙ=}(CJJ tssW=.?ߟ~UCAa6fk̤yj-Rs)%%F 'Wp- [UsaA^kZR9u4XXX0x`FAAW˨1X!XV#  +5߫ qb    i"8"  Ÿf.wQCX~䣗8瓙AĈQ;|^p)x>rv(omGAAᕉe߾}ܹǏk`7ݤ i+6#H6״tj @'94&:ߧOO7#n#\F"UMKH@fLyѫmh=ާgd WtҥmJ͛;Xd 1kְؘ7o΍/DQVX328;jrqƗ_}Į]cc2CCNJ85h,-9ҡYaanU@&7&W?{ML=|BGIHO?UG^\LؼylԈ\>UAAҥKtڕaÆŋ_mGZڴym&/}Ht|܇1z r"9''/t<&oNiisNpĉ;z͚} u==߿?SRhX~IvzZڲejum2 opk&=֢T@SOOxܹh1 :CǸq(d2ۭJAr2f̠_㓐I۶\:??(_K`޽;G1trz:|BVX]|۷z!cyDpDAAx%077^zhjjJrr2 UMxHLz7Nx`!+~RXV߮_jRWybR(+liȰҲBAsiveqRൽ^՘ƨ!@_[WK\f,e,uM?o@|~t+z+lO}-~V~ֵ#ֶ#:J+l::vrK&[5;ܟΞʅ>;\ϝ$Fa)-WjϒH$hjJhX~pb򜮮mmI_ׯSՕ0+Hh"T,|D"AS_vslA^ZZc_XFԶnF?{7[&Ύ?&Ld|1#P++,$\fFNO8+ e2c={raC~mْ'Ǔg4i9s(5Wp.zzwD*~~ʙ# 4tu={1B-/-%b6P\sD;I{z5ASMhЀNȹvMf?֬]ssL۷WkNaar>w0k|Yfu]07dDž(1b[\0Ea[sHJ*!$$ҞˬY88`ݿ?V}(@gfrqcn}=:qs_Y~>o߮/i'NPø8DFbޭgrhe bb24ҥʺgPEϞ*m'&зڲedgokKܹ͛Çw}Nw#ʗ43~ %7:o^˖qq^HbJQE{M/:1t^$ ,ܭopsfM̠ͩL*Z=S*:5%H9yc'-7SQ(^6k,xz -w嫯Tkblu\+W7YM7!dffM=9{6&sD OLYZaa PUSJ$aj1kWO]##TJ%}}Ĩc*. ڔaܺuTr'PVWSSZU@r~Bٻ~=aF18"LglF_~w=^^Tfgt &RQ_GR﫲 l;LʜZBPW56ƶo_}|ȉ 3s']/O7Ζ GAAh1^^^xyEȨ/Xƒx+?!˶O`xDZ1'E,Г`,>qٱ #}٩KuښڑUUyq[Yd ΓiehlCoP ]靍H>{\H/LFYSo4X5PDFy|EfGV^͇~X!Cؼyy͙'9ŋ뽾&?w/.h}YW)(**PPTThR))! m lƘu+ʪ**sr8xPs*'_K nHݳPyH q<+).&okz\ƌ!Q҂Q)&'sUyz"52Tks__ϟ0&T*lAC''Mk?|Ҥ&.I\*Q)Đq+W&11quEvظoEY]MSGGcxWS#  B|2d2@gIe<>A uO igdg*Π9,1g QPD"aK&Q_1Χ% onփUx YozϫLi/y7{%c+((˧; 4b`/U^2 }]}?)M:)?Oü=/:R{/'N̞L۶040c5O>i`ހ# X2ٳ9I34{wc""ajjiu7$y,;w,5= ufˉg|+XmΉj#gg&כUgʕD&{<=d8D̘AYj*ָՉ1˖q၉+=ׯ _8x1^x[[? oɩLM 8Am>xϛDZIX. 뤦u(]/oV7ux b,cQUPI۶N0"]?N"r lm XBaںu+DEEѯ__or$M6SO5QAWںø/rrrgܦT7wJOOǵSEobkȂA7Yfl{'f؃ 33aooϘ1cIJAAAZVQa "  ТT*<3_g۾-YiSh!w"GAAAhQa)[:_@,AAS~c'uOޞ򴴖u.ӧҽJSR|"a GAAZslrI o=zёz uf?''MHen_2;'K.go\ZLR*9OOvrrlj#jiIND_A]P;8kW7o֤ ξ:;]\ۡI?I;>y2[--^ԤrlDv8:ݝ?UJ…@Q^ΈK࣏bѩm׷ogYXpl$b-NjΝlݚ}]zVzYj*'Of;{[R1cf&q(KMդ?aZ`ၞFR@ݸM#a#ɐ 50#Gh9CyF]6m0nN4u>=_z˛._&Y\ƌъUj`̙mxٺ5…&tJS~ÑT& !GAARU^qVךWy/Y̙Gj`:4T(ʚ"iFEng_}U3>s&AuQ%^];)vBn;i}Yf* KH-[d M/\`ɓ嚲>> >~qqyyqJ"f̠ɌJHeUVW7l`;!:om#P'WS0 ԕ꥙Qx"ڷYlӆC&#<?==EG3,: ??Mz[oU)@bTb"I9jv}w_r T  bpDAAx(E=ΙT*μ m~v4[y{c?`}}<}~gOɏo͊},t kW2Ee%r6c>>::bȾ.]ն-ƴ}iD͟O>q4@$2&nn7nq虛4|lsѷAGWקҔ+ gDK/ի r c=c2F̈́ fv{_AA:QQEFvʔF_r>D뺁潾 99zD'|4B|}ճMT*|=^^MCK^վͭj{rlLr~aϓ}fC_|A xǓqHX@)KsB;w&ח,- CGGtt؂P石?0__Poe)g`gȐa|drO&evt))߮]x%%蚘h;{T Ӻ mLAjh /Pԧ!5%%\c|/n4ߝHd2͵pC'ͺtO1  ( cbVg0"}> /_ڵA;vL2;:q>9q;;u߬[Bwo^]cctq<ÇOL8a@ 4X򢢸s'С&;1knӦ_БH2EjjM޽lF]00&Cꁤ"M"RH$z8L)~@ƺ{w~ Qj /ibOtJ պ˫>IY]MUAzuRK/Or ĵkIٶ7#ZQx$~jդDDPUP@At4oh5k)-%irϜv LJ[QVUQCyz`KoQviz]2336+B}|БH(z̰0ME:8pᣏPS]XHzhzVJEbcsM=<зƁz.-]JT{77X]ѣ~=d?Nvx8ΣFi*˹cIiӳ' oߎJ q[ť7j}m߾Z#JUU7ŭ\D.յ:O&WhH[|9{10" 1sDAA׹ה&%gל9ׄ5k81}:6zw4WƁHh;yreյ+~~H:9r%oOO$2AAx.i7w. DY]e.t]/e}U1tpv 7D͛GpǎHi=n7~E ~nH\3srϰC"iҋ{q1ttgbkgΤu튮 ˖a)97%52 } N߻9Nv9(S'ml4'R##va"-!]nx-/%O'((HuVӉ_ٷt\ԦMxꩧZ: AouKq_<ϒVTTC,q{HV#  <0 cǎEyy9ƍèl)))Ύ޽{c``Ђ F|ơ+5ƭɌIq/J C^'ՋlbqWyv^g@A톱yZ}^J>ɤ\y'=^o#GAAOOO~w 0:t耛:u}P¿Uy35/P)/6gtCƏ}rJQ(1&ܷ{%W O1f +@׏+$#t#XV#  <0RvaaaQ/-++ڷo>;v(TPTϓFq!#7Ik>ge_&2tU_ھgׇ,=&tJK^dVoYRdagE޼eZ%/lg-\Ie`kPY]u5y^2wc̚AxLOۢT)IλE>rⵝQQ>{K&F|ן>CmozG9~K^MՏWT)*]"w3V鸿o9߅?]? /c>'* /oՏXDW7S"$DRU_+ /5޷& ojo =%rl[VSP#hȌ[6iYO0T뚾>j!n,$%r֞}i~yRk/;z]y M{ |ș{ǯD,)jߗ9rj;O0v: [5wFb  " iժeeeӾ}{ %%%tx,,'WagfnEr5^?!׈|+ ]_G?4&cY6j%T.%.ż.+d0I_ cpwl>v}w_gz<Ǔ]\Sߝz빟8‰b((b7=a\cm|<{.}\/7]eIF0,<7IʽZlslĮY6!> *#k'18vg[̸{k;S{'%j["7moZq<)a fʏc87f=mR~t]DH(,Ta$f  -BP J)**"11WWWpdmζt=9q G_bf 9xeKwLMVYTҋ07mKM[2glo;e*&vY{{m]5 t ~86n&N۬nV9XhoPaL*ڃn>о.t ^7TpDZ5ĵZ[}j큡[;rJ=VbpDAAhRBcǎLƙ#63[::tJ$,4-Pfc_ӶҬ:33a{ icZZay/mNe.V 43hy/ʨ.|^䯄_;'y.e\lV٥Y38C+X)D"̢iN[{ѡVf]צ6Pfk pヨ[oK0:9{}]DZC}%Z*jS 18"  VZQXX)**B*bbb‘ _J=R7fo~?@Rakj|<_+ǯ#Wlr [S;|q*μ>:*E=O4M %B%aQxUԮXfީU*G2'̃7γjHz %"H4]CF @ f@cvgדUIjA w˳+z+Kݵ6+ + m̈Ҫz6fK&JJ47w/#.++Ͼ r/'y{+ $D=~ZeAG6j5Wzi}iaޣRTq(n?W.˳0 Ʈ|Q_ay>H@X պ1Kw jE51;(N'P<ؐUAAxmFez۷chhJ׏'NpY*< -xϫ<{.5dRKF~ɘ5150 ymsUbцU~Ԥ6C!H!GtW4ig 3GVo`)S}3!vϦlļ=/џƭmV{vߧ yrOp˝ڽK7NӬ{׾~O]hehNg.Zk/)*%(g7? x,ZsN}g۾Ht:8I,5xk[ֿeﵯWM;f5Y6pnJzz:QQQ;KkӦMкAddhΰcJh #宮ѣ}{i?v?BlЅ>+4 W._N?׵+ɛ7tHZB֭d=!CL7?X]sO=SsHLՋK IDAT[pT g_..Ё~c>1t|b=W]N=;9> %I"#חc&/!\Y[ %EY'g&b :D^GotKbYҊ;}ym[02PrYYw9~j<0k$~*NܲKϝS }U4b ;U׮w: ̅tꄾ5v<=2_oNΜ(6+(+*={!N1"6G3OĨDT*ʪz/qq61GGc~L)q'Jʮ_49UM &nn\1)WS'WS0lpnzT*eeTdfj]Nݳ}wW_ѡ_w^D<<שSI ahd$QSTJ%3fvdF%$PSZ[Pz:/^dDll3ârsTμ*kt,u+]x}v夅p駉1;w @Č[[3%znșs)KMՔ צM >qGɭr=={q$TstXgdTb"n<É J5SHnΥeȉh\K#  Ciz0Tf7opd׮ykI;tm1zz:̛gϡCŔh3^^CVN hC[wkƀ>bn.E.'3/kkzɓ'k: =XtUyǎ=)NÆkbD_QH JC/hlèjjp8^ZUA)۶>kU׮;=^D~emI\ @e^U\K"fДmLcufx9B'NDfjehLܐ>ki!!ZyTJ%G"#bJQ\ex̞D& ㏣gi`zU})@1 $oތT^SVD͛)MJBTEMY&&\߽SPSRU_C}X6={N~T5%%::(=G݁{{-3bjd,c,c[[*uҐ8oPëL Bbۯ_ƁXwU6c&g"zmڄcP^ՕQQ 4)'}=$G8xPS1(]ccwHq# Q=.J ӓǣ#"4J^mYBC=n'G  B 0`3x"j2Y{QdfVcokO{{))_|lmuH #ww}uW[+[YCyTGRV/Ù5kOm5x͙;Ξ,5Q=àAD͛m:9iH7Oʜv7zSL̙Tdd6}:>5zOP/1|6 i?Ey9:QTT[ܰA5ekh23[,imlcյ+Wg%ii[n*ʜLolgoAA: /!@eb̀BPNFFFsfiYY5(2u};`EիWK2d ): ;, ͪ8>3g~}4UM kvuΝ)|?gBn@UA> "hա0p 9Nw VXvJɓXrm,Б4>:#PL@G{CocCen.ttuէ7ɛ7cѩf+u(QoͲ 77P)H$!BK%.s__Ք&'c.NQ\꾵̅;ucuM[^19'N``gFRŏ>²KrO"?3Р:m(CPh%P]tkoꢢze:.X@ (!8E΍Vk`kKӚϕdVNJbϞݶQ);Ҥ$d&& 9y6+v=Խ{)OOq`$`C''lzצMw,+kp?4bY  "SQ~ъ{o-13 QF6g23kHIbǎ|MڣrZ%{R]O3OtrpVMnn6mʳ>ŋ뽾r;;bhgGѕ+0z3d*23Iٺ!C:T}dmVԩZhgX@jh\;eM E/kZuJe.\' ׯ&0[ޒ]:2:/-]JT{zDM.رZ ]$&R]TDkyzbLš5  ᆱ N>,a₢ezp&0> OMq1W aÐ!54q`bWoAfnxr=SIdkS_7V'7l*( :7 zÍǵ=U'ev~%+Wrzl[M{x{߻668p@kCӛ] *}}T*UNq4ΞEQ^Nb$EYEE::j6eX.Ǭ}{.~1rUUd9BɵkzF<|m4Kj%iCCQ)Ty`;E1(ᆪxRwntK#  C'$Q̩#[}W_0iULL4{L`Ɉx{_d ukk]lqeTlls|?жY1y{ _ҥ zr~֗m>o9tyމ)^VVV7y><}z'm8D&co{{kN?ǏgϞ8ȑZeKճ'e6e ^~=ֱÃrl99 _~fƆ̙;]9e0]X_7UgUw@ؘ1?_{Vښ5;= {Vq|9zȑqmx>;۴fH\O?РA'$hX)lӆ?{<̚6ٹs5'@]bL :}]bﯙTpfz[Gqbģ/l3bCqb"vz]͛r2FSNJ?;53j::F..6f ^y޽M'((HuVӉ_7]RM6T&P ¿_dd$͜rO>KNNy'i_obh(,4٘5g]] q۲Pn6aiU[36#Lqp:TkEKj`ja-n8@mc3݁Y}UAXQg̘1b  RU"8rrjر#_O2 U/H#r"" D&CYYun'NloiUYz:, 8pEYmԬN #  ThQO?} 33)/hK߾&MaE?4f^^t]P/S[-Z=2uwo䔇IZp0'}!vbY  |9ge3Ӈ#GmS"_zw;"X lk锈G*b   G_}S)?t|Rm.^|(|ۋ=^`%;3]0nwHfL%{P*X:^eNX18"  ˘1-?BTRDKB;>i{Qc]N%lJ{$GAAR7AAGGSSRB7 L(ǤlۆשS?ttP)_͛1Ū{fYr_|˗>cfo%o)Ƞϖ-Z'LTpǺ{wjp:6OC)3_Mŭp9"zR=f[-Kpf<][hcʾT+<-"Vq5ٻ︪{/pٲ7xe2{̲ԆeY}S3eH%s 2bȒ){E@̾x~>sgϹO.#bcq6gsΟc?57LŸukB_S__FF7q7 D:u S]h(zTqrX\McdB.W_EYSD&;UBnsZ S7=t ?^NAiZ\Vq>W$&Y.΋L |K,cgXpRM'DObRW5ڸ6Gܫw>\߯qLAi>9שj5ō,_JGz ʫyt~tsFŊ_O9}skXٲ9t*]K8N#DggёɉD$|:%$%~B>\iJ8/W?A1؛8Eу/leʼny #[~Bt  O舩/!!$ #@ )SH  A'ke #F4>TJiz:haѵktu4#wwt1ļsgUis D/J}ZM vZY;N!PLHE~>6DB;kcQ3+n;VEmIeH|RCAAZebed $y3&^/r++* T;`}66tZԀ.̞QH T‚L Zj;h#u>RvO@DE2hAR)(yrZGJUQf>k IDATd2}zXI-[{m L;{?<۪)JN$˅ ;0Ѭ3>v~ap}mNSܷ6vGzk~_C*SWNGeu% l|VgVFj4Q6 ?V#  <׭mnw26kM(&NgQ(ϜߟԽ{QVUQUTD6͑*D[-F;FJ?PSQAFHׯ7ʂtkG($1z^Zm .^$5fi?d.\ =8eu5$oݪ9Rɏ`VwO>Ç{ߦSncת:F9tb O{3a h=sIɼ=Lf^2n- Mv i;_/m$v&)^ݭqL@rVqN5GAY>VkS\Qw@mȤ2dRYgve* AAeֱ#HuuNh{D-]ʑg"?V׿p;˗9ܷ/-Z`ھ} ܹT`HիUiuҌ ϘL.Op7 9ܰ ݑHy0{F>}4>׶-R--/\@O H p޴²{fn% }ur9}r&FY3[:8vܨRggaV9TT-fonFƴV z{fRRQiKVYΩ!$T- n3F~ǘv㹜vn<5cPܓ²rTy|̭9r}?FrcE|v:ʶmჽhgwϭoVwiv!@$;vNxx8} £e^|' A/œn#ԞKNN~~~:._(==gg͘زS_rEV'xn`xծ3tS؂yjO)?18q___lll3fV#  *3aa''?ЈAyU9ݙ'ݔf@j5*au{I7K'DLAAǬJea!VVt]VmYlJ/`03to#BAAtw o@/#גsbvؓn bZ  3dHNNH$۷ȑϙ߲%c4%oKMѣɉKtsLt  O"$K|E`7>6}-zOտ-G>}O?}Fqc׮Ӯ?\;Q4AW>o`Zc V|9~~첵eǎ$o@ɓ<zzrծ9е+lm95n9KefrbhvrGrGDEqbh8:rsFsセd :wfX{VYP@BAɍͿ0 ߟZIxAA6?馨onS͛q7W6cH$q˚DzGzmh:\IDG?fNqJ FP}f7o&_)/̔)8Okאᇪ{F4v,gLAYUT[ń xu|CFtX Ccjش }wt|$#  S+(  -&;YyjjR173)S6-Y~P!^^QD0p`7nTW(Dck{^H`,+iLquʸq $$P\AѣE*%/a8={ƐS{.Ӧ%ӷu/`A:QoMQQC])9-׮%LJ ||8=q"Pr+Iٱ ||.)kqnT n݈SLJ ktKmx{cܦFZQ|<.Ӟ@OO _Ĕ(< 48=q"zj}Git;u SShiQhuXA{ݹ4w.w-]n`__nWrW/89qebvPy~?iHk;'8v,zvvu^see͜ gHMEեXtߗ_242/PXvhd$|qHdr._ 8~̐꽝HYV}f n5z5&OFȋP;(!-CCz" yN^iwx߶5UP;r7WWڵCnau߾ Ug/RCʕjԔq%2<&Aԩ(&NddBJRMeʔ!/2osՕқ7SS^Nɍ''Ņ+}FE^ Eq;{}sԽ{[ZbޥKΫVDGf p5 _&-(!aaOUQ_s cЙ3::>"nV[VXZ.غfgȃjNӓ2dH bcG 8;qvci BNff%˖-B3i$U^Aеv4º_?dzz M:-CCr9#G>,*˖57QUUVOΝNgޱ#yTV͍htpI8=a SAMe%6>Kyn.\[Mۘr8?N+PLQ#%J߿Q0tqAI R;FYSHd:;SKIj*nT[v-Gcf`zzE20(>[dU%%HRUU$oJqRZFFT7yu i=66qWY6-@@VV'Is% #@ )ST .kǹilr ڌJ%I/>~.N"x_B,+  < xYc}֘ORv(23+ocMJJ/YYiVVZHQ\ٹ`n^r\Bii}gL&Q}jkL>_GOO3lC~IjS8 簾eg7;oյ# 'igީ{"66d;F͛w숉7TRCcޱ#ɋ ߩ,,"?lZnYG>I}$Rvj,;[KZcT.GFm_yv6rss$ZϐRUgO V_p0R *.ƺo_FFR_SY\ZRH59 i=ur/]$5#5H`ճ'zLdOv]mݚ;ϸŋ:;RIiz:}RSQ6}H.]ȽxgQˣ'+@׮>UK݉AASPP̙|M˕8Q0ѩ}骮PcmMFFj;3zZVV55yVQ R#[`E5kְd"JQޯR}W)P[xȽx7j)[n׫7mߞk8v O>MOjkӢm[6lvrBCɽxN0ؑ[`ѭ6aDlD*%?*]KK23Uewu|Ғ[PVU!Ҫ]{u+aᡑ?IϧݪiA..ǣA"RT.Geuum,XL||89#7FϱNc~kO{cY %1k»TGeuu7 D7o˫| &^^D-]׿v((NIQMq;rsoPĴAAs@!2JK(+ ނ}˰IFqcUT{w*#ٷJ%_~A>Sw,[[m*uyH7eƌ\zUjժGR}WYѳ_Sd23Iٱҏ Rd=bW//Qw/z66߳,LOwcݯ5UU^i@5kHNHظnݰ׏ի5Ԥ Vۜ:kר'߸yw:d\Nn`np;!B׮ոv 1vwGuPVWǨ(T܋9?c6l@ё2j*k;-uC&v-UEE\;ELOAY"mUJ,]4vk*+kG1)TU7Ҵ4n>s1U]tZ oEt0q+4$ QLȕ><;eM QQd9@yN̡Ut\_~!ܹڌ \ljﻤhAAȑ&#Ѧ/wL᪘$Ǜ1| W6,C˱bvgO2/O&O(+DG⋉(WVtxU <=pp$$|422Acnnʭ>kMNf?|8RmmmKj;Nq1#''׎nKE^);wv'ǽbKwꄑ+Rmm,:w@@J.-ŬCl,+ 3?ڴ)ĺ_?,vB4X4VgQ|<'Ff_D.Z…P,3S#ͳ>kG$C}zcmmtNAx l‹w@ |aaaX4s]NNS{.999yalJ#K qvv~lϛŋ>Xh^ۛo@b/'3mjG72UɉH9sd9ê.)!ۛ~73F+,,l066nq8q___lll3f9"  oPX@M T{w*Nd蘘v޼* liDιs݉ǦMXT[r,:wF1ana*IO룏DLjaAA)J%,XK/%bl,7I7ZCq"X5)UÙ^uk:XNiOi#WG^Et  Sr aamt3n$)SK /jAAs=|K/(lGUNq6?DȄIG=<6VモclߣοX~|i6B9b sDAA)qt=˿ĉ|@եk]Y#aUU03 dOo_ަXdGR>_DZ~|)f% ~S/>>CܕERm˓eێ{}d9|b| -`G<:r.4㡱lk{?G0wO+) O9"  HQLQe_7y>Pԋ&11Bu-Iy:2wDz<җhgAkލdܭ4_4kp1=ӎ;W؅?%ca"v&`n`[$< sDAAxjOܙ=NN4Ҵ4i9~< .+%'ii첱rwow@E_""0m׎|paZ7ЖiCOme3^t~TJfzG/6i17(/io{L$U_'Sj޼Lo>lp ǰ5}Yzxj[&.R-jKy!M>wկSg8js*+W YUܑ Vʫym닸,eJ g῰,UUG/Ln 7ggr<2/VgV XՕ}X|#\[ݯ]?\N Sݓ{Sנ<>jpˎ_%,1#5?PLF^T,U[^d5> zO̓׷Oaiԋ ;E& \ՍhUZMOhĆBreʖq$&TR'҇y!n.7:2& OR+O~͎_TT28.0K)8,mg3So یN^bkVxv\`o/sO2imݖ׷OqS(}*+yI\1T1s39}? r~?Nɒ[r˂_Immys+!&?BvM=D =Sjl}%͡kUuFY_'0+/̡=iDOb@){'mݸqLEu9)*/lnkg߁&}yrfU;eGW2v u{xL^yj5,=c-.΋K*Gadz2s!r~GxeDngsͪס#x*WC1A+ߋ`L g=ȪS_g.΋%&3'@rn"O/8[#  S)_q5 //$2ݻcв)%oϚucǎsg8*aoȨi6 ,jFkϬdt雫~k IDAT]Sr ȵtc?iE;_bgį3kl]}z9zQw0&=FWKphJH$TVWr=+ʚJciQtRSx[h˴3"T^`ZcLi*-L2`N4BKŋrz ڿ@kO$ S8)e$Ģ%բ3jljZbptxLvmw ;S0,k%ƺ(̜qpTAvQ픍Ʈ>Y1,LcZ7БfPL]=Ҳ;:L1.U2݄F2RXZdL 6F(̜=V|'!ʘeKV< RI/LHnDWEQQ]s:29L 9Q>bᆱn wx7GKtt삑܈hyXWAAx*">?D*7.A DKP]V}TY&-,esZg4K^vpkUR۶=mI5^Jj~ #ǩ(kUZuf|3f _}+mXE};s VH$V>Rɲc3W+R혻ۭ%ӦqHMdگ(ɫ]g`M#vV[uUy f{wL"C&!TSs9)R'F6jaAVQ&VFHnXgKU{}=VF6NJ˿ALfZPje59ppgRg\[E_2L4(ttU)wwʵV>ٷhgJ6!(Cfm\w{oGAAᩤooOIJJ#7<afs20o ͛ErΝͮwkfaVm(\XΈ_io߁׺!H%R>fx0jˋ2k%0[} |վ55ρek, UAQH@K:ͥV랚W H&5?//$&34Y琶FngBwBY²BmVW"LJK6F,H#VhnF\ϊn3$QXV3ߩ h=sIU'ö4\#GAATrf22hѶ-]ׯ59ѣUCY]MIJ F..,_{e ϸHoJ+K(*+g Ku'%&Yr(*/hubcDqn<.m'v TZl雳uJ ۧ גN ,s̤GӖIҬ:JehIMM_iN^cۣ%¼X y`fSVU>-Y:[Ƭ1m-Kxt$;vNxx8} Ÿe^|ŦA°xePrrrsϯ}qv֌4yn`xծ3Y%%xDSj}8-4&cIڴPLˏI̓Y 硫G7}QXG166nq8q___lll3fV#  SyU9ݙA3^G#N/l:#=Z#Pb(,-6׭vFEu ﻺ)uĴAAAxZr߸{Vxb2y%V[[euyF=6=-= ^J%^/a$7fZ7ުcSAAAmJט' /uxk91;/O&  8CDr/}hi-[r<81HS|NAxd=J{ZI7I~~ܺpI7%:GAAVHH%"CmpaamH>=O_ږd>M䧟>݉QkWi.͝(ؠ+}ŷn0-1wJcψY`??vڲcGn}MRst`Rv In==jڕ]7UZYf&'Ff-{ 7~GAT'Ff#:wn=j;9w2+ P((qy>ghd$-ǏS<̣G9zzRU\j9.+c'qfUޚr7ggv;8p73D   Q}tSTƏ7A(}Xee$nތ̆W]3@"[<=zk6GGiJϧ :I7 pSRp5 4ܦO?47y3Ծ2ɓ~R?PUV{cbˆƎ)(jk0ϻoȽ:22gZ-M}GHy/c֮hhY=uF3&=1(&MR\ܰ00%/AAT…v]լ<55{GŔ)ILJ?t/(LL"807*+Wl|| /$|y tt4ո^eܸQ(P\"UzU^J0=cɩ=iӒ:֗Y o(ڷ桮ݔUUΜnS~kC}A>>8}ʕAA>>Tp87u*dž `nD~Æ5 rmx{cFsxr/\@q=^~ω) P(83y2 6hqzD.WŮZu+u4=N0rsC}$ZZ7Zf+V^ww.͝K]Kgğ&חnnį*zbW/f iiNTN1q"cǢggYHکh9Q]V̙蘘1{6iTTP]ZJy[oRKNh(F?}{F{Xu">>@"5Xup5%'ǎ%fr#{v*ʩg1˗MB_v))H T]=Kˉѣ n^cDEIj*Oomݚؕ+s1mߞp|}U3@.(~Zӧ %U+?a3C֬Q5ԑ E5$nB]kk,woֵ;#  S)6r OO= o۶\UOse|nU1vl_|@VV;x$2lEH;7ocB[̱")ɛmu5b\\$%yMF'o3y9˥][?GCnu櫯28u[[mN(Ҩ!mñWK>s32.j ĢK0ehd$|qHdr._ 8~̐FۉeeݷqkvQ#qWjdկf:vLQBZXEnxFyN^iwx߶5UP;r7WWڵCnau߾ Ug/R\SVƭKlsS8 (J΄:7o~ .Sܷ yx{מS|k/LZPCH߿M‚t߼sPJ9s[0YN֝#eu5'Luk]1P(x1 iAAK:{ wg"F%%/0''6?Sa RW_12!^۷d9y9ܷ/;u"}$J%jH޾]-_c݈AAᩴo_>]bd$ec9+X`kcMUi!!Ej%gHBB}[}[oYaiYc֚!];= 0FOOʐ!-1,Y&&2 9,[ Ϥ f~~kV!vfnhСh"q9`aQQtXG¶ߊ|Rvut>ɋnnDGwN HX j*+yYssrn4U4Vg1q:Xb)V8 ƸNNZP1ʚGD&ٙXJRSq{5ڴϨkq=3VՋ/PU\E$oݪ-PU\LUI eU[Rh^7VgMe%-7nj]8+ĬXAWeP`P`LO]++ur$rΟOĦ2Qc2==L(JHjқ7#dzz蘘`?l c:;3$kTV633 wROB*J2j͵Tr 12Dݮ3}t"""4>+VhVw$Kr)IwSSʲ7vڑ Iܴ wpP3ԉ܈r/^DƆcT&''SYXHyNǏFHRڷ'/"$5|ʳ1jݺ66Vg>(&Mօ 'ʲѵVmZ[kب+FnnD[Zj\Rh$KZPqW_e`@Uq1}242eMj>ʚr9â0҅нn5k c{%8f;p UX=}$2S]?~JL[:ү:w@d9CE~}#mc:.-UճSݳ:.\ @رd;F8t! _O3z{sx6:RUќ? #(+]k9hi8f ..XIŋUPVWh`ջ7& w':GAANAA5~?nFSTT6DGZ=ZgGffw++*jjjܭAHGR k֬Ko<:$R)5X*j1ju-,U_Q\/0A#MYUEܺu/Sxǎ'd9ScGڴhۖ 0r/^ļS':vXtFM"6X Hտ1_zY ِ[ZR~e<'G$o݊iv v,d  eիx̙dktwĨ$ ]\pᅬ3*) >}TܺXt•ŋ>>,aa Ħ_?=޽|nJM!P.2~003lf6 ac˰f&w_r+Dn1)I*U:͗!|h۵߾}Qqeb^4Hgͭ[1_75ٙ7bӢy^D:u%0,YMX֭K/(3<կOʕo-xq%517lV[>/ΞMKd''e vπg?C=zٷkǍ͛I '5Kx RR\] F,Ph=yÆp J9:"6DؘK￧Bsb߶-}GnZe('Z-#?I}z/_NwCn͛ڽJr7l_QȾ{Vy+WNpn쌺Z5%L򲳉?p"+[vvdfB4KfMYu}:rӕ.XqwGGk9|=}AAd'%z2R/ ̰iт+Ý`(߰aB!xlߞL.exݺ% @Hw_k*{cINeQ<;^CXYn]%&L|=O?V1ըqgs\ŀ8;_>CXQػx>~ʕ+}ʈj8ܧN^N34d[ԨV,Ջ5¡sg*tSDTshc_ WW j3ggMLo|y'Ort`:9S`QP{lXqqrgO*o̰(iO)MF D]Ğ1ŗZtP?˓q!<<<{2sD!BZlܘl*JQ2T?мT~F qڪUo=CCǪABq))OXjN(ȿ0B!╢780Z#iԼJ+llZ|~yIst@UR􌍩%9YSX>vūE#B!bl%=$,!* LBJdYB!B!^iB!/dFBGB!KoURƱSIE!KH#B!⥦ӰF>sw\݃V- !xHpD!BԶ@ysP֝&3)CƢyd& B$8"B!^j1嵙@И0~賊 4ƍqF(B$8"B!^ZnM^ZU]mʙYp9N!  B!?(h.4j9_BV]tэ}~G T!ċL#B!t3%|eNGth76)]B#BB!x)-;uQNz]B J!H#B!⥓e?\ %6B!DqȆB!өj]"?@f!BU\0BG!B!B$8"B!BW,B!όF!((233ٳ'JRCCC~:4mggBWB!ϔ M4)4OVZ~ΣB!#B!͍B]\\ӓ !(9_!!B!B$8"B!BWGB!BJB!B!^iB!ϔFAjh4hZw!yjB!3~z߿ 055W^>|ׯDPP]tt%6^! !BgO>k֬sBQ8YV#B!BWGB!BJB!B!^iB!y=c]_˧d&c9AEn^36㳨sYS{ܿUI???7oTLMMkk͘1c4iRI%Օ0$8"B!_bVW?>7r )‡sg iW^W\Vz7ISfVd{L Pոu֧nrr2*Jj[700ڵkcbb 7n,VwUo矣R8vNz^̌F\:vԩSummm͹sXo„ tM'믿E:i={SN:i}US;;;{=UVZiӦ\t8s *ɓ'+iZbE}TΝ10xxfɜ9sPT_|sܸ=2`bsUz~rss7o*}) /VJZ͚5 ##SSSڶm[q>P`{$HpD!B07FR=ÑEvK'5:ZUp~&OñôSzӶm[;w.}Ν;2eʐIff&իW/vK.ѻwo>#аztڕ~7tȶmt}}}9~87n`tܙb4h7oe}||8xNGiҤ ^^^~ϬY~:Ce$''ӸqcXzj߱}v 7mڔ$~'֬Yĉu7nܨ'?3..NfLLLXd?'ٽ{7VVV>>qڵeqwwy茎k׮/_[[[:&&& ٳٳ'ojOOOe .T޹sSSS3g:ubȐ!jjժ;Cy&ϟ?' @֭[䄥%NNNǓ@ZZ{L24k֌6m0o}zc577_g(} ohѢmuܙ_~EgꚘ(M6ѣܾ]ԢEUVvڥ7e\2Ck׮:^ zzzOiZڶmZ--[dܹkIB!xi<5}FVSȌ+PoG#*u"D0w~S L K)%hLXZ-C "AՒS&[ˤO}kWKXl:lBX\Lr4c7cq㫜?tM_̭8&l`_b`Í(zn,aSwTaE_=ag, O*mS?:1-DMK棖迲3=zrA&nk<)Iieϩ79uDR=z4dgguVYNIIIn___ FSNTVh.]DJu>}&M ;]VyqF6mAmݺuѣ.\:o6e˖}ߟr0c eݻW޵kh۶-SL]vźFZj3BBB(]2ۛ( vٵkժUˋ5kCݻwi۶-#GΝ; >޽{^ ~(>ꫯӧ666OoݺuIOOիJᅬ3={Trޜ ?ѯ_?V\2˗/gٳGDHVhժ;vtRBBBؿd(OT*V\w}ǹsPT3Xm !BNе4Τq4qiA؄|s9+x{x5jUURp9"9y9סu}^NLr4GaoX ʹrw>-scsR3SsWv3#36|Oɋ{QG~6԰E*msy'z*=RH\(ZVi,9֏,]*JN5cd`Lԝb8?,g/u07bTlO{%g?۹ ~(A.]vZy7Yt)J۩iӆ-Z0j(%ԩSܼy/SSSʖ-[`">>KKBuFhh(11ׯRCҥKY@֭ã sԩSӧm6FIRh֬Y{{"g\)lI͓̚5 Jʾ' <^zabb͛3g7o͍7|}}} DRRN]v\t7nqADD;qݝ8q"۷og˖-T*:vHnn.ޜ:u N:?p-rrrpqqQsqq +++Fӗ/˖-+t cǎeϞ=X[oiڴ)[&00P ,m۶޽{䄃C{ 8PkZ'}Xp!{f٬Z.U|$8"B!^:Oݾ5jcalis/[ ;%VmlYݗ0{4*Md=%O3rfV PttԦ,o&?. `G"939E/7X;k,Sw'i]Շkd}U(y6z7f:Sk3əI;wo߾:Wboo϶mPoܸOR܇:޽[h9;vdݺu$&&r1w{7dož}hժ@C)|*3KtfDuvĻ޽{ eժKz聕DFF*6m׮FѰo߾Oh4l޼+8pqq߿ϨQx7ƍ\p777Gѐ *<>3f ӧOظq?اtnΝQkfժU\rK.QN]FJJ ٳY#̙39s/R'Օ *(Gaggsoу5jDRh޼9IIIƽE#|{0KrС4oǼ/ !BNZ +x2v/ ߒx6z*=ʓ0x[qsFs;=Qms/Qه$mڿaqSd9k 9M&!ᆑ8bnlq(9?co*;tȮXyb gni_Om~[Vi#:BUX?=ak0M"lB&jڇm>DgK*vdtO2|Jx;5 1~H:zqHWիWwC ȈƍSjUN7n̖-[R ǏW!7ӕ v=Kk6mD6mPԩSGPT):{%##mbbb·~ˁ T'Ny uQ{{{ʔ);vYd gΜ!00+*6HuppbŊ;v%pS\zzzt[ vCz_>FFFrU"""pttE+?qqq" :UVlwЫW/T*h4T*UÜ>}ss{@===044ݝŋӾ}{ݻlj' Gqppehq˄{VZLdd$fff:׭h.abblr)c7i$8vw.D#B!dal cn%y7zg6 ¸p(E#GQ{= IHH`ݺuܺu[n1vX*V{̟?6mŊ+ȣwÇ \2h 򰱱r㏤sN"##]6qqq3g2SN͛7YfMW,]+V0dȐ$ !Bs~ Dr%Z~_Jc.1.VUgb9jtɼ5]5ә;T*__ \C_O#}ceRߐ5niu`趬ɩl=,YZ3їИԚLP"Ƞ{Px:oE߈HBNT†f)y qNSJqI3>i563j|}3tZUaWvިpʹ0XZ}߀p. J;Y]_9_ Ƚ{4>QJ.-f:Cq4#N斫>i԰@?j 60w\j5]ve4k֬@Y{{{͛ǐ!CHNNF__[rEZ*?dܸqX[[o3w\? ?8₹EZXv-fʊnݺ344GDGG+>ho|XYY1qDē@P ر#a<==qvvgӦM_&MBVcggǴiӞ?={ߔe̴x۷o/СCqvvfŊ,Xggg}>U~}jԨ hѢSNsΨj{6l؀ʕ# 9s`eeE*U ]4l0 }PCQlYL~/ٳgSreܸz*۶m(TÆ ̤Amۖx4hPd222_~:3_̼Yv-ׯښ+WD`` UT v֍0aСԫW͛++I߿ҥK)[,͛7gϞyRh%44f͚bcc[tM!??B7Bti/ėZ, >wogށY?Tta/|2~~~Ͻ}B"Jŋٹs'7o.SS|דwS||[[qB!BbGv U'~k͞y`dWv2r2/d+ċ%--+V0hРB!/t^|aÎK[c33&j|@07`yXt)+Vm۶nݺ#xDέB!B0˗^ww=]ϢO/w}w}Dǰv_KzϝB!/%WW'R>xT 餥OzoU4yȵOle6MO݁dB !BQ, 4o߾L8ܹY#۶m{*}^~Cxbiܸ1>Ab϶)s9z=5#q+;>>. c Y IDATs8bv)85*a !$!Bڿ?...XZZxb%Ν;tZMΝ{q^^gzj*V-:+oNHHGf֬Y+WN߾};SL!88۷ogbb",YDI KKKCJJ ֭̌[Gݹx?{Cղ_Յsgt&^!$8V<~}|e.ӭS%/$4K\y|#*M+Gok޺|2[έBgSպKq. !u1'iC#†?|#D~qS^fI;]`^DލLgjtP>ZU G3Vrn$lԲT_>49UfV\@&i;&UEf), EMX{zuL#B!uΞ=o`񘙙!C8xVː!Ch4Z ==="##1b+W3fбcG ,-p̹gR]z,o; G1o{gȚih3LcMԥRWY9,]E؄(%uUFR9VU}r~ Hx;8Z\G#S<·?Û?ܿȔ{_'J#B!5b͕cQoݺ/o=z4&&&tڕU;l0.]/>;vqxyyaaaȑ# kڴ) lRgɶm‚֭[uJ6mhѢFRҗ/_/uȈdzyf -_+FB!/GI^^)yFGGΥKpww !!z>Çg…*ehժZw`hh@hh(;wf۶m̘1RJp Æ -q!ƍǁ]6K,7ŋwN===<<<8s:(onMMT*Wi<ҲJ[TT%P  [T&FΚ~ĥb&uAk [bW{MJ>o& <Φ\0g&3v~Є>VwFvOVN&*Q?3泀9uMө_߹Z-7iekf[Sڞ|Í W 2B!Oӣ|$''+iIII2p@144Ɔ#&&;v`aa)))9rG*'ܿ*ƍe:tYp!tڕѣGӧWח>ʖ- FQk49}4ڵ+О9111y1!8VU}xPTjJ"7/eGo:˛HJA#}#&6ӹr;v?4{T[`,! iO,8iӈHvu,>+qI Th][ ;Z0>X?D*Z ?ݧUUB,߮dsxYJbɑ.FFh6 ?iZ9 ~k5jw)~!Yf u*ŷ(ZGO% īGz!Bӹsg~W +Pqabb´ih߾=AAA?4?z,nrr2}ؘ>GsNʕ+Gff&YYYdeeѩS'Sk .tR~Wv ?>Ν7a}0ÃSNVsssr㏤sN"##]6?Opp2O,赂_N|i8gcOl4pҝP|Uʛ۰+<?N</j17F3#"?ջJI&e69۵ Yr%}WW}ޅc^܂&OCjV*kOV qq+qiK 136Ybekaǭ͸Sf>?vGRbF)R˾ǺЂG2W.Fht$U&1]H&[Ww0_y\Ct_#}#vxzzZN__+WR^=:uꄗϧu֨jj|`ĉx{{@[ti&MĨQ^:]tYҭ[7f̘ug޼y 2sѲeKON߾}ښÇТE NJΝs...lذ3 R׮]ˈ#FJ~BzMBZMI)TsQr^3ǺP?U.ӽvgڷsyj:Wo_i/niJij*u?;/mE6-pGoLb>Ifʤ(x|||JfMy&GB<+~~~ 0!99}4VVV%="11񥽖qG>KTT:yə >2 } ! JMMujuq!<<<{F!BPjV v_\#ٚl~@6;v~#+)xqɲ!B!e6ϽOV˗'3|@,ռp$^kSB!Ds>/.YV#B!x&z.oW+f|5y#Fk,qr)$(9"B!FCPP dffҳgOJ*{&!!V=^^^Eqd&S{3Gűӿn]m{ç0|Ugr;==>exXm~_?Ɖ#\''!BLPJvޭ5uEOO#GB&/xܵS @N=X;x;\XaDu[wWjV7lYI#BdYB!xfqsslٲʗ/O5P՘SreK`I.ƝӉ%:< ˎ-`cw軲#'鷪3Y~{ߵpnt+AZ3ujC旎Ͱcrxg u\>=kԈWc=igSz_x̌ƄCU_7n48osD!B4<믿EIwn$Zw:T~2nM^JZ6;Sl xw#ygM?n1{}YxⱶU^<{q{{M][9yFrWh%OC|U2QݶN9'J:Z-7izaٚlTʨMJ+ @R]2s2Xժ 5j}>!^UB!%޽{ٳzaggWt}af%[ԘP(kvEAsyH zz*=ʓpFrf򻑾Lgb\Nٽu*<|/ !ǔ iY.];@JͩPڑ52V2b%T*c5RWW`[iWoYSRD'EnZj/!+.:>?S^fh WF!Bi5E_cUR?2s2HJLgʳoTe*2 Z|ٚlFv?33hҒm3tM_b2mz_VOL'r49x8n{*o,OrFY2scͿ^v:-UXyx:6؇վ:ߟXBCCi֬)66oO (a!xNN>UIHLL|i%11OOϢ ?yRJE\FN_U$paԗmWvEg\!^>:UjX8xҽ{wYV#B!(ؔ&#·An^.~!+h^s_'jB!jUW*Ͻ߀[ҼrkF>A&!B! 粒?L!B 蹼B]>%3 *rrtmEM[w`&穵|]qZyj ! 9"B!FCPP dffҳgOJ*{8|0兩iILY}DzN=SR=*0:HwObPWbWԴ{T7Ku:z{b1kTƵ7'x$8"B!)Tݻu 3gLfJb/UKiFxO-qF`l`ɏ4dN=>rfV$Q)ޤIשRB'!>YV#B!}}}([l V4Qwib`Qߋsڎ ԚLZݍZ3Yzt5Ӊ*_⳨ kO.9 ! !Byf~WΟ?OժUKz8/MήZю2I(onS#%m22ȃJ^ٻ(ߥWQdž(--j^kb{Ac%FCԈ]@Q bX ~+9{E[$2/z{, aiOsxdN,k{ҫG$NFP]sܵ; O\?p a/JRL$-~3;Qq8=(v1%s Յ-^;HgsyvG\8᱿;>/w+,99 ITT_n!n|g_|E||$C$΋R;n6XTd`D<ƦoI L\ޒ@Znj "  Zrqssc߾};7h4r}Mt'G=}7yVHSĝLm%TV ZƬ?:Ŋ_iX`WThs1[`7 f{3/;sIǾSm,mjK 'j:֛/GINL*  d2<|To:f2/>@]110`y|hR9=6ϖe+Q=!]zd͉ o6ؚs'/E13 3/ƃ5vc9|T==7\md2,*&j:yj]Y:ǼhfvZ=ArS5RNصT~AGƻsO+#>i  KVQ@%oݺErr2J\Μ9HC$9+PU!036/3O[o+bEjuXڔ'im 337OT)+#m©T6zwXaN8R'&}^]_q?"MRAF=$ ۴?̗y?Ѿz [6y{s4L6g1~8v;Ĉ B_AAڸq#œvnڴ \NHHXb IDAT28rrr֖f͚h\tݝK+\DO/,+X?%`&C7|D8X8QDDϕx Mh_3b9zYR2̶{fO3䷾``o5khm瓵}&VR;?![P;T-FĹ /hУ^nf'뚍}o{"_dQƝ  اgołw(Owrw2Viwgرr[^d$/oE| pv|֊8 ZMxx8o&&&͛ao/&^0> 9s ֞Kff& >@~_f۸vtT QCeWĐWd Rk yvG2L|% j:.--Rn] jAAmtMj3'F%&5xeǗ_KO쎏vr-"})^|mF 7£ Ú>{XA%#   [?AJ\m% $ ^bX    j  Ҩj>Lzz:JnݺQB.^ȩSh߾=vvv!RALAA^*{{{4iRNAx=DrDAAxiҲcN:E:u5A^/  ڤRXXHʕ_w( ¿H  EQQ'O~;A_N$GAA">>{{{*UCAĬW  kIRRҶ]v燗kLAAARըj4 j}}}5kFfͤ֯_OVR +'#  KqF شir  <"#  KգGr׳gϗ  ZuFΝοI&,Zx{RXXñʊQFQTTUW֭Y~sř3gz"9"  O07b`/1[gqj_RZUs͠ޛ[ UJ.I%Fĭ&&1g322bZM^bٲeeƪU~:`߾}ZIKK?ׯ3`.]Jvv6ET*233 &88\Mƅ r 7ofĉCϟ;2ɂ^uz'?wb%#9+I7`mOj}a5S\&Y0mח8|m­{77i0ۛQZN%6Ԇ\E_;T7vOV+5gT{#Qo֣nf3k4.nDUXuloR~# }HPE5-v3u&_?JX\L=tj*4_ \Zԝ nd4 &&4qkhC h\ K2 )Gd nl+#,i3kJmgϞҐ҆ 4\֮]+.ˣڢP(Xpt'Ɔʕ+SbE AAAl޼'NPV-͛ҦM ŋR9wwwMFFR KJsssc/_NߕJ%:u‚Ν;s]s400Ԕn߾-?{,>>>XXX_bbb͛ş 60fhܸ1]tuPZ]\NʕINN_~A&yV<{ɉE1}tڶmK-011aذaԩSGy&&&> ϟgcڴi4nsss:vHZ!/  a5Z_~~r +VCBVV( ݻ5kpqqK|Sre ͛Xh #G0b R|4jԈ5ko;7H  o?dڞͩ[ߟ?\ȼ~kﲨ*?^q:mi4Iz8Bu1B2/[ǹ>L@^*Nq㓘\gdFmO8s1${QbJ ?"f.$q6|?o #n|z2}8I$F4b:p/Rd?$9<#z.^ZQUK:,%G m0_ؘħ]`Y<+!MF+sar$ݽUv%>sٲe>}:׮]#11oÇK/\9sFHpp07ofÆ tMG͚5p:vObb"Ge׮]|"ر i&BBBĄ/R\ XnXZZhٳ'}w ? >իZsasrY 99X#ϊŋ1ck֬a٥喕ŵkh5lؐ$ﯳ{lڴI}ƍbllLӦMӧÆ #''?SbE]СC ʕ+8p@){8FCBBÇ')) .\HRR۷o:>;;GIII)W\oAA:^-&D[K_gaSG0?бFm2 Z􋨊Tr̮6ʈfv2G`oA\ݹ!NOK~?Fm4l$ɓvGǯjcLLScڨn_LFZNI[4C}Cl044D,>:pYRSSپ};M6Z:^zK0֯_OϞ=~:iii``PYCO ˑdRPpnffV6K>%=G222ҥVrY ˋLG9<9 4̼ dzؘr/?[:>[UG>zs_&q?5'V٦=w2yH22h(ƃ5vcI'Czn:Mk|@$ZzEF=647ǓCJ1h HLL,3N\N@@[lP_4t{Q@*U8tdffJsdddHPZ5zȔMо>:t777jԨ˗y&nnnό !!sh"f͚ѣu< jBNΣς%UT2ޞGJ%yS2WWW(~/^LZZ׮]zLJѤh%1&AAdnlNC9<2yA8{47 @6ŮBJR=&g%q0q* C#d2f<)7Ε\Xyt1"5Ksdrgw|=S8s$J2v.NlwG%(UJ~>^(%)䃞Lw8pc,V`1|wzi'sɑ"v M\[<5?O;o*V 0 1+xzs\=8q(;.lFsO6g1~8v;WΞ@% XbspBrss|27nr]lݻ8q~FNݺu9}4qqqh4033J*3$$իWM׮]űk. X`T*&!!qqqaŨj~Aܹs}6ǎc֭t޽6u}<==9ucjj)rxСCC( ̙#/((%BZNɥl &MѣG>vfU ~w֮]h4׏QF` 5x:pa)<+FZԅB&J^TbI½ ?`U0r bnvV }p|K0^r?e16L:|v: 5f83{tNsBוm3{zj8E(&W~Iƽ7]w~?ºѨZSccNHk ;6rE,+<2xΎObr@(. Ν;BvBԞ 4`Ȑ!Jń Z*iӆ/B3"77~D`` [fe^ז-[2e`…lڴ SS2BCiԨ֐(^9eѢERPP~+KKKАJ*1x`{d_UVamm>Pɓ'鉇>'O|quuEOO_ƻ+M0x6l8qOhhMʕqvv@PPjU9sB6mtz} 2ݻccc_!P< 3g`ccx!233s̤^{1oy۸}+Wd RcoJ3 +{aaaA~~Νˏ?ȑ#GJU^lݺ뿠贇/=Εʁ[.#  P^ǒ[<~Wk##yzâ\E /=A(ÇKsM,_͛DěϛlѬXԹ^ ^w  OH_eOOE 7£ Úym BY.^HppgwaʗYxyӵl;N$GAA ߠ|t[ɂnAOx 4HkMߢdޖbX  2m4 FFF߿u#011zH|ڷo\+/ ӚVx  2i$|ׯA,u,SSS?ѣGcoo5|ϨXd:?fffeݸqNǗ|ߟ_Ur9ʒkÆ v fϞԲǏgZ>D&qr|a}a`` -)[^AAA2kӦ .ȈgJj5+VϬwʔ)d2ȈN:=W|ϫkp)d2{AAZO>SN:\.שI:ӰaC}ga@ ttnZg`Tڴi\.VZA$GAA0a={|rӦM… \r͛73qD,^qfڵ[SNLJJ ӦM+;vT*Q*sJ%w)lPPJɓ'R,גԩS$%%ѽ{wi۞={ܿ\ؠT*ٻw/(Jk0j(Rd̙ԨQu~yzzruԪU )pi7nSiuoܸ-[PZ?VիVVV,[ (cǎӧufffJe?c}7o}X###IKK{2~)ЫW/uÇMT͝;w9gfܸq_͒%K022zF$GAAB(uHC~;RRfffOaggŋoXND5kZ 60fhܸ1]t~: 4J*ŋNqTTFI 111###LLL066FTbnnNrrTi߾=zzz`M| IDAT`` !!SSS>34h@ tzL( ~Gj׮#{ֹcذaZ+DDDУGؽ{=,,L `ѢEZ (Ϻ*₣#'OF/`ƌOzp;s >>>XXX0e}B@Po>V\IF LJ.]-9s&+WҒzꑔTf,gϞ/a>=ȑ#qh%GG֭lԬYJ*Ѷm[nܸ!۬Y3>LQQdlaaA``+ѣNseSEվ}{{< Dnn.k׮E__оO @|.IOOKK,xݥIZd J0aN"tѢE( 9ˆ#P(IK*Uرcs'/_sss e^3www:t耣#899ѯ_2=~}WNXX;v ޽{ĢE>}:m۶}   cɒ%RwZW6oLfʹW^^^9RQ(t֍W괙E6m4hᆪGԗ? .$))۷p5T*|ܽ{777oI߾}i׮wz5AuVپ}{=bbbm/fƌYٳg?W}H  j5SN}}}Fiy&nܜ&Mj49rLӦMqww333[[|>}YYY$$$OHH6m:D.]} J"\|k׮i1bxyyi[h=zZv233i֬mڴ!**$?j*N- >[n1l0;vsT^=yo۶ LJŋT*|||KOrr2#FАѣGK<==INN&66ۓqwwG&QV-̈ARq޽{V;wȈ/{j%uưaä %""" ]t5jIVVf͢W^@p_| 0\=JDFFιs瞺/I&ZvڕYxxׯ'22e˖o>F%ꫯd۶md2:u5%''mҲeKF!mOLL$<<\C__Jիz*ܿtN8ѣ^.tOϝ;Wk+xxx ˱zϔYx333ݻ'A ^DrDAAx̝;W7]6666|?~\ڷ~z \1RB|}}V6l!2 R)T*ICg̘ѣGy&999XXXHI޸q .-j.СgϞ%55۷ӴiSdųajj XηЏxRdŊ4nܘ *ТE 8qD;4 nbСҵݻȑ#YbN/Kll4@zz:6660$Xeii)+˖-cԩXYYY4xxP:t___ٻw/RquuY:KMMMlIEEEdffj}~b9}4'99,ө^:o5jP(h׮*vϞ=L0k8v,\Pk:ubÆ dffrQH=E̞=-[HeQua_\tI;v#G2m4iWyVaS7&77333122RJ@0 +Wl }}}DOiְP̔zDrDAAx 8PϺw&66 Pn]i_`` 3f 66V_ZƵ<Yj)))̚5O>FժU#11Q:611Q6̙3Rre۷/;UTe˖K?Rge˖RPXXݻw?tz1v֬Y;DDD>|HDDP 7L&rlڴIvW^z@/Сְ8;;LʕZI{ӓ6yRJJǯk>}8rɤiR{{{233뗑;۶m___nݪ)w!5P y<NQQ0ۧUZsdeeI6lȑ#Ghڴ)+V~yf֮]޽{IJJ"**4NNN;###`T* b޼yOP2f˖-mV'PY3Jym6<==7nɓ ɨ[.jLF||tjժahh(%`T*W^ ;;; {3??_sdkk+O=Ǖ_gI\rxܹsYhfb:I7H  osss?888쌵5FFFҾ *`iiu oooiՓRV^l߾]z544LZ^4$$srm;֭[U\֭ˎ;Ƞ 6PzuѣGٶmj֬YS(!!!^hv*m/**"??j~ܜ9sPT,Z//rFhX`pTN>M\\3{liΝ;G^^ZC^򟏿.K~ꫯ8=?ٶmӧOb/zyyaooOTT1ܺu멫;w;wŋܹ3P<}PT!t` bUx{{ŋQZe<==p5kחǏk%3ZnͱcسgVrUV\v;vR9s&͛7/Cu`` ֭ ..8i_Æ YlM6m۶̟?_R#h4 Lu g``+;wa888UD@@W^eҐ(Nb߿wr%&N(_lذRRR5j...g$(vݺuٳ(|8Cˋ:P(3g+AA_Q,#Gd899iŴxb222e͚5lܸQZ%g͚5ܹ+++222t&{}Yr9-Zʊd26lL8Q:'44^7Y@@&<<۷oCͰ$+𲄅0AW̙3ؼ0^̷\233^o[_۷o:%$$㣳*Gyoߞ.]0dȐoʕlݺUN2aaaA~~~K ._Kصk4mOՕ6$Iii8pu@PP9"  ?WPP@ƍ۷_Ґ˗ӼysyZ>u"F"9"  ?fll̤ItV}^/ヽ=۷oz e[b...k׎6mڼpu   ›o 'H4H bРA/,kՆ#  [ݝcǎ=w0ڷo"z9ٿׯg~꾷=y5xP(&WDrDAA3}t:uٳg^^^h4LL&#::(^Hj :,--޽22 XhƍdZ?ǏDZ]-ZB Ҫ.d2)#4nܘ'NĬYtkӦ?q :uD ZLNNN|gT*ߏL&cұK>;;;5j2W 5kƥKʌ6_8p VVVXXXСC嗟dI+@llѣGcoo5|@:SLѪaÆ|eٰaC^vttdԨQxYTNCCC<<<|b``b:up},,,HJJ|j}Z>ܽux۷/8880r2{ldÇd?\&"9"  ?>}'7n`˖-TVM:vĈ?G@v파$//e˖1n88'J%'O& R)-TR(6l؀oРA;v7nзo_)**ӓׯԕߎEcooυ 8qZɖ-[Ԓv*K,СC;wsαsNVZ0`K.%;;rO46ϊ+9rd}F!թT*i۶mɮ<1m4.\+Wؼy3'N/ "9"  qssʊKÇ۷/fff4i҄Lc/;F~m͚5Npp0nb͚5,_0` B*1[f͚>>ԨQkp56l+pssښO?CCCxwR {&$$Μ9#ի SLwa@qATRSSSW]vejkBCC̚5 WWW,,,W/=== pwwqƜ;w(^θQFC.]IIIO?憝/:\Npp0{mll &&&L6uһwon޼ 6v|WZ1>C j___ 3f̐*Ą_h?&)~OLLYfXYY %ʲaƌ7K._˗/,k9wބEΝ ͛K%  :rgϞ~cܸqӧ?u0`6}}}6lHDDw8*VH՟nQQǎŋe~E`mm-mѣDFF2x`RRR066&((Hz⇣^zɓr .^ȅ ԪUm{ァ%:twX[[KɑT*111ddd prr`DFFryo.%sٳԬY޽{SbE8}SQZZRٲe ~Wѐ5k$**رco?kkt=ĄK.m6ϨQXvS}wW^\~D:3fƤIT={YZZsN0aP]z'm;y$wTT?KDATD`CW[D$1xc/kDł]"v, "M JjL;s̼s߼=}wR3gdffrE"""x1۷/ km۶mK_x?999K  &&]]]._LLL |Mˆ.q gϞMFx ߧUVMTT̐ݻwQqqqJ ߊh֬JջwoNR$))ѣG*8"@ `5j&&&R NqҨWm۶ERѱcGݻ'OMMd̘1ݛѣGӵkW,11QfQֲ":t@WW֭[p7lo߾'r1,--eu~)vvvs($ 6...ʗ/O~d(>&M$mt l2._ٳ۷/(66p>qrr%Ӈիcmm,xӧO'@nӧ,VVVRN&O,u҅[bjj*G^^^l߾ǏCnJFFbȐ!x{{T* *6lQ7B11Abb"Ŏ 8;;cooObbbךs y899aff,J*U?~y IDAT+RSSϞ=,%Çb̘1={Lrj9w+WIͥM鄅qaj׮-HyÆ CVcooϬYxSfM7n\h *Č3ښΞ=;...?~\oJ]'FFFXZZ2|pT*&M"**J:h"nܸAFF{yЩS' &͚5~xzzҹsgLMM'OԩSdee1vXZn]HGQܘZ?~PxiJj?kMKK|@g8ynccc<<BHQ8,^ 6fQVbuo>iڴi:"3srr@W\!<v~-))),]T*}qF<4h 4mzf̘iڴ){ye{ {{{=ʾ}#jzꡧGÆ d]ҩS'j׮M݋gGؽ{7sΥ|ܹ BVVÆ ښ &/ůhٲ%gϦk׮b q3f?мysn߾]'CQ<}X6qFhРC2$]xSNIkԩRkkkظqc9::2qD5jDv GZF>j"--5kHu-[Ғׯ_u'OpQf̘!{ٙ¼yϰCN8Q\F F,˂ ͥzhт~1`Y fTz|2 4|̜9m۶I3gRF hܸ1cǎ╜;wGѯ_?ϔ)Sʬ;oooN>-;jG222T+WӳF :իWi+,K@@TL ~°xb%|sIII)8 {/6K_ŋݻ7Ŧ}!((H2DE֭߿XmYظq#AAAkQgYt)W_}cOݝEaŋz?Q)N>+ٓ_ @ x9֭ #eڵk\|{kQcZfҤIWs}ө_>/_&:: c _}@ /]vZsQ-[&e'[|}}ߵۗϟceeŏ?^ @ @Wlݖ-[AIg#^#4lP!}"@ $ڴiŪXp{a9  P(dq<ڴiڵke}̞=iӦٳ7gx:u[[[BE_| %oFZPTtЁdٸחڕհ+ƌ%L8Q&ZOOOv̞=Bɓ'tܹ1ϝ;Iiii1sssLMMرcoʕ+3qDrrr~f͚T*:ooo*W1M4ҥK!!!ԫWR,/J5ksN,YB +j5BUTaܸqeN\GRRݕ+Wh޼9TZCx a@ |TXӧӯ_Bu0wBu={D0k,:tFRwZXXh8~8h4Μ9# CѠ˯FATJTV+ҥKٻw/GnݺEVʺuVg]gϞѴiS||||<ӫW/Fnݺh4s Ν˭[>{eƌ2ݖFLL_k zzzlݺN͚5:ޘ1cPR%رcFݻR}RR;vJ*%ˬ,zg}F||<?^6Kh4 %fΞ=ˍ7qfӦMejݻطoիW/%1rH;w&//]j_ٳiׯ_gԨQ^OT˅ x!>>>tU2jܹs>}0e D__HwΘ1cHJJbΝ|Ǔ)Ֆ/_Njդ}deeq!JoO>wL:x2dtܙ޽{oFzjw0@ >HڶmK׮]  AVZ:J%zzzZJ%CEk )]ˡC5k.]*f=2_DVCNqdڴiۣRdjΜ9pBYِ!C0i$,,,]6QQQR]\\ƴmz۷{./^`Ĉ3~x#֭[W@ @p=ӧk&$$DV|}} {{{233IMMڵk<,q?1114mT_fȐ6?ڷŽ{.U۷s!nݺEPP>cԩCHHYYYҩS{ADEEɌ9NNNGEI___֯__|…`ggǾ}a̘1Rĉ%((H8˗/y)۷g}JHH[ܳp]s[zutttw666TTѣG&;UTڶm/#G,va@ <ʕm۶<~8tTQQQdeeѬY3nܸÇK=BC^سgѱ7IQ3x`Pղzzz 2oooJ%AAAR5j`jjʺuߟ,2227oBhCRIשT*JGKڵ>kԨ!OsNsi888ZFRaeeERRAAA0p@tuuqD @ M6@~PУGʂ!Y-[0w\Y Z-qww`ffVޒqdϞ=x{{pEɐ{aaa!m\O*UNbɜ:u,ƎK֭y5n:,-- SSS}5>={DRP(h4uF Y|ȭ[pqq>999$%%ImGիW6l(ˊ+HNNJ*L2={GzQXVqF6mTPO?W=Ϗf͚Q~}<==ܹs!YKe˖jiiIJJbX[[PjVVV<}:BCC9v\z|XS###|sssʒ֞Zf֭$%%}vM^J*U8x8"@ %ϟ?_e(Jw˗/9}tM*Uhܸ1fذa҆GV~qww'((́W5kƳgϸ~:6K!k׮̟?p)NϞ=n aÆc``/< 22R3,,/^~z޽+J c\Gե> ?($yT\Y고!EٲeKG^^ժUe˖DDDH(DNN#F`ܸq̚5KZ*ٳg\pGâE,3ݓLDDJ;;Bצ3|rrr>}UVHtt4ƅd- ===N8QhZZZtPd̐?Rn]<¢5?ٳgBddTTJYmrpQ U,2vX^|I\\\zBAvv٩^z%_,aaa!ꫯС [ʗ/e3ժU#&&Hy#//gggs5ihQ(jՊ rMiӦ߿ggg7FG@ $999dff#ײ333lw-R˂!?6uoGbŊh4233̤K.Eftvv&44www\]]eA&KC0uܸqJGj d 333lll$]{ny,`+W\6,,L 8b6lXhT,]x.\@PP})U-YYYn333 e0Z*u֕t֍ϳ~rrrx[nϧB ,_???*Edd$III,Yׯ.7o!+W˗|wtYnDGGsqYƐgϲfoߎ|˗IMMv]OYO]veŊR̊ݻwKHS{?ѣ#F **T֭[ׯ㏹r oߞM6GIAkj䱰w^sovv6޽[zƺw͛ILL$66Vٳ[Ǐβe(_rrrٳС=bͤ2o|J%QOIIARѺukQTsnnnT*rrrhڴ)*L:Dnd=zAԔ+W7ZFGGWWWJ%j*S-ޜ>} lп<<|LGJZ޽5kPR%222ժU# OdÓ'OStJ6++aÆamm̈́ w@ZϚ5kJӱmW_Ɏh[nqFhРP(055 OOOT*#53f~yܾ}[eHʳZ IDATSV-Ҥyۗ.]Pvm:wLNbŊ޽Zjaeeݻٿ?gܰѣ۷Oj[.;v`ѢEXXXУG`SF vw}| k֬!;;QF1k,ӦYf4i҄Yf,X333ڷoOVVV,,,صkښH6o acbbh޼9*T`,[ J*3l0CysUZʺ@@@,=@ ݄r'PIII`璒[+6oJ|||bܸq]"k/PT̝;]?ˋ/t۲ILL3bmmMϞ=@ @S)@@ x撒޽{ʂ-qD @ xOw&&&T^!CPVw-@"//3gbaaAÆ +tH޻@ @ 3iiiZ _!aaaZ {@ RYdjS@vZ,--P_I #@ P wVVZ>`ԩ( cccS~6j֭['gdd0j(,,,033O>oy… 29 ~,YRbۜV&&&hтk׮I . J3[nv]hSJ֭ccc:wLJJJ甑J}oYd 4z ZM@@<@PȼkԩC@@sssi׮'fffѫW/t8"@ ɡ &L@Ѡh|>x .d̘10vXn޼y۷+,Ҷm['6''}}}8@dd$nnntڕ< ?͡Cx6mbܸqܼy={h5k:t@0o<Ù66ZVk$99W.+0`l2˓'O;w.nݻ3ft[:x){fԩcooϐ!CdݻW>DqD @A˗/ՕuuuQ*(J \LBŊ]6C )&]WWJE^prr"<{{{F 666TRBABB} .ݻ7k׮EPH2顣#Pbkk}+Vꊕ'O&66VÃ5j`nn-&];N19sOOO022/ ((Hz{F155eمGpp0}ёf͚Bvڕ۷0k,,--Q >ݻϊ+6m,^իW. 3gĈXXX`aaA`2ٳgӭ[7rXYYaeeŝ;wk,XC|2ǏgѢETXӧO矣T*iҤ -[dRÇSn]u&(I8J%/Zŋk޽Lrښ28yJ?'&&&|x{ RE@@...ӷo_?.)J }_h޲~\]]ٻw/׮]_-;ȨQZ*[yVy!Ç~GJZ{4hЀjժ1`.stP}'c(_<_uOHHر#yyy׏dݻԬYuqyj֬ݻw'33T]ʒ#PS*ܹs3l0&Nȏ?V:(iNCLL 6DP0~xy‚:uꐝMTTNNN NNN2ݻweW6fݻd@ ˡCHJJƍ.]p?Nvԩt&((CRreիW~֭KŊi׮SNӓD;7֬\Rɓlܸm۶ؼy3/Rɀ ]S&Ne36>>>:'gΜ!444.]DDD#FSL #%%OOOڶmKHHxxxp ެ^Ǐk9x }"-xzzD֭ˬ[3rH,,,5jd$a۶m̝;vZ*U$kYΝ;ǘ1chѢEjJvXz5;w3gΐ:z(m۶UV<|[nYФIisĥK4iL4IJ$yʢJRモ6?K100 7n$<"55HZ4hÇe˖JRi*kJsss)777O^b_:::\~L͓'Oh֬XZZbee%sf188sΡT* ;;`裏Xv-;v$==/JKKK~xO+:t耿?lڴ Bo-999ƭ j={JAi!?Bxx8+qp}i@\\ :/Rjժ:tgϞq=z$ŲիWTPRIhh,CY)%^Kffl})IZ1cϞ=U2tڕK)jժW_ADDw%))XCWA9q/_ǒ')))3,[βx)ԨQ_wwwd^#ŭDmrI頸yăHOOgذaдiSjԨSӣzԑJZβv111\Hm R0@ >hذaK.nl޼(>}ʦMx"ڵ{l޼Dbcc@accC%gd .ײXZj5.\uP(}n:8}4gΜ3JXXd)i 頸y6mm#<<7oJ1Yt)\p :tGؼy3̛7-Z`eeERRv<~X ]fR0@ >x5jÆ :t RjUVXA``I۷/]tvtܙN:5իqpp GGG4hXZZJZoquueƌxzzynK,777طoܹ~ ###T**J2ݽ{6m`mmW_}… c{ER1g>JySjUڵkG:::ܹٳgӸqcݻ' YG\~X7nFQFoߞD5j@>|8 6ˋ.]ͨQ5ktԦYf4i҄Yfcvލ%cƌa[m۶7 4h 7nLJXf  ŋ2w\hР ]v1<<<С˗g;\bccZjräݻ)nnnTV)[raINNKZ`T^-ZЯ_? P<%(1o 3Uv]V;w.\rhB ♒Ju֤R =+ O*S(رM6QbEtuu144ѣGZZJZj\]]Q*ԪU ''2?KI:(i%ajjʞ={XltޝsJqbf͚3jnݺ1k,J]ڵyammMdd$7o/^ZŅpBZ#:իWi+.w7\ aaae !%%%Ez\4o/@R1wܿm vZ9R@X[[s8Ͽg2`bcc x)E(>> Ν_-Z|)@ yyy̜9 6ls޵@ ?!aaaZ A `)B͚5Kyf:wV}]KKK*T edcjj^ٳg( YF"SQԯ_cǎk1 a@ |\r]]]͛SP( GPP Bŕ:NŊt钬,//CCC߿_b *S(L:MYL/===jEB;fB͓2_̜9hO>< E7sΝťPynn.| UVwwwN8!O>)+IY3fL!Y9"/^KKKLLL6l,m,@6mh<Ν;`3gRК*%KP(ɉoV8N:9RÇTRh$Qլ[NvMNN'Oŋ<{ 2=Ѯ];bbb)ڿ.]0awbjjʃ2###VXQdJ;;;T* 6$""B+n̞=Bɓ'|g山)h]@ %77'RNY !!!DGGrSڵ+LF-j]V I-D4 /_baaAϞ=}FÓ'OުիWs!NX6nHƍ mΝ;ҫW/YJCCCYٳg޴ry^z%lmmiذ!wVcnnP۷/-Z͍zK%2o`Ŋ2j(\\\hԨuUpttې2 IDATۛAl+ӦMKϟOAA1114k֌ڵk+W>#:: ; trsңGlll ڵk:WF j5{ˋ#G%4pRӸ)/++/(B۶mYgi۷oӷo_lmmi׮:ubxxx0f}]6o̹stFu׏:u;/ӦM_ښݻsTz'OƆƍ+m%ְ04 +ڶm޽{)**kSTTĻヒ~~~DFF2x`fϞL{p4I~~>h44 ݻw/;B!x,̞=I&O>YxzePmڴ^cĈܺuAtRj֬G9ɤ 6mƍٓwc_yٿ?׮]cXXX(ۇ'|7ؽ{wO ٹs'W^I&篿:qqqts,]3f֭[QT;w8<==IMM%55Uyܼys/^իysEDDիW:u*T8}N'ˋ3gP\\ȑ#IMMUV̝;T6lؠWFrr2}aluGf(GHͱ'$$W%EqA~J74k֌;w(S?9y$=d̘1|w@Ɉɓ'3sL;wdȑ\zsss&NX9;v")) 333l޼E?___IOOСC^jʔ)hт?gl裏~:L6;v;a~7W_}[jǎ;׹p[.>Uݻw)..?S\\ k`ڵ :4$ղeKvglذ~M6o6̐!C#C ###BCC133#%%E+We"""T*8q7nȈӧOsMկ_ccc:vȤIt(C U9LӦM4zh&L̙3quuszSʓMRRǏĄسgO޻wO'Z@NN7o/'Һuk'n"66h.^Haa!ƍJ,ۗus<4GipttVKVVTT ХK5jD.]033cz#6l:ub„ /{]ﯨx ڵkgP122"== u?#F‚vѲeKcj5Li:W&&&Jٳ?O?3336lHNtӇ#FT8ҩ<ׯg7J#@6lhPFFFܸq3g`llL SuB!x:t2hР { 8vMLL8p ֙~PvH~xNp$>>-[*C֯_`mVgn .dԩӻwodWfϞ=ԩS~w2O\I\$wPF& Q*;ޞ&MWփ,Rjǎ9s[n1u ~1@#Իwo4 M6… JG}̘1 qww'66V9ݽ{7wGSZƃӐFEm`jjP\t (9T^=իǞ={toooo GVV5kGȑ#lݺՠr_}UBCC ޠ<9I#B!s!=1*ɓ'+={6/r<Ʉ~!#Gd%K.78poooP`bcc /d)5^{5~goQlْ~ZjU###ڵ+[lֶm[^ӧ355-wCYV(m {{{ bP@ё+W Jr3'%%E/ke}뭷Xv-w H5k<1:;::l+;AXYY{TU\l )++Kg3SlPo2c -[fPc;;;j׮޽{+ܧ6(((899Qvm9y$NԩS\t+sN%hkH}988(9R ԔiӦq Xp!AV !Bΰa(..V^z?4hQFqa޽˕) Pҡd̘1̙3GI `ccC5h4T*ٿ?ܹsׯ$,4_~!==]oTvIAAT*111ܹsGPv>gΜS4xr֔>y~PU\\\[flܸ7oMtt2-J:yyy?'|BAAdffҦMO(** ,,Xs18?_CDD˖-0U^=~FQ:5jIf a._ RQAAA,X{O?~?5kcƌ᭷2ZƏOTTIIIܹs͛7o)+ i3gRPPy 4h@TTܿ۷M߫HE fҥdeeƺu **?3ǏW}tBɊ&L|||TPN4 G 4@V@vx>1bڵXZZ<};777%1QQQR^=X`r\xx8j'N믣V0+Vh4Xf @IHZ͏?ĉQʔΝ;{ѭ[7pppPzٰaѭ[72d}۶mCV뼮\?ԩS Ņb>svʠAh޼9gϞ=Ixx8ӧO_^z%͛3J ;'N~z~7Zc V#ΰa۷/I&*߫jKKKd^3gPn]O n/=z{)Z'NVeʕ@ɨj֬Iv54U׮]ôk''ÆT|J!ē%99FSx$rrrkyJ_'̇a!ػw/+N[tt47of]'N~hٲeDƍCV,O2dTAYYWؽ{M4ٙP9"B!xnݚD7n\Uw}(**"''u֕I'!B!SE,bh44oޜaÆdĸ+ B!Bװa6lXuWdH槉]j'#GB!(ˋ]vUw5Mzz:]Lw֍:u,oJؿ֭[hZ x4j7n@RVwU6B!OVZrJ̙CnǼ꫕q9T*wQ>kР111a޽ԫWO`T*Ϋe˖-[ヹ9p!x֭7oA+LJJ:L=z]Z6={дiS@ɊRsWZE-011)wjӴiSݻRv5kwQh֬mXSO?GGG8p @RѮ];eqơR8tPmp=|M4 vvvۗ7nwʔ)L47n\wE={6*;w*s)lllHMMe^c^^:8;;3{loٲexzzbccC۶m9y$|R$VTWYfaa!o&ԪU%K(6mDƍŅq)K`aaݻwظq#*J'Ըqc}: :uC1c e{vv6YYYl߾gyF`ԨQ$%%q9:wx"p‡iӦqqΞ=˺uSGFF2~x:uꄛb 5j...4jԈuL)촟;ҳgOj֬LG_缦.^H׮]nݺ+ҿGZhѢݻwW~cM6%$$>B&MČ3X{+(.KKKԩɓ'~.],}\:UL4x]uB!xbeffoR~:G͛o7o۷osEDDիW:u*s'K/镱b ڵk+Pґ,qwwȈ3gTZ333BCCu:zj6l?Mt>oDDDlQ-[$!!k׮w[n1zh,X׉[t)3f ==[R8z(4o{{{BBBC^GPn]eDQQA~ǤҪU>8s2wެ[XcPǎf͚zBJ͘1T֭K||<9ҠҥK ?{i|}}IOO'==Cakkܗ?< 4 11|޽NII!))͛7{)SN>MYp!IIIԯ__qvvΎd*=Ν￧CeĈlݺ7orY=Jǎ4iDy?}tbbb֠Q"[޽{9r( nܸQ>* ӫW/eժU;vWB!Oy/+5jϧuo\tI'8rJƌqtt<$%%1~xLLL ɠe-YDcv=FFF=h4ʫW_}իW%~z  zO3f cÈ#ٳg1qDZn]`6m 7*[nKtt4/^q\oōXƭ[شi#))kײ|r;<}j Օa:uw|ym٠3'//OlС|7_cnn.z喽gdRDDokؘH077gW^JIRqe޽qqq/X[[חÇ.]0vXVP#??[[[|}}>|2R/*[o1}tf͚eT###zڵkضm:A /_AWz$8"B!;_|It 6D$UeѢE:z{{yf6l?;v;;;e{?|fdT: JLC8p4BBB,,,UHȑ#ʫ瀀޽Krr26mO(kYfff9t%I# ӻ}ܹs˽FXXXХK7o5ÇWھ}r&daaqqqAҷo_ϊG377WTjРAbPymו}~'Zl0>5j9j>*VQTz>4Iv;î]gԨQtЁJʎR211Q`iiɝ;wؾ};o֩1...tԉu$~ ^{ ƫw… N"X( R~lذAo UI\0%8vZߧݻw @_0j(bbb[=j~UV) K{=e˖=#-M!B!C۷//8;;<^|9M6g9ۛW^yƎ7fܹXYYѥK:v숻;J[YYHAA׮]S$W\I*jddLW(ŋ ӹbccNqssS`/j*222 Pҡ.*Ƀ)es0tPf͚LTy Q===uƔV...zAxzztX˖kiiYwisss%Bqq1o^=\/)))% - {9_ٳgक़}||ʭσ#_wNBɽNJJ -ZP-;cϞ=DGGHNNyJ^C2zhRRRwYu_)..&..駟%E~M4Q;6m˖-)# #GB!cZIXF qssS3gƌwߟ&M166e˖ :STT|ܾ}͛7s5j#?<| ǓI6m2]\\(7knn.V&޵kW222Xt)ׯ_磏>]vwZ^}UVZEbbL-K.%++4֭[gPyUqvvw)ȉ'c…o^g|2K@ u.-o\|X֭[ȹsaʕtx{{&Rۢ ...zӪˆddd0gΜZu6pss#<o߾,\+W{n٣Xp!{ڵk8p/" ,\WrvAFteʕܿWyf%/ 66u뒗GAAA$GN:.M]Zj憝ʿ&&&ܗ*XXX`ggO( B!x̍?^eܹѤIyg+D|_% ׬Y##Gdҥʪ*+Vh4Xf JY 8???رcmݺuӺukk4bcc裏pvv&%%塒c6k 5k} Ϗ=zн{G2c۶mjWUӋyNJ`` ...(]9믿%+ joP,Z(ybݠAiҤ !!!JW^W^xxx`ffme&O̷~K6m8qDΝKxx8VVV$$$йsg{=ṷR`xxxдiS<==qvv6h .](qL8Qo?SSSF2d<==͛|M^JeddЯ_?eE43ez쉍 seڵXZZNtt4Cɉ7*+TeĈMxx8_}}6ҳgO裏,vၿ?]t t.䄟͚5`ѢE\|^xA.NLe;v]4kL/oVUVRV?*5qD(:#]őÇi׮-NN7\MG%&&FYN!ē/99Yg,''籽2 Q,7OEetBppDӬCDgqơV6m?XC@@G&44w^a$ӮtƠ{4i32rD!Bj2rD!B!O5 !B!&!B!B<$8"B!BGB!BTB!B!jB!B!SM#B!B!xTwB!B!1@qq1fff8;;uZ+%^;'OZ\ |-jB image/svg+xml These SAML tabsmust be active This window showsSAML Message This window showsweb page Selected HTTP mod_auth_mellon-0.18.0/doc/user_guide/images/saml-tracer.png000066400000000000000000004113111410105036600240300ustar00rootroot00000000000000PNG  IHDRNmP pHYsodtEXtSoftwarewww.inkscape.org< IDATxwx.M((v vl}.,XPP@D*{{1M6d! }gdΜ9s̜9eT(T@l/X:NB!b4'MCNVJӒ%K3fXٌbb4jB! OVVh4Ko'Ւ%K>=zXшbj6v|B!jw޶M6-.\Iվ}{d2;B!BEYw>Ek׮vڵV!B!DE*GƫKZ#$B!hL&SbXB!)ZmUKA!BQŢR7v$B!MTB!.IA!BTB!.IA!+}Ռ{97>w?ܦ-[8lWMRi=GbTl 6Fa&71LIMB!.4RqB˂_y9r͛5k. ٰNU`=Us՚MNvÏן{y]B!oƎB\l-FÏ|CttVtTKJJX~;uX޽zVZ|}9; $;;avL!.rkW7XS6nތdo^<b0n瞭~ !ąUl,{f0wPT<4 2&땮JB|F[ɛ˿;]Uh4 p /<|E'M_-Lb"k׭cUW]x^;ac5xᇸfU?j6jݺFߟ W]/:_} L6^6V+OqG3bPVY+oBj[oҼY4 ǎc3x'*2wTAZpu_%,} C dӢlܴ޽z˰2/XjݺҾ][gE/u< {ٱsD޽{a?YV^|5Gc޽DFѽr]v˞tv7'NdӖYBTG^Pi8v[:|v69ֱUnO!s::zjTxxx0nhƎ>iV|+W+Mǎ&6lȨ>?LnM+ҹS'~p:EL(-Z nG:|֢YzB\x223y8y>S< >z]Bk^8!D۳w]:wB`enxͲYY\5q"+VvZq|, h۲NQ+C \.m:~}zs4>YmXv-l]tzQim! >7Y@1Q]x?JG }L8R((( >>;뺂V UL9eNժtV(кX,1tZ~m!׌Y'_m1ZR@at& YE/KO4 \*~ޝ ^`` iii̚5M6PV%%%l޼#Gw>n""%B!.LJC*.jHJJ"..A6N4qۻw^v튧 Z6*\3+*2.2sPT*Vh޽+KUW]W^Sr̼^1h4*ҭIRkCKeGؘ]/au"m5wVh4Fh4Kqq1fŌAAGaaz__ !j[*V Ì3V˨QضmQQQ/1W<ȴi o߾ٳ]ח4/Tq~!>]q-kqrQC%Պv8X,v܅hcǎ;vCGb0xsdED~zF^E֡h ! w)=ii[^Nd2j e˖cZ)..t KFk0MX,<==h4B!. + **/jQhӦ Gtؑ6l.kС۶mc̘1ѥK=JtίqRy@]q0 U+_yZU6׸J\+YvGAV8:v矡WFAB.]ؾ}+LJ|>^F5:ZljDN$o.$PM{zu"@N۟3i?ۛ,} orOwU< IMMC`Z$""(}MKJ~6G~W8|h{'C $$8[N[.uB!g喱-ЦMnʉ'{qF Ti;>4[)|о]\cc $,,۷ӧo_#1)X{RͅG|skwXJl Kd[Ϟ}r-QGNN$'/oo&%J˖- CWzh4*hڤkU3L3q. k۶6QmT;vd׮<&MIˢ`B֑lLyA0ϴ{cFi&>Kheo>:tCѡQkhw0l`Z),,DWC߯r󑷗|M&9VBqZmh4: FEزe+sؖ 5ݻ@еkW{6[ht///ʟCmC.N%}|G1+mHvvaq t\#}9>j6'Ouӓ] Q90O#Y.>V4V=3-NG`%0c<<҄Pd,b*RLaNqztCvdfeqíwW_+WtϾ2۶`лW^}EJJJf߸|ڷkEXf-?T˹~ҵ0ظi EEoC'"OXX($4$i*UW\xyyLzz:hZL&e tQQ1!.5Td0)((W.'g0y$&%Ԍ|)^;Eȗ_o#*2v-oWެlf~=K!ܼ\;_~GU>3II<3~]SfZf}9\BPP`IW]o|R$'}.^`lݶݾLN C2rP ))NwBHp=aӦ-[DEEa40Mhh"#i^w5>14:lOs<:5τiΞɓק76o>geU<<>b[܍VeS n]KdD͊kxm7qe|VSKPPPXX@QQ!AAԢrǴ?VJMMc}=vcİaܵao;Tuk^e\Vt"h[z&'7a;i??v3x'''o޻Ӣ9^^FsNqBFj' @ !44????Nhh(V¼y֭+-[okozǟaɟXz}paҥS'"#"xi$̙ @`` >>dffA`` O J";;>9-iY]1RZjELٝ];3bP=j$fgotu_Eſg6Tbޅ;S;c0ddf]MNۦf3e#{_~G)(,weLȿ@Ӡ OZ[&LL1oMمN'&8PQL11Ǿ6lBdd~~ӹSG~e>'OeLZ?1)ZMvecKZ-;Lb"c.%#3;z׎P$%hh[ )殶ՔLppIIIЬYtu덓9N)=bTzZ:_zݫ'O?#]ʕ\Z%MFVצZvmېt,z֬U||Sj4Ĵ}\*JS q(n6ٷoÇŋi*8$=*g߷/χ´;Ye')) F]}CMgjv%+;gܩ>zxx)33 vm*"PJ 5鹩Xbbb.//=z +[ C{ǁЃ x%4$5k&+;v~ 8Inn2LP #Y8D~A_̞Z1La#d䐘tejѐ` ǟ~Azz:l߹q-p<7$#SUxxxpIrIϗC9}&H|1{蜶j&[QPP@n^gS-cC 9_t6B'*ׇ oDNn.{g'8(Ka7ߑIqq1#'7*jZ)T\\LNN-Z`_5kҹs' .2"GtО>uc.eҔlf~)ڶ}۶5:;S>ݛo>?Nn3WUd`^PP@FFIII:}cǏ.M!]dk-4rr [F#EEElݺ+?.=$,[KཙtkMW_KNN{՜oy%,,k'{.ӛ{_oƎMXXm׵#GOLL'z-:b#՛܂dǤ.KM%PhC+ /ooF118z,ye6ma ___6n+d#ǟմn݊{; ПXmwMkNߋr7< %tؑx Yd)|<ZGﱇ/b30hѼ9N@sBW-|+oH9Lt-3v݋+V1ԡO?wq+3m qcܱ TU| #-[`=wڿ;;<^~M ڷk?^Lp=wlقݻtlo]|<J4殩NqB';kX(..&333g%<<]v0p%և||a3}waZ T*>y!g}Ĥdy! ޭ/?=n8F/Z\©'qb6H~~>IgϒJ"ߟ5kV3q:[SHWUr_F>b>>t+ݸLV* :ZENN9f طo/mtt[¹;Ӧ`?ƀHJJkW7MSBB>("#Ynyy :^ZvygգGm$%%%\| gim6+hIIҽ[7JJ3g6OQM%]Y6ԫ8غ'6y{{{ԹEQih޼9Zmm}ܹڂHan1xuuj1 xyyV[ص:u-t4oDؙectkBO?e,223gƍ$''3dI id8KII1gfqu^ihJܟUbmUfLF#EEl\qm.)=<<;v4VNGpp'Nӷo?z-jrZ:VQb`0r@CyftJ>8?k !P(OPP dƌV,#g)%l6STTDA~>a̙31c])n8BS>06R[4؞@pQzZ?`͚դ&uv*-[Z{@K(lB4X,ʵةS'cرr QǬ׺^^^,\+R٧bqYk``ǎ뢟Deرx#::իWq뭷j ?u"L&"\s|tUBQt:7ov8LaQ1 i݊=z4h ~nm5NDZcǪ}>`$;''3qD5nM%]ݪ8jJ@ŌXnjVłZK˔TFÐ!C8rgݻ7zH`( aǎ2d44"ssrrrزe3G bt:xzzNc&N5LfeJd2\ibQfX,DGECBBZFQաi*峋el6hӦ5EENkaFZA2d #999EUqh4 >r[nSٶjՊsЯ__T* 0@05eO;K/O<==r!u^_T m |۴dB؟V/a VjT_jhTF4-Z؟PX2h4L!Dj2IB!BTEA۟z.DML&zD!8DppiiiRy5f2HKK%88"B4 ҩK\IJR'o4;:<%*~BQSo!B!DJB!B B!pI*B!B* ޹s{cC!BфYƍA"#B!_? U)L* !B!\B!%8!B!\B!%8!B!\B!%8!B!\]jª0?_ e ?*28^h4W6ζ]9~k}uaubZ4iHc:\>68?(Jrlq lb'O=$v\b B\؍!PV}K9َX?~Tkp~vȹŹ%rs9_jtVŐ->L.|RkNlYU>r/<#`]5SC)M r5{?~BXګ j=Slml{1,s-8YVug[]_r\og?#V9#DEM9SW)I=p}>Xi$̆?TZܹM|h1Q\UYJFV\ Qir `Íw<?{5Om!y > aŐo1Baҭc el?XgO\Tz\}k#AǕΖMꎝvfJזaKqP;OposWa_JިKʝSa+UiQ{iZ⼺`E59v09Sy9XZܽ0 Tvm#2-p]ݏCulǮ"wjJyUo)\>dVRmr/kHݭ\xEiJ:HӆRt=IYni !~ۃ~P:5_ >-Tjo xZݮ4TdlԿ˖(* ,*3B^%rcw*>2NgB/Q22{C,NQfJ8WU*+JA(JSYIڹiEB>jنgGvCJus!]V4qWHe'5Iӊe>Hvs{%$|Z:?V}![)=B/~+(Ę O9)r$C*tɍ۔ wUIej 96Uf胕2x[]Jep uCIaK,(bro}~AQk J;1U1?n5(oa¥0xe )ŀ JRʴ ciNf)Qj¶[j]zFߏ l0 +3]O93]9QG_syU|}GtzP)W)ٚ]TfWe&s}PJUY:Togyl%~QtBǹiEB>jFϔ*:3)tj[MU{WiZLQ)e0V snPuS*(On %s` {r+/v<F+I]#+ N/,ݫi3bR;Q*%kʔ&+ wɹj¹6Uf5sR>ovul?uCIn@Z^x6UrY6Ý;3nܕ <_m}uO݅/2_tǻҢiߦ8IuY%^,zrQUGd# _o3h >}zYU=i;q9:~Nv]Fٶ+ ]@wa)iה):h1Lj|;x.ƷPbϓjL?ȷ# +;AVqVʓ碦ǧ)\gvKUǪ1|\R Rh '?j]7Ljy/W,Pb. 9?>OX'm'8aٗ<zV{X}|5nXd_&36n=q IDATR[3uTVϤNPkr&.NoyiԆ{~%7-qX.)/WֽЖCϗ/cH>*TL?G=7qXEuR;t󇿇?^Z/>3BCY}|5,b.=){3aBq*C.+VZHO8QGUڞ֓׾Ȃ8/=}nv_V3rNylc?Thx&u8;E?3̨mԤ^QUv{`t Ĝ snw.V3w/WFB_4/抶WUkyjS\Uw7:xyӾ:9Ys*CqPt c$nlf}>M0;3\e3҅e- l6)[}ܛθ]q`xC;VZGCwh``etjńltV׊M,97/6L\V[ɽ}expM *xrœ\z<މ'?ăKtnF¿RQޮHOo79nf ѝzG Kk_bzn.N?C]r; C}?O'ZO'Wc֙qZ5[U |#NC/jxT=NZfcp*48[in3XҚ=N;ٛ(32{+9;X3A{1xIۙmw`}~ʺWhQ{_gO8v;ADɧ?uXN)2GiA+Bʸq*T̝}qO:w&!l_22i!~O珣ל9EOCXOڔeeϕďǣc pwPo&N0ԌTVXETjwerwCrh!K} (Գra | + ee1l0v˕O` [99g_>vI8]iA~{*s0",`jdf0;RjB#BYзF_Gy-Cm֎r$>noJeaØ5:~W&t'!lH}~aLmތUa Ճrykz3+U m/JS(XNc(Ͷj|c][Zv3:`~|bgeǠ>MSۣڞz-,3Td?꽡f_IG =;xI햶JEa&䦃]9p^EF^ kW ֫oKJçiJǔߦv9a. x("9}4pE:!W\\9Bv[`78Tv gIl3 C1UYhU#:T3y/˩Yk0Ӝn>OhPkMy6Зغ0yNٟ^)zԓwOk!Dž3nF K{Oi=(_dMbπ=Džs1"_:Qel88-oK.z$mZw O)N'iZ[ӵA=kzӓ,6x7~F1885H'u&f:LWظ{Jۧ X]=˶R%fA"iӌA?3Qob]܆?nvnsF;:Ko0l~t}2nEK =j7xՅG, 9h:w(߉U:kc/fEb}ql]c]FVE;+3hgDŒYF ."[cFZ-}3YW9y<iu*ʤ6_I +iS OnD\e\l]hm\Еf5׋_JNh%zQٹ2 {|~3g)ZIO_@Ŧ*vXij' }@jйq5INJZe%짖10+ry{t͇7xRz o(ԋgvޗe:S99 ԝSkTyDߊ{"yTqZQ7Xzăt˳CQ|u>l!ܪ IC]r(szHuf. j~x;xc`lɚi>5h#PK6);KLh0g}S˜^M9C`ȯ/Xإ`z֛>_)6^0ocOiteY=M6,պX(,*K~/Q Q>w5C18] _sS%.’ycџ{z1 r BPp[ScBi>7UIy摖ƔvSdK|]#kaL=$j/m[`7( aiJ%YmQl9]8ZEj=Ur)'n`xp~|G6/PM8=D/˟J fKXk{h\V MI*O<@GHOGϮLf`vaXauw-F£%SJ 0 Uf=$OWz?g<<9t0vx$F&4iB*_baCR2RK ;q5P(kMW_XqC.γ'}aCǣG7 ozHٕ14l(/q^?_Im'Ѽbs5e=IK؈mH/ -~;uJҨ܅k5^cdHn"=7$g$3s)x1{w?le6ˎ/|yv/qL94 Gqca6);W;W|}3ȢNC~eS˜^\A192$z0&\ғ79_))6^8ocԧ J_9ꪴsqcЅ 6YKF켴oGoNWo'q0n,wґ.U0.b`oU-> ^'#7kJK+/d([.lX1pnC6շ{Yw,comjl٨}QYܨT ow?嫓DߟYgz+o=ra ~ӫc4(j.$˿n(WNZ^`x7{y$ŀ27#`Z2R?eQR8X{V~d:բO}z*]gd*] m;jW7Uk0:Pmq5j%;ށII,^J|G#m=Cڹѷ`pCaAN8=VҼbs:|߁T%Ajй__}RY+BaAj=HH4u҆U;VBi>}5*vidm﵌Kz3|}vd7dљN~4/(R8t璤C""uF.M"4ȧ GHssᝓ4XIQdHτk]~ژT$sPܽ{GU©S' Qp+<[^"x:Uw*%vL. o A =Y:wR.æ~`e k;k ߕ??6>>+- {CfߦqzB"7prؗgwpt_k^ѽ+?aC \Y"Ƴ//<3*ItI742pq;<Abi 9O 4&g_頷̧WnϧʒK۹jNؔ{Bua4zJ8[ @ U>]}zd?s rW*GAIc0CnVI!ܓQq㨴NAu{4gA<W& N+) @ Q-iZZ \˖: J\.Sh QzhNW# DK+) Hw6?~=n8dX4PXH_*Y":π'?cFE 0 +灟)X xxՇZo@πZ $чZ OtOY !} R' (tDA @ þ9 $D~Y9ߕd/%DA @ 0F.` Qs^֎]9nmcKq@ L EK08@ @Ct@ QDA @ Et@ Q8;TSfz.˶h*yjM+rѪFL?̼L<ӽ2Pyaemܿ@ .\,auqfiK/A9>@XA^+W˧{?kN_9J9A1UAML?|7\/W'lʡ)T^XdmϪjԬ9N,*̫@]L4*ŔN:gf SgyS/#U]<.qI\qu7@*^{:ƴéN14N9I-I-pа>k~|G)cUILd&HJ-ߓ%P.4&_7,ra jBa!&w2;r6?63,"fTҽZwGMzJ:~VpqIޫyo<"e/Iл#69O~Z+s˶nF{[C_2@xwҺrkrUF^:_ỆNVU|14[p'kNaJ)Yٕz=fojeaEIۻ3V\I(]Ysz R/q8q \vxEݖ IDATL;<]Rqi[J*TJڼٕ_ֺ,ڼ_yMqRϩӋ[ete=o2dǐR\9^ >l<93 e=i4nds}gr vحٗ`ඁ%)3T^-Y(,Q0+rZM*B5_eǗ81 6eK}QIYsz u~= V6콳tЍQ~i;VgE 8~ȸ}㨳_9\}pUdžsm[ǜIG/֑iս.KJ- rhQ|ۄ3#',B#Bٮ.^2Xc>}o^B#Bw֞Y ؈.v-trd- +'ԇm s=Ŋ+tӋ9r#3/1{P*0W9+6(W7T7\kc b~gާ}qB (#fؑHiHC[gf`egSgy>nMы!3TrSr<+0f/r3=Ce-m$Gù{QQ;0Ei9i un_QyaegLle6de!'j~=fk% Qo?AnA:Y陸't%<.z/S.ҥj@88>>Z{t&ÇGV@^g ؛۵։3Ym_NCaه Ggr ϒnKw5jsYsz \qƤq5Z{OiSNaь)kS=W?*3w %*!aďgdӑ8TgE ȍ#?14>m?䛧cD-x 9.1'j_uJ'MNC~ِ ˜^͑9W}mLA[~}kzMn=+>:Ä)z1{re6lrSc吣~agJɵ{Z.5t$Yy\Ng*8VӘlˏ/uϒ%Sd+{e6l4j,t% C'Nw$)qwg=@8v7Yδ[NgYq} |vhZd+ _͢kj%S.UW}d( M?f(ެ&-*7kILRL_?sci\{x G ??p’|OH@6le6;.q6}a ^Y@mv6Z993Ywf+z{-ۗgQ"-)"p5^)o_*.Up0.eݗ1 V\ɦXsCp`*9UšwCV^\ө纞u^:;ίޝT)rIz}z1W$'׷rS'vRairZ}vfF:Dߊ&xT7j_yr%JaMXn8"FѡJftE? &>Md8sUo<~Trdn3fdei;'G^Pյ* (-q*}qzixDVq=4iFH`T[\AQ]&+NM1W[W\Kokz n01-hMccil@Z"GC.re,:NjmaMNWҌ$ytޖw F}6y]L9x9Qs64 tغ`omoz) w?+ +\ӂ[g);?ʖwaf(L:8_Edks#wR.qXqbm׵%vx,zmi_ג-79 7>k~akPPgɷMtA1뛦n_ێޜ*K8:Dg,w?^-[2ܢZbkeK%JǞ+{`qlz}Q Q~N8)mMoVk( oEE5j4J8̄Vh:B'ݹ]:""ߩHmBa([5RօU;vZVjɶ[N{e/d8[.lao<^_g ?{Ql: U5ϫnW/njf*|3oQť4_fJ80..TtPյj3wϐʥz: tD*赱"gzsE{Q( ,l{k;/dџZĢEm 399j(8Q]ߎ!..XYXE@[5mK\ }b (@2˩ę[gb@Vmom7 ƶzYϘc[/&<2 llkr0x΄s7/T 4־LO._M9o[ 3wϘX #9`ȖJ7 s1s{S|\hS -.^6[5Ԩ ڰ)2U uW#~i;@AH`{NS2 7Ka~:wzʖg])̅~oR4oݐv ~i>@\}p|O*6 K2E.Ҏ@Q45jfGiAح0sUhӎL3xƄ)?~?obqb(i6ѷxn{^VJqr6E£^;HpU HH]~†sM,^څ/K:||`H;i<~DRzv O4!W/0!))ĥfHHs iV䐋,;uahP(Ba(Rrvx{ ʅ d+9~8oav~m`RI4؜aFGYbb8q16b˜!ԝS(Jr.7x#vt"Fl̥pvVvZUC8,;ؽu9GL;2iCmRNwv8 $g$/~[/nϐ_T0Wc6WFEί _/ [ףݟpu2r3se#^|O_u귄iN_SSm cÚQC gssp`nfcF Bhbbt)P5+ˎ/axp.$ 7佭ѣZj4 pB#B|F荣nO9rtґԎU:ҥjEN,3gUOS4YՄ6subb:k8l>2r3(#lvz]y4{Fa¿b˅-LmO6FiQKg/jcZVO~J5jDgYOmڲފz^Ү4XYXQ* 5`WWCV(P, w/$>֣l]?#}+ cmaMW_XqC.`e4؜w7Uϑ`ke[ K-o+JmcPUk9 X΁l:ndב6օJN(!Lk?YoQsM5TrDU&8h|[~]{] 6Nx-YJcFMy]ˑGTv&t@>k{u/~ Wh!29~!Wc6WFAί ɯ7滅3orߕ=VRýMmBEUn8PERI|\>oy{1cy{c:3׶5lZ6xuXP*T]Tj^F=jPXУZR3R#66tڙ"x,MSEᕀWty7bo0-W$|7yLM>wֵ:rUu}$'4KbN9]%zFGhPY4Zeγ‚Mob\8F/ފzt_G~vJ}L94{k{N(@h4"/oc;鶤!Sw_Vq!% =kww?u!!=Df:ms p92AKH,EM@𲑖Vb&w蔿T`מz5 (@ IHz6ee4/+ g'534Ke0@ q)WEVصku*KBʕi߾=SN%33S'O_PN[.C !>>FWs_J*qСRMرcjBRɆe߱ v,`v'((#F.@ ^ VIg8Ve(4 u@ݗۺAany+?02?t*4+4K#UT!""ZMFF+?Ϗ< ͛7?~(\#Ga,ΥQ&3c \ڵkeÞ;vW^XXXȆ '*!?~:M?ȯtM̫4j pe\@^BJ99Rj…5Ji`mmYz5?FP`ooO\\$_6lUVжm[bbbx7oݝ7xCo/'O&88___z͵k״yl޼͛MzXx14jԈUVtAiڴ0___ΝKӦM\2 ŋ > WÇ :jժQV-&Oա)dee1i$4h@*Uׯ.}}}oh۶->>>s)3i$ZhAvv6'00P;4qDԩ77f֭:rS/tg/~믓hrSg؉2Xϐ?CVP/\3VO,CK_@ ̆pz\ v`euY}:A HAaܓהT{ՓԄ`qPդ~znܸA HKKƍhB=5 ..Bg}g}׋+Jbooς 8u|lڴQFѭ[7ٴi0̟?H<<#FpEvI˖<65kL;vgϞXZZ %11kײo>N:Ei߾=QQQ5Ǔ!Cرc~/_nr=}g:t7r *WLyV;w0|Μ9?}0a|7L<ڵkӿ4GgϞٳFIKK+V\1vXO~'N:Ň~ujNYa'`LGry!.R,\PkI.ϒX˻%ُ Kj4q҃ny( w߄t+Ε:3-q QX IDAT\rQR%&O̴iӨ]6nKvxzzr4'<<ƍ3zhJ&MO>3&kFfeΜ9ܸqcǎiT쌯Ceiiɲeԩ6lvM^dGZVm۶UV׏ӿJ%W\ƍիW͍YfammG4jwww^{5uiLe2w\ϟϺuضm ,оm֬>>>X[[ӯ_?HHH0^\ ?… ^:NNNtKKKԜP Xr'ɓ)WtЁ֭[3qDgqc.~߉Gu@ +;H4w)Uސ+62.EOOvf@ s8G8͛dܫRHNNGָqcvÇ?>M4aϞ=ԨQX*MKe___iѢ!!!tЁSB>|Q}aҙ쌽=999ѝuWdW^Ғ 1qyyyKSܹ3ݺuO?ei?ŋ9uvP^r 6[ceeE͚5NOSPPGVVVy;f;v0sLF|@4<(XryT@%rxYZ|aOG9p+ZZ8Q .8R4g\Aq{ٗq,vssIg3*Tĉz=w+ ڵkǶmhѢ .4WR HA֯_ϲeˈYf_};vGڇCa쌕ܹs;w̟ij@GTy4f { CǎٶmvMJi]q֠RP(E JNa'A#cy4}];w6X='OSc K_@ ג}/w ^QG'-t5*鳌o08V.K/#X`:JٳgghG. @;weD\wc{'''}ӬP( a֭L2iӦ/77pi̥rʨjΜjZ '//gɓT^yf&L̙3"## # GGG󗣰 LG UT!''GgZҮZ:2%Oc'ϖL6UVqRO|qc Cyk@ @Jri £p{^?*4jm?iU MI*ftqxdq4hу9r ڵkL0Ǐkyf.]DBB~-W.mJwww.\9􅁴ǏINNfܸq4hЀ ˎ;xkϵPT|'MN@>G6{0s+;иo:I\5QkvlN=0jVBt@ LKrzd?s rW*GAI0CnVI!;'8*SP!tYUD쥅8@ 2dW{؟WÕ=p#,,-u@]?$3F_49,4"xȿ']p㐌i ګpd :VBw/?4#Du 孀Z $чZ O\ !} R' (ȩ*J:pu#:@  j;7k"7 Xd}YGeҭ&5DmVjI@ @`,ݩ%p$/iAUG}2@հTqpuu,@ {q5:h)dx)IN@ YDA @ Et@ QDA @ XZŢ?q 9 fɕkZib, \L)faK/ApCaExyw؉J먯VJ'y_RDD, `ޢƂ ĎhFFMbQcck7*J ǽO" |-]nngf;Ŝse^,i7"o/ܖs:Zn~dbV\[гuXaa>;Oa~<n~}_A(^/_fРA<>}w8Ū@_AJ2洘^+ 6 .ԥ܎ N7\^hme8uB>"86XvwvRcM hS*,T*Ur1 >+]H`ڞuwW=ǠC0[f2s~lشXRS,dJ*唖ƔSS^a#hG@zE,jPN\4=UL955Kҁ.(&KM|k3OMO^*[#GA%+H܊f2DJag] (>OTp!.HzDK]vt\^3Ze=(NSv9(;džζ~{; ieX [O BE=ԩ>I&hj~N.Pys<õkOL%.-N*PA~HzXy!C03e&qw'L|(\ Jy_;ʝQw0/cNȒfE?1TDq~yZTn@)R4Ԅjb=PP,CAX|zo~~4ܜ6m}Mg0X轿7 FWq ~)4g4 GXYlS3^%H`]u_eHA 'AՕG2e9sfqV px s`uߜ%F/|QDa?^^42x9zq7nԇ&VMbX5ʥKĦТJ bXP`k׭tAN$#GdΔ-{}Lg3gN6Ғ>N}05aPAH$>$4.'Yi-uSհ*C x|Y3¯~đYa%ZZ쾻[]ݩoYS=Sz쭴,GOK_:K Yws{e}@&XBK]N}IL#46W4bc~{;lđf놆F˴ yQuO~EXۯ}bSc~ 3!gԡYfߖs T{BA9Ad8uNNN,ZpR)fff4n8?}zRŰ wgS!!4K\ʔS QAB(cmD*Te~quk<6mcmd-[w5*K///RU.]w $=xMuMLr+lK~ETW|[06K/9xV9tktm_zO'u z eA>'O0b]˗/soѢjjjYR. *'k8W}B$YPWSPC XA0sLӧOncbb¸q>pd%S[Nl8-&53}S)11ŇɾiVC.]8=4K\3edepaTUêHRnFT¿mْSٺ̬L"p4q|/\~r`^y>;8;1w!;98:.-̢̬3+cƝ;L}ɓ'JAl0$}WKW\-]Kysx@9r@!Gp*݇\MMUZUuXk #$YtG^ mJ_6ܗFs3x^I^1n5sZ̡aņxFS=SF |jr5eu;g>-XУF%"1dNYʳw.ӜT'ccZWm33U9ʮ(*4\ZA>޽{D@@s\nnnn]SC)Rx^5{q ʐ+rh98hni>nջIU>t×q UvH>j$ǰo VVO esiE%t^/!%KWѲX~G{~r;uѰbCZmm BbC8"ښy;g>gdIx*jZ΄aスٺMm>چXzQVwAU܊}VV[_|s߹LsRu_~tILrӀAuIulN{bB,uԓ[/F  '… 155e޼y.]kWh+ϡwpAA>[[l޽ Æ +ޏr/{YN{AA֕+W9s&/^ 544qVV6nޤHPSb Vs?pxf%wB!}uOA+,, 6mڄmUV-> uuu찷+~)AAA(RSSѣӦMCGC4AAφT*e8880a„磢p:VAAAܹ /-}_Ч&t8@AA>'Nٳg9qDD yAAO^PPӦMcmPHk  )KHHd cCGAOrr2 dq4mڴX?b7E[[HII٩&SMGBBBINI=89:($j:8HJJ±F ž9j>>GyI kibog^@rJ cAAޫ%Kpe8Vq?r˗/=]]n}A3p`ӦM1{!N1AAޯw1uTvE;Ox|CPɵnAAOسg8p /ٹGUA%_IMM!==Cyo,P ,..3#ݻ7]t)p>y̔~OMM6wܺ!|2""#s˺ c&NAN>!M6ŷ~+[vM<=x|Q=z|Çi۶aaa޷:uO/O^#+++Wl <-Jl7% IDAT}]:vw|ŋի7͚5c֬śشiUTaΝxyyѭ[7&Nٳgpu={޽[im۶eϞ=r.^DRR7*|䔘ȣG|"!Hx1/_,14.))pD~eddNxx8^*t:oCZZ;]TJtt4*󛕕œ'O ###t=|ǑSDDQ^uEV\ŦM` pdJ([VX`dhzzPؘISQC~>ڥy>W|Q˹&M7˗=|k3ڶidˮ]'i]:u, NJÆ:tϟ'""G~^x\&Hطo/=ˋ ۳n[ ={ХKttt)f___zVژ[Z Zj 6jUkz\\Gzhذ5k:1q}WZruxA-=k+Ν0rk… 4jԘuQVmjժ رXW*W\|V6N:ÓQ& s\v]y tbæ =.^L9г n 6/ɶٛ;w1fd:wIjjxxs^1 y8?gxQ?e'&&dӟafهeje19w!#̓_ e?TSS_ެݰUkש|:s5֣|rXWe%ݕz߷n6g7 :?]^|ɪUi޼ݺug𦏽%Kq?͛7ƍL8g)牊gϞ8;;cooeE6mʞ={.L'NcW۶mҥ̙@vcgܸܻw_ΕXv-aaټy6y 6йsg<СC\v== u?oLLL3+::ӧ3s ß#@3}txZjLd=fŊlڴ}/\ݻwiF< 㯿 1RefflٲGBy/??iڹb9sWz][pԔP__:llld׬=;waذ4lvѿ?֯_ǨQ?4nݚ~аa%W\a}) dڴdee^ D+hhhPF eˢ\U0aqѣGwΝ;˺u"--71|Zj5bEųw7#\xrJڷo/7uyJ0'OOOo;wprr`߾Ѽy3~m#իWoS2e2>>>III8: <[zo9rE2>fDFFTSvXCv}` >$70#}mF< ]LK&S!k`zDB J}ގ٪% ḡɴoۆ]1\rPSSNZrv ٽ3dkxQQtWn+Td+}s3Sw۸blJ:uS<}͛7s1tcԩ>BKK ӠA} F-ٺu+8@FF6l`444صk=ztϕ]tfɒ%ܽ{{Xr֭řҥKs%NTtgweKKKC___1JeL]]=WZ}ɻAfǎȑ={K2{l-[&=O[4H$ubeevgMŊg޻wʗ/… B]]ٳ琙݅R]]r)^^[o___"""zӈ}%*П*UI |܎=ʑ#G8q'l‡CQʂ<|=8ZhCbճҴ *zzhhhpdQjuuu͞xF/̵cUO/q KpQ]O+Ws/|ՠ|ŭJYXX0k,f͚۷嗵ܼyXqPT)lll6޽ !mPP7nɓ'XYŎ;iѢ*T(54o=9?:u^MqqGC\\\d. ʒAJJ[FFFYJ*G<}:ӧ`ժXYYEHHk֬ZyJOOg0?ډ7җj>>GuÓsfQ<rpoQ̘-q89:ݷs.Ó:k !))8;3~7Fo!g~égؽ~EaR_7l3ӥ}۶8uofޮyoqonhV#-=q/ {Ww:uhl'g ϟ?g }WT\Cno-,`|Eq{U93h<}۷o~lmme@vwuu ?Ndd$5j ==lmmѣGH$G }w055M MEKK+++ׯOR9m%(&&&j111ddd`m-ܹs[nkז{ Y.W^q}tttUӧQ\rDLLL[GOۤIIHgÆ %†CQQ$gApAS/k=z_<*BqJ'y)8  /azm& %h8|J~  3kl~B T*  %Ybb"Ō3T/=G}ԈР,zzzN.))ԯWAb(o%{X| EMMMܽGrJ N5 }DRr2N>v<&..vvDdnokK̳g$''cggKبA?>̙3CT AYj:ʖtt$tx[/Tq|ko6([Voiv AA>Wĉ?9qGn l#  7n0w\5BAA|'3z7M/_AA>tvƨQ9rTq#䃿 1  HR&N5Pb  ZpE( pAA>gϱyf|||)p AA{!Ǐg-XXXw8B!1  {G5k&u)pB AAὑH$x{͍޽{w8;]AAoE*b֬ge m5EAAAx/ACCޑh8  E,\=lq#pAATDDC㧟~ں,>$""5Bl!Ϣ   4jGu ; 1  P$R)&LގÇw8[##s-{{*≃  P$~'Czzz"  ;;qOv؎ J*Uh8  $((iӦ}6,,̋;=cAABeA̟?Zjw8{$  B1buVܡh8  faaA;  AAA NNIaqS}_!}>zPF!# )qs=qe= {򄶝iɝ=<3q2nSdG=|h+ m;0nTn߹Szʲbqܽk~)<`ҴtޓruٵwlwѶ;ɾO%%%ޝs`sw`H$X } KO&ϘIpHh;/cc>:uc@.jE]Gu\ |>>tdJ&Mp)) X-7Ĥ|qQ˔H3͚4bilVuLLw]nyE \ bnnNdeeɼ9zw Jyꗵ+my3,^LDN %OwU)^|XQD>}W8 MMM:u^=|$>! .NN>{L `9V}~E<%55hټl}tL 3ˈ!_ܾjjjhkkӥmJxx$vՈc}e0t@6n2byŏ+V08FSMAV_p?_gO:xT&>!Q&)8: 6.qvL# ]̓:*iRa :S&Q=}ETWCٽ;NI|B<9mTUf3eeWYq0ŋ,\W֭n {*30! /&99L7Xjt8vO8|/^Rr%fO&k\)J3::;wB]]mmwu;gߤ_^X qȽ4mܘG:ŢŘextGyK9ccis+~\J2eލ]{pg'G5zPv^UGU~ "k8sG}|-4jHGv,[=y]FOB._7gJiqj<`άV܅y~HJJaa8t$< ?.N}ڷêbEi֤4| s3ttt044eLMWLh)vvo,+TԱ|Ѽy:S QQQT)VN:4UOY,qqq{?ԤiFp=~-[dWr%x2VSzuZ8x{z(h^SVG%\0}YG UvmhԠA v(V٪1SYu*H֭nU2Μ{l\!Ei.]Ƭo{`9*[:5եnm]ԬIcV]O3?KHHd톍> iNff&;sO!5-=_SuC~䵟+ %;?q'66 bS*9j6oI)UҢYSr 6.BbR2oe?IVb/SF~H(K=4y))Rnh$׶JWy07W\Yy*yGEY SzƳg13+86i"RbokC\~ڵ\RK~K 5mʝw)ә{ڵiCsyNdee[7v|/u!?OEOYxBɗ::: ;v#{#88gC׮]Y38HRʼu-'yYʋRƁ=->]phѬ)up~Z,ڷ_ЀrL0~U4oڄx,-{E*USziii%Q|Mlll,lY}G cjSqrǦ,U Fq*WkNl۹ cldtCAW%\:~$ٛkzu*PLY ))QSS o~:bcTYQQAST׭*j(0lbRf-555j8cg[GS}v@FF@`EpPƚ<󵏹)ckcCff&O_sTTGys״i3:w… ?s,n8_W3fL@viڴ +V$#FCtt4'NZױc'jf4h@\>HU#<}GȖiiiamm̈́ ۣ?qE<եs_cFa0~ɞ kxQG[555RRԤr..ERRRYҒOk>}<^ڲu11 :ko(MJJ԰cXS[w$-= oݒuP?URΎ= 33\ŋtt3ʎWڻ=]=G c,yEfRP9~p? {M2, 鲟5EdeedOAd}/Pv4_}_*OUkEsTTGysoI,*9>>'\2vbҥˁ9u$NСC9B,͛9sr7͛9u$ƚQ9y;)lE>#Ndddג 'Oݻ3vʗ+mMhU"~ɠ#H$mCv 2RK1iʗoT ѭ ,ã;_O/6P$ ?z,i'5x鏩Iy̚1cN*Neqq֮^=0e:z(L@ys)g۫'uԮ%](+3eY2eT::j ix~t)-G/Zny}VUM4̹ l\ :ڴh֔,/^RJe'P$::Ig ޮqr4j={ylں]w&3sX^xɅKpEU?.ޮr3j$KDϾ`aΜ|EUy_+SYOUuŋbmm-[vmHLL̔&MPJ ==G)U^[[[sYptt/]gΜ&RT^Ν)?oJNN¢ TR%ʧvZ8:ӴkNo&%%ggg9Btt 6aÆHR.\ȭ[R2;w5ٳgI^xuUZj%w-feeqi\2͚5^iii8qG`ccMfdbbbt/ß```H:{={ҥqs@ZZaaanZn7np545S6^ٳgə]Kt/5:uGYt"D %189;4q$"##ٳg7+V`ٲ4o##CPSSì\ ~C"PfT{Rmg̘1GGGLMMz*NNN;:::;˗DZPSSőӠAvUj޼ےƼy1bxBCCiԨ1k+Ga\rYaUvիdž 땖i@@矸y&׮aee˗i׮*ܹ 6 KKKlmq?HR:HŊH$ӗ;wдiөSGMF||<:k=RRR;̝QVRej֬%1111uT&Mzݻ4i2jœ7qvv]0&L4muu .]D^^ܹswwwlmm͞B\p c et=?AۿiVQ^?qAAlce7IIIa۶*Ӊ`֬Y ޽{tƚ5k2e Vb֬s{3H,Xk wr9l(rm6mڄ[ƍgYtڕ Σ Ӱ&>>#GзoΝiiixxoeƍr.](u>}ϟs-,RSߌ5J d޽S1h@IHH`y 6yB6m011}9Nt 7lق;wKKKs&Vr  JU_|ŋHLLbɲm0?/_8y3He{]GK"55SN=k:zou+Pϟ?:/^ؘ z*qqqrml3ru&k4Rvm^*7<<_kdڲ3T%)D %1cK8q76m0bp ~0ʗ/AYժU󅊭jժ 5oޢP嗫k=aΜ4m4ω4,,,͵0BBBJ֗733daԩ,]~X+;wo߾hiiѾ}{ݻ=4iBϞt @֘ۿ?+Wmmm {d?-ё{c)5Cpp0~~~)))XYY0j(ov튙-Z`РԩSGeO |))3t*WbYf()>3kkP%b[=b?-J)9b1m0[ܽ"ֹ#6E5Yc'Mׯ3g٪52nM;Izlxi?۷j4m? $9b1koG؅ YMHL$`Lufф?|Im) uuu2qD6lXϳgx9 3Q1'3o߾Ν;2/^!ڵkItt4vvv &fE13gULظqޜ9sao_^f}ŋeV"Hf&A,s͛7*$dzۇCRLΝ;ϗ0pPHFV09b13dza}ͪeKp//s15-Տr[[Yr$~ҕE]]93kf5o~mYa#~My2br>k@-={JHHɓ' Sv:qqqp-BBB8Zxyyرu̞=w2xU+ 6ӓѣzׯ_ѣXb%aaXf < 99Gq9i`v$'p ܹM*UIӧYhtv'ӧ/n7~8^xaø{Ow֭ҥK$KWhQHJJ"44uhLʕp}̞} 7MMޥ3|+&,jIݤ׾z)3g3t|Sv=v,-:hիV3֡4TQVXd{Dg??B 9%U1e6SfXV ID6<ʞ~>L?gLFqL/116q] Կ/ [·0bM6oD}k:|2=`ES^^"99vmPSSCWGd̵ӫ{7n 1)RwOʽKx4hgϋɢɨaR/vjߞ!$$&RFIvfVanf2 ׁp~8ժ*{q2e~_vJ}+Tݷ_.DD<-lٲ4oޜ2e K6NTWWgt֍tԨVw ӧ7\tN~} ɾ}{IiX,ښ aMBҥ03SԴ,jj|,--ҽ455Y<>vvvL2v+4ko[#صk' ,dihCXWWOt2b1x{7gBI6۶& @CN["N:ruFIvv6,_  26X|9˖-cǎhiiұc'*V˗T\C2w<|||hkkSn]^bq.3fL'&&333O7dggK_ ?cF|Z6]iӂDEEɶtهS'șsKy'Ӽ)޳g'/^HFFdC%rrr.Y Y0g&V\̹ ڽD:(B]MGнk'>Ξeɂy21eT"b,^*,7׮3wb߶}==vQjOkߎa g+,] {vn'*:i3prt\b[q~;r5.\b3c:lw?\fwbyu-Scc#7ne8T MWxy4#\;;@M]J ~ԔXy>ǂ?)))Zj8q\RSS122Y7Ѻu+ZnEvv6lҥKYP_o̜93gJk{{{`*!5o≋GOO|4iH渲Z555<GKSbbÁtѫߢe)QHDݕ~G{ү6L]xA}Js{*(Wr 臖q&\9ok SL:YxYL9}|$DZ~u*G .ٰ6n쎣P_JJ*lcA*gffFNN';CLL,zzdd~V^q2U~G=@q.sB֖;hPH$X066VZSSS544.R_9x?Ν;Lg80o*г["]cn^ uu^DE@NN1X>T>LQt)"c=J}=}F L>X?VVD|IrJs%,@uNWGG4(.#3 l%/1.\۷EG[`uF >+K+>*UEQ\)07 ~̬,y4 $r.]/d/x7mGxy>|]/y6?7~!~h^:\ WR8W,ʒkdC( IDATeɊU3_uwyhk^6? &==C`hh@J.J+ITTP[E(k3q=HQ6[ @7qCL+ښ7h(b=PyGY̬,ʙ0aLy̏Y*?eI鐿I˧ҳ[g-_kqYCi֊OA]&Mtћre15 E EVV& ,^ m-ME&?E&azX,ɑ#O ۙst; uumڀ^$'0R/o'3%N ^z޾Mì)ّá!4.Xaz4Q]پkߞ 3cd'p$/r[gGv5|KVs>XZ3cJ~B]ʷ|LUȷ{L@@@࿆ر<7/w:O}=?zi!'G 3*ٳ<}a X:w˗/^"q5loY^z!9yW0W9RǥKqp7//aÆS:tƍ8w<};va ܾ*<#),--t?VynX5>}JΝяTN8!֭[ʅ222 k׮E{nݺy믿ү_Nʹs͛7t'O͙3giɛ7oiAR&1 6J=z4?# 4u֘sodM_G\BfMF[[˗/KdeeѸ8q6m|e69r~chhH͹q:$004@2X5j43gщMΝ?i˗/_߶$'кu5~=⧟~|y{Zncǎcݺus d\xubffNvv6ݺugݺ@WNFmHLY#FGG .0 @衟}Ŷ]3s|֮\ٸQ/5_S{_ N[2&_Ct8r L?gLPACiM~xwo+ɔ=v,-:hիV-Q{1\ȓLMM9lիVQ)KI,Zoߧ7|ERz@}: *(_q, Op(ogIUhOU:5 y}^Wgd>"ڶi֝ g6SvWe6S777._DhQ.\ RJ8eʔ$U|]tȑ¢P]v[nDNaKHD֭T1ʗ'** /_M3^AժUc͚dffҼy3?(W }}}f̘A@WWl/^?:x\סC~7鿯\"}9/(+!Ѳ'!!:uD͚5eQdd;Ftt4YDGGkRSS14OSǎ~2^|J*@,*W," zzzXYZ$)*`2lmlسs;QLfʱ{^\Xx!D<͹8;{V~ܷ8Ǝ!ӆ_voǰKy'Ӽ)`L,-t%s{g[|%U\]Y4o6W]g|m˞"YђlݻvJp6[p,XOJuP۷]W>z,URti>'''h]|\+W*R&6ҥgfL#gΝQi_*.?f$WeU]V>-ˍ.ŕS.\b3c:lw?\"{E:>G+32wq3hiiaff (8:(ɩbbbB^=ի'f^yBZZ~_baaA`z._̒%?2/Z)yZde 22+Wz#%%;w0o|&NHvv6kfԪU cc#~,YJ~РI&̙3;;;rss4h/KBD 7o(] |"##y )zr lʮ]cbbB6m ޽K~899憮.jj2"##y\-7 q(.iir 臖q&:&RM\ILjD"tB`` nnn zoذ6l;wѣC]]=zH^փ7Ouǎ'8̘1ӧ Tx*VFdm``\YjԣGΜ9͝;6mdɒ(b͚5ԩSǏ`|OF˖-[0*߆u֕+֭[ Gq(Dffz~qʲ`o~}򟀖ӣK_CCw$.^HLhM#''$%'ч/LJEB,Z4ˣa g̐Q/ZZRJa%(#޼]I'KšSFQl!5B&@YTd|$r*͛B_@=555U5 e}^TW/~1RV͚%~&Ge6S'OX`v $__fV [z5vz ߭[7hSR e˖ݽLY^^"H_;{xxG]JZnhhHV-b2ʸ8I?,'?m>{|7D"ٳgLM4槟~ԩSGfff݋sIâM\ܡCT&,]gϞJ[PA@t=JǶv2K3q@r:y4&Bו␘Ē+Yh!]֓r}j{&ŤJ4)ebaOBkrcZr(:tPVVibb"&&8SU>RơۺPeeG9ӲJb++JNX@RϽ.K*RT?{QSӲ2SQfϷ3(kt#UiddȈ7TLU{A+IRի7K;;[ OFFÆ `߾ tuu^kkk+w`aaӊ$ˬY֖9ֱc'---/^̰aHMMVBBBp>>M6m~wssȈaÆѳgOٲe >lD"̙M=h߾nn5ttF$Ä/g^ <q:1c:=<<<05-Ǐ9uSh&Mj*c<|H6߿?GЮ]{=o޼ŋcȐu?$Hbi#_dU8S 攷a?Evv67oߖ_\ # ututd~$J.͋Q钐vO.\IVLLLLCpZYu[`uF Z<动p=~BaP|kJ2yǵڧ2EKVVD|Iق++򑧻*9?>#uKV^C^^^2OE"z7mGxy>JdϯOAT9lB?2WXz5smmΙ3g3ԪU LƏW"I|[[[+wo|<==qs,(____222IJJS4jgg'$___ʕ39ްaC_ЦMkBCC044d-lٲ'O"ل%,+VvEP>D"...ԪU'Ot2>YLdjjJP>Yj5׮}6vJ*<<<8~85k ##1cзo455kٹsSN^`J.-&ոqcJ2Xt׮]g͚KFxBCqw8::yf|}}FYKp!gXrǎщC3댉1uj(ʹIlش!qrrd<|5ʢ)Ѱ9GPWgۦ 0h(2O`Uhkiүw/6iP>K ڷcDʖ)#IƳ:p=ziYfL PH*===? E:*Tљ>QeT(''BLBQʔ/X[һ9 E, Ee*GBqjw̸8zmTOe(2US~^$'0R/o')+=>JE>R^qء- ,Sv%z&Ge(kOomԫ~iEٓ=%  iӦ /O eC6<-[6:7 б5jvZukkk3~$' XY4%l>իW/t^*2C,qР_~On[UVeO[xرc -j/h?sssaȑ9;V8 }0kT\U\223=`+/֦es'To|:7v:7nAhhhpN>͖-[hJBe 7oШ lC7o3ӧG/P| }kA@kB{\\\t2ٔ/_.Z0p kkyՕ 7נJٲe1>ot*TgATtvG,I?S&狀߇0 59555V-]L2˄e 瓒9xvn݄wӦ%~jIpӄy!E]]93kf5otCEKWPۭ&A{6h ,˦)ogW$YTadhHn]hwߖ'2*rpoP$ 17`;hȓS1E=Q\(jO SR˓4@Kѣ^F5Y~ݞdž Xdi`6l@`` qqqv}ϙ3?IFQr6BVOFPtiLDo˅)okˍ7166|y[@8HkV,cE܅KsfL-xgmE|LA g;sbi **{놌 IDAT`L,-t%s{':7TT.|T\9չ|B޼ߐa޾:TΏϲUkr5ii4Ȉx2ȻQ~&O<6=I3?(їGժUnl9slܸ:Ȕ|'))'N0x 6o;wnװaC.]DDDϟ/3fL/qVOBGG4S^-[ H^ZR;eJ.Q{HK ;'W^g6ƍub[Gdeeqp99deeԑ->d4W/кC'-[ɸ#y?;}thG;H+N׮-QWWZUB)"YTϕkJg.Y.,ʧ6ٰ֨>Z)fge>*K*~g[=ӸQC"G(N?' |>}rA>|w ||o"CQ&6ԩS)S!sޭ[hKK+j `ժOb1ͧbJҽ{w~7[+=/11 TT[[;[*^rr2C ήL8-|HK;j(6o@X5[_9r-hWǏӮ];ݫT(_w5/Hpp0L<'^4h@P~pO:uT2rHlRHf͚amm#~~y왴4o NNΌ=i[n_GlllU6;w,2.O"33}=}6ˣar-m=eh#jT=Ǻy3O[ٲ}'š=z^l3=!-5{ә>{C Y<|szb22x cF4Y,y&30v՗$vlވ%=Q(2 ˩S4:萛&{vm ==]/ݟ*2{p8Db$\(m999ŶET)OUu օn}c_sr*#U)E@$L:+VLܻw3gŋHNNGԬYcǎüyoi]7nd̝;w\tytwҥ yyy_'NСطo/7۷oefضm;[lP.s%ڶm˻w8x\zM?Mܩ)bʖ-˷~ˠA;v,ND"6'0j(Ν0a"5jԠRJreruuECCiӦ3qDjrEcϞn===tڵ3gvpg]pB&NPP^^0WXc>ԵkL2Cb ._"d]w>tؑ+WÔ)Sѣ'GNjj*ƍgԨ̞=Ç0ej֬!wv@8t=Gž|jrcZ=eCMM0<$]a*@[8Ka%8:Tf믿ħy3D"U*Wʊ;w3Z%,瑑L;Z,eee} (ѓ' eB򉋏֭;eܼ꼈bbc.,]:vPhOCC,_ɲE u$/OTElmlET)Oeu)#efG(n?S$@IX"e˖%66jժ)۶mӓr/ڵooɬۊ˹q uեvڜ?mr ttth֬)ϟiӦ\ze˖24 6DGGeY;D"lmm-YD80bx 'Iu9a6_gLScF!0F|Z:tNҥ?z$V0?uj1eD8w" CϺ+pqvduȐzRjArr /%.>vY݇_O2zpOA~vlda,Y=`iaΌ)hhqԞ~1"e˔Sű޼SjE>275S*2]t/KWf#Ur*j3U?ػw/&0b||Zbhh[Xh1YYY듖nk >||ǿPSS#22R渕$''U(,Y/^pEt邇cǎ%.. .Ҽys(cՋBY[[&MӤIc[fΜ[bŊdggsA:tW<|h$Hڵ+ݺu#.. iK)+s*11##H$ȈDQCCJE- ز!PqgG~ =h)KOٯ= 05-nnvsSS, @6hۦUڴ]6r(:eQHQti.(Y̞{twn[ŵbE>R&*ՙ/ҽr{9SS-Ro(r~j?S%}ٳSLx8[[[N}JJ~ɓ'ԩaǏeiiҥK~0"++X?<ؐG5044رܾ}ubaa3wwyӴiS6oBTT+V_~!!!ooo45?֮](_xIwXXXp~nʗC$!߂Qjj*qqq%#dU\NMMe2t؉ϟ{co 0:::iӆ 6Jӌʄ)k׎7o:Gheˇ;@Ͱ+ϩSҥLÇ{Q^=4hH߾}1bL.^Hjj*1fX"#!ɓ';vW.]ʕ]iҤLիa 45=z߇v򈍍SԩS%To߾%8g*UH9} ݺuIOENN=z{'(] u*x.\˗deebccCfͤ!O>ҥ˴n݊r8bG*ԨQPʔ)#]D111:u6mZST)@۷֢K=L۷o9t(GOժUn.feeq!ݻ͛7>_Ν; N?"1pB!+@K H2"0__jTWԙqV:9Xx#Ӫf ߇U.!1%+Vq,-3F_YY>?fC6ݻwlڶ/<=8nʲyS=Ç 3=aLLL[Ť@[0}&ɲ72*2eJ3_HHEdeeQrfffM*[+`C9zlm0[ם9x f2-^gد{KiT:US} **Z)&`in*ӡC[ʗ#:/{puKd!eTUP{FH!R{ԨK Vժj*U"b!C"#rD޹Pqq~>K?it={Svm,,,8}jCl#g'&-Dйg˳ѷ-۳=2iL6[i}L Z]X[Yq/L2_0o6:'Oc+zS&3̧YF,?g`ʌYl\GXV-[Qьt |}(nRJl[yoٽ?>G\_rە(^ݺӱ_ Cmv|'DzGӕS.N)W?SP?|4C7x|9W^ǧ\Aμys9qW['O*XWjXs& Ff ,b ;'Q27i Owq3$%'ӽK|W(XZZ^%"%%܅ 's9s㯛T*۶:2($zujs 233ѭ~x,Zkpttd~u+O*μ{?n#UvyR9i--;DEE%fE~{J*Vpw,7 pxt\@5-Hjj*X`‚x:ťE~;;=wc夤ޣ999IJAz(JޮU\Xb|WJ zMJX\M_b$ۺJe_<7oAH V]ANZ֧ϜE7̔BS.N1fy<{CޘW$r"rqww}95Q D܎U0(>>0c^|>GlYGϙE0q\8.#!1ȽC7;U+xzC,?Nd$]\eּxmS$ztgʌYKLtT?C6Q*IJ&))GTZ5_y4k(2xs3ԩ]Kї:q*.rHGwl(,Ne$3TXbofmYÏDPЦUKKBThޮU kk+2Բiӱ<$[!!!M}9PW6-o%kg~'<=$cAF*NvʾX[[SFuN:EP̙mҒG G.p !={TWL\[˵Yޯm jL[Kk3|89/%5U2NCLҷْ,̞YhOO9췂 Fb dkc[2{wlE~ȴYsu65k N7>]V02r IwJj*K]񗚒%-#Ty"KV-hҰb., +LqJ]\<0ooH/=qajI\VOOA01qD #N&q,5kV,+bR,^B }FJJ v١jIK{&%5EeGTy"߲iK.ժ.INNePq36oOO4 q񸹺ɦ\7h'NŨCQ*އͿ5!TPDq]ٿk;u߮ͯ'O₅#999\vd`L,:]:\?~|qQ3 njQt:j5ZV S6N)W?SP, IDATMn{:0>| W~Ap0Z-^ tJ+,Z,2/ʕ|շ,9|Kr8f>[NAq,̈́q>qZ# 퐁=!{ى cw}K3|`-\L.)PCOJKs9 Φ}L)r'gvΎڵj2r >Muڠ^]6nʠ0q\8k֭gɊϱ+Q= ޥJRIƒ77x( 4WMkOvmyf޽{T(W# nA}}Aoڤ63\ruS~X. >>yѺ OCuArWJPH(EA"V*  `8UI^Lkݢ3]* "JɻMpP  ,q   # RS*DzqV>xP)jڷa#?o^TV VWr߿'cnJ /b%\9l|8Oصkoкes}SjSrZ~g>qu2TM3ϲX1*@tL KW[*}BZwȏMxH"Gn;Sp"W~A۸! ȥ剹r#?W zt]]zv̵k`IcWAG/Z[Q*,3M^XI:33'իө]lزyZ5nĴY<|ѭ M7̗gEت,?[[[irY11̚6uWw\?:L>~sss͜[fΙO-ڲa}>{ 0qrɵL۶aזMxSgDʦt\AWI}n\O)m̨6AA\\2Ev\ح]ziQi!a}YNA=wTΜM`ǎ0*Dn̰Qcݗ1嘫c>r0C`뎝ۏ4|.\0Xt HZ}Y&_ bŊGiXXX"WF5|(ޞ8,ɛժ~ruPTs^NR CTv(k 0Nr0RymgLum JEǀʵkiy~{J*Tl|-,,V<+V䝦MdcA;A+|}֭Z[ի&gٱi]Džh26>>0c^lX|}X;n&DF&2b5V0}DvmD.]8e:YOL +Pr̔JF  @psu-uW_vv'jqwueHLX rWi;_+W^\|۷W=KƮ={ Yїpȑ|!;vA/q KPr')dKSjUҤuiN‚gh M4Br( TImJwXl![vy۶׬؏FP(hӪ%yiiY ̌,J%Z"3K]9q7/NJ_+Wރdk4$$$i/0~*\ X~/Jo3ggl޶KKK& #X..ڷvݻvV˦-[0y*/]lT? Y7WWmwZӌ}%[AV˦D[OHpjl2w`cƮ &&޽X~CF܂>!4i`;I p,5=KmSRT*3>T 'غ}')OF1XZФa[?=(OZͤi3hռ9ciiN\ڌL,￧;6tBjj*kWEx +V拈u}m<+z`mm-v9*\eGZ^VAAUAqџa%4nB!faaF<''L8ښ5+P(8 IIIe̝9Cn=1.[6mZ൵j1>#4 Sg˳.7NNePq36oOO4 q񸹺gRr2qa ~YܺɕT J'NTClr{n߾MVV[4GPF*rE'P*t9rY8UE ܣzё2?_ sI_gs{H(*33֬\3:r4K+9݆5INџkY`>>^ 8ҹG/\06\}/<C Ŀm5#{ a #W!p1tCIF lC3T)rMΚuYsJ'gW{|+ڼ_YӱuN}}2 Tr۩T*8{Oں]ˏcF&^Z3再\|A?Ł{sj8AAAxNΜ9M%io/OĩJ   $&   $&   $&K3`臜9{e!  ALVIf{n;ݭAQhZZq V-'w_ڿ߅QFy7%O&^`g J+AAx\Z59t:s۵j6nL?}t:ff*&OǗWɧSXԭ0KƖҬq#͚ƈuAA^WbWJet:nҮjgk+O9NA=?o0;@9ǨvA!Dnڬf׻  m@`Sh'tҝ~qŸDn̰Qcݗ)R2ޞ,iOHݔjNvn]֖NIKOTTtAAx /11WBϙE0q\8.#!11_ӧ%Kzu rWx [Wӿ>C)W\xf[76EҫGw̘Ń _dzxfLkRɈ޵nkrpN6D4Z-]tL  NAv͚;s6u~~58|woGI/n,(J$%5ժV5Gjj*/@T* ףM&P )lߵNA= NN ݋70d(,-BF eHw%J?WҾ$)O:dkc[2C5m[I(f![Nj5͠U4.$YZZ077gӗdddbeY̨AA^Gb XGGGr(X+kk֬XB^( })F8 ?‚Khܰl>vvhZ'))oٴekkc|Gj4ΘgEzt">lٱ>=S$'卪UdkצM7*#:bkcCh^չYXXcMHLd̸ ujX1]m;vA; ŋRly  31qxv$4$8ǰ| C w9 K#FA"9.-9{n5ϖ.SP0eK3a\8eY[[ch,rc'K @4!#3#߶C`t g'& G  ^zq  9s%J>6^*  `8  `8  `8  `8  `8  `{{6mPGAAxE#qҒ=v8`cmMdj~0h- P/ WKQUQ:hZZq…+[qwu{a6nʢe˟[~1W0rX;ӽW.\/h^_mݦ.rW|/Nۀ@>41'mǮgCղhrz]X]aԶwSRd"vD_Le* /}B{+==]PpaQ4-@tL CF"s7B ɓ^s ;qVҦΜM;3|cb^h+Zh!{wlETp,Jb^vX\4mGGP`QhZ&LFغ)sgQ!k~=u G۩|ϻӱrW)NUxyzivزyZ5nĴYhEU$Ƽi|soT3 3̧_ vmĀL={iitNT(_Rim[dѼl\Awat^~T|{w:h bnuJ6u`뎝; puqX(իUk^RݣZ*h.\b x{y!!1'ѿo(uj?NU*|8BT @,=r󕜻p[[а~=}at dov1$u/-F0~hVLJ= O?Kl-,$?iƩϐL.o`e)H%.>r5<ի!.s׻ٵg/w6y"~oeT?)W?SOn0%1W6kdj9V-_%~{HNK$|fay&$$p^T*$C@1۩ٽ.'%%V*._hX>oϨqXX, ON:k^PA2,fт)#_mNTT4oV*年A4S;ܘ z>boƿmʗs/5gϟg?O`۶7o/($zujs 233ѭ+M72r}Gղg~~44Ϫn(N4#5>7+~&Gi 3633񒓓Ý$ԯRZ5Qqq ؕ(oQR%bcoS*f˖q$55<Ӽ.~, ե}6@!|_o͚Aff&n<>nkcquq'8e:["`naΚKң}GdHFCl-223_.N49̥;E{'I29̞>v+]6y-f,^[QLJ[Ejxϟch] ]_FK1S*1t0ݻv#6v5ooT̩SL1kVacm-.! aM?r)qxms_ؼu'|B_ ˳tRٕ`銕iϊQ*E|BM6=8uwZ6ʕ|8yqv*kׯQ{;;ټhv}$LXA~nnmKuQ˼%fYn44v3X< :joTCicO`ooO*)M}K3Rƙ&HLSTSƋBC{~>v qJPB9 G]{h\,_ris.į'IfMIsڝt ʪY _0jPT*IJ&))kkkU߶mVT*iԠ*nT+++qsul2 YFC.N4)qDE]_ho,,,_ ?/Ky:bf\Z~~Ԯ$M%kg}ZQڬiܶP )F]~߾k7zx̜}lӪ%14>j3vIMMReJazŸ )cИ) hgExZ|gbŘ?k&Y,MD>ڳW~STښ5sTӕ5ϒWqSb adeeT*h4|wqqX[[z.LRi/}P} ǵDGGņukޟ@HX?|>Ovm{ie~n{ hiR}$UwSƋ1WCLs(GR}djr :Huǟ~]N̜Yӱ94eyui-qHMGJJ Kh^]!#GaanAX4juNRRsh?@S*V΅S.MJ$}8R\VsvrzX۶n{ADlmlTf<|%Ôy{V-hҰE1 #jnTI{PQDqs%Kr")Ν$ʖ} \]3r999;SMʕ8y5ު@o1m}&4jؐ YdL6V=Q IDATӸq4KKKt:l2L, _eJ{1ךQ<\v(< sS6y_DpNRH. Hc0ƌ3͢|IQQ"~&f&}>ISJfM| L糹TsU X`1W%bkkKn]֫<4EM4jH~>rKhݲ6kJI{;J980a@иaRS1{ΜA%_o 'G!Y֓i _$h,8 l9F_FF&eKcem͚P(Pz4VgK=ouơdInv/M[ աVM?Ƈ$T=YH7Jۓ*.ϻ m'P1?%NB/vt,MBBU+@PV7ErDE]"R4m ;;OLTff̝9 L4 Sg˳"=u1j'2̸'x }(cP*R}+En3UZͱ_wsvmZ`y.mEruK{EJJ ~/ʾ=kJU\<)34Τ>7M}?#GPgCa,u(۷ʢu( ިR7WW]=q _I_Ydӟ NNJCv*X[[߮ -f\z%so1wIrݱDPJ|?#==2\]]'!1>lٱFñ'HNUlC bj7o~c߸NϨ(gD|ɭ۷Q՜9{+WPDq]ٿk;u߮ͯ'$EӢV'Zat:f_ {{r/l;bŨv-EFF;wxq[Tݮ(cP*R}+En3ծws׎_IA.mdffu)+WT S;wk]bp4S2^ghI}nH*cz?+/<,,,?߶5?թCHH6v\{'>!TEn@~HŊnopUZjU +,Z,23:r4KnF-V˦k~a{AGͿ1c-Y┫@n9ov o;\8>| W~Ap0Z-^ 9gHtL {gŒ ẇǰ{|ߥȵt[)rƒ뿢j1Fet ci& k~8:̚bɻ͚RrCQlؠ>{H(*33֬\%M5޽4͚CR]<*WayT*>-}{_Nԫ-[iN32/#oԠ~,Kr]| #?~ɂxIn0df_Hpqvb|$5=(R}+71Su Hy:J\ɇ^}aQ̂Ç'++YFɥb'5>7;W#>*CTϊ2 +ښYn=KV|]aݭT*~6l";;rnL3_s(8p`o_[ ʧ'EEphg,pKY4A#̙/!}zya=ʕr ԓgfڬYg.ٴ.[?o/Oʖ)cR-Yb)]zb¸џ"rWx3o 233v4.ru*Ϙ>"{?Ҡ^]ڷk#' [?4fh4bco8C`bػ '۶4_uAAx5;w!0}ڶnJF aaʷܹ۵ʊ\Ob0<Q]m1.>K 퍅=<:8; {{{\]([ )(J$%5ժV-\fƔs.oPVEDPjlQÇ_Xrɵ\¡}iָQ7)mf<=8:ҷw/6[C|wBqo  ‹܎8DpN6'غ}')_?@/UoaaÇ/v%S'I`Ї#eeQHuP*Z޽X~CF܂>!4i:d W^c*Ϙ# ~Uw+VvKAʮws׎_]LAAx<#YF(Q<.v`є.Uo/ѭ33-NW7lPG=$kV./pcc1d|>ahZ>x:@9mZwXZ-֯5X+,Z,2atEʓIժVZժdggSa>V-73ӿEۧA^1qxJƍR4Z-LQzl¹^==!jS2ϔmݒEq]ySӽˍdt:TL0/WӓON/d lmlټ~-7bڬYhbjT,r=|i߶ l;͘:sfkc")7 U^Dh ~␒[Y|W^e ܍M_{8F%[ !rf}ίwFۀ]sͺo'$&:`Pw/ĩl;Dn̰QcݗouNzG@n >B 嘫c>r0C.zr cϾDG<|$A2s|ΜM!\Z/Ok~^X2֏n=uwCc"($%W0l>4٨6ˣjٳ? ȓC.ۤ$6+/))5jT %JlGRSSo"q|4:vN8w|R1c6377gȀ~x{zR=:t NwSRR9iuy[[[:v$->QQ&"][d<ȠmVT*:'->W]EO}:U==>:v1~wK?_U` ^ۉ >{.}&>!-[1g>Mdff+>jViXMr򕓓Ì9si;j999܅bco[xtRٕ`銕\z/E)/>>0c^NnkcٵeݻtadeeEdj؁w5!2b5 .?S%=0}\ش.Yt ؕ(abXlm[YJyC{ɨEߡ{YcIRɈ޵nhu1*K1>|4_D%;;[ߍ,?NdUIǎޞU*LMS_nݺ5U*&\yy]A|2i ͚4D ̚omWL11!מW_vv'jqwueHLXb1v'ݾ%|>}i:vΪz=z%))]su}~:XYZ>~ǎϿzGex&.\࡬ˋ")) annNU*IJ&))kkkUjǒxvT*:u (WMƍ+VfbItî={KyMS|9 wShۺnn(J5qq7Ev_܈'*B{caaAEjq3G~ KTv֧9; {{{\]([ )F(.]Գc( ڴj @:u(/ovmf;͚T >>6{{ =PXZ%_^FsP07?\Je_d<Ӵqmʥt}?JE(ioDž?/{1LKϊ/0_fffdeeT*h4|wqqX[[69M.\]foPܽlZ>Ox>]v-"׮ىm"i԰>î_YQr.H޽ذn  ǁo5 ‹VuiHII~ݺxzx`mmO] Tff8,BЧ2r I˟}<帹0frrr8wNۛ*+<[*~;~w~4Qp,5=_֭z>>2GuP*ZqK)OZФa/,9(^6Eq[|;Ν$R*W%b}$/ST)~ITIirckk-=u[>xT=>=Tff(Jڗ$%%Ș]=j&MA閖t:eXY39M.y5uRp("S[u+ԉZ֪ں(j]8pխj:jl p 2I4InBpP{?ϓG̛y{Ͻsϱ&zzօmQ^'1%7 ZM~ddfr:#(^9N抈u&Rf N:+QմiՒϚ5ށWS5/ /VGp,Y HIIER!57G$ ժV˓{ L'%3Ν=znft겴@`dggZwr, V4z JŲP:u`˶ԭ)Ŋu-,Uy>sȱî\ejլIT7d?d8}׾-RiE˓{s'HKF>0I&7H$*>88ۣTHM}<ȒeupxC T*9g>գ{gg'<RIl\.&˄|1TrJ Y <ѧ?}LپPWW6oʜfB8<~K- ]Hp_7U mYr9!*6+V E:h^$%5ը߃v~;ڷmڕ+r}lt= "%337nMӠ@wB\|(  X*DOk{~222w Q2C'T.*:,x._fy|yć8?DEˋȨ([Mi8?;gK:{ÿÛTTʕ*S&N`YJ܅][QFPȤqc4!Q($%=eޢ_Po>k֔)'`2lAg?,,,:RNLZZ:%@|=mOK"?#ƎDx{yZ6auuu]ϋ쿎(H1{减yqG8U_@]xbcrh._=^׸q8]ܭ'7nz+:EDDDDDDDD*}`zt2"~]QQz5&CsC"Z G*7nU(%ߕ"""""""""o >0M7z+z*ӾE.3oHII^~XZ,7OF RRSQgg8p#2Y2G<ӧI:hGZ}e˹{Rvm (v:5UӔ}. Gw~ٗ!>!g0xy?%>>T:`ff^ciѼLbR=uů]M,B__WbgkGp4P\,,,4KZ!{'Oql$;Fk̞COV-QY-g{8deeiʄo߁{wleTZY# Oxzܴdgg3gBZ~֜;r6yUű/B5DEGV^\\/`η߰~&|۳}SӧL"$t% b}͝CXXX}-ZӧWO3 ׭Nc䩜,J$# ի 6aaTRa7j^ d2:8 W(8sǏciۺ%KQDw揈Lj8Ulllkߖ+3job*ʸP.^SZ_@ӧ@Νsk++wʸH#T*Dn@sq9GP(| Qx׿tQٙre˰y6 YYY\rDZ㒕BA|888{T*9sĩJ""""""""""T%\܅'O#Fsʕwчm/=#t b)ܞ<7o[O^MmwA^mŬ)|~BDߊ8pѣqq}kvGЕa÷Vlml[ψ_{/ٶs!+Ws{wЪ?W4o]Vٱk;c]g̔*:?rzB~:ѹ[Oso($̜;A} ʨ]cRY~y7}5wj~T&cЕAFpVQS1h oP9!{B:MCoc!2*~ضsz =0g"'%F8hڸog#L0][7ӧW *6YݞKi~=sٹ[EK?+ωOPz5K:Iiצ!mS-7gwQ-jͥ|;m ׯÃofl̹bjlعe#7bּy(_B" ӫG76nh/ _P;j֬\ٲy{]<<3߾Dl/,?$F BTt4CBKg>eܽT_`߁?ARSf};ժiݾsKCcv;xMk\'$0y @گއٹ{/}{Rb-ٮ8|8).T\ș*Ѣy3.yĤ$zv_6ű$$۷qttdؗZpq6ZV`1[4gp233Ҕ ߾o/ʦVp@'e';;9 ݱl-\L# ={F\|f&vv:NnݘlrF 7߸Icsޢ%x{{{{8}z9H0XN_Nvv3u}5OF !{c0o}Ԭ^|= [;Y&'uDGFT50u; >&˄0ܐ/b"{Ҵq25ό 35m`Ƞ`͑嘙T*)bc㰱&S/ob=]:kըA5;i2IO.GbE|$6msG?lmltuԮE8rfp5ד'<=_ f99:2_n@~t ?IATHNNA&Qn]<ݱy`OIM޾Rss- ЯleqXZXܿ/M =MNࠥS/?K{F"gΝgמ}Ȓe/~Rlm5K<@.DjnCQd2`9C۬X2.-8yV.hKX[YsV''G"ARq ""o.yE sxnÑ{im~jw Ѿmk֮\3|w꧐,Q233~&Qza}}ȓ'qOڷi:2B(]zիT*w!^&˄*86COrr2·P<|r:M3Cj/^-APP(ƪP!|Z{(\؎|esP(VQ( Mnr ܱk6 ۶ay B;BxkÉ5zuзw/n.DEˋȨ(jJo8?;gK:{ġSR%*WDVVfffL8e+ٶs>^^Z[QFPȤqc4!Q($%=eޢ_Po>k֔)'`2lAg?,,,p拍~ۑά IIIƏtRtg(^o/O8n BW%0'L2 צ@C~M5$%%Yre:iΎ% YՕZ/x0*[6bgk+hogyK}4z֯ 5*. ;9֬JО1b꤉ZT*<n}~ !P^K_f@zu.~B9.rԘFkDO!Y_o e!KƎuׇ<|fM($!!!TJHIMPB T*巳ٴu;YYYqe|iJI$ KV,Y'Ç|KګK93m$1($;|+_wO:vmO=}:oFFrVX@]=~"њ}H!_:u{>2P9xJ">!ܳ6\_MO)ш5r/'D.ߵG1ȿ1O`|3u >ߙ̔ yss-($y빛Tsߍ~˗/Q){+|8UI^t |x%Q#')1>:/"dunAٻ O$ )Ex!3ͭ cG[Ĝ𸺺{w!}[b.|)H;b}2ĩJ"h߹ O^)m _+WޡG޿qV5'$=#t b)'/5_V =J^~/"g" c9e\QQ8ӟKCީ=Cl۹޹;h+ @ZZzLʿ/?Eص[󝭍 a1|y1STꐳ#ȱU .됖Fn=ϽaC3s|/+Ofdv ЎJ"d* dn߹SI_OC@W FN+nj`^~B:_EkNlۙ{]:M].+ҟ֡a [D#{9+Vy7-[}IA>&ā4Ji͢Qz҅p/4\]Js=;yѳŋ8:?.{c͗w'QMDŽdvmZh!6Ѣys7ڸ}%LVcn.iSؼ~-|3sf]Vcgk-iָCb/!Y…գM74C:!gφ5(WVN9'^o ܹ;Qxy>4S"b."lJ anEF2oַlZV}V}7ǀrtG&K'x4CPb_뷏ccYl9w?@*׮f{}PIIOtκebؾb5v|B`ԩ>L||<))u k++:T{}Ӣy3.yĤ$zvYz}>zT\JbpV̭hf͛GzzJK^`P<Əo&B]{d\4 >vUɒ ~ ]Ұ~n+Uj~={s=M*gO+EIKK McXJ8::2AT `N⏋һgBBW!KN B֗/bd))TXJk`w IDAT=cZO A`^DݾKffztiF{QT:rܙjlM@@ *3,[j͚[mWɑda_`g:]1`4.:`Oe2lmm%B,Ύ.ٱkׯSJJ_|Zo"W(u<2JgJj*CFaTXYr CG曯'Cta/05kϞ=CVkt EB:A9G=%RTk؉Y4rw+o6ZV`1[4gp233Ҕ ߾o/ʦVUɟif΂9{wl%;;[K'1160*Q8E]\w c/..0oXqFngko֍F.Ixz>܉Ϛ5!^{{{o cI$>!|p"R(}YIYQSyՓ#=#`Nx4l@Y77.(PPK~bfaiաwZl:ř1#г{\]WeOOcꐖ tBBWi9w*U`PEdto=z +$Ur =g|G&M <3xD߹Mbp'..JE6n YZJ"QXŐΔT {Wu)ؑXl9r+WҮMk|ӎu҇PL.^s]ƾo t֓wu EB:Ai׏~ A9wqq@`l=z/4e x3׭Nc䩜֓9^eа jl [ǸQ#G$&&ס=t ULbƓ$rJ=xCB#Juޱv)]Ҭqcw brLAϾ(k$NnH$hצ5e\]133QzXZHykЦ1tk#TՕظ8_;5-hԠ>+V^WjSNm܍Lz*#997oR) ף=Wf0'8E*`ccCUpt%uތu~3R%WҔtrB̞;gœ[PN]͚\ oo/|uξhۇ(Jq=nGSdƔ@V,]DP`O6n`0V1}x>c 9r333J%?m [/^?/$B^bggztGsljSP0cZh[YYV`0222*$(3]ͭhF'/iצ "3zPE ?Pnٸv5.K \F!WRԏLΝ@@{ZStBq1tWwCןx3zC R%+++U;׮_եtX^,D߾5 b:ѤQCj׬SYb%j6ZY8;jZz IŋӼt,Y ^H:;RBjn,9EK1H$U'8jO&Kf%,;_/z";U$eiiReggZwr, V;iYP9C~JEj3 N,GZz:ׯܽ,͜b )j.uތ15fȱCfjլI_O!O-d2Fk!ڷ9%޽\r}zs ҟѿO=<=۬X2.-8yV.hFKX[YV''G"ARq "t؅Ap=~B+DFEQ^!E<TJYΐbŊA 71vƍϜ;oSRΔ+[[!W(+ SNpppۛCTryIsBL"9o7G=6_x5]ŅN/y۶f>Ç ~~ "%337nM^uԡo^APص[[m۰xYu]ć:WSsP?}//"2~1}q1WB:9>%SVۀ#ne\f;RܾsO*& 'N-!>q(TTʕ*S&N`YJ܅Vd!+89:2i,xp 9IIOh),-ԛϚ5e ,X-v $f:w^9S*IIKK#CgtRtg(^o/\+i4lPNӳlX k++zEKԨ^M뱴S'Md՚uFR(iƖǍaYJXiS&abzPNK_>@zu.șw۶UKFR}FlmOO!ԘFk DO!Y_o e!KƎuu<|fMg! HR*U@JjfSeB~+'Jy6mNVVne\>+tgD0w,));K!_2RCRL7##9tW,g?hMՇP?U`jWT'R{RuD_\9UB%k>/[9||nXXX0j0-]Nn=)Z(cF 4%}y%3="b:G#kȽğ\ٿǨѿ͞ElwLtC\NЀ`ϛkA!+^,]ǘb#1K.eo<|8UI^yy |;߷=~$!NU*}.ȱؿU+W 3|s,Ef|Z^{?dq@r֪Ip8[ٰ_~iܨf ׮j:b=ե4JEkpOҞ=˃QY%t5 "ĩ_eܾs6Z0jPe|_; {3v<ɘx)׮ߠt)gƎYW_xԸg?ějsdgP;C띳۶H䙐/1nq̘= ÅTՠXlUR"̌ 5h0;ws1Q9H}2ֆM[ɼY߲iZ}s?kڵeiټ3ERV17)l^O9KU:Η=֚j5(WQHэwՅ._Qޝ4aggG*bjlعe#7bּy(B_\ SE=C!/''ta۵iEȢlF͙ljq$g~L/"""M',?$F BTt4CBKg>eܽT_-8C$%=eַ5ܾsKCcv;xMk 0<}Nma$ VVVtЎ6oOddB__WbgkGp4[SxOVVA L׀;NJj *WaqEf\2IIv69|8"))ʺ8:0苩˵R2hÃgϞV)bWbC,--)Vƍk>ܸy >z(JEq/W@ḷGdgЮMk:wdS6nۛ_ҩFT&XѢ_ %/A…53\JNZ&SХh_N6@Xr%cc)n{B^+.Bmd!_ ${W˯xjhq$)y@o~}ԋ \؏?6,^ݻiDDD߇8p(`$' ɨ_.A@Jj2E4wm-D"-[>vKFsrSu:88h4D](9n=zLIxc5w/O J9;繜oԜsNO$Rd,L׆^^W̄h׺%M6e!Kr]{+PGFf&_ =6c3U!T*ݺ`eeEUpw븶l_Md DO?ţv~ f̚C-h|=c&ТYSnEE1,[8L9lH|q)ݍ[$''qj\JVd^_:?3-7Vj,,,ؾ9 L^]tx)ÙsٵgdQ*&Ő=!?ubHO/Ύ^=ѣO˹\qdSL/^,D߾M|B~LāCIԮYNtJj5mZfMqw =tt +Y5ogY|68;RBjn,9%/')R`4o'Xذa< B-'hߩ5c MM LAPZZ{ ś1ӫȱî\תY&-gVV^CE_\]\ټu+sat]^DZWq.鄙Kb&!|\B9w9{Nj;JsQ^ǏiD'+_Wp]h~ݿϸQ#}ưk\EFGe zĤ$bWNHyJظ8\_[-tx+EKX8wfE6.u"~neE_d勐?uH$j\I82y} Μ=~XڶnIE(0*@lllkߖ+3job*ʸP.^SZ_@ӧ@]uk++ne\qrr$T*""r&>!5vuQ||wqQU32( HUn "Td₢ !jfY27-EY9:}9sspqvv|~*EEEG̜M`&Vvoo/ޓqgM1csj\zs,U`܇@ɴ&[[ǚpFCFfnn|e+MXO{]B{pp1jp޼7|ы+ *|0bn%DN'6Mݻ4~ƍ ;&>C ŔYvKW|CJ חdTw} >]}1ݡz+q=طaz>vw`=ř&׻;[[ ̈QcRU|n6\ݸ%Y.bT]N).ed2~ko5ܙ2CJS#cv1[ #4ޏYXٓXܨܳo) @ 8y*;?<%S>sF7i}R4@ w\YsYF/g+7ujJ.B vT /am@ rBLU<!=Έ9yS|Iܳ m@ SCq}4|}{÷'In^b1h@B =|7P.&' &tz{=-p+d2:m;ؾ3̘6F ҥ\E150m ;YS݇L2!"iҸd{kC.accgODd=CC^rri ƍfmڳܼ<\]ID 7qdeg֫ns)L `EZz: cbINIɉa w4;ވ (ssWF3g9# ڲ#q׮ЙZ5k@ @ q {Et Veּt L|."]&z, H|Y' Lh`񾞞bf͛O@Nw `mn sRϏӧ|2k֬MBEr֬W|4C|8p' ҼiwIFP@-7Efee'@ 8ӧ..6>X1? yyteGBNC+C/\F]Mժb#6@ OA-AYl ._N w7=~;r(7o%O+V`ATDh4v$&>rNFf&d2jL:!MJk :>>|ujC}~![H`ȺUDA@ x ??d⸱,]Ɔ/7хĮPGE JUHvMD/Z~ٮ-ǍeŬ۰ r9oj5if}0..v fĨ1T}*>^՛!FeSU>8{{aЖ!U@ LbϞF^/o;/([%@ 9y*;>Vo/OT%33i7ŠA 9Gܭ 9W\eԬYQ#E@ cع;@ @LU<ƍ,gs#'{/| I&iO0(T1Y9vⷧ`4~> T_l +=AmV&/*'S&u3[7#ˉ=ĭٲiV-oLbe%cGbJ<=x ݦKΞ/}N֭1gjL2CH{.3#(3ۿH71}l4& ӧQf 9ńƆ3?l7+ҷjR^{U՜V~>+Kzf )r9֍ T*,==BCشy gϞ,+~m)T ;t@HpWobJ u|| 晛Б0iE˻#HMfuT}@Rr2ޞܺu Vkϔb?OΞвy3p[3,Y?=oӳdRH]@JTYK4 7SQ=plj(iW ]v$"u:w3⍃\fΝϠHȠShZf[@Ƿ`x (**ҥ߸ _onZڕ+h_' L|jj֬Y7غi=zyBɆspn?2wt~c)lj#az1哙Ҥdn5q)D/Ǜ7ӿo̚;fZΚW:ݺrOF}NMFzFmZV͚7ᵾ8냡x!x]]49{ϖ0iV}rw77=yi4Ň;,Y0O!&vnc0^>B^]:$%O_v:mmW,R(L ڴ1:h)mH'@^R陨5jxnBTeRA_OӡreFF%d2:u 6̹up BzruvAdee+K̜76c.##~uQT$\ݻ=qV-ꮡ>ce- TSHc֯HX;oMPBO1&q_J}I׉|5.|1V-3y8oބ-gϞcp@yÃF6һJk---i٢9quqՕty)u@JiņFc}jYxz7#c1991h`֯]CH gOĿ$xT*i޴)dy988TFfe@GG,,,tȁ|zܚЦUKi}y* <ˊ#>-P4zoVur{;;2wtʊʕ+9*Q*oJKR⦅Fc|ya^mАsV%7CY|( +gm-PtlOޠZXI_Ydo)?IIܸER)MV/q9w/dggQח:>fɤw{{{ۧ}Gm93S,T:czK'PZ\/sl̖I"ښcլ[Kϗf):ubG..V׾<>Gx?T*4Z-}"rG/^gлUXeZj[hя9{ېϊERy[Z[ XY7ڔWxz}4֧wscd Rב ڵ 9%ŤP8)mZk .CҩC{l۷xzCBTa#8/,\-[H rsPk4ȬPYV,,T07z!gϢ7}R\l|b5n:eGBNC+۷9{gϝ˯PTT*jÓA,-d [6~5be_N)كo-J% 5ضc'AK&⯿Sݿ{#- /OOdRH{ Z WJ̭g!SV3}\<_o+9;WCfeŕT==QդnLcX(y;ѧxzxOѰ86nA]XaM*c`j'eezzu|qwsT䛂Y6K~m.]̯ǎd<9U5gbHUɲؼRm|})}6ZZS2(..ΝGyz7%FRuTRq{_Os.YވJg-AYl ._N w7=~;rnަLfE ^5kSvhؑ9 [ dԪY!&5?BAں Z͡GξI}zes'~;Iʕصkvo[hzt%w77zܼ<18ce-KÔ>Pƹs%:|Ĥe;ط.naA9燿EEEXZZ2qX.c×_HHL U*991~:YTdgdN"l Ǜ2qX-\̺  B.cay͜Ņn5/U%bXTsGck'ɱ'h޴n@fMf_dh5Fucog's};@ a"YY܃MxH)%[LJcmcͨu8u®ݴkJoڃ):u|D&W.y̕Ii(L&_vFYÝ)?ԛkl(Osꙅd5Tf79!t 0w"z폫3Mk̕IO*ݩ?d){Ư7b¸+m|R;wfcƑkޔCHϪh4ddfʗ[?l4~?[EQh4xoPnقvDPCR<4LS }dvB~=vF6,(!.bϞF]O 7RXXHp>޾FRq(;"xDZPPXHDd ~d?)>")c7=Ϳ1(Tlx`ix{yOe4ly+ y>xQoʕL55k0jrDġqww]ً<Ӱy,Eÿ:$@ S/!=ϺʦΈ9yS|Iܳ m!"20~ܸLYWʳeG *N *M Fzƣ Iʛ;bA=yo8'%I~i4b-'| v'rܛJ%'OKhO M'0I'9g;$1%I/1|tCTy;P5-A *ڶneR|2e2[7u3rٳHܺ-?+#7/mq1͆{z8NNUq'?T_4GWq ԁlXOfi`iZdLh"_^'O`i׺3A:Rʕ*ѷO/ڶީI'狌8hZ>[GZK,|/b2^On$?@|-(W|7P.&' &tz{=-p+d2:m;ؾ3̘6E1@ʥ_CZz:]ؖ+?hfdf2aT I}d2jrLoyu{h(Kn^. :Q IDAT_a¸l޺]{ C"iLYyv$YلIP@'0i?NjtĒLv6~Q_jg2sG&uTǎF>.G ;K&m01goApⷓ\0;zyAe*)J'e+>Y\LLAM֭tJhع{7Ǝ1* V)..&w8666̝9e/5ёU˗UDNNq\Y23cmHie&`GnlOHRG f|¦[8{<>>^e|?qZm[C U*I}c4n ~ݑ ^{*בRC짟Z6o&iKv}m ;ξIZ5<~I 3oG:uHMw\HJNӓ[njMj[MHEt׬#x67rg9w>#=#Nڣj5oz-)((H&~&|}ٺi=kWa,[0qYbf͛O7` %E^3k?ܙ* ΎӦŔOfRXXKΒ5cVVgѳg3.rǭ֭='z!>>|1}dn4;r֬ұ[t֕HȠMԪYJXP}}[Mnٮ|4Iǰ*s=斋%@X鯢Q>);M!?KcS][CP(Wר7Iz\vԫkLjwT)}_Chp&Ovm4y̔6 R /UL 5|]fˤ0%bh"hۺѲ2҆+ϼ[XKKKj5?@ZZ:)(TILEʖ1U_iyި_kĨȾwlnbtΞ=ȁX[[^mR=]|+++^3iʧ֮#kvv/hu?_3_oUL07ǯg5t ?0 j0{~X ^j4rk@V|srT(zygG:|[)1Zɬ{6Ͽ\dVVz;*Q*oJKR⦅Fc|ya^mАsV%7CY|( +gm-PtlOޠZXI_Ydo7f瓒q#֥Sn_s^z%~΢/u|͒I!ӷO/sͭg!RYR1u,:N^BZ-r_pN+HLAʖ0ET:#qo_z@?u+psu%}B/{-7ndQ4.$%sb2# 5xNi}@~ UhZD NA%_)"?kArJ i4b xiՒƯ5b,Z VKy][ n߾?m m݇/U˜G%.YJ-$ y5dVV(sr,+VzW*sgQ>}).6>X1? yyt7D% 0V6os9Ξ;ϗ_o?S}\.CG*Oڇ0)oգZ#&[씒=BT0q>; \`47'Oسwovfxyz%BJXXXjz5Ur`n=3 1Tj2}_ٹ2++Z&-=w7wI)HŶ4Lg(OFXua݆M4m8^3r+_^_bzf}2tT0{ܱ=%  * [[[;r>),]n8:*8zKwQn̊*}{P;ժ9k7ɀ!:eLF%oRZsS(᫭P:|W6G pⷓT\]۾f-޾i̯GOWrwsWJ*\jҼdcveK :)%KܽΞ;Od^aCƍ,NΠ ..!^{!j2);]OKcݤgdTwyڒy[ό!L2w"JJ 664|u;wa[N*U^]_I T*Z-*JW7bk(1}Rynڼ;[;_;wbꥵR8;SV XB"N:*eK7dkN.]VϳgIO7> Jg<:oW׬IkÅ)z{t"Of5%C۾#} K V& o势~~aiiqcY _n[ ɅĮPGE JUHvMD/Z~ٮ-ǍeŬ۰ r9o=a3quq![0#FKU2yP)7.Gx՜?0Ir 7mТYS6|w(޹C{F AѰqI\вEs8H؀HdVVlŏԺ'Ru|?h06֌zo^o^'M6"SǗLd2~꒛[i\d2~ko5ܙ2C9߆4YXXH]Ce}?J҅ bw`=ř&׵wR]|òOWfwxw(A$'NJ<'%snV,] @X?f vM1vBIDZUDFۋ [PڴjInn3#+&kt#/N")?Q'k.5k`lыC{K^t ̞-ϓGO4S@ b&07Yk>'Oz/{3<^a;@ (bd/+Hn*}zvkK1qTNGWaGh4V^ê&4*@#ٳw^mD m[z"|2en>}4|}яKn^b1h@A z(9v7of3ݡw3aݱ,%-=!z\OKc%\ 2D`ێL$;&3M8 r2ŐN%dOu{!ddf2aT Iosd2C=hѬ)];>B^]dWY53]:xto܄7sfQPP@:Yh`BΈt3k|:u"Kk7 %;^3k?ܙqT8p55{;;fN+9ʔOfUZ*Vzz:K㯿ޘqtj&r\ҿٳq#!>n sRϏӧ|2k֬BEr֬W|4C|8p' ҼiwIFP@-7Efee'@ >~iʸ8U~o^JmKIhpW\FVVA] ;’YdeeckkQ[\M%3AdGboWZ~l_pJv-ԑXZZҪE32ҴmSvXXX}Si999;Cdl G)7hݲXn^uO8:}S9?666 5rCmSP<\9vFKDdQʿZtdVܽ/+WsT8T*iiiOњd ڵ@֐iV@ S^pڴjIA-]VSٮ; A ߾Mqq/U˜Gp_Xd)[(hYY˳XXOQ*sgQ(`<Pk4 9J I; ql ٢{Cfwsz;'vQ4UV5)@ oؙ˖\LNaܸűة^n8:*8zГ9z7'+T0f wUs"qn4 ;9'#3QCtzBAa2Z5K&5?BAں Z͡GξI}zeÐ-w$ow d]*"ĠA و7=}k7ڴқ^%Sf&elr3zo$.$]$&v*՜?,jpTBo2'z6rFvm8n,.f݆M!˱CVIKO7quq![0#FKUҭd )7.Gx՜ۗ  !w2-@p={5z<ł9e9B{a-z@ 4'OReJ)$0HvV6ß3i7ŠA 9@ܑ 3yUϚ @͚55r3/@ (1psԩhQ{V ¹ʜan߹CDd=#3aby7O̞W}JynC O>=Sͽ\HJC`:t WD/^BNnSg L̲z7nC`N2yt,/ț_1gYfj]Jv49j,>mrZ); #=эds1О7f瓒$@4 1˖>p]bHtYwM%'&?MPzpGŋt Ɔ/79W;QR'b EsoOE5V_u̜:ݻwY3k^D/^}|M Ҷu+85]۾fݺŔi3>spwsCȯ8;W/W{qrʯǎ)EGW\cʛNφqLd&Z+3ViM+3t3.]{;{\9ZbƜ95ʕ*ѷO/ڶni-j|:ڵj.]x{y==0e$nLͺ :֬]Dž$̘՟7mo qT}ܼ͈wr191#$Xii,X_A&Й>lۑdgdƴ)4jP.e/!-=lKڕn 3230e*CEҤRԪYG@Ͼ9wuO!iz{-7BR23;5Nvc0Μ=V_~e\|K'g 8̎^@~mAP@ɷ i,%9%'''3$gc_Xbb$.TYK Lm e7{˥+L7`l-8D׫Z̜Qo4J'e+>Y\LLAM֭t(y2sn&cT> ӧ魢V\\Lhplll;scV-_jpA)}/W_^͉nPd ̭gڥL.3:yn؞M;;;;A#4Mpy|| ^~)7-*d|:uꐚzͤn5Cf6ǭ&>nfʒu|8z8qy!}8s<-a1\ǜxx Ys}Iz ڴlA59n+cem̝9⿠_xoplRfrk9kVr_-wʁ?ҒF  wW+M% 1).-W ƯCP(Wר7Iz\vԫkLjr)}_Chp&Ovm4y̔v  R /UL 5|]nW^#++.rz襵FV6YYgTߕdf 8(LVSp7WWJ]֒yUPwn/-=g18r ּAW=Zh+iؙ֤+FfTù=LFpT8p3F'őcǩ닭-ڀǍOW*k);[P)a RZ8WbŊ( \^ʜlOGxG~Qs4kDtјF68W^}1vlKIhpWdu||INNAV3<Ŕd-”3a IDATtC;mFzfJd׮U uowexg4i:ř7Ӫes&O͛|h[n ԑXZZҪE32}8Pr?)L< iQk4ܾ]@ZMFF&׮h2R]/x8~՜4?׮!kQٳW x1U'''RIMN%7/i 8:baaG3|hD=6P(| 3[_? TG-GW^ŅV-[r?Mgʕ*sdVVT|_pDTRbIqq^mW4dyj:'7נl}RZXZZhtlOޠZXI_Ydo{OJƍ,ZOiu|[Ϲx饗~';;:6K&{cooO>m!'GGgH҃?7HKK_/TяXYK %923DϬ1l3*2|ȠX:m{E||/X0?*x{yNdTnnF#ҕ+st>r7nڵi4ϞMӀOXj ժV1:.rt xiw?_ memMBX<\3o3NcnGFmGQ$$$tJT0糶3S%Ce)Ӱsjj5Z\TR7̦ȓǎ4 2oj5ڌ 24j]0V h[uWJvmuceͺ ң7cfwԳ;Os4alܺ]###Ο'**#qUJz{se_~kÔT*gv?@RR`طqq1o"\\\p/F\X}6mNzz:Q`c6o=?/ $ts'BOYb9Zzf\t瑢VŅ!m :8O%CQ^] ĩYj Da$藲^$TT}zNzC3t E]>7?5>`}RɢLox|E gϡyp]1lvo1V=~U CQ~\G5l@Юh4V/_Ѳ6Nc!666z^$c4R%hY߷ޅwe0թ7dǜmJΝ;T*|JGD0ϺX: ST?x+W{7FRo̷8) mPƱo sGfMBսLE.TC较ƶ'|@Ow43Z4P.]bێ0͚@V-7p0[w '2yyА:jxO$&6O 2FCh\] vFUԹ<; "i 93%fo2VyYqSB佒%vWXZZзWO&OImp{>0%C<mZ~"ijPܹ]u?!^[&l.pˤ_p|=|%K?MÔB:2e,S0&{1e2Oԟ0ٔŽ n;y Ustv\2/U]&e>88ۛcuTf/#-oRDIWM╻y3c'^}zU{IY~nne?m:CmM*7)"ī&CB!xP%7k6G8yKL=ދ 9Ρaa;9:֮c/}M ChWp1Θ͊k^Bzmw]Y6ϔrU/*nvr8s2TIT[|J3&7+?ȜrJvꀣ/xXv11C%ˈuQ_ }n&8qc ekcÊ ߁l {˳/)f{%f[˗;u[ 鑤$B:μY3(X ? "P]5 Js"n͵0z\z W ,իL[GPח 7@pˁ `0z:+I.*xڵm8~,䲶f24 .b,uqgaNO$z[ҡ][ kըA@cJ FB=aȀ/p/b|͜‚ ܪ ?oŤ3x2 Onn{54W3skܛy/tKUvߎM뉌fѲ;fNRS+UdϻMgzTTS噓|MB:9eOɷcdzj"۹k7]؟5kp =.thLÏmGǍcz+>Aعk7GJİ=l'3&OQ~^þrE_Cyi\yN;._7ȯtn˸odzs'޻GV: dRҮM,ܹg1l@<=ryԿk|ujvjU̓/lxM}=˕Brz ̢pp_K%Lv~OGѢj۱Yd!Nw{֬zyzJv 7-w0ܸn0^CǑt%Up!@ڄVnL 5>U*UDDFhFP` T*^^T\׮qi1*fMw>^5ܹrѩ};/[Ve|X6$޻Gp8wKHMNzOB:mNC8h9{č8E6JZN?rڇv!u[ܥ<<"Ҥy+f̙Kvr,ip?TPco]HHH~Rvml1O2W C닁x W^K ^#4+lC}8}/6cq>]T(Y#ǎ4Cq^x:u-X[athOjJxzxϟeԕ5F۵BЋk}ԭU+++u`aʊFp!mrsmi5 #֧ۮ=]5ƉO:ȼ3auITt4~ ؉Sٰz>$--Mf5,5+Y:/4wV؉hPC6YVՋ2vpZFY7f$Xt1# a9D߹c4%Kg|ڬ)aⅬXܹrR*-_g2od.]f֖1ߌdiӲ%#C/S jUɝ+C`tv>]ժVIڄBi8DZZ:9p0~/۴'p\ ^HzXX(XrCPߧ'$%%sc:{~ cFe ne0-e|CV/55#ǏS_|!C=n^Ōq8˧P?5CmטcOz\ċ#wgҳ7/reYxƻxqn"&&Oy wcbƆ>>&w3<;wJy,.[WѸr̓|Ȩ(Ο.;beeE1OO|+~{TXgp/1>ԪQ+K## EeL5tj]]9|(yR%ch^4 bW#iӲZjZ7ٵ ǣ&M5z{.׸w'^ yH||<T'6?H=urܹc{,_I"SԪixq:::ř))}Qn~SM1ϢϜ?ȼ)h ?Ϝ%qcݶn&Z<J<=ɕWsbY[3<|5j4]C;Q\|ߎa.]jzYԬ^O++KRSդλRrqrt">>ެ8A ]1;Cݣ;pzvvt_/:|֡=իVyǼT̮][YY]_Ü=mX8o6n cp-\: w8IMMLN&w?} Ȯ6u3+k+N>c:Hv.׸'^8ԪI劾ٻi搑_ԯWGG3gQf 'IGGѠR*OHԋ\ܺW1O;^~grذhޜll* M3...pK[!S4n{Phmi"C5N++K5'_VKrCyHQ9p~۷#iqfOs19)OCmۤ# eJՕg:l#Q3=xsgLSJ wx:Mh4L=|UB^' cGRv?c攉R*P(x\φn$=x-jdahѬ);S(ddhu kS t{ߐ'nM15/I^pS?t1F'O\\9wlݙ:䱷٬4dأջ\kxydƆF̟3sUf͝K7Wu/pٻW/#G2 ̝+_V܋? whز}{}ܡChl_KMMpOƎW`A<aUդq)cUS˛7/7oFd;KJJq&{yfyE3$pn'=] ߙ/8tX ⬻YLHH`Ǽpqqѕ˳pttd o6oFzz:m'##CoRzdys;2m;ˆ&!!+VVR.vml9::RD ~ܸt:Llle|J kNtvvà /ia=EhiVx!ǎKz~;3g6&Sߕ_~C ӥq8u t۲wgJ,5k>;~EJ 1vDYd0sX@HP4 ۳Ԭ{MΨJ0WhJ߸Bt.T?|Δ."nn4j؀]h4^;[[ڶnS1|+`(̓ѦUKm< 1&lml9l.gּqCvmyl^Z0ԙXn=iTsi.]bێ0͚@V-7p0[w4/[e+tתQCv N!_Xddn&LR{%Kȯ|| ߏS~|bQr%V´ݣuIDATfդ^čþ* ۵m0xL=!wqfİ!a[ƘѬ+RvM.YJz7|7ax3d`yeL:]:wei}SBёիҬInz2:5n(lmqqvf݆M_++KjTF=S>vG=!O^p5ƉOsvoםJII!Ek6oxi ^׭X{aJ !C2~Eݍ,^T:ZooU/ jZmps5<ػH h4|3b3V/c\tq; ųd|2TIrgϝ= ۵reʼܫ>B)ÂbcbiR./22OtT*:h -Nz7Dqm&-Kr7o3zl Eߧ[uA.$'PƧ4 {\tKkuAR2dʊ2M2z7Fqm&CB!xP%#O/soJ,el߹!4kFoz!BzP$B:μY3^M[|Jg"v|TjmmMQ"v@˙^0"̝mrR!BjH?lV(V|k`JgmÏhZĨ1Xtngc#CNB!D:ovXz?ڧFa[XC=+L1Ȩ(7 ԋvd$SڍT*7"$5le7#-_^?ט4mQQ4o̦X:{w0t(~֙ߕQ(ʕ&4feDDܦd'08},vvv@0p0nGFFHPKB:"(?"^"˖cFuG؉Ҧe ?^$hOشuqxu!qqq6;uƍ^B!/tPgΞcΝ5W#؉Sh߈O7bo s%<~,>kmAM jHj;q1KWҋ2B6 /{beeE.8 "EXt1n1d{Q ~&ݻ1tҍe,#{~ cF@xDYᢢ9e"oܤ5%d|3+V /OOly'¥KܽC5t%K^v^''v:wlϒ+VVv@Z5%Q?NGG-}:T?[nSB;S~u)Yh\+T`R)IMUpn  d}œ8UJ%EիD߹c4B!o:8aԪI劾ٻi搑_ԯWWK-o2#<.ZVwyدdYԮYhG{k4J4}ztCa:wq& Q'p g=39JKŹ˯ܾI0{dstl!B7 Uz߈sfҷWw._ʬs_?^"n899rqݫ#%U*%s2y#^č]ؾ# FÖ۳}BꎛW$Px-²HQIKKS܎4Walܺ]###Ο'**hleğF: B!x+7\Y^J޺#zujaoGo? sXv%H\t瑢VŅ!m :8O%CQ^] ĩYj DaÑNdTYLLf 2?, S(ohxS&nDbb(!<mZ4B!bZ_JÔB:2e2gJJ -Zy^D!BYNKWi8{nSخݔ+SF: B! y B!0J8!B!"!B!IqB!B$!B!IqB!B$!B!I*Ȝ^I!B! QDEEj_w"B!o6$B!0I:B!B B!0I:B!Brqewh^Fڄ۸wTt4 ? $.>-B!T99!!CנB;T*u,--_V^%+VqC0%-9_ם!BQaO?UV-t_ OIאy0 .zB!uGHpoֲ ߎ-ƍ[t_Rnm:=HNNf9~$N)ͯ{l|Ztd,Pݿa톍,~.7uDE!#ӲyG}ñ'kҌ3k,YC0!%ӧg o2/M>mɄ1Y::9E^|7~"d<؉eM7B!G)\rrVK?x+VGBLU Ƭyӳ[W\ ;wn- &a #= 3s ߍFR۾}SSԸIS τ)߷5W'3k.YFx-1 /*Fx83;zd,/O]& /FR$55Of3elD3q2B! 66ǏrtZMjj*]1A,[=vo4~2˛h>=@kؐǎӣ^<)^F )S߿@2e`*Q`|9?MHHE^xuƆ+T*M}RIJJKy VV1+ z2B!o8?V5C.kk]&^^|ȏ(m8RdTw}h(\oP(*V,2#"nTZ^#l09mTJ%Es'Q|y9O8j֨J4+ 3B!x;=4TTB@~xTINNy3+[[J%6w57Q9‚oGy:wAn _sNӣЂBBqاծ?nD8|(C7+&\!B}|PbEsCU9~O#"(X Y (Vիn/BknW)39.0?dvW1 ԁKWmj3r1Sp!ӉuU/FFe˘kae.B!-usqo1GälF:u oo?dy\Njj*.^ٿpttn̝?hp1]!Ɂzz:"))Dnݎ>.׮޽ܹ{ϛ|yRFu|RRR8~$ &fw cԨ9L4TZEO<9B!: ߿GQ;;Jh q /Ol_:׫:By""nlJVZé3q-\\.\EK‚7o<)6sΦmYv7#"(W n űx \M[HOOo:9X~oܠ|FTї/\d۰oRLik2uiPC\MOjZo?;pBfB9x0/XϿ]n irmnB!o8;;;۲4<"**ޮjQ(DEG.]NN;Yodlll^w2B!KbhS+lҫ NB!ĻǜNC_B!B=4hh!B!NãwB!/֣w4q1Z UB!]N3-B!oKNN=A\232~BIENDB`mod_auth_mellon-0.18.0/doc/user_guide/images/saml-tracer.svg000066400000000000000000005103331410105036600240470ustar00rootroot00000000000000 image/svg+xml SAML tab must be active List of HTTP messages,SAML messages areflagged with SAML icon mod_auth_mellon-0.18.0/doc/user_guide/images/saml-web-sso.svg000066400000000000000000001347611410105036600241550ustar00rootroot00000000000000 image/svg+xml Browser Service Provider(e.g. Mellon) Identity Provider 1. User attempts to accessresource at SP Is there a session for this user?No, then create one. 2. SP determines IdP. Creates<AuthnRequest> message andembeds it in URL redirect to IdP. Login form sent to user User replies with userid & password 3. IdP authenticates principal. 4. IdP responds with <Assertion>embedded in form whichautomatically posts to SP. 6. SP replies with redirect fororiginal resource (RelayState) 7. SP accesses resource again 8. SP responds with resource 5. SP validates <Assertion>and creates user session. Is there a session for this user?Yes, validate & return resource. mod_auth_mellon-0.18.0/doc/user_guide/mellon_user_guide.adoc000066400000000000000000010501761410105036600242050ustar00rootroot00000000000000= mod_auth_mellon User Guide John Dennis v1.3, 2018-02-22 :toc: left :toclevels: 3 :numbered: :icons: font :imagesdir: images :data-uri: == Colophon Author: {author} {email} Version: {revnumber} Date: {revdate} == Document Conventions .Example Data used in this document **** This document contains many examples of SAML data. For consistency we will use the following: * The SP is hosted on the node `mellon.example.com`. * The SP `MellonEndpointPath` is `/mellon` * The SP `entityID` is `https://mellon.example.com/mellon/metadata` * Mellon is protecting the URL location `/private` and everything under it. * The protected resource is `/private/info.html` and hence the URL of the protected resource is `https://mellon.example.com/private/info.html`. * The IdP is hosted on the node `rhsso.example.com` * The IdP `entityID` is `https://rhsso.example.com:8443/auth/realms/test` **** == Introduction mod_auth_mellon is an Apache (httpd) authentication module providing authentication and authorization services via SAML. Mellon plays the role of a _Service Provider_ (SP) in SAML. == SAML Overview SAML (_Security Assertion Markup Language_) is a framework for exchanging security information between providers. The nonprofit https://www.oasis-open.org/[OASIS] consortium is responsible for defining and publishing the various SAML specifications. OASIS is an acronym for _Organization for the Advancement of Structured Information Standards_. All SAML specifications and errata can be found at this location: https://docs.oasis-open.org/security/saml/v2.0/ The SAML technical committee has published the https://www.oasis-open.org/committees/download.php/27819/sstc-saml-tech-overview-2.0-cd-02.pdf[Security Assertion Markup Language (SAML) V2.0 Technical Overview]. This is an excellent high-level overview of SAML and worth reading to familiarize yourself with general SAML operation and terminology. SAML is a large complex standard that currently comprises 10 individual specifications whose total content is hundreds of pages of printed material. SAML is much too large to cover in this overview. Instead we will focus on the most common use of SAML, Web Single Sign-On (Web-SSO). This is the target focus of mod_auth_mellon, although Mellon does support other profiles as well. SAML organizes itself into <> and <>. A cursory overview of these two concepts will help you understand SAML better and is especially important if you have to refer to any SAML specifications. === SAML Roles [[saml_roles]] Participants in SAML play different roles. An entity may be capable of playing more than one role, however we typically only consider a single role when discussing entity behavior. The defined SAML roles are: * Identity Provider (IdP) * Service Provider (SP) * Affiliation * Attribute Authority * Attribute Consumer * Policy Decision Point Of these we are only interested in Service Providers (SP) and Identity Providers (IdP). Mellon is a Service Provider because it provides a service to clients. Authentication and user information is provided by an Identity Provider. The SP relies on the IdP for its authentication needs. In SAML literature you will often see the term _attesting party_ or _asserting party_, which in most contexts means an IdP because the IdP attests to or asserts certain claims in its role as an _authority_. On the other hand a Service Provider is often referred to as a _relying party_ because it _relies_ on the _assertions_ provided by an _authority_. === SAML Profiles [[saml_profiles]] A SAML profile defines how SAML data is conveyed using <> on a transport to accomplish a specific task. The _Web Browser SSO Profile_ is the best known and the one Mellon focuses on. Other profiles include _Single Logout_, _Enhanced Client or Proxy (ECP)_, _Identity Provider Discovery_, etc. SAML profiles are defined in the https://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf[Profiles for the OASIS Security Assertion Markup Language (SAML) V2.0] specification. === SAML Bindings [[saml_bindings]] SAML bindings define how SAML messages are mapped onto standard messaging or communication protocols. The best way to think of a SAML binding is as a transport mechanism. A key concept is that a given SAML profile may permit the same SAML message to be conveyed using variety of SAML bindings. Or by the same token a SAML profile may prohibit the use of certain SAML bindings. SAML bindings are defined in the https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf[Bindings for the OASIS Security Assertion Markup Language (SAML) V2.0] specification. === SAML Messages All SAML messages are conveyed as XML documents. A SAML XML message may be transported in a variety of mechanisms known as a <>. Examples of SAML bindings include: * query parameters of an HTTP URL. * parameters of an HTML form. * wrapped in a SOAP message. The exact way a SAML interchange operates and the SAML bindings which are utilized in each step define what is called a SAML <>. For example web-sso is defined by the _Web Browser SSO Profile_. SAML data, and its XML schema are defined in the https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf[Assertions and Protocols for the OASIS Security Assertion Markup Language (SAML) V2.0] _core_ specification. === SAML Web-SSO flow [[web_sso_flow]] The _Web Browser SSO Profile_ is the best known <> and the one Mellon focuses on. Your ability to configure Mellon and diagnose Mellon deployment issues will be greatly enhanced if you understand this flow and the two SAML messages conveyed in the flow, _SAML AuthnRequest_ and _SAML Assertion Response_. image::saml-web-sso.svg[Web Browser SSO Profile] (1) HTTP Request to Service Provider:: A user agent (e.g browser) makes a request on behalf of a user for a protected resource hosted by the Service Provider (e.g. Mellon). The SP asks if there is an existing <> for the user. A session is established by a prior successful SAML authentication. If a valid session exists the SP immediately grants access to the protected resource. A user session is communicated via a HTTP cookie (see <>). If a valid session does not exist the SP begins the authentication process. (2) issued by Service Provider to Identity Provider:: To authenticate the user the SP must send a `` to an IdP. In the _Web Browser SSO Profile_ the SP determines the IdP. The SP uses the _HTTP Redirect Binding_ to convey the `` to the IdP. This binding embeds the `` in the URL query parameters of a HTTP redirect. The browser performs a redirect to the IdP which decodes the `` embedded in the URL query parameters. The IdP also maintains <> information for the user. If there is an existing valid session for the user at the IdP it immediately responds with a `` response unless the `` has enabled `ForceAuthn` which requires the user to be re-authenticated. See the <> to better understand its contents and how it appears as HTTP data. Part of the data communicated along with the `` is an item known as the <>. The `RelayState` is the mechanism which permits the flow to return to the original requested resource. (3) Identity Provider identifies Principal:: If necessary the IdP authenticates the user. How the authentication is performed is *not* defined by SAML. Typically the IdP responds with a login page where the user enters their username and password. After successful authentication the IdP establishes a session for the user. (4) Identity Provider issues a SAML to the Service Provider:: Assuming a valid session now exists on the IdP for the user it responds to the user's browser with an `` using the _HTTP Post Binding_. The HTTP Post binding is a bit magical, you may want to review how this works in <>. If a valid session could not be established for the user a failed status response is issued instead. See the <> to better understand the contents of an ``. (5) SP grants or denies access to the principal:: If the response does not contain a successful `` response the SP denies access by returning a 403 HTTP_FORBIDDEN status response. Otherwise the SP processes the ``. The SP may apply additional constraints on access to the protected resource (see <> for how Mellon can apply additional authorization constraints). If the constraint check passes then the SP establishes a session for the user. Mellon associates the session information with a session ID returned in a cookie (see <>). (6) SP redirects to original resource:: The SP uses the <> which identities the original requested resource and responds with a redirect to that URL. (7) Browser accesses resource again:: The browser upon receiving the redirect to the original resource URL once again tries to access the resource. This time however there is a valid session established for the user as communicated in the session cookie. The SP validates the session ID which should immediately succeed. At this point Mellon informs Apache that the authentication and authorization check has succeeded for the URL. (8) SP responds with resource:: Since the authentication and authorization checks in Apache have now passed the contents of the resource are returned to the user's browser. ==== Sessions [[saml_sessions]] Sessions are maintained at both the SP and the IdP. After an IdP successfully authenticates a user it creates a session for the user. The IdP keeps a list of every SP the user is logged into. When the user logs out the IdP sends a logout request to the _SingleLogoutService_ endpoint of each SP. When an IdP receives a `` from a SP it checks to see if it has an existing valid session for that user, if so it can skip authenticating the user again and instead just issue an `` based on the existing session. However the SP can force the IdP to always re-authenticate if it passes a `ForceAuthn` value of `True` in the ``. The IdP may further be restricted from interacting with the SP if the request contains a `isPassive` value of `True`. The IdP can inform the SP how long it wishes a SP session to be valid by passing the `SessionNotOnOrAfter` attribute in a ``. Mellon respects the `SessionNotOnOrAfter` attribute and will limit its session duration based on it. The SP also maintains a session for the user. The SP session is communicated between the browser and the SP using a cookie containing the session ID. If the SP verifies the user has an existing valid session when it receives a request it can immediately make an access decision based on the cached session information for the user. See <> for more information on the particulars of how Mellon manages it sessions. === HTTP Post and Self-Posting [[http_post]] The _HTTP Post Binding_ is used to convey an assertion back to a SP. Assertions are usually too big to embed in a URL so some other mechanism is needed to transport the `` response data. The data is url-form-encoded as HTTP Form. The form's action attribute specifies the destination URL of the form data. Where does the destination URL come from? The IdP will have loaded the SP's <> which defines among other things the various provider URL <> where SAML communication occurs. The `` needs to be sent to one of the SP's _AssertionConsumerService_ endpoints, specifically the _AssertionConsumerService_ endpoint URL supporting the _HTTP-POST_ binding. The SP's _AssertionConsumerService_ URL as read from its metadata is set to the action attribute of the HTTP form. _The action URL is *not* read from any data in the _, this is one safeguard to prevent SAML messages from being sent to a unintended nefarious party. Note that many SAML bindings define a `Destination` attribute that is embedded in the SAML message. A further check compares the `Destination` attribute to the URL the message was received at, see <> for a common deployment problem. But how does the POST data as received by the user's browser get back to the _AssertionConsumerService_ endpoint of the SP? The form will _self-post_ due to this: [source,html] ---- ---- As long as the user's browser has not disabled scripts it will immediately post the form data to the _AssertionConsumerService_ URL in the forms action attribute. If scripts have been disabled the HTML will instruct the user to click the `Submit` button to post the form data. === entityID [[entityID]] Each SAML provider (e.g. a SP or IdP) is identified by its `entityID`. You should think of the `entityID` as the globally unique name of the provider. The `entityID` appears in most SAML messages and in the provider's metadata. This is the mechanism by which a SAML message consumer associates the SAML message with the message producers configuration properties. When a SAML provider receives a message it extracts the `entityID` from the message and then looks up the metadata belonging to that provider. The information in the provider's metadata is essential in order to operate on the SAML message. WARNING: Any mismatch between the `entityID` the producer is emitting and the consumer has loaded via the producer's metadata will cause failures. A common mistake is to modify a producer's metadata (e.g. update Mellon) but fail to reload Mellon's metadata in the IdP. SAML places two requirements on the `entityID`: * It *must* be an URI * It *must* be unique within the federation A wise administrator will also seek to fulfill this additional requirement when choosing an `entityID`: * It should identify the organization instead of a specific node within the organization. When migration time arrives it is far easier to move resources around when those resources are not tied to a specific node. Thus choosing an `entityID` comprised of the organization's domain name and a generic identifier such as `saml` would be one good approach, for example `https://bigcorp.com/saml`. A common practice and one that recommended in the https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf[Metadata for the OASIS Security Assertion Markup Language (SAML)] specification in section 4, _Metadata Publication and Resolution_ is to use the `entityID` as a _well-known-location_ for retrieving the provider's metadata. In other words the `entityID` is the URL which returns the provider's XML metadata document when a HTTP GET is performed on that URL. _It is not a requirement the `entityID` be the URL for metadata download_ rather it is one common convention. As discussed in the SAML metadata specification other mechanisms can be established for metadata publication. IMPORTANT: SAML requires metadata publication to be integrity protected. A provider's metadata is literally the _keys to the provider's kingdom_ as it contains the cryptographic keys used during SAML authentication as well as other vital SAML properties. _It is essential to establish trust and validate the metadata._ Metadata can be signed but the easiest way to assure metadata integrity and the most common is to make sure metadata is only exchanged via a secure and trusted channel. TLS provides such a mechanism. Therefore if you publish metadata (and Mellon always does regardless of whether Mellon's metadata endpoint matches Mellon's `entityID`) it *_MUST_* occur _only_ over the `https` TLS scheme. Make sure your Apache configuration redirects any `http` for Mellon to `https` and that your https certificate is signed by a trusted CA such that others can properly validate your https cert. _Do not use self-signed certs for your https!_ If you do and you're on a public network, you're opening yourself up to a serious security vulnerability. Note, the certs used _inside_ the metadata can be self-signed, see <> for an explanation of why. The key concept here to take away is that _a provider's metadata provides the trust and is the validation mechanism used by SAML_. Thus the integrity of the metadata is of paramount importance. Mellon's metadata is _always_ published at the URL location `/$MellonEndpointPath/metadata`. See the description of <> for more details on the use of MellonEndpointPath. This is why most of the tools surrounding Mellon generate an `entityID` as the concatenation of the https scheme, the hostname, the MellonEndpointPath and "metadata". Thus for example if the `MellonEndpointPath` for `bigcorp.com` was set to `saml`, the `entityID` (the URL location for downloading its metadata) would be `https://bigcorp.com/saml/metadata`. The only reason why "metadata" appears in the `entityID` is because that is Mellon's URL endpoint for metadata publication. === Username, userid, SAML NameID [[name_id]] ==== Userid vs. Identity (or why userid is so last millennium) Many people struggle with the notion of _userid_ when working with SAML (or any other federated identity system). That's because historically _userid_ has been used to describe _identity_. The two are not the same. _Identity_ identifies who or what something is for the purpose of authentication and authorization as well as binding attributes to that identity. In most of the literature the terms _subject_ and _principal_ are used interchangeably to encapsulate the concept of who or what is being identified. Although a subject is often a person it need not be, it might also be an inanimate object. A good example of a non-human subject would be a computer service needing to be authenticated in order to perform an operation. Userids grew out of the early days of computing when all computing was local and users were given accounts on a local system. The userid was how operating systems tracked who a user was, in most cases it was an integer. Clearly the integer userid only had meaning in the context of the local system. As systems became networked integer userids would be shared between systems but fundamentally nothing had changed, the userid was still meaningful only among a group of cooperating computers. Tools such as Yellow Pages, NIS, LDAP and Active Directory were developed to provide a centralized repository of userids that could be shared between cooperating networked computers. Along the way the integer userid morphed into a string often partitioned into a local part and a domain part. The domain part is used to identify the realm. Realms are nothing other than collections of _unique_ userids often serving the needs of a organizational unit (e.g. company or institution). A key concept is that whoever is providing the userid, whether it be local accounts created by the host operating system or a network provider of userids such as NIS or LDAP, is an *identity provider* (IdP) with the _userid_ being the *key* used by _that_ specific *identity provider* to look up the *identity*. Hence _userids are only meaningful in the context of a specific IdP!_ By definition _federated identity_ is the amalgamation of diverse unrelated identity providers, each of which utilizes its own userid as a key to look up an identity. Therefore while deploying federated identity if you cling to the concept of a single userid you are likely to be frustrated because you are abusing the concept. ==== How SAML identifies a subject [[saml_nameid]] In SAML the user name (principal or subject) is conveyed as part of the `` element in the assertion. The subject identifier can be any one of these elements: * `` * `` * `` The most common is `` and it usually includes a `Format` attribute. If the `Format` attribute is absent then it defaults to the unspecified `Format`. The `Format` attribute tells you how to interpret the `NameID` value. For example if the subject's `NameID` format is `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` you know the subject is being identified by their email address. The currently defined `NameID` formats are: Unspecified:: This is used when you don't care what the `NameID` `Format` is, you're willing to accept whatever it defaults to by the provider. (`urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified`) Email Address:: The `NameID` is an email address as specified in RFC 2822 as a `addr-spec` in the form `local-part@domain`. No common name or other text is included and it is not enclosed in `<` and `>`. (`urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) X.509 Subject Name:: The `NameID` is an X.509 subject name in the form specified for the `` element in the XML Signature Recommendation. (`urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName`) Windows Domain Qualified Name:: The `NameID` is a Windows domain qualified name. A Windows domain qualified user name is a string of the form "DomainName\UserName". The domain name and "\" separator MAY be omitted. (`urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName`) Kerberos Principal Name:: The `NameID` is in the form of a Kerberos principal name using the format name[/instance]@REALM. (`urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos`) Persistent Identifier:: The `NameID` is a _persistent_ opaque identifier for a principal that is specific to an identity provider and a service provider or affiliation of service providers. Opaque means you cannot (easily) map the id to a user. In many cases the persistent id is implemented as a random number or random string. Persistent means you'll always get the exact same `NameID` for the same subject. Refer to <> and its `AllowCreate` attribute to understand if the IdP is allowed to create a persistent id for the subject if it has not already done so. (`urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`) Transient Identifier:: The `NameID` is an opaque _temporary_ id. Opaque means you cannot (easily) map the id to a user. In many cases it's implemented as a random number or random string. Temporary means the id is valid _only_ in the context of the assertion response which contains it. Think of a transient id as a one-time id that cannot be used again or referred to again. (`urn:oasis:names:tc:SAML:2.0:nameid-format:transient`) IMPORTANT: The important concept here is that SAML's `NameID` as used to identify a subject is not the traditional userid you are probably used to. Furthermore SAML's `NameID` _may_ only be meaningful to the IdP which issued it. ==== Burden of interpreting NameID falls to the relying party [[nameid_interpretation]] Ultimately the SP needs to provide some sort of _userid_ the application it is hosting can utilize. _Only the application knows what it needs!_ Let's take the example of an application which wishes to identify its users by email address. There are two basic ways you can do this with SAML. 1. Specify a `NameIDPolicy` of `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress` when the SP submits a `` to the IdP. This tells the IdP you want the subject's `NameID` to be their email address. 2. Ignore the `NameID` returned in the assertion entirely, instead use the assertion's `email` attribute. Solution #2 is very important to understand as it illustrates how many organizations utilize SAML: they _build an identity from the attributes bound to a subject_. They can use one or more attributes to build a userid meaningful to the application. They may even require the IdP return an attribute unique to the subject across a federation, in this instance all IdPs in the federation must support that attribute (this is just one approach). The `NameID` is *not* utilized in solution #2 in large part because the `NameID` is likely to be uniquely bound to the given IdP. This is why SAML's _transient_ identifiers are often used: it simply does not matter what the `NameID` is because the SP is not utilizing it therefore it can be any random one-time value. It is also important to understand either the `NameID` _or_ the set of attributes _or both_ can be used to ultimately derive an identity to pass to the application. Another approach is to utilize SAML's _persistent id_ with the observation that the pair (IdP, persistent id) always uniquely and repeatably identifies the subject. The SP can maintain a table that maps the (IdP, persistent id) pair to an application-specific _identity_ using this technique. ==== How Mellon handles the NameID Mellon extracts the `` element from the assertion's `` element and sets this to `NAME_ID` attribute. If `` is absent in the assertion you can change what Mellon considers the user name to be to another value in one of the assertions attributes if you wish. `MellonUser` names the attribute you wish to use instead. If you want to export the username as `REMOTE_USER` so your web app can process this very common CGI variable see <> NOTE: Please be aware that blindly exporting the SAML `NameID` to the application may or may not be appropriate for the application. See the explanation of <> to understand the issues. ==== How do you specify the NameID format in SAML? [[specify_mellon_nameid]] In SAML there are 2 configuration options related to the use of `NameID`: +1.+ A provider declares which `NameID` formats it supports in its <> via the `` element. The following metadata excerpt illustrates a provider which supports the `transient`, `persistent` and `X509SubjectName` formats: [source,xml] ---- urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName ---- +2.+ The SP indicates to the IdP in its `` what `NameID` format it wants returned via the `` element. The `` should be one of the `NameIDFormat` elements enumerated in the IdP's metadata. The IdP is free to substitute another `NameID` format or to return an `InvalidNameIDPolicy` error status response if it can't satisfy the request. [[nameid_policy]] IMPORTANT: Mellon defaults to a `NameIDFormat` of `transient` when it <>. You will need to manually edit the `NameIDFormat` in your Mellon SP metadata if you wish to use a `NameIDFormat` other than `transient`. When Mellon generates its `` it selects the _first_ `NameIDFormat` found in its metadata as the `NameIDPolicy`. === Example [[authentication_request]] Here is an example `` as emitted by Mellon. [source,xml] ---- Version="2.0" IssueInstant="2017-06-28T13:39:14Z" Destination="https://rhsso.example.com:8443/auth/realms/test/protocol/saml" Consent="urn:oasis:names:tc:SAML:2.0:consent:current-implicit" ForceAuthn="false" IsPassive="false" AssertionConsumerServiceURL="https://mellon.example.com/mellon/postResponse" > https://mellon.example.com/mellon/metadata AllowCreate="true"/> ---- <1> `ID`: Unique ID generated by Mellon to identify the SAML request. It will appear in the SAML response in the `InResponseTo` attribute. Used to correlate SAML request and responses. <2> `IssueInstant`: Timestamp of when request was made <3> `Destination`: Where this request was sent to. Used as a protection to prevent malicious forwarding of requests to unintended recipients. The recipient verifies the URL where it received the SAML request matches the `Destination`. <4> `ForceAuthn`: If true the IdP *must* authenticate the principal instead of relying on an existing session for the principal. <5> `IsPassive`: If true neither the user agent (browser) nor the IdP may take control of the user interface. <6> `AssertionConsumerServiceURL`: Where to send the assertion response (see <> to see where this was defined. <7> `Issuer`: The SP which issued the AuthnRequest. See <> to see where this was defined. Also see the general description of <> <8> `NameIDPolicy`: The SP requests that the Subject returned in the assertion be identified by a transient name. See <> for more details. <9> `AllowCreate`: If true then the IdP is allowed to create a new identifier for the principal. The `` is sent using the _HTTP Redirect Binding_. The above `` appears on the wire as a URL with the SAML data embedded as query parameters. You can see the "on the wire" HTTP data for this `` in <>. This illustrates the need for SAML diagnostic tools because you cannot see the `` XML message and its assocated data (e.g. signature, <>) by looking at the HTTP protocol. === Example [[assertion_response]] This is an example of an `` response as generated by a Red Hat SSO server (Keycloak) in response to the above <>. [source,xml] ---- xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Destination="https://mellon.example.com/mellon/postResponse" ID="ID_d06daaaf-64ec-44d3-95a7-08da893aa9d5" InResponseTo="_59126C3306E4679F653022F0C4DA7F04" IssueInstant="2017-06-28T13:39:27.331Z" Version="2.0"> https://rhsso.example.com:8443/auth/realms/test V/3iYohGv2Ot7pzy6q/BfAdXgSxmdCD7K+XEmFIZlUs= ... 1VPndjfABB6S4lb4zwMLjBUhxfzPFnfrvNYvRgcxiUM ... ... AQAB ID="ID_c463a141-d471-40c3-860a-6559ce0a3556" IssueInstant="2017-06-28T13:39:27.331Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> https://rhsso.example.com:8443/auth/realms/test w8bELRshtX7xHcwZCdglgfpyYBMJmVQJALPAclHHbLA= ... 1VPndjfABB6S4lb4zwMLjBUhxfzPFnfrvNYvRgcxiUM ... ... AQAB xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> G-803528aa-2f9e-454b-a89c-55ee74e75d1e https://mellon.example.com/mellon/metadata urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified Name="groups" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">ipausers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">openstack-users jdoe@music.com John Doe JD Doe John uma_authorization manage-account view-profile ---- <1> `Response`: The `` element contains the entire SAML response, which includes information concerning who issued the response, when the response was issued, optionally a signature on the response, and the actual response contents which in this case is an ``. <2> `Destination`: The _AssertionConsumerService_ endpoint where this response will be sent. The receiver will verify it arrived at this location or it will reject it. <3> `ID`: Unique ID generated by Mellon to identify the SAML request. It will appear in the SAML response in the `InResponseTo` attribute. Used to correlate SAML request and responses. <4> `InResponseTo`: This identifies the SAML request being responded to. It matches the `ID` attribute in the <>. This is how SAML requests and responses are associated with one another as a pair. <5> `IssueInstant`: Timestamp of when response was made. <6> `Issuer`: The IdP which is issuing this response. It is the <> of the IdP as defined in the <>. <7> `Signature`: This response is signed by the IdP. The signature information is contained in this XML element. <8> `Reference`: Identifies the XML element being signed. In this instance since the signature reference points to the top level `` the entire response is signed. <9> `Status`: The status of the SAML response. Because in this instance the status is `urn:oasis:names:tc:SAML:2.0:status:Success`, the authentication was successful. If it had not been, an error status would have been returned. *The element is where to look for the success or failure of a SAML request.* <10> `Assertion`: This begins the assertion data. It represents the _content_ of the SAML response. <11> `Issuer`: The IdP which is issuing this response. It is the <> of the IdP as defined in the <> <12> `Signature`: The signature on the ``. Note this is independent of the signature on the response. <13> `Reference`: Identifies the XML element being signed. In this instance the signature reference points to the `` element, because that element has the matching `ID` attribute. <14> `Subject`: This begins the `` element which identifies the principal being authenticated in this ``. <15> `NameID`: *This is where Mellon obtains the username in the assertion.* Because the format is `transient` it is a random value assigned by the IdP. Note the `MELLON_NAME_ID` in the Apache environment exactly matches this. See <> for more details. <16> `AttributeStatement`: This begins the *set of attributes* supplied by the IdP. <17> `Attribute`: This is the attribute whose `name` is _groups_. It is a multi-valued attribute because it contains 2 `` elements. This attribute could be written in pseudo-code as `groups=["ipausers","openstack-users"]`. <18> `AttributeValue`: These are the values for the attribute. In this instance there are 2 values, _ipausers_ and _openstack-users_. The above `` response is conveyed back to the SP using the _HTTP Post Binding_. You may wish to review <>. The "on the wire" version of this `` response and its associated parameters can be seen in <>. Once again we see the need for SAML tools because it is impossible to view the XML message and its associated parameters give the contents of the HTTP response. === SAML Endpoints [[endpoints]] When two SAML providers communicate they must know the URL to send a given SAML message to. The set of URLs where a provider receives SAML messages is often referred to as its SAML endpoints. Although a SAML endpoint may appear in a SAML message, this is *not sufficient* to identify where a response should be sent. This is because a nefarious sender could insert a bogus location in the message. To assure SAML messages are only exchanged between the expected parties, the message endpoints are established outside of the message exchange via a trusted mechanism when a relationship is initially established between the two providers. Although not mandated, this is almost always accomplished via the exchange of SAML <> forming a trust relationship between the two providers. The presence of a SAML endpoint in a SAML message is typically there to validate the message against the previously established trust information. Also as you will learn below, an endpoint must be paired with a binding type, which is another reason why an endpoint appearing in a SAML message is not sufficient to establish a communication pathway. A SAML endpoint is defined by a (service,binding) pair. The service component identifies what action is hosted by this endpoint. Examples of common SAML services you are likely to encounter are: SingleSignOnService:: Authenticate a user and establish a session for them. This is an IdP service, it's where a SP sends its `` message. AssertionConsumerService:: This is where SPs receive `` messages from an IdP in response to a `` message. SingleLogoutService:: Terminate a user's session by logging them out. Both SPs and IdPs support this. The binding component of a (service,binding) pair identifies the format of the message. Recall that SAML offers many different ways to encode the XML of a SAML message. The binding component allows the receiver to know how to decode and parse the SAML message back into an XML SAML document received at its service endpoint. It's important to understand there is no requirement for a SAML provider to locate all its (service,binding) pairs on distinct URLs. It is possible to apply heuristics to a SAML message to identify the binding of the message arriving on a given URL. This allows a provider to collapse its set of endpoints into a smaller set of URLs The choice of how a provider maps its endpoints to URLs is entirely up to the provider. A common mistake is to assume that because one provider does it one way all providers follow the same model. The only way for you to know is to examine the providers metadata (see <>) === Relay State (How you return to the original URL) [[relaystate]] If you've ever wondered how after all the redirections, posts, etc. involved in Web-SSO one finally returns back to the original requested resource, you will find the answer in SAML's `RelayState` parameter. The `RelayState` is set by the SP when it first initiates authentication. SAML requires every party that handles a SAML message to preserve the `RelayState` and ultimately return it to the original requester. Officially SAML constrains the `RelayState` to a maximum of 80 bytes and recommends it be integrity protected and not expose sensitive information because it often appears in the URL of SAML messages. This could be achieved by pairing the URL with a random string, using the random string as the `RelayState`, and then obtaining the original URL by performing a look-up. However in practice most SAML clients set the `RelayState` to the resource URL. This is what Mellon currently does. When you are examining SAML messages, the `RelayState` will be the original URL and depending on the SAML binding it may be URL-encoded. _Just be aware that there is no requirement the `RelayState` be the original URL. It can be any string that the client can use to establish the context._ At some future date Mellon may alter its `RelayState` handling. === The Role of Metadata [[metadata]] When a SAML provider needs to interact with another SAML provider they must know various properties of the foreign provider (i.e. its configuration). SAML provider properties are encapsulated in an XML document called SAML metadata. Examples of the provider properties conveyed in metadata include: * <> (the unique name of the provider) * Organizational information * The roles this provider offers (e.g. SP, IdP, etc.) * <> for message exchange * X509 certificates the provider uses and for what purpose * Should messages be signed * Attributes and attribute profiles Typically at start-up a provider loads its own metadata (to configure itself) and then loads the metadata of all providers it interacts with. The SAML metadata specification can be found here: https://docs.oasis-open.org/security/saml/v2.0/saml-metadata-2.0-os.pdf It is vital that SAML metadata be trusted. The SAML specifications do not prescribe how metadata is exchanged in a trusted fashion. Many providers offer a URL where their metadata can be downloaded from (see <>). Metadata can be signed by the provider, which establishes authenticity. Some SAML practitioners do not approve of downloading metadata and instead insist upon the private exchange of metadata as a means to assure the metadata is valid, thus providing a higher level of trust. *Practical field experience has demonstrated that the vast majority of SAML problems are due to invalid metadata. Therefore the ability to diagnose SAML problems demands the ability to read and understand SAML metadata.* To that end let's explore the provider metadata used in our examples. ==== Certs and Keys Inside Metadata [[metadata_keys]] Cryptographic keys are used in SAML to sign pieces of data providing integrity protection and to encrypt data to provide confidentiality. In order for two SAML providers to successfully exchange SAML messages between themselves they must know the public keys of the other party. The provider's public keys are declared in its metadata and are always encapsulated inside a `` element that defines its intended use (signing or encryption). Furthermore one or more of the following representations within a `` element *MUST* be present: * `` * `` (child element of ``) _The (common) use of a `` element is merely a notational convenience to encapsulate a key. SAML never utilizes any PKI information inside an X509 certificate; the only data SAML utilizes from an X509 certificate is the key material._ This has several implications: * Certs are never PKI validated. * Certification extensions that define key usage, etc. are never checked. * The certificate validity period is never checked, thus a cert contained in metadata never expires as a consequence of its certificate validity period. Instead the validity period of the *key* is controlled by the `` or `` metadata attribute associated with the key. * Using self-signed certs used in SAML metadata is fine because the PKI data is never evaluated. In fact extracting the key from an `` element and placing it inside a `` element instead is semantically identical. A `` element is just a container for the key, nothing more than that. Certificates are used only because they are easy to generate and are readily available. IMPORTANT: The consequence of the above is that a provider's metadata *is* the trust mechanism in SAML. *_Any compromise of a provider's metadata is a compromise of SAML security._* IMPORTANT: Even though the keys and certs used inside SAML metadata for signing and encryption are not PKI validated, the key and cert used to establish a TLS secure channel between SAML entities *MUST* be fully PKI validated using a chain all the way up to a trusted CA. Do not confuse the purpose of the keys and certs used for the purpose of signing and encrypting SAML data with those used to establish secure communication: they are entirely distinct. ==== Service Provider Metadata [[sp_metadata]] This is an example of Mellon metadata. It is the SP metadata used in our example authentication. [source,xml] ---- xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" entityID="https://mellon.example.com/mellon/metadata"> AuthnRequestsSigned="true" WantAssertionsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> ... ... Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://mellon.example.com/mellon/logout" /> Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://mellon.example.com/mellon/logout" /> urn:oasis:names:tc:SAML:2.0:nameid-format:transient index="0" isDefault="true" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://mellon.example.com/mellon/postResponse" /> index="1" Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact" Location="https://mellon.example.com/mellon/artifactResponse" /> index="2" Binding="urn:oasis:names:tc:SAML:2.0:bindings:PAOS" Location="https://mellon.example.com/mellon/paosResponse" /> ---- <1> `EntityDescriptor` is a container for all properties belonging to the entity identified by the `entityID` name. SAML metadata allows a single metadata document to describe multiple entities. In that case the top level element will be a `` element which will contain one or more `` elements. If the metadata describes only a single entity, it is permissible to just use a ``. <2> XML namespace declaration. This provides an abbreviated shorthand to identify which namespace an XML element belongs to. The shorthand name is the string preceding the equals sign. If the name is absent it becomes the default namespace for XML elements which are not prefixed with a namespace. Thus for example `xmlns:ds="..."` sets `ds` as the namespace prefix for XML digital signature elements and `` means the `KenInfo` element belongs to the XML digital signature namespace because it is prefixed with `ds:`. *There is no prescribed list of namespace prefixes, rather the document defines the prefix.* By convention certain prefix names are commonly used. _A common mistake is to assume that all SAML XML documents will use the same namespace prefixes_. This is not true and leads to misunderstandings and/or parsing errors. <3> [[sp_metadata_entityid]] entityID. This is the unique name of the SAML provider. It *must* be a URI. See <>. <4> A provider role. In this instance the role is `SPSSODescriptor`, which means it's a Service Provider. A provider may have multiple roles, therefore an entity may have more than one role element. In our example there is only one role. See <>. <5> `AuthnRequestsSigned`. If true then the SP will be sending a signed `` to the IdP. <6> `WantAssertionsSigned`. If true then the SP desires the IdP to sign its assertions. Assertions should always be signed. <7> X509 certificate information. The `use` attribute of `signing` identifies that this certificate will be used for signing data. There may be multiple keys of this type, permitting key rotation. <8> X509 certificate information. The `use` attribute of `encryption` identifies that this certificate will be used for encrypting data. There may be multiple keys of this type, permitting key rotation. <9> SAML endpoint. Logout messages using a SOAP binding are sent to this URL location. <10> SAML endpoint. Logout messages using the HTTP-Redirect binding are sent to this URL location. <11> Zero or more `` elements enumerate the name identifier formats supported by this entity. See <> for details. <12> [[sp_metadata_acs]] SAML endpoint. Assertions using the HTTP-POST binding are delivered to this URL location. <13> For indexed endpoints if `isDefault` is true then this is the default endpoint to select. If no endpoint claims to be the default then the first endpoint in the list is the default. <14> SAML endpoint. Assertions using the HTTP-Artifact binding are delivered to this URL location. <15> SAML endpoint. Assertions using the PAOS binding are delivered to this URL location. PAOS is used the the Enhanced Client or Proxy Profile (a.k.a. ECP). ==== Identity Provider Metadata [[idp_metadata]] This is an example of a IdP metadata as generated by a Red Hat SSO server (Keycloak). It is the IdP metadata used in our example authentication. [source,xml] ---- xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:dsig="http://www.w3.org/2000/09/xmldsig#"> entityID="https://rhsso.example.com:8443/auth/realms/test"> WantAuthnRequestsSigned="true" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> 1VPndjfABB6S4lb4zwMLjBUhxfzPFnfrvNYvRgcxiUM ... Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://rhsso.example.com:8443/auth/realms/test/protocol/saml" /> Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://rhsso.example.com:8443/auth/realms/test/protocol/saml" /> urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://rhsso.example.com:8443/auth/realms/test/protocol/saml" /> Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://rhsso.example.com:8443/auth/realms/test/protocol/saml" /> Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://rhsso.example.com:8443/auth/realms/test/protocol/saml" /> ---- <1> `EntitiesDescriptor` is a container for multiple `` elements. <2> XML namespace declaration. This provides an abbreviated shorthand to identify which namespace an XML element belongs to. The shorthand name is the string preceding the equals sign. If the name is absent it becomes the default namespace for XML elements which are not prefixed with a namespace. Thus for example `xmlns:ds="..."` sets `ds` as the namespace prefix for XML digital signature elements and `` means the `KeyInfo` element belongs to the XML digital signature namespace because it is prefixed with `ds:`. *There is no prescribed list of namespace prefixes, rather the document defines the prefix.* By convention certain prefix names are commonly used. _A common mistake is to assume all SAML XML documents will use the same namespace prefixes_. This is not true and leads to misunderstandings and/or parsing errors. <3> `EntityDescriptor` is a container for all properties belonging to the entity identified by the `entityID` name. <4> entityID. This is the unique name of the SAML provider. It *must* be a URI. See <>. <5> A provider role. In this instance the role is `IDPSSODescriptor`, which means it's an Identity Provider. A provider may have multiple roles, therefore an entity may have more than one role element. In our example there is only one role. See <>. <6> `WantAuthnRequestsSigned`. If true, indicates that this IdP requires every `` submitted by an SP to be signed. <7> X509 certificate information. The `use` attribute of `signing` identifies that this certificate will be used for signing data. There may be multiple keys of this type, permitting key rotation. <8> SAML endpoint. Logout messages using the HTTP-POST binding are sent to this URL location. <9> SAML endpoint. Logout messages using the HTTP-Redirect binding are sent to this URL location. <10> Zero or more `` elements enumerate the name identifier formats supported by this entity. See <> for more details. <11> SAML endpoint. `` messages using the HTTP-POST binding are sent by the SP to this URL location to establish a Single Sign-On Session. <12> SAML endpoint. `` messages using the HTTP-Redirect binding are sent by the SP to this URL location to establish a Single Sign-On Session. <13> SAML endpoint. `` messages using the SOAP binding are sent by the SP to this URL location to establish a Single Sign-On Session. == Installing & Configuring Mellon === Installing Mellon Mellon can be built and installed from source code located in the https://github.com/UNINETT/mod_auth_mellon[mod_auth_mellon GitHub repository]. However for most people the best option is to install Mellon using a pre-built package available from the package manager on your operating system. Pre-built packages relieve you of having to know the intricacies of building and installing from source, track and install security fixes, and track and apply bug fixes. Pre-built packages also are tailored to your operating system environment, often including OS specific configuration and support files deemed useful by the packaging authority. .On RHEL (CentOS) ---- sudo yum install mod_auth_mellon ---- .On Fedora ---- sudo dnf install mod_auth_mellon ---- .On Debian (Ubuntu) ---- apt-get install libapache2-mod-auth-mellon ---- .From source ---- ./configure make sudo make install ---- NOTE: If building from source you'll need to have all the necessary dependencies available at build time. Determining the exact set of dependencies and where to locate them is operating system dependent. It is assumed you have the necessary knowledge to correctly perform the requisite operations which is out of scope for this document. === Mellon Configuration [[mellon_config]] Once installed, mod_auth_mellon does not do anything until it's configured to operate on a URL. Mellon is configured in the same way as other Apache modules. See http://httpd.apache.org/docs/current/configuring.html[Apache Configuration Files]. There are two independent steps necessary to enable Mellon. 1. Load the mod_auth_mellon Apache module at Apache start-up. 2. Configure Mellon to operate on specific URLs with specific SAML properties. ==== Load mod_auth_mellon [[load_mod_auth_mellon]] To accomplish the first task of loading the mod_auth_mellon module Apache needs to execute this configuration directive: ---- LoadModule auth_mellon_module modules/mod_auth_mellon.so ---- Different distributions may handle Apache module loading differently, but as of Apache 2.4 the preferred technique is to drop a file in the `conf.modules.d` Apache directory with the above content. Apache automatically processes all `.conf` files in this directory at start-up. [NOTE] .Red Hat Specific ==== Red Hat RPM's add the file `/etc/httpd/conf.modules.d/10-auth_mellon.conf` with the above `LoadModule` directive, so there is no further action needed to load the module after the RPM is installed. ==== ==== Mellon Configuration Files [[mellon_config_files]] To accomplish the second task of configuring Mellon, Apache will need to read Mellon configuration directives when it initializes. The preferred mechanism is to place those directives in a file located in the Apache `conf.d` directory, Apache will read all `.conf` files in this directory at start-up. Although you could place the Mellon directives in any config file, a good practice to follow is keep the Mellon directives in their own file. See <> for more information. Mellon relies on SAML specific files as well, for example: * IdP metadata file(s) * Mellon's SP metadata file * Certificate and key files Although you are free to locate these SAML specific files in the `/etc/httpd/conf.d` Apache configuration directory, they are not strictly speaking Apache configuration files. Many deployments choose to locate the SAML files in a sibling directory, for example `/etc/httpd/saml2`. NOTE: If you are running with SELinux enabled (as you should be) you may run into SELinux file permission problems if you locate files Mellon reads and writes outside the standard Apache directories because externally located files will not automatically receive the proper SELinux labels. === Mellon Configuration Directives Mellon's configuration directives are documented in Mellon's `README` file. The README is the best place to learn and review Mellon configuration directives because it will match the installed version of Mellon. [NOTE] .Red Hat Specific ==== Red Hat RPM's install the README file in `/usr/share/doc/mod_auth_mellon*/README`. ==== Mellon configuration directives are broken into 2 types: * Module level (i.e. global values shared by each virtual server) * Directory level (i.e. applied to directories and URL locations) The README groups all module level directives together at the top of the file, the directory level directory level directives follow. Most users will only need to configure the directory level directives which comprise 2 basic types and are documented in http://httpd.apache.org/docs/current/mod/core.html[Apache Core Features]: Directive:: Enclose a group of directives that apply only to the named file-system directory, sub-directories, and their contents. Directive:: Applies the enclosed directives only to matching URLs. The critical thing to remember when writing and reading Mellon configuration is that like all Apache directory level configuration it is *_hierarchical_*. The path portion of the URL is like a file system directory tree. If a Mellon configuration directive is not _explicitly_ defined for a particular point in the tree, the value is *_inherited_* from the closest ancestor that defines it. If no ancestor defines the value then Mellon's default value is applied. The default value for each Mellon configuration directive is listed in Mellon's `README` file. === Mellon Configuration File [[mellon_config_file]] For our demo example we will place these directives in the file `/etc/httpd/conf.d/demo_mellon.conf`. Let's briefly review what the demo configuration is meant to accomplish and illustrate: * We are protecting with SAML authentication the URL location `https://mellon.example.com/private` and everything below it in URL space. * To eliminate redundant cut-n-paste of shared SAML directives in each protected location we gather the common Mellon directives in a location _above_ any of the protected locations in the URL tree. This permits all protected locations to hierarchically inherit the same values from a common single set of directives. ---- <1> MellonEnable info <2> MellonEndpointPath /mellon/ <3> MellonSPMetadataFile /etc/httpd/saml2/demo_sp_metadata.xml <4> MellonSPPrivateKeyFile /etc/httpd/saml2/demo.key <5> MellonSPCertFile /etc/httpd/saml2/demo.cert <6> MellonIdPMetadataFile /etc/httpd/saml2/demo_keycloak_test_idp_metadata.xml <7> <8> AuthType Mellon <9> MellonEnable auth <10> Require valid-user <11> ---- <1> The first `Location` directive on the `/` _root_ is simply a convenient place to locate common configuration directives that will be shared by all Mellon protected locations. In this instance it defines the metadata files and certificates and keys. It is not necessary to locate this on the `/` _root_ URL, in fact in a real world deployment you probably will want to locate the common shared set of Mellon directives lower in the hierarchy. The only requirement is that _all_ of the protected locations are positioned below it so they may inherit those values. footnote:[When you are protecting just one location with Mellon there isn't much added value in splitting the configuration directives. But when you need to protect many distinct locations, it's repetitive and error prone to cut and paste common values in each `` block. It makes maintaining the configuration that much more difficult because you will need to edit each and every `` directive to change something like the name of a metadata file. This is why Mellon configuration often groups common values in a location that can be shared by all descendants. Using the `/` _root_ location is the most obvious example but the same idea can be applied to complex trees.] <2> Mellon does not process any directives unless it's enabled for that location either explicitly or via inheritance. See <> for more details. <3> Defines where the Mellon endpoints are located in URL space. This is a *critical* value to properly specify and is one of the *_most common Mellon configuration errors_* leading to a failed deployment. Please refer to <> to understand its requirements and what it influences. Also see <> for a discussion of this common error. The important thing to note in this example is the `MellonEndpointPath` is located *inside* the containing location directive of `/` (e.g. a child). <4> The SAML metadata for this provider (i.e. Mellon's metadata). This metadata plays 2 important roles: Mellon reads it at start-up to initialize itself, and you provide the IdP specified in `MellonIdPMetadataFile` with this metadata. Both Mellon (the SP) and your IdP *MUST* have loaded exactly the same Mellon metadata in other to interoperate. Out of sync metadata is a very common deployment error. See <> for how Mellon metadata is created. `MellonSPMetadataFile` is optional, Mellon can create its own metadata from its initial configuration parameters. <5> The private cryptographic key used by Mellon to sign its SAML data. See <> for more detail. <6> The public cryptographic key associated with the private key. This public key is embedded in Mellon's metadata so that an IdP can validate Mellon's signed data. See <> for more detail. <7> The IdP used to authenticate is specified by its metadata file. See <> for how to obtain this data. <8> This is a URL location protected by Mellon. For our example we've used the `/private` URL. Note that this `` block is simple and does not contain many of the necessary Mellon directives, because those other Mellon directives are inherited from an ancestor location, in our example `/`. The only Mellon directives in this location block are those necessary to turn on Mellon authentication. This configuration strategy permits you to define many subordinate protected locations all sharing the same common Mellon directives via inheritance. <9> `AuthType` is an Apache directive specifying which Apache authentication module will perform the authentication for this location. Obviously we want to use Mellon. <10> Instruct Mellon that this location (and all its descendants) will be authenticated. See <>. <11> `Require` is an Apache directive that instructs Apache's authentication and authorization sub-system that it must successfully authenticate the user. ==== Load Your SP metadata into the IdP [[load_sp_metadata_into_idp]] After you have created your SP metadata as described in <>, you must load your metadata into the IdP referenced in your `MellonIdPMetadataFile`. How to perform the SP metadata load is specific to the IdP you're using and you will need to consult your IdP documentation to learn the procedure. WARNING: If you subsequently modify your SP metadata you *MUST* reload it into the IdP. Both your metadata and the IdP metadata must be in sync at all times. Failure to reload any modified metadata is a recipe for problems. ==== Obtaining IdP Metadata [[obtain_idp_metadata]] In order to Mellon to communicate with and interoperate with an IdP it must have the IdP's metadata. You may want to refer to <> for a more comprehensive description. But how do you obtain the metadata belonging to the IdP? There is no fixed rule on how this is accomplished. You will have to refer to your IdP's documentation. It may be published at a well known location (e.g. a URL) for download or there may be some other publication mechanism. IMPORTANT: SAML provider metadata is extremely security sensitive, it contains the cryptographic keys used to secure SAML. If you download metadata from a URL do so only over a secure channel such as https and make sure the download operation properly validates the server cert up to a CA you trust. _Do not trust a server offering a self-signed cert_. If the obtained metadata is signed you *MUST* validate the signature on the metadata. === Mellon Modes [[mellon_modes]] For any given location Mellon can be in one of 3 modes defined by the `MellonEnable` directive: off:: Mellon will not do anything in this location. This is the default state. info:: If the user is authorized to access the resource, then Mellon will populate the environment with information about the user. If the user isn't authorized, then Mellon won't populate the environment, but Mellon won't deny the user access either. auth:: Mellon will populate the environment with information about the user if he is authorized. If he is authenticated (logged in), but not authorized (according to the `MellonRequire` and `MellonCond` directives, then Mellon will return a 403 Forbidden error. If he isn't authenticated then Mellon will redirect to the login page of the configured IdP. The most common situation is to protect a specific location with Mellon authentication. This requires at a minimum these 3 directives: ---- AuthType Mellon <1> MellonEnable auth <2> Require valid-user <3> ---- <1> This is an Apache directive that says authentication is to be performed with Mellon as opposed to another Apache authentication module. <2> This informs Mellon it is to perform authentication as described above. <3> This is an Apache directive that says an authentication module must have successfully authenticated a user in order to proceed. === How is Mellon metadata created? [[metadata_creation]] The purpose of SAML metadata is described in <>. An annotated example of Mellon metadata is presented in <>. There are multiple ways one can create Mellon metadata: . Use the `mellon_create_metadata.sh` script. The mod_auth_mellon RPM installs this script in `/usr/libexec/mod_auth_mellon/mellon_create_metadata.sh`. . Allow Mellon to dynamically generate its metadata based on its configuration options. The metadata can be downloaded from the `$MellonEndpointPath/metadata` URL. Mellon only self-generates its metadata if the `MellonSPMetadataFile` configuration parameter is not defined, otherwise if the `MellonSPMetadataFile` is defined the `$MellonEndpointPath/metadata` download URL will return the contents of the `MellonSPMetadataFile`. . Use a third-party tool such as `keycloak-http-client-install`. . Write it from scratch. (Not kidding, many provider administrators hand create and hand edit their metadata). IMPORTANT: Before proceeding further with Mellon metadata it is essential you understand the <>. ==== Using `mellon_create_metadata.sh` [[using_mellon_create_metadata_sh]] `mellon_create_metadata.sh` requires two positional parameters * <> * endpoint_url The entityID is the unique name of the Mellon SP. The entityID plays an important role in SAML and you may wish to review its description in <>. The endpoint_url is the concatenation of the `https` scheme, the Mellon hostname, and the <>. Using our example data the entityID will be `https://mellon.example.com/mellon/metadata` and the endpoint_url will be `https://mellon.example.com/mellon` ---- $ /usr/libexec/mod_auth_mellon/mellon_create_metadata.sh "https://mellon.example.com/mellon/metadata" "https://mellon.example.com/mellon" Output files: Private key: https_mellon.example.com_mellon_metadata.key Certificate: https_mellon.example.com_mellon_metadata.cert Metadata: https_mellon.example.com_mellon_metadata.xml Host: mellon.example.com Endpoints: SingleLogoutService (SOAP): https://mellon.example.com/mellon/logout SingleLogoutService (HTTP-Redirect): https://mellon.example.com/mellon/logout AssertionConsumerService (HTTP-POST): https://mellon.example.com/mellon/postResponse AssertionConsumerService (HTTP-Artifact): https://mellon.example.com/mellon/artifactResponse AssertionConsumerService (PAOS): https://mellon.example.com/mellon/paosResponse ---- The script produces 3 files containing the cert, key, and metadata, all prefixed with the entityID. In this example it would be: * https_mellon.example.com_mellon_metadata.cert * https_mellon.example.com_mellon_metadata.key * https_mellon.example.com_mellon_metadata.xml You will need to move these files into the Apache configuration directory and possibly rename them to something more sensible. You will refer to these files inside the Mellon configuration as these Mellon directives: * `MellonSPPrivateKeyFile` * `MellonSPCertFile` * `MellonSPMetadataFile` ==== Using Mellon to generate its own metadata [[using_mellon_to_create_metadata]] Mellon has the built-in capability to generate its own metadata as long as you provide a few necessary Mellon configuration directives. * `MellonSPentityId` * `MellonSPPrivateKeyFile` * `MellonSPCertFile` * `MellonEndpointPath` (not mandatory if you use the default) When Mellon initializes it will check the value of the `MellonSPMetadataFile`. If it does not exist Mellon will generate its own metadata. If `MellonSPMetadataFile` exists, that metadata will always be used. If Mellon generates its own metadata it does not write the metadata back to a file, rather it's held in memory. Irrespective of whether Mellon self generates its metadata or if it loads it from a file specified by `MellonSPMetadataFile`, the metadata is made available for download at the `$MellonEndpointPath/metadata` URL. You can perform a GET on this URL to capture the SP metadata and save it in a file. It is recommended you do this as an initial configuration set-up step and then always subsequently load the metadata via the `MellonSPMetadataFile` directive. The rationale for this is you want to be sure you know what metadata Mellon is initializing with and that it identically matches what you've loaded into the IdP. You may also wish to customize your SP metadata by making edits to it. ==== Where do the keys and certs come from? Please refer to the <> section to understand how keys and certs are utilized inside SAML (TLS connections used for SAML communication is an entirely different matter and it is mandated keys and certs used for TLS be PKI validated). The main point to understand is that even though most SAML implementations use x509 utilities to generate certs and keys, SAML's use of them does not involve PKI. Only the key material is used. The consequence of this is it's okay to generate self-signed certs for use inside a provider's metadata because they are not PKI validated. Many of the metadata creation tools generate a self-signed cert for use in the metadata. However it is perfectly fine to use your own key and cert instead of one generated by an installation tool. You can accomplish this with Mellon by pointing the `MellonSPPrivateKeyFile` and `MellonSPCertFile` directives at your own key and cert files and then downloading the SP metadata as described in <>. ==== Signing metadata [[sign_metadata]] SAML requires provider metadata to be integrity protected. Publishing provider metadata over a secure TLS channel goes a long way to accomplishing this goal and may be considered sufficient depending on the security requirements. SAML metadata can be integrity protected by signing the metadata with an XML signature. Some providers may require any metadata they consume be signed. Unfortunately neither Mellon nor any of the tools currently associated with Mellon have support for signing Mellon metadata. Fortunately there are a variety of tools available to sign an XML document and since SAML metadata is a normal XML document any of these tools can be used to sign the Mellon metadata. ===== Using xmlsec to sign metadata [[xmlsec_metadata_signing]] The `xmlsec` tools are commonly available on most Linux based system. In fact the `Lasso` library which supplies Mellon with its SAML implementation uses the `xmlsec` library to perform all of its XML signing and signature verification. `xmlsec` usually ships with an `xmlsec` command line utility, which can perform XML signing and verification from the command line. NOTE: `xmlsec` may be packaged under the name `xmlsec1` in your distribution. This is the case for all Red Hat distributions. To sign Mellon metadata using `xmlsec` you need to add a signature template to the Mellon metadata. When `xmlsec` reads the input metadata it locates the empty signature template and replaces it with a processed signature. The signature template should be placed near the top of the metadata, ideally just after the `` element. Here is an example of a signature template: [source,xml] ---- ---- Because the ` URI` attribute is the empty string the entire document will be signed. In order for `xmlsec` to generate a signature you will need to supply it with both the private and public key parts. ---- xmlsec \\ <1> --sign \\ <2> --privkey-pem demo.key \\ <3> --pubkey-cert-pem demo.cert \\ <4> --output signed_metadata.xml \\ <5> metadata.xml <6> ---- <1> `xmlsec` command may be named `xmlsec1` on your system <2> Perform signing <3> Private key used for signing <4> Public key used to verify signature (included in signature) <5> Output file containing signed metadata <6> Input unsigned metadata (with signature template) To verify the signature on the command line: ---- xmlsec \\ <1> --verify \\ <2> signed_metadata.xml <3> ---- <1> `xmlsec` command may be named `xmlsec1` on your system <2> Perform verification <3> Input signed metadata === MellonEndpointPath [[mellon_endpoint_path]] Mellon reserves a number of URLs for its use. Some of these URLs are the public SAML <> advertised in the <>. Others are for Mellon's private use. The best way to think of these Mellon endpoints is as a way of binding a URL to a handler. When an HTTP request arrives at one of these Mellon endpoints a dedicated handler processes the request. The way Mellon identifies a URL as being one of its endpoints is by looking at the beginning of the URL path. If everything in the path except the last path component matches the `MellonEndpointPath` then Mellon recognizes the URL as being one of its endpoints. The last path component is used to bind to the handler. Let's use an example. If the `MellonEndpointPath` is `/foo/bar` then any URL with the form `/foo/bar/xxx` will be handled by Mellon's xxx handler. Mellon enforces 2 strict requirements on the `MellonEndpointPath`: * The path *must* be an absolute path from the root of the web server. * The path *must* be a sub-path of the Mellon `` directive that defines it. The reason for this is simple. Mellon ignores locations which are not configured for Mellon. Therefore for Mellon to respond to a request on one of its SAML endpoints, the endpoint has to be inside a path that Mellon is watching. === Mellon Endpoints [[mellon_endpoints]] Mellon endpoints are hung off of the <>. Mellon reserves a number of URLs for its use. Some of these URLs are the public SAML <> advertised in the <>. Others are for Mellon's private use. The best way to think of these Mellon endpoints is a way of binding a URL to a handler. When an HTTP request arrives at one of these Mellon endpoints a dedicated handler processes the request. The current list of Mellon endpoints (handlers) is: postResponse:: The _AssertionConsumerService_ endpoint using the SAML HTTP-POST binding. artifactResponse:: The _AssertionConsumerService_ endpoint for SAML artifacts. SAML artifacts provide an indirect method to convey data. An artifact is an identifier that points to data. Requesting data using the artifact identifier returns the associated data. paosResponse:: The AssertionConsumerService endpoint using the SAML PAOS binding. login:: Mellon internal endpoint used to start the authentication process with an IdP. Any request whose URL needs authentication is redirected here to start the login process. logout:: The SingleLogoutService SAML endpoint. metadata:: A HTTP GET request on this endpoint will return the SP's metadata. repost:: Mellon internal endpoint which replays POST data from the original request. auth:: Mellon internal endpoint retained for backwards compatibility. probeDisco:: IdP probe discovery service endpoint. See "Probe IdP discover" in the Mellon README for more information. === Mellon Session [[mellon_session]] SAML sessions are described in <>. For each successfully authenticated user Mellon maintains a session. Mellon allocates a unique ID for the session when it is created. The Mellon session ID is sent to the user's browser in a <>. The <> is sent back to Mellon in every request the browser makes to the SP. Mellon uses the session ID to look-up the session data for the user. Internally Mellon calls session data _cache data_ (this is subject to change). At the time of this writing Mellon session data is local to one Apache server (which may have multiple worker processes sharing data in shared memory). This has consequences for High Availability (e.g. HA) deployments which may be running multiple Apache servers on different nodes behind a load balancer, see <> for detailed information on this issue. Mellon limits the duration of a valid session by the length of time defined in the `MellonSessionLength` directive. Currently the default is 86400 seconds which is 24 hours. The IdP can inform the SP how long it wishes a SP session to be valid by passing the `SessionNotOnOrAfter` attribute in a ``. Mellon respects the `SessionNotOnOrAfter` attribute and will limit its session duration based on it. Thus the validity period for a Mellon session is the lesser of the `MellonSessionLength` or the optional IdP `SessionNotOnOrAfter` attribute if the IdP supplied it. === Mellon Cookie [[mellon_cookie]] <> information is communicated via a cookie. The cookie name defaults to `mellon-cookie` but may be changed via the Mellon directive `MellonVariable`. Mellon always forms the cookie name by appending the value of `MellonVariable` to the string `mellon-` to prevent name collisions. Thus the actual default value of `MellonVariable` is `cookie`. When Mellon first begins the authentication process it sets the mellon cookie value to `cookietest`. The primary purpose of the `cookietest` value is to confirm cookies are properly returned by the browser, Mellon will not work correctly unless cookies are enabled. The `cookietest` value also serves as a temporary value indicating an authentication flow is in progress but has not yet completed. After Mellon successfully authenticates a user it establishes a session for the user and generates a unique session ID which it sets as the value of the Mellon cookie. When Mellon receives a request for a protected resource it looks for the Mellon cookie in the HTTP request headers. Mellon then uses the Mellon cookie value as a session ID and attempts to look-up that session using that ID. If the session is found and it remains valid, Mellon immediately grants access. A Mellon session will expire, see <> for information concerning session lifetime. == Working with SAML attributes and exporting values to web apps When you receive a SAML assertion authenticating a subject, the assertion will likely include additional attributes provided by the IdP concerning the subject. Examples include the user's email address or the groups they are a member of. You may wish to review the <> and look for `` and `` elements to see how the IdP communicates these attributes. There is no fixed set of attributes returned by an IdP, it is entirely IdP dependent. You will either have to review your IdP's documentation or examine a returned assertion to determine the possible attributes. See <> for the various ways you can examine the contents of a returned assertion. Mellon communicates its results via Apache environment variables. For every attribute received in the assertion Mellon will insert an Apache environment variable. You have some flexibility on how Mellon adds these environment variables which derive from the assertion attributes. * Attributes can be multi-valued. `MellonMergeEnvVars` controls whether each value is added to the environment by appending an index to the attribute name or whether the values are listed together under the bare attribute name with each value separated by a separator character. See <>. * Attribute names can be mapped from the name as it appears in the assertion to a name of your choosing when it is placed in the Apache environment. This is controlled by `MellonSetEnv` and `MellonSetEnvNoPrefix` directives. The distinction is `MellonSetEnv` always prepends a prefix to the environment variable name to help to prevent name collisions. The prefix defaults to `MELLON_` and can be configured using the `MellonEnvPrefix` configuration option. The `MellonSetEnvNoPrefix` directive also remaps the assertion name to a name of your choosing but it omits prepending the environment variable name with the prefix. See <> Using the <> Mellon places these environment variables in the Apache environment. See <> for an explanation of `MellonMergeEnvVars` and its effect. .MellonMergeEnvVars Off ---- MELLON_NAME_ID: G-803528aa-2f9e-454b-a89c-55ee74e75d1e MELLON_NAME_ID_0: G-803528aa-2f9e-454b-a89c-55ee74e75d1e MELLON_groups: ipausers MELLON_groups_0: ipausers MELLON_groups_1: openstack-users MELLON_email: jdoe@music.com MELLON_email_0: jdoe@music.com MELLON_display_name: John Doe MELLON_display_name_0: John Doe MELLON_initials: JD MELLON_initials_0: JD MELLON_last_name: Doe MELLON_last_name_0: Doe MELLON_first_name: John MELLON_first_name_0: John MELLON_Role: uma_authorization MELLON_Role_0: uma_authorization MELLON_Role_1: manage-account MELLON_Role_2: view-profile MELLON_IDP: https://rhsso.example.com:8443/auth/realms/test MELLON_IDP_0: https://rhsso.example.com:8443/auth/realms/test ---- === Handling multiple attribute values [[multiple_attribute_values]] If an attribute has multiple values, then they will be stored as ---- MELLON__0 value0 MELLON__1 value1 MELLON__2 value2 ... ---- Since Mellon doesn't know which attributes may have multiple values, it will store each attribute at least twice. For example: ---- MELLON_ value0 MELLON__0 value0 ---- In the case of multivalued attributes `MELLON_` will contain the first value. If `MellonMergeEnvVars` is enabled multiple values of attributes will be stored in a single environment variable separated by the `MellonMergeEnvVars` separator which defaults to the semicolon. You can override the default separator by supplying it as the second option to the `MellonMergeEnvVars` directive. Thus the above environment variable list would be this if `MellonMergeEnvVars` was on and the separator was set to the semicolon. .MellonMergeEnvVars On ; ---- MELLON_NAME_ID: G-803528aa-2f9e-454b-a89c-55ee74e75d1e MELLON_groups: ipausers;openstack-users MELLON_email: jdoe@music.com MELLON_display_name: John Doe MELLON_initials: JD MELLON_last_name: Doe MELLON_first_name: John MELLON_Role: uma_authorization;manage-account;view-profile MELLON_IDP: https://rhsso.example.com:8443/auth/realms/test ---- === Map assertion attribute name to different Apache environment variable name [[map_assertion_attr_name]] Sometimes the web app is expecting a specific name for a SAML attribute but your IdP has sent that attribute under a different name. You can rename any assertion attribute using the `MellonSetEnv` and `MellonSetEnvNoPrefix` directives. These allow you to rename an assertion attribute to a name of your choosing. The `MellonSetEnv` directive follows the same convention as all other assertion attributes added by Mellon in that it always prefixes the environment variable name with a configurable prefix, which defaults to `MELLON_` to help avoid name collisions in the Apache environment. However sometimes you do not want the `MELLON_` prefix added. In case you simply want the variables prefixed with a different string, use the `MellonEnvPrefix` configuration option. If, instead you want to use exactly the environment variable name as specified., `MellonSetEnvNoPrefix` serves this role. To illustrate let's look at an example. Suppose your web app is expecting an attribute which is the user's last name, specifically it wants this attribute to be called `REMOTE_USER_LASTNAME`. However your IdP sends this attribute as `sn`. `sn` is typically used in LDAP directories as an attribute name for surname, or equivalently the user's last name. To map the `sn` assertion attribute name to the Apache environment variable name of `REMOTE_USER_LASTNAME` you would do this: ---- MellonSetEnvNoPrefix REMOTE_USER_LASTNAME sn ---- Also see <> for an example of setting the `REMOTE_USER` environment variable using `MellonSetEnvNoPrefix`. The `MellonEnvPrefix` variable might be useful e.g. if you are migrating from a different SP which used its own prefix for the variables passed by the IdP. For example, to prefix all variables with `NOLLEM_` you would use: ---- MellonEnvPrefix NOLLEM_ ---- If you recieved an attribute-map.xml from your IDP that uses the `urn:mace:shibboleth:2.0:attribute-map` namespace, it can be converted to `MellonSetEnvNoPrefix` entries with `docs/mellon-attribute-map.xsl` and loaded into your webserver configuration. === Using Mellon to apply constraints [[assertion_constraints]] SAML attributes can be used for more than exporting those values to a web app. You can also utilize SAML attributes to control whether Mellon authentication succeeds (a form of authorization). So even though the IdP may have successfully authenticated the user you can apply additional constraints via the `MellonCond` directive. The basic idea is that each `MellonCond` directive specifies one condition that either evaluates to `True` or `False`. Multiple conditions can be joined by logical operators. You can also specify case insensitive matching, substring matching, regular expression matching, substitute values, and use regular expression back references. All `MellonCond` conditions must evaluate to `True` for the condition check to succeed (logical conjunction) unless you use the `OR` option flag. The directive is specified as: ---- MellonCond attr_name value [options] ---- The 1st `attr_name` parameter is the name of the SAML assertion attribute the condition applies to. If an attribute with this name is found, its value is retrieved and becomes the data the condition is evaluated against. If `attr_name` is not found, the condition evaluates to `False`. The 2nd value parameter is the value applied to the attribute value. For example when matching is performed the `value` parameter is searched for inside the attribute's value. The `value` parameter may contain format specifiers which are substituted prior to performing an operation on the value. Format specifiers always begin with the `%` character. Here are the valid format specifiers: %n:: Regular expression backreference. Regular expressions may contain multiple sub-matches (often referred to as a regular expression group). To refer to a specific sub-match in the regular expression pattern use a digit between 0 and 9 as `%n`. This only works if a prior condition had specified the [`REG,REF]` flags, otherwise there would be no backreference to refer to. %\{num}:: Same as `%n`, but permits a number greater than 9. %\{ENV:x}:: Substitute the Apache environment variable `x`. If the environment variable does not exist substitute the empty string instead. %%:: Quote a `%` to prevent it from being interpreted as a the beginning of a format specifier. The 3rd `[options]` parameter is optional and if specified is a comma separated list of option flags enclosed in square brackets. The set of option flags includes: OR:: If this MellonCond evaluated to false, then the next one will be checked. If it evaluates to true, then the overall check succeeds. NOT:: Invert the result of the condition check. If the condition evaluated to `True` it becomes `False`, likewise if the condition evaluated to `False` it become `True`. NC:: Case insensitive matching. Ignore case differences when performing any match operation. SUB:: Substring match. If value is included anywhere in the attribute value as a substring the condition evaluates to `True`, otherwise `False`. If `SUB` is not specified then the condition value and attribute value must match in its entirety. REG:: Regular expression match. The value is interpreted as a regular expression. If the regular expression is found in the attribute value the condition evaluates to `True`, `False` otherwise. REF:: Used with REG, track regular expression back references, So that they can be substituted in an upcoming `MellonCond` directive. MAP:: Use mapped name. Instead of looking up the attribute name in the set of attributes returned in the assertion use the mapped name specified by either `MellonSetEnv` or `MellonSetEnvNoPrefix` instead. If the mapped name is not found then fallback to using the name in the assertion's set of attributes. Here is a simple example illustrating how one might utilize `MellonCond`. Suppose we only want to allow members of the group `openstack-users` to have access. Our IdP has provided us with the list of groups the user is a member of in the `groups` SAML attribute. We need to instruct Mellon to only accept an assertion if `openstack-users` appears as one of the `groups` attribute values. This can be accomplished like this: ---- MellonCond groups openstack-users ---- If `openstack-users` does not appear in the as one of the `groups` attribute values the check will fail. The check will also fail if the `groups` attribute is not defined in the assertion. === How to set REMOTE_USER [[set_remote_user]] Mellon stores the authenticated user's name in the attribute `NAME_ID` (see <>). If you want to export the username as `REMOTE_USER` so your web app can process this very common CGI variable this can easily be accomplished with `MellonSetEnvNoPrefix` like this: ---- MellonSetEnvNoPrefix REMOTE_USER NAME_ID ---- == Deployment Considerations [[deployment_considerations]] === Apache Servername [[apache_servername]] When Mellon is running behind a load balancer, SSL terminator, or in a Apache virtual host there is the opportunity for Mellon to identify itself incorrectly. If Mellon does not identify itself identically to what appears in the matching metadata, various SAML security checks will fail as well as the ability to communicate on the defined <>. At run time Mellon asks Apache what scheme, host and port it's running under. Mellon uses this information to build URLs. When Mellon is running in a simple configuration directly connected to the internet, Apache typically gets this information correctly from the environment. However when Apache is behind some type of proxy such as a load balancer, then there is a distinction between what clients see as the front end and what Mellon sees when it's running as a backend server. The trick is to make Mellon believe it's running as the front end so that it matches the client's view. You may wish to refer to <> for more information. Load balancers partition their view between front end and back end. front end:: What the client connects to. It's the public scheme, hostname, and port. back end:: The back end server is where Mellon runs. It will definitely have a different hostname than the front end and will likely also have a different scheme and port as well. When a HTTP request arrives at the front end most load balancers will terminate the SSL connection. This changes the scheme from `https` to `http`. The load balancer will select a backend server to forward the request to. The backend server will have a different hostname and possibly a different port. Mellon needs to see the HTTP request as it appeared on the front end instead of how the request appears to the backend server where Mellon is running. The host and port appear in several contexts: * The host and port in the URL the client used. * The host HTTP header inserted into the HTTP request (derived from the client URL host). * The hostname of the front facing proxy the client connects to (actually the FQDN of the IP address the proxy is listening on). * The host and port of the backend server which actually handled the client request. * The **virtual** host and port of the server that actually handled the client request. It is vital to understand how each of these is utilized, otherwise there is the opportunity for the wrong host and port to be used with the consequence the authentication protocols may fail because they cannot validate who the parties in the transaction are and whether the data is carried in a secure transport. Let's begin with the backend server handling the request, because this is where the host and port are evaluated and most of the problems occur. The backend server needs to know: * The URL of the request (including host & port) * Its own host & port Apache supports virtual name hosting. This allows a single server to host multiple domains. For example a server running on example.com might service requests for both bigcorp.com and littleguy.com. The latter 2 names are virtual host names. Virtual hosts in Apache are configured inside a server configuration block, for example: ---- ServerName bigcorp.com ---- When Apache receives a request it deduces the host from the `HOST` HTTP header. It then tries to match the host to the `ServerName` in its collection of virtual hosts. The Apache `ServerName` directive sets the request scheme, hostname and port that the server uses to identify itself. The behavior of the `ServerName` directive is modified by the Apache `UseCanonicalName` directive. When `UseCanonicalName` is enabled Apache will use the hostname and port specified in the `ServerName` directive to construct the canonical name for the server. This name is used in all self-referential URLs, and for the values of SERVER_NAME and SERVER_PORT in CGIs. If `UseCanonicalName` is `Off`, Apache will form self-referential URLs using the hostname and port supplied by the client, if any are supplied. If no port is specified in the `ServerName`, then the server will use the port from the incoming request. For optimal reliability and predictability, you should specify an explicit hostname and port using the `ServerName` directive. If no `ServerName` is specified, the server attempts to deduce the host by first asking the operating system for the system hostname, and if that fails, performs a reverse lookup on an IP address present on the system. Obviously this will produce the wrong host information when the server is behind a proxy because the backend server is not what is seen on the frontend by clients; therefore use of the `ServerName` directive is essential. NOTE: [[standard_port_issue]] Browsers will strip standard port 80 for HTTP and port 443 for HTTPS from the network location in a URL. For example if you specify a URL like this `https://example.com:443/some/path` the URL which will placed on the wire will be `https://example.com/some/path` without the standard port. Since Mellon and most SAML providers validate URLs by simple string comparison, including a standard port in a URL will cause URL matching to fail because one URL will have the port in it and the other URL won't. The Apache https://httpd.apache.org/docs/current/mod/core.html#servername>[ServerName] doc is very clear concerning the need to fully specify the scheme, host, and port in the `Server` name directive when the server is behind a proxy. It states: ____ Sometimes, the server runs behind a device that processes SSL, such as a reverse proxy, load balancer or SSL offload appliance. When this is the case, specify the https:// scheme and the port number to which the clients connect in the ServerName directive to make sure that the server generates the correct self-referential URLs. ____ === Load Balancer Issues [[load_balancer]] High Availability (HA) deployments often run their services behind a load balancer. By far the most popular load balancer is http://www.haproxy.com/[HAProxy]. As a consequence we will use HAProxy examples in this document. Other load balancers behave in a similar fashion to HAProxy and you can extrapolate the HAProxy information to them. ==== Server Name Because backend servers do not self-identify with the same front end public address, it is vital you force those Apache servers to identify with the public address. This issue is described in <>. The reason for this is because the SAML protocols require URLs to match what is in a SAML provider's metadata. If you allow a backend server to self-identify, the URLs exchanged in the protocols will not match and you will encounter an error; see <>. ==== Load balancer proxy persistence [[load_balancer_persistence]] In an HA deployment, multiple backend servers run on distinct nodes and cooperate to mitigate the load that might be placed on a single (front end) server. Because the backend servers are independent, they do not share state with any other backend server unless something has explicitly been done to share state. HTTP is technically a stateless protocol, which makes web traffic ideally suited for a HA deployment: each backend server can be ignorant of any other HTTP request. However in practice HTTP is stateful by virtue of cookies. Authentication protocols are good examples of HTTP transactions that require saved state, in particular <>. NOTE: At the time of this writing Mellon has no support for sharing session data between independent Apache servers. The consequence of this is Mellon will not work correctly unless the same Apache server consistently handles a users HTTP traffic. HAProxy has two different mechanisms to bind HTTP traffic to one server, _affinity_ and _persistence_. This article provides an excellent overview of the distinction between the two and how to implement it: http://www.haproxy.com/blog/load-balancing-affinity-persistence-sticky-sessions-what-you-need-to-know/["load balancing, affinity, persistence, sticky sessions: what you need to know"]. What is the difference between Persistence and Affinity? Affinity is when information from a layer below the application layer is used to pin a client request to a single server. Persistence is when application layer information binds a client to a single server sticky session. The main advantage of persistence over affinity is it is much more accurate. Persistence is implemented though the use of cookies. The HAProxy `cookie` directive names the cookie which will be used for persistence, along with parameters controlling its use. The HAProxy `server` directive has a `cookie` option that sets the value of the cookie: it should be set to the name of the server. If an incoming request does not have a cookie identifying the backend server, then HAProxy selects a server based on its configured balancing algorithm. HAProxy assures that the cookie is set to the name of the selected server in the response. If the incoming request has a cookie identifying a backend server, then HAProxy automatically selects that server to handle the request. To enable persistence in the backend server block of the `/etc/haproxy/haproxy.cfg` configuration this line must be added: ---- cookie SERVERID insert indirect nocache ---- This says `SERVERID` will be the name of our HAProxy persistence cookie. Then we must edit each `server` line and add `cookie ` as an additional option. For example: ---- server server-0 cookie server-0 server server-1 cookie server-1 ---- Note, the other parts of the server directive have been omitted for clarity. NOTE: The <> and the HAProxy server persistence cookie are entirely separate. Do not confuse them. The HAProxy server persistence cookie identifies the backend server which issued the Mellon cookie. For Mellon to work correctly, all user requests *must* be handled by the same backend server that issued the Mellon cookie in the first place. === Forwarded HTTP Headers [[forwarded_http_headers]] When proxies are in effect the `X-Forwarded-\*` HTTP headers come into play. These are set by proxies and are meant to allow an entity processing a request to recognize that the request was forwarded and what the original values were _before_ being forwarded. A common HAProxy configuration sets the `X-Forwarded-Proto` HTTP header based on whether the front connection utilized SSL/TLS or not via this configuration:: http-request set-header X-Forwarded-Proto https if { ssl_fc } http-request set-header X-Forwarded-Proto http if !{ ssl_fc } To make matters interesting, core Apache *does not* interpret this header; thus responsibility falls to someone else to process it. In the situation where HAProxy terminates SSL prior to the backend server processing the request, the fact that the `X-Forwarded-Proto` HTTP header is set to https is *irrelevant* because Apache does not utilize the header when an extension module such as Mellon asks for the protocol scheme of the request. This is why it is *essential* to have the `ServerName` directive include the `scheme:://host:port` and to have `UseCanonicalName` enabled: otherwise Apache extension modules such as Mellon will not function properly behind a proxy. But what about web apps hosted by Apache behind a proxy? It turns out it's the web app's (or rather the web app framework's) responsibility to process the forwarded header. Thus apps handle the protocol scheme of a forwarded request differently than Apache extension modules do. The critical thing to note is is that Apache extension modules and web apps process the request scheme of a forwarded request differently, demanding that *both* the `ServerName` and `X-Forwarded-Proto` HTTP header techniques be utilized. == When a SAML party responds with an error [[error_response]] SAML is a request/response protocol much like HTTP. In fact the two major SAML datatypes are `Request` and `Response`. A `Response` contains a `Status` element which includes a `StatusCode` indicating if the `Request` succeeded or failed, and if it failed, the reason why. The `StatusCode` element may contain additional nested `StatusCode` elements providing additional details, but typically there is usually only one or two `StatusCode` elements. The outermost `StatusCode` is called the top-level status code, the next nested `StatusCode` is called the second-level status code. `StatusCode` values *must* be a URI. The top-level status codes *must* be one of one of the top-level status codes defined by the SAML specification. The second-level status code must also be a URI and should be one of the second-level status codes defined by SAML but a system entity may define its own non-top-level status codes. In addition to the `StatusCode` elements a `Status` element may also contain an optional `StatusMessage` with greater detail and/or a `StatusDetail` whose format is not defined by SAML. In most scenarios Mellon, acting as a relying party, issues a `Request` to an IdP acting as an asserting party, which then replies with a `Response` containing a `Status`. Occasionally Mellon will receive a `Request` from an IdP for which Mellon will respond with a `Response` and `Status`; a good example of this is IdP-initiated logout. *When diagnosing problems you should examine the `StatusCode` values and any additional information in the `Status` element.* === Top-level status codes Below are top-level status codes as defined by SAML. .Top-level status codes urn:oasis:names:tc:SAML:2.0:status:Success:: The request succeeded. Additional information MAY be returned in the and/or elements. urn:oasis:names:tc:SAML:2.0:status:Requester:: The request could not be performed due to an error on the part of the requester. urn:oasis:names:tc:SAML:2.0:status:Responder:: The request could not be performed due to an error on the part of the SAML responder or SAML authority. urn:oasis:names:tc:SAML:2.0:status:VersionMismatch:: The SAML responder could not process the request because the version of the request message was incorrect. === Second-level status codes Below are second-level status codes as defined by SAML. .Second-level status codes urn:oasis:names:tc:SAML:2.0:status:AuthnFailed:: The responding provider was unable to successfully authenticate the principal. urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue:: Unexpected or invalid content was encountered within a or element. urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy:: The responding provider cannot or will not support the requested name identifier policy. urn:oasis:names:tc:SAML:2.0:status:NoAuthnContext:: The specified authentication context requirements cannot be met by the responder. urn:oasis:names:tc:SAML:2.0:status:NoAvailableIDP:: Used by an intermediary to indicate that none of the supported identity provider elements in an can be resolved or that none of the supported identity providers are available. urn:oasis:names:tc:SAML:2.0:status:NoPassive:: Indicates the responding provider cannot authenticate the principal passively, as has been requested. urn:oasis:names:tc:SAML:2.0:status:NoSupportedIDP:: Used by an intermediary to indicate that none of the identity providers in an are supported by the intermediary. urn:oasis:names:tc:SAML:2.0:status:PartialLogout:: Used by a session authority to indicate to a session participant that it was not able to propagate logout to all other session participants. urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded:: Indicates that a responding provider cannot authenticate the principal directly and is not permitted to proxy the request further. urn:oasis:names:tc:SAML:2.0:status:RequestDenied:: The SAML responder or SAML authority is able to process the request but has chosen not to respond. This status code MAY be used when there is concern about the security context of the request message or the sequence of request messages received from a particular requester. urn:oasis:names:tc:SAML:2.0:status:RequestUnsupported:: The SAML responder or SAML authority does not support the request. urn:oasis:names:tc:SAML:2.0:status:RequestVersionDeprecated:: The SAML responder cannot process any requests with the protocol version specified in the request. urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooHigh:: The SAML responder cannot process the request because the protocol version specified in the request message is a major upgrade from the highest protocol version supported by the responder. urn:oasis:names:tc:SAML:2.0:status:RequestVersionTooLow:: The SAML responder cannot process the request because the protocol version specified in the request message is too low. urn:oasis:names:tc:SAML:2.0:status:ResourceNotRecognized:: The resource value provided in the request message is invalid or unrecognized. urn:oasis:names:tc:SAML:2.0:status:TooManyResponses:: The response message would contain more elements than the SAML responder is able to return. urn:oasis:names:tc:SAML:2.0:status:UnknownAttrProfile:: An entity that has no knowledge of a particular attribute profile has been presented with an attribute drawn from that profile. urn:oasis:names:tc:SAML:2.0:status:UnknownPrincipal:: The responding provider does not recognize the principal specified or implied by the request. urn:oasis:names:tc:SAML:2.0:status:UnsupportedBinding:: The SAML responder cannot properly fulfill the request using the protocol binding specified in the request. === Status code examples .Example of a `Status` indicating success. [source,xml] ---- ---- <1> _Top-level status code:_ Because the top-level status code is `Success` no other status information is included. [[invalid_nameid_example]] .Example of a `Status` indicating failure due to invalid NameIDPolicy. [source,xml] ---- ---- <1> _Top-level status code:_ Because the top-level status code is *not* `Success` this top-level status code indicates a *failure* and the _primary_ reason for the failure. In this instance, the requester sent a value the receiver was unable to process. <2> _Second-level status code:_ This second-level status code provides the additional information describing what the requester sent that could not be acted upon. In this case, the requester sent a `NameIDPolicy` the IdP was unable to fulfill. === Finding the `StatusCode` [[find_status_code]] Recent versions of Mellon (>= 0.13.1) will log any non-success status in both the Apache error log and in the Mellon diagnostics log (if enabled). The log message for the above `InvalidNameIDPolicy` error will look like this: ---- "StatusCode1="urn:oasis:names:tc:SAML:2.0:status:Requester", StatusCode2="urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy", "StatusMessage="(null)" ---- Or you have the option to examine the SAML message as described in <>. == Gathering run-time information [[gather_runtime_info]] === Apache log files Mellon writes messages to the Apache server error log file. Depending on your Apache configuration, those messages might appear in either `/var/log/httpd/error_log` or `/var/log/httpd/ssl_error_log`. You can turn up the verbosity of the messages by modifying the Apache `LogLevel`, for example: ---- LogLevel debug ---- NOTE: Mellon's use of standard Apache logging is limited. See <> for a much better way to capture Mellon run time information. === Trace SAML flow [[trace_saml_flow]] Since you're most likely using the SAML Web-SSO profile, which is entirely browser based, you can use any of the browser tools to watch HTTP requests and responses. The Firefox web browser provides the FireBug add-on and the Chrome browser offers Developer Tools. Each of these browsers also has additional add-ons to display SAML messages; see <>. NOTE: The easiest and most complete way to trace HTTP requests and responses during SAML flow, capture SAML messages, and examine how Mellon processes a SAML message is to use <>. === Inspect SAML messages [[inspect_saml_messages]] There are many times when you need to see the content of a SAML message. Perhaps you don't know what attributes your IdP is returning in an assertion. Or the SAML transaction is failing for some reason and you need to diagnose why. In such cases being able to see the contents of the SAML messages can be immensely helpful. Examining the on-the-wire SAML data is seldom useful, even when it's contained in otherwise visible HTTP data elements such as query parameters, post data, etc. This is because the various SAML bindings encode the message in different ways. It may break the message into independent components which need to be reassembled at the receiving end, or it may encode the data in a variety of formats which need to be decoded to recover the original message content. It's best to use SAML-aware tools to examine SAML messages, because they know how to decode and reassemble the raw SAML data into the final SAML message the receiver evaluates. NOTE: The easiest and most complete way to capture SAML messages and examine how Mellon processes a SAML message is to use <>. If your version of Mellon supports diagnostics you may wish to skip to this section. The Web-SSO SAML profile is by far the most commonly used. Because all SAML messages transit though the browser in Web-SSO, it is possible to write a browser extension to capture and decode the SAML messages exchanged between the SP and IdP. ==== Firefox SAML Tracer [[saml_tracer]] The Firefox https://addons.mozilla.org/en-US/firefox/addon/saml-tracer/[SAML Tracer] Add-On will display decoded SAML messages used during single sign-on and single logout. SAML Tracer is not capable of decrypting an encrypted IdP response, because it does not have access to the IdP's public encryption key contained in the IdP's metadata. See <> for how to deal with this issue. To use SAML Tracer you must first install the add-on. Then each time you want to use SAML Tracer you will need to go to the Firefox menu and select the SAML Tracer option. This will bring up a separate Firefox window which looks like this: image::saml-tracer.svg[] The SAML Tracer window is divided into two panes: a list of HTTP requests in the top pane, and detailed information on the selected request in the bottom window. SAML Tracer examines each HTTP request and response, and if it detects it is a SAML message, it flags the request in the request list window at the top with a "SAML" icon. In the detail pane are different tabs which show you the request/response information in different views. In order to view the decoded SAML message you need to make the `SAML` tab active. The `Parameters` tab shows you the query parameters (either URL or POST). SAML messages are usually transported in HTTP parameters, so this is where you can see the raw SAML data before being decoded into a complete SAML message. The `http` tab shows you the HTTP headers associated with the HTTP request/response. ==== Chrome, SAML Chrome Panel The Chrome Web browser offers several add-ons to display SAML messages. The most commonly used is https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace[SAML Chrome Panel]. SAML Chrome Panel integrates with the Chrome developer tools. Here is an example of the SAML Chrome Panel in the developer tools panel: image::chrome_SAML_Chrome_Panel.svg[] ==== If the IdP response is encrypted [[encrypted_response]] Data in a SAML response may be encrypted for confidentiality (usually encryption is not needed because SAML transactions should be occurring over a secure TLS channel). Decrypting the data requires access to the IdP's public encryption key contained in its metadata. Most SAML browser tools do not support decryption. If you discover your tool is showing you encrypted data you have a few options: * Disable encryption at the IdP. See your IdP's documentation on how to enable/disable encryption. * Use <>. The diagnostics support in Mellon operates after the SAML message is decoded from its SAML binding transport and after it's been decrypted into a final plaintext SAML XML document. Most people will find Mellon diagnostics to be the easiest and most complete capture of SAML data and Mellon's processing of it. === Inspecting Mellon environment variables [[inspect_mellon_env]] Recall that Mellon communicates with web apps by inserting Apache environment variables into the Apache environment. While diagnosing problems or when initially setting up your Mellon deployment it can be very useful to see the contents of the Apache environment. The typical way this is done is to substitute the resource Mellon is protecting with a script that dumps the environment it received. So instead of getting back the resource, after Mellon successfully authenticates the script runs and returns a page listing the environment variables. Once you've collected this information you need to remove the script from the protected URL so the protected resource will be returned instead of a data dump. If your installed version of Mellon includes support for <>, there is no need to alter your protected resource in order to get an environment variable dump. The diagnostics log includes a dump of the complete Apache environment at the end of each response. This is a much easier and more complete solution than substituting a script for your protected resource. ==== Python WSGI Environment Dump Create a script with the following content: [source,python] ---- import pprint import webob import webob.dec @webob.dec.wsgify def application(req): return webob.Response(pprint.pformat(req.environ), content_type='application/json') ---- Placing the above script in the Apache `cgi-bin` directory is a good idea. We'll name this script 'dump-env'. Add a `WSGIScriptAlias` directive to your Apache configuration so that it runs the above script when the protected resource URL is requested. For example: ---- WSGIScriptAlias "/private/info.html" "/var/www/cgi-bin/dump-env" ---- ==== PHP Environment Dump [source,php] ---- $value) { if(substr($key, 0, 7) == 'MELLON_') { echo($key . '=' . $value . "\r\n"); } } ?> ---- === Mellon Diagnostics [[mellon_diagnostics]] When something goes wrong with your Mellon deployment, experience has shown that it can be frustratingly difficult to gather sufficient information to diagnose the problem. Often you will need access to the following pieces of information: * Mellon configuration * Mellon metadata * IdP metadata * HTTP requests and responses * HTTP headers * SAML message content as decoded pretty XML * Apache environment variables * Session infomation Although Mellon does log some `DEBUG` messages to the Apache error log, that information is often incomplete and mixed in with other irrelevant messages. The SAML message content has to be gathered independently via other tools (<>), configuring Apache to log all HTTP headers is obscure and verbose, there is no easy way to log all Apache environment variables associated with a request, and the SAML metadata and Mellon configuration is independent of the log data. Finally all these indpendent pieces of data gathered from multiple sources need to be correlated to produce a coherent sequence while at the same time removing a lot of extraneous irrelevant non-SAML data. Apache logging also suffers from some serious limitations when trying to record SAML data. Apache enforces a hard limit on the length of a log message, which often results in truncating SAML messages. Apache log messages are reformatted, newlines are removed, and other characters are escaped. This makes trying to read XML documents extremely difficult unless you post-process the log. If you are a support person trying to help an administrator with their Mellon deployment, it is very difficult to get a 3rd party who is not familiar with the various operations to gather the necessary information in a cohesive form amenable for remote diagnostic review. It would be really nice if Mellon could gather all this information in protocol sequence in a single file without other irrelevant Apache messages, and without the need for any post-processing of the log data. Mellon has been extended to gather all the above relevant information in a human readable format. The feature is called _Mellon Diagnostics_. The diagnostics feature is new as of July 2017, and it must be enabled at compile time; thus your version of Mellon may not have it. Because the feature is new the format and content of the diagnostic data is expected to evolve. ==== Using Mellon Diagnostics [[using_mellon_diagnostics]] Currently Mellon diagnostics supports these new Mellon directives. These directives are module level and as such should be declared outside of any location blocks in your Apache configuration. MellonDiagnosticsFile:: If Mellon was built with diagnostic capability then diagnostic is written here, it may be either a filename or a pipe. If it's a filename then the resulting path is relative to the ServerRoot. If the value is preceeded by the pipe character "|" it should be followed by a path to a program to receive the log information on its standard input. Default: `logs/mellon_diagnostics` MellonDiagnosticsEnable:: If Mellon was built with diagnostic capability then this is a list of words controlling diagnostic output. Currently only `On` and `Off` are supported. Default: `Off` To enable diagnostic logging add this line to your Apache configuration file where you keep your Mellon configuration. ---- MellonDiagnosticsEnable On ---- Restart Apache and perform some operation that involves Mellon. In your Apache log directory will be a file called `mellon_diagnostics` (or whatever `MellonDiagnosticsFile` was set to). IMPORTANT: Diagnostic logging may potentially contain security sensitive information. Diagnostic logging is verbose and will generate large files. Therefore _you should enable diagnostic logging only for the minimum duration necessary_. You may wish to review diagnostic output in <> captured when the demo authentication was executed. == Potential Problems === It's the metadata *The vast majority of SAML deployment problems can be traced back to metadata.* * Is your metadata current? * Have you loaded your most recent SP metadata? Did you restart Apache after modifying the SP metadata? * Has your IdP loaded the exactly the same metadata Mellon is reading at Apache start-up? * Have you loaded your most recent IdP metadata? Did you restart Apache after modifying the SP metadata? * Did you make a change to your entityID? * Did you make a change to the `MellonEndpointPath` without regenerating your SP metadata and loading the new metadata into both Mellon and your IdP? Remember the `MellonEndpointPath` establishes all the SAML endpoint URLs that appear in your metadata. See <> and <>. * Did you modify any of the keys or certs without both updating the mellon config _and_ your SP metadata? === Behavior does not change after modifying any SAML file Mellon reads its configuration at Apache start-up. If you make any change to any file Mellon reads, you will not see those changes reflected until after you restart Apache. === Are the Mellon configuration directives syntactically correct? Apache will not start if there is any error in any of the configuration files it reads. An easy way to test the correctness of your Apache configuration directives _without starting the server and examining the error logs_ is to use the the `apachectl` command line tool with the `configtest` option: ---- apachectl configtest ---- === No AuthnRequest sent to IdP During debugging you may discover the entire <> is not executed, so the IdP is never contacted. This is because Mellon implements sessions. The session identifier is communicated in the cookie `mellon-cookie` (or whatever is the current value of the Mellon directive `MellonVariable`). If you had previously successfully authenticated against the IdP, the browser will have been sent the Mellon session ID in its cookie. When Mellon gets a request to authenticate a resource, it first checks to see if it has a valid session based on the identifier passed as the Mellon cookie. If there is a valid session, Mellon will use that cached session information instead of contacting the IdP. Deleting the `mellon-cookie` from the browser will cause Mellon to believe there is no pre-existing session. === Incorrect MellonEndpointPath [[incorrect_mellon_endpoint_path]] . <> [listing,subs="verbatim,quotes"] ------------------------------------------ MellonEndpointPath ##/mellon/## ------------------------------------------ <1> The definition of `MellonEndpointPath` in your Apache Mellon configuration *must* match the path component in each of your Service `Location` declarations in your SP metadata. See <> for more detail. [listing,subs="verbatim,quotes"] ------------------------------------------ ------------------------------------------ <1> Each Service `Location` URL in your SP metadata *must* have a path component that starts with your `MellonEndpointPath` and appends exactly one directory component to it. That final directory component is one of the Mellon endpoints as described in <>. Here the `MellonEndpointPath` is highlighted in the `Location` attributes of the metadata. === HTTP_BAD_REQUEST - Invalid Destination on Response [[invalid_destination]] If after posting the Assertion to your `postResponse` endpoint you get a HTTP_BAD_REQUEST error with status code 400 and a page that says: ---- Your browser sent a request that this server could not understand. ---- and in the Apache log file there is a Mellon message like this: ---- Invalid Destination on Response. Should be: https://xxx/mellon/postResponse ---- Then you have failed one of the SAML security checks. There is a SAML requirement that the recipient of a message verifies that the intended destination of the message is the actual SAML endpoint it was received on. This is to prevent malicious forwarding of messages to unintended recipients. To perform this check, what Mellon does is build a URL by asking Apache what scheme, hostname, and port it is running under, and then appends the <> and the Mellon endpoint to it. This becomes the URL the message was received on. Mellon then does a string comparison to see if this manufactured URL is identical to the `Destination` attribute in the SAML message. If they are not the same string, the test fails and a HTTP_BAD_REQUEST is returned. There are two potential causes for this failure: . Incorrect Apache `ServerName`. See the <> discussion for more details. This problem usually occurs when Mellon is running behind a load balancer or SSL terminator. . Mismatch between the Mellon metadata and the `MellonEndpointPath` in your Mellon configuration. If the scheme, hostname, and port are correct then the problem must be in path component of the URL. The SAML `Destination` attribute is read from the provider's metadata. The `MellonEndpointPath` is read from Mellon's configuration. The two must be in sync. Verify that the location endpoints in Mellon's metadata match the value of `MellonEndpointPath`. See the discussion of <> for more details. The `Destination` check may also fail because one URL has an explicit port but the other does not. This can occur with the standard HTTP port 80 and HTTPS port 443: see <> for more detail. === Mellon metadata out of sync with Mellon configuration Mellon's metadata and its Apache configuration directives have data elements in common but are maintained independently. The Apache configuration directives in common with the metadata are: * `MellonSPentityId` * `MellonSPPrivateKeyFile` * `MellonSPCertFile` * `MellonEndpointPath` This can lead to problems if you: * Generate metadata not consistent with these values in your Apache configuration directives. * Edit the above Apache configuration directives without regenerating and reloading your metadata. * Fail to load the current SP metadata by restarting Apache. * Fail to load the current SP metadata into the IdP. You may wish to review <> and <>. === Microsoft ADFS issues It is common to have problems when using Microsoft ADFS as a SAML IdP. This blog post from Microsoft contains many of the ADFS configuration issues encountered when adding an SP to ADFS: [[adfs_blog,ADFS Deep Dive Blog]]https://blogs.technet.microsoft.com/askpfeplat/2015/03/01/adfs-deep-dive-onboarding-applications/[ADFS Deep Dive Onboarding Applications] NOTE: ADFS calls SPs a "Relying Party" and the SP configuration a "Relying Party Trust". ==== ADFS Signature Algorithm [[adfs_sig_alg]] One of the `Relying Party Trust` options is the "Secure Hash Algorithm" which controls the signature algorithm used to produce an XML signature on the SAML message. This is the signature algorithm ADFS will use to sign the SAML messages it emits. SAML does not require both parties to use the same signature algorithm; in theory, it's fine if Mellon as the SP signs with one algorithm and ADFS as the IdP signs with a different algorithm. But ADFS enforces the requirement that the SP signs with same algorithm as set in the `Relying Party Trust`. If ADFS receives a SAML message signed with a different algorithm than what is specified in the `Relying Party Trust` configuration, you will get a message in the ADFS log like this: ---- SAML request is not signed with expected signature algorithm. SAML request is signed with signature algorithm http://www.w3.org/2001/04/xmldsig-more#rsa-sha1 . Expected signature algorithm is http://www.w3.org/2000/09/xmldsig#rsa-sha256 ---- Since SHA-1 is no longer considered secure, many ADFS administrators set their `Relying Party Trust` secure hash algorithm to SHA-256. This causes problems for Mellon versions earlier than 0.13.1, which always signed its messages with the SHA-1 hash (specifically the RSA-SHA1 algorithm) and there was no mechanism to specify a different signing algorithm. See <> for how to modify the `Relying Party Trust` Secure Hash Algorithm. Mellon versions greater than 0.13.1 added a new configuration option called `MellonSignatureMethod` which allows you to match the signature algorithm emitted by Mellon to the one specified in the ADFS `Relying Party Trust` for the Mellon SP. For example: ---- MellonSignatureMethod rsa-sha256 ---- ==== ADFS NameIDPolicy [[adfs_nameid_policy]] By default ADFS cannot handle many of the SAML NameID formats without additional configuration in the `Relying Party Trust`. Please make sure you are familiar with the material in the section <>. By default, Mellon will generate SP metadata with a <> of `transient`, see <> for how to modify this in Mellon. When ADFS receives a SAML message with a `NameIDPolicy` set to a specific format, it is supposed to respond with a `NameID` matching that format. Because of the architecture of ADFS, it may not have access to the data needed to generate that `NameID`. The necessary data is contained in a _Claim_ controlled by a _Claim Rule_. To get the contents of the _Claim_ being used to populate the SAML `NameID`, you must also add a `Claim Rule Transform` that maps the desired _Claim_ data into a SAML data element, which in this case is the `NameID`. Examples of the `NameID` formats which require this additional configuration in ADFS are `transient`, `persistent`, `email` and possibly others. If the _Claim Rule_ and _Claim Rule Transform_ are not configured for the `NameIDPolicy` in the request, ADFS will respond with a `InvalidNameIDPolicy` error status because it cannot provide the requested `NameID` format. See <> in the <> for an example of this error and how to identify it. This Microsoft blog discusses `NameID` configuration in ADFS: [[adfs_nameid,ADFS NameID Configuration]]https://blogs.msdn.microsoft.com/card/2010/02/17/name-identifiers-in-saml-assertions/[ADFS - Name Identifiers in SAML assertions] === Time Sync SAML, like many authentication protocols (e.g. Kerberos), relies on timestamps to validate messages. If you see one of these errors in the httpd logs: ---- [auth_mellon:error] [pid xxx] [client xxx] NotBefore in Condition was in the future. [auth_mellon:error] [pid xxx] [client xxx] NotOnOrAfter in Condition was in the past. ---- Then it's likely that either the Mellon node or the IdP node are not time synchronized. You can check the status of your time sync with the `chronyc` command line tool, for example: ---- $ chronyc sources 210 Number of sources = 4 MS Name/IP address Stratum Poll Reach LastRx Last sample =============================================================================== ^? tock.no-such-agency.net 0 10 0 10y +0ns[ +0ns] +/- 0ns ^? static-74-104-167-114.bst 0 10 0 10y +0ns[ +0ns] +/- 0ns ^? 69.195.159.158 0 10 0 10y +0ns[ +0ns] +/- 0ns ^? freemont.nerdboy.net.au 0 10 0 10y +0ns[ +0ns] +/- 0ns ---- If the `Reach` column has zeros in it you are not synchronizing your clock. This may be due to a firewall blocking the NTP port. Trying opening your NTP port or using a different server in `/etc/chrony.conf`. == Glossary entityID:: The unique name of a SAML provider. The entityID *must* be a URI. Often entityID's are URLs, however the choice of using a URL as an entityID does not have any meaning in SAML other than it is a convenient way to to have a unique URI. It is best to choose an entityID that will not change over time as SAML services inevitably migrate between hosts in a deployment. Once again, an entityID is *_a unique name for a SAML service_*, it is nothing more than that. Assertion:: Data produced by a SAML authority (e.g. IdP) with respect to a specific subject. The assertion may convey authentication of the subject, attributes associated with the subject, or authorization information for the subject in regards to a specific resource. Identity Provider:: An identity provider is a SAML authority which _provides_ authentication services proving the identity of a principal. The proof of identity is conveyed in a SAML assertion along with additional information about the subject (attributes) which the service provider may choose to utilize when making authorization decisions. IdP:: Acronym for Identity Provider Profile:: A profile is a set of rules for one of several purposes; each set is given a name in the pattern “xxx profile of SAML” or “xxx SAML profile”. * Rules for how to embed assertions into and extract them from a protocol or other context of use. * Rules for using SAML protocol messages in a particular context of use. * Rules for mapping attributes expressed in SAML to another attribute representation system. Such a set of rules is known as an “attribute profile”. SAML:: Security Assertion Markup Language. Service Provider:: A service provider is a SAML _relying party_ which _provides_ a _service_ to a user who must be authenticated and authorized by the service in order to use the service. A web application is a common example. SP:: Acronym for Service Provider. [appendix] == Appendix === Example On-The-Wire AuthnRequest [[authentication_request_wire]] This is is the "on the wire" HTTP data for the <> using the _HTTP Redirect Binding_. ---- GET https://rhsso.example.com:8443/auth/realms/test/protocol/saml?SAMLRequest=hZJBT8JAEIX%2FSrN32NKWAptCQkASEjQE1IMXs6lj2KS7W3emiv%2FeaRHFi5w2eTsvb76XKVDbqlbzhg5uB28NIEVHWzlU3cdUNMEpr9GgctoCKirVfn67UUk%2FVnXw5EtfiQvL%2Fw6NCIGMdyJaL6fieTgZJPkiTeP8JstHk1U%2BTOMkWcWLbDkfreJMRI8QkOengu1sQmxg7ZC0I5biwagX571kfD9IVTpRg%2BxJREtmME5T5zoQ1aikDAdE34ejtnUF%2FdJbNc6yVGrmlgF0ZVES%2B%2BQZSbYsIlp4h9BG%2FQdVnoZU2YTAb89whikNiWjlQwldt1PxqiuElmDLJZh3%2BFHm507asMZC2EN4NyU87Da%2F%2B1uoKu8uAb4lWXukHWDdLiFmRbu36moKs%2BtmC6RfNOlCXvqK01HcMel6ufUM89miWH2liFYxL73XblRR0A4NF8KIHPax4J6JsSk0IOTsFPn39GZf&RelayState=https%3A%2F%2Fmellon.example.com%2Fprivate%2Finfo.html&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=GJ%2BC%2BEwcPpOmAfYwMdMcAPSkX2y1Da634jrm1oWWs8Ozyb7P%2FumIk9HaNbfJZvaHc6HyOxYXhCpqb6NJrRm%2BrY1NSOJqjtt3kXldNLKecFfhKamzfOfAufKTPGGSZNAuwRTiQCkrLuFt8A%2BezEuCswNDADSRJGLL9aYX8A8G23IcLeVuqobtrCH9bSr2wgO0uy61o1s5bDlA6ceKwrjle%2F6TofFUWBYOB6pzRL40AzmNsvKieHqSOCxo6uNKQdEZQYomF9fi%2FuCPovIn5AdRFC1wcx%2BeGYi%2BDS6R4lbEnrhu8RfEmxhA8PJHDoTMH2fcfD0jyUh%2BejtLdqCUFJ9ppQ%3D%3D HTTP/1.1 ---- === Example On-the-Wire response [[assertion_response_wire]] This is is the "on the wire" HTTP data for the <> using the _HTTP Post Binding_. ---- SAML HTTP Post Binding
---- === Example Mellon Diagnostics [[mellon_diagnostics_example]] Here is the diagnostics output as described in <> for our authentication example. ---- ---------------------------------- New Request --------------------------------- GET - /saml-test/protected.html log_id: (null) server: name=/etc/httpd/conf.d/ssl.conf, scheme=https hostname=mellon.example.com port=443 pid: 21576, tid: 140251630954624 unparsed_uri: /saml-test/protected.html uri: /saml-test/protected.html path_info: filename: /var/www/html/saml-test/protected.html query args: (null) Request Headers: Host: mellon.example.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 DNT: 1 Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.8 Cookie: mellon-cookie=aa8aefac8bc813f194b1a8d97e3a4058 Mellon Directory Configuration for URL: /saml-test/protected.html MellonEnable (enable): auth MellonVariable (varname): cookie MellonSecureCookie (secure): Off MellonMergeEnvVars (merge_env_vars): (null) MellonEnvVarsIndexStart (env_vars_index_start): -1 MellonEnvVarsSetCount (env_vars_count_in_n): On MellonCookieDomain (cookie_domain): (null) MellonCookiePath (cookie_path): (null) MellonCond (cond): 0 items MellonSetEnv (envattr): 0 items MellonUser (userattr): NAME_ID MellonIdP (idpattr): IDP MellonSessionDump (dump_session): Off MellonSamlResponseDump (dump_saml_response): Off MellonEndpointPath (endpoint_path): /mellon/ MellonSPMetadataFile (sp_metadata_file): pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient MellonSPPrivateKeyFile (sp_private_key_file): pathname: "/etc/httpd/saml2/demo.key" -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHUXILDtuMxyFP 6OW81hqfj9z+gC7sxR6U8bRPI4gFiNjkNF+4HMyrKtVtfwA5WJ1tJ4IaZbg4fGap iCcWQ5tjjuZ7CpZ6XLekq63OGrkgZxNm8L+AjvREv78iR4Myc4x6ybN1LwQnV+oD F0i9SAEPnfJCaCA+1+DC3RKy7+xtK3SVimUWwAHmkNU6N0wCVSNYn1KgnxBSCPTx 2DifbyRl21SiokCfLSnktE++WBzRzldSxzMrkSfFoS7/uT232tbdw2NsJmHGLRGk myBrEC6BVhQ746NM1cLDtrOots7bNfZ3yfb3W1tpoKVOhM2se3OTKdidAQsaGlVu 7QIhBqinAgMBAAECggEBAJtU662WfJ9vqJRgCnpp2QG02iM0vl0jGbw1ybFLHXxC s9TUxCv1tcNHdGEK8p++YaFpgskTsMfEmzVPuDZvpa+m9BO7op3ll/CrIp5W0SNh cQtuX6/OuKrDTC9oz+QHjNk8S7DtXS1UJDkYckWg0cLb8qqx/z86eh0isKnmtLg2 H1+6L6mB9fcZldkcrU+kXT+dcDX85skMZAeBsrG4yaoX26AzVl8lEl2rJAQvpxj8 5wGBC4riWY6TzMYiCjcS5JfZIlbhcZe61ej3A48NVBSKCP1XKo0xbKuOHIQuMeeW wSaboBwRzJ9JdTXlq5UWfLvmjXDc/HCwk/N7cj021uECgYEA5KkQr3cSKrMIkYoO H0Vnkw1kYYGUjtTL00Nfdkv7uGMF122nyt1ND0gpdS0jgNx3LSEam/gY35UWEjGH i8DGD04k8pvKDUsl8cuYPcC3oce1lLCGAnw+aHPC5wtA829CLOLtBfxXIhVAI0tp ECosD/A63/m1LC19XolOd10/PC8CgYEA3yZChEYZupDGJFZltiy0ZgUarvD2Ss4N QiRVR+CmpBrOKZdD8q6uUuqWQN9Rw7kXm8LxAPYwiIDVjxjYALF+j7/9Q1oZyKuv eHJdMe4eRUeqeaIKbxnFfKwGZ5rj97jwPrkUCxio75KZhpOcDHMSgBCBtzW0XIZl gTeQYOshZQkCgYB5TK6LRnEesabj/gaL1DejrMEJcMIsGvqdceocSSaZo/4fUA5o 8YjFiJRlkrJ403ttN1h0UOJxCReSQzASlQr8Z4n2IWrILotMf6Kdb7R6YAUVgac1 fk9k/bPw+OlVujmyshbmy/w1GmzRzFlJt/Vz5w50bnULoH4XPmOfspmvBQKBgBcJ rihVzGY0eCBcQxfxuZYmxMB25BaI+1luwtcu3EVo9wvYMA2n9xtcWLLN23UncMaF 87ezswMEugeR+wrnSDezDISdkrfi8bSvqetzt/BTG8h+8DDUKk1avTaJCSwUDcmL 9gPHQfmp2uvH5X5riudpzNqLUtmSjnwurlszKzlxAoGAR8STlDJhNph+p3cF8k25 ydT1kypxnjzVG8CAV5/h3dUmc7j7gyV8NlWZfWacxMZWOBsrdVh0zhMNUPiLJaGd I1isOkmiN9JFYMMhHSnhPnTCIjmu6uBLxf8wotHAvzWOJPV7lUZbw21KIN3DS79F sGZ2QzGYn4inHG4UHClhZxU= -----END PRIVATE KEY----- MellonSPCertFile (sp_cert_file): pathname: "/etc/httpd/saml2/demo.cert" -----BEGIN CERTIFICATE----- MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= -----END CERTIFICATE----- MellonIdPPublicKeyFile (idp_public_key_file): file_data: NULL MellonIdPCAFile (idp_ca_file): file_data: NULL MellonIdPMetadataFile (idp_metadata): 1 items [ 0] Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress [ 0] Chain File file_data: NULL MellonIdPIgnore (idp_ignore): MellonSPentityId (sp_entity_id): (null) MellonOrganizationName (sp_org_name): 0 items MellonOrganizationDisplayName (sp_org_display_name): 0 items MellonOrganizationURL (sp_org_url): 0 items MellonSessionLength (session_length): -1 MellonNoCookieErrorPage (no_cookie_error_page): (null) MellonNoSuccessErrorPage (no_success_error_page): (null) MellonDefaultLoginPath (login_path): / MellonDiscoveryURL (discovery_url): (null) MellonProbeDiscoveryTimeout (probe_discovery_timeout): -1 MellonProbeDiscoveryIdP (probe_discovery_idp): 0 items MellonAuthnContextClassRef (authn_context_class_ref): 0 items MellonAuthnContextComparisonType (authn_context_comparison_type): (null) MellonSubjectConfirmationDataAddressCheck (subject_confirmation_data_address_check): On MellonDoNotVerifyLogoutSignature (do_not_verify_logout_signature): 0 items MellonPostReplay (post_replay): On MellonECPSendIDPList (ecp_send_idplist): On enter function am_auth_mellon_user searching for session with key aa8aefac8bc813f194b1a8d97e3a4058 (session) ... not found am_auth_mellon_user am_enable_auth, no valid session [APLOG_DEBUG auth_mellon_util.c:2055] have_paos_media_type=False valid_paos_header=False is_paos=Falseenter function am_start_auth Loading SP Metadata pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient Loading IdP Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress [APLOG_DEBUG auth_mellon_handler.c:3498] Redirecting to login URL: https://mellon.example.com/mellon/login?ReturnTo=https%3A%2F%2Fmellon.example.com%2Fsaml%2Dtest%2Fprotected.html&IdP=https%3A%2F%2Frhsso.example.com%3A8443%2Fauth%2Frealms%2Fipa === Response === Status: 303 See Other(303) user: (null) auth_type=(null) Response Headers: Cache-Control: private, max-age=0, must-revalidate Location: https://mellon.example.com/mellon/login?ReturnTo=https%3A%2F%2Fmellon.example.com%2Fsaml%2Dtest%2Fprotected.html&IdP=https%3A%2F%2Frhsso.example.com%3A8443%2Fauth%2Frealms%2Fipa Content-Length: 388 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1 Response Error Headers: Environment: UNIQUE_ID: WabkjcTYa6iga7y800KGZQAAAAA ---------------------------------- New Request --------------------------------- GET - /mellon/login log_id: (null) server: name=/etc/httpd/conf.d/ssl.conf, scheme=https hostname=mellon.example.com port=443 pid: 21576, tid: 140251630954624 unparsed_uri: /mellon/login?ReturnTo=https%3A%2F%2Fmellon.example.com%2Fsaml%2Dtest%2Fprotected.html&IdP=https%3A%2F%2Frhsso.example.com%3A8443%2Fauth%2Frealms%2Fipa uri: /mellon/login path_info: /login filename: /var/www/html/mellon query args: ReturnTo=https%3A%2F%2Fmellon.example.com%2Fsaml%2Dtest%2Fprotected.html&IdP=https%3A%2F%2Frhsso.example.com%3A8443%2Fauth%2Frealms%2Fipa Request Headers: Host: mellon.example.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 DNT: 1 Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.8 Cookie: mellon-cookie=aa8aefac8bc813f194b1a8d97e3a4058 Mellon Directory Configuration for URL: /mellon/login MellonEnable (enable): info MellonVariable (varname): cookie MellonSecureCookie (secure): Off MellonMergeEnvVars (merge_env_vars): (null) MellonEnvVarsIndexStart (env_vars_index_start): -1 MellonEnvVarsSetCount (env_vars_count_in_n): On MellonCookieDomain (cookie_domain): (null) MellonCookiePath (cookie_path): (null) MellonCond (cond): 0 items MellonSetEnv (envattr): 0 items MellonUser (userattr): NAME_ID MellonIdP (idpattr): IDP MellonSessionDump (dump_session): Off MellonSamlResponseDump (dump_saml_response): Off MellonEndpointPath (endpoint_path): /mellon/ MellonSPMetadataFile (sp_metadata_file): pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient MellonSPPrivateKeyFile (sp_private_key_file): pathname: "/etc/httpd/saml2/demo.key" -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHUXILDtuMxyFP 6OW81hqfj9z+gC7sxR6U8bRPI4gFiNjkNF+4HMyrKtVtfwA5WJ1tJ4IaZbg4fGap iCcWQ5tjjuZ7CpZ6XLekq63OGrkgZxNm8L+AjvREv78iR4Myc4x6ybN1LwQnV+oD F0i9SAEPnfJCaCA+1+DC3RKy7+xtK3SVimUWwAHmkNU6N0wCVSNYn1KgnxBSCPTx 2DifbyRl21SiokCfLSnktE++WBzRzldSxzMrkSfFoS7/uT232tbdw2NsJmHGLRGk myBrEC6BVhQ746NM1cLDtrOots7bNfZ3yfb3W1tpoKVOhM2se3OTKdidAQsaGlVu 7QIhBqinAgMBAAECggEBAJtU662WfJ9vqJRgCnpp2QG02iM0vl0jGbw1ybFLHXxC s9TUxCv1tcNHdGEK8p++YaFpgskTsMfEmzVPuDZvpa+m9BO7op3ll/CrIp5W0SNh cQtuX6/OuKrDTC9oz+QHjNk8S7DtXS1UJDkYckWg0cLb8qqx/z86eh0isKnmtLg2 H1+6L6mB9fcZldkcrU+kXT+dcDX85skMZAeBsrG4yaoX26AzVl8lEl2rJAQvpxj8 5wGBC4riWY6TzMYiCjcS5JfZIlbhcZe61ej3A48NVBSKCP1XKo0xbKuOHIQuMeeW wSaboBwRzJ9JdTXlq5UWfLvmjXDc/HCwk/N7cj021uECgYEA5KkQr3cSKrMIkYoO H0Vnkw1kYYGUjtTL00Nfdkv7uGMF122nyt1ND0gpdS0jgNx3LSEam/gY35UWEjGH i8DGD04k8pvKDUsl8cuYPcC3oce1lLCGAnw+aHPC5wtA829CLOLtBfxXIhVAI0tp ECosD/A63/m1LC19XolOd10/PC8CgYEA3yZChEYZupDGJFZltiy0ZgUarvD2Ss4N QiRVR+CmpBrOKZdD8q6uUuqWQN9Rw7kXm8LxAPYwiIDVjxjYALF+j7/9Q1oZyKuv eHJdMe4eRUeqeaIKbxnFfKwGZ5rj97jwPrkUCxio75KZhpOcDHMSgBCBtzW0XIZl gTeQYOshZQkCgYB5TK6LRnEesabj/gaL1DejrMEJcMIsGvqdceocSSaZo/4fUA5o 8YjFiJRlkrJ403ttN1h0UOJxCReSQzASlQr8Z4n2IWrILotMf6Kdb7R6YAUVgac1 fk9k/bPw+OlVujmyshbmy/w1GmzRzFlJt/Vz5w50bnULoH4XPmOfspmvBQKBgBcJ rihVzGY0eCBcQxfxuZYmxMB25BaI+1luwtcu3EVo9wvYMA2n9xtcWLLN23UncMaF 87ezswMEugeR+wrnSDezDISdkrfi8bSvqetzt/BTG8h+8DDUKk1avTaJCSwUDcmL 9gPHQfmp2uvH5X5riudpzNqLUtmSjnwurlszKzlxAoGAR8STlDJhNph+p3cF8k25 ydT1kypxnjzVG8CAV5/h3dUmc7j7gyV8NlWZfWacxMZWOBsrdVh0zhMNUPiLJaGd I1isOkmiN9JFYMMhHSnhPnTCIjmu6uBLxf8wotHAvzWOJPV7lUZbw21KIN3DS79F sGZ2QzGYn4inHG4UHClhZxU= -----END PRIVATE KEY----- MellonSPCertFile (sp_cert_file): pathname: "/etc/httpd/saml2/demo.cert" -----BEGIN CERTIFICATE----- MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= -----END CERTIFICATE----- MellonIdPPublicKeyFile (idp_public_key_file): file_data: NULL MellonIdPCAFile (idp_ca_file): file_data: NULL MellonIdPMetadataFile (idp_metadata): 1 items [ 0] Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress [ 0] Chain File file_data: NULL MellonIdPIgnore (idp_ignore): MellonSPentityId (sp_entity_id): (null) MellonOrganizationName (sp_org_name): 0 items MellonOrganizationDisplayName (sp_org_display_name): 0 items MellonOrganizationURL (sp_org_url): 0 items MellonSessionLength (session_length): -1 MellonNoCookieErrorPage (no_cookie_error_page): (null) MellonNoSuccessErrorPage (no_success_error_page): (null) MellonDefaultLoginPath (login_path): / MellonDiscoveryURL (discovery_url): (null) MellonProbeDiscoveryTimeout (probe_discovery_timeout): -1 MellonProbeDiscoveryIdP (probe_discovery_idp): 0 items MellonAuthnContextClassRef (authn_context_class_ref): 0 items MellonSubjectConfirmationDataAddressCheck (subject_confirmation_data_address_check): On MellonDoNotVerifyLogoutSignature (do_not_verify_logout_signature): 0 items MellonPostReplay (post_replay): On MellonECPSendIDPList (ecp_send_idplist): On enter function am_auth_mellon_user enter function am_handle_login Loading SP Metadata pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient Loading IdP Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress SAML AuthnRequest: http_method=LASSO_HTTP_METHOD_REDIRECT https://mellon.example.com/mellon/metadata === Response === Status: 303 See Other(303) user: (null) auth_type=(null) Response Headers: Cache-Control: private, max-age=0, must-revalidate Set-Cookie: mellon-cookie=cookietest; Version=1; Path=/; Domain=mellon.example.com; Location: https://rhsso.example.com:8443/auth/realms/ipa/protocol/saml?SAMLRequest=hZLNbsIwEIRfJfIdnED4syBSSEBCohWCtodeKisswlJsp94NpW9fJ5SWXsrJ0nhHs99opyh1WYm0pqPZwnsNSMFZlwZF%2BzFjtTPCSlQojNSAggqxSx%2FWotcNReUs2cKW7Mbyv0MigiNlDQtW%2BYy9pcPBchmNsnDSi%2FNoMl%2BEy0XWi6N5PFmk8SBjwQs49PMz5u3ehFjDyiBJQ14Ko1EnHHf64VM0FNFAhJNXFuSeQRlJretIVKHg3B0RbRfOUlcldAurxTiO%2B1x6bu5Alhq5qiS%2FEvEGhQWZNQhN0n9MxWVIFLVz%2Fu0oH6EKRSxYWldAW%2B2MHWSJ0ABsfAfqBD9Keq2kCas1uB24kyrgebv%2BXV9DWVpzu%2F%2B3xCuLtAWsmiVYMm32Fm1LLrlv1kByL0lO%2Ba1vermJR0%2B6yjfWw3w2KFreKaJR1L5zaEcFOWlQ%2BUI8og%2F7yHzN5LHJ1cB4con8e3nJFw%3D%3D&RelayState=https%3A%2F%2Fmellon.example.com%2Fsaml-test%2Fprotected.html&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=gBd8iP4CvbWajeMQHKOMgc5NBx7i6Kf5gXcbSa54oehMXgpPJJuwlY8BLTH861vGnl7AxaO%2F2soJPai4D96aNowm8hr9FBokjvI%2FjwdEVtRiFlng18DpEXPTE1SAa4cuxWcLE3BAZD2HZ0sW%2F91sRGnymFH9lC4cDiU1pG9OBBI1pBYxjtrAM%2FHvEjDNZ0xYTwji8S6ltrM0bBFbTdftcn5YCwI31SAFVhopbPRTfiEhanTYChbjy7h%2Fp6BHTwfvcLw4Pud98phEIhXTdK4XIJGSN%2BmCYeXHQZPyGnRGZcmfmPrEcIpptT4a5xRkltfJPHUSLnI%2Ft9QsEuYm02%2F4%2BQ%3D%3D Content-Length: 1318 Keep-Alive: timeout=5, max=99 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1 Response Error Headers: Environment: UNIQUE_ID: WabkjcTYa6iga7y800KGZgAAAAA HTTPS: on SSL_TLS_SNI: mellon.example.com ---------------------------------- New Request --------------------------------- POST - /mellon/postResponse log_id: (null) server: name=/etc/httpd/conf.d/ssl.conf, scheme=https hostname=mellon.example.com port=443 pid: 21593, tid: 140251630954624 unparsed_uri: /mellon/postResponse uri: /mellon/postResponse path_info: /postResponse filename: /var/www/html/mellon query args: (null) Request Headers: Host: mellon.example.com Connection: keep-alive Content-Length: 15654 Cache-Control: max-age=0 Origin: https://rhsso.example.com:8443 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Content-Type: application/x-www-form-urlencoded Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 DNT: 1 Referer: https://rhsso.example.com:8443/auth/realms/ipa/login-actions/authenticate?code=qv8kqt2kFiT0YBmG8TIzcNzgxfFp6q_N15M5pS931Eo.caa7c606-3404-4961-8af9-ba27345d1f7b&execution=10aa0b63-d5d9-4960-8ad8-16720df6fc8e Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.8 Cookie: mellon-cookie=cookietest Mellon Directory Configuration for URL: /mellon/postResponse MellonEnable (enable): info MellonVariable (varname): cookie MellonSecureCookie (secure): Off MellonMergeEnvVars (merge_env_vars): (null) MellonEnvVarsIndexStart (env_vars_index_start): -1 MellonEnvVarsSetCount (env_vars_count_in_n): On MellonCookieDomain (cookie_domain): (null) MellonCookiePath (cookie_path): (null) MellonCond (cond): 0 items MellonSetEnv (envattr): 0 items MellonUser (userattr): NAME_ID MellonIdP (idpattr): IDP MellonSessionDump (dump_session): Off MellonSamlResponseDump (dump_saml_response): Off MellonEndpointPath (endpoint_path): /mellon/ MellonSPMetadataFile (sp_metadata_file): pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient MellonSPPrivateKeyFile (sp_private_key_file): pathname: "/etc/httpd/saml2/demo.key" -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHUXILDtuMxyFP 6OW81hqfj9z+gC7sxR6U8bRPI4gFiNjkNF+4HMyrKtVtfwA5WJ1tJ4IaZbg4fGap iCcWQ5tjjuZ7CpZ6XLekq63OGrkgZxNm8L+AjvREv78iR4Myc4x6ybN1LwQnV+oD F0i9SAEPnfJCaCA+1+DC3RKy7+xtK3SVimUWwAHmkNU6N0wCVSNYn1KgnxBSCPTx 2DifbyRl21SiokCfLSnktE++WBzRzldSxzMrkSfFoS7/uT232tbdw2NsJmHGLRGk myBrEC6BVhQ746NM1cLDtrOots7bNfZ3yfb3W1tpoKVOhM2se3OTKdidAQsaGlVu 7QIhBqinAgMBAAECggEBAJtU662WfJ9vqJRgCnpp2QG02iM0vl0jGbw1ybFLHXxC s9TUxCv1tcNHdGEK8p++YaFpgskTsMfEmzVPuDZvpa+m9BO7op3ll/CrIp5W0SNh cQtuX6/OuKrDTC9oz+QHjNk8S7DtXS1UJDkYckWg0cLb8qqx/z86eh0isKnmtLg2 H1+6L6mB9fcZldkcrU+kXT+dcDX85skMZAeBsrG4yaoX26AzVl8lEl2rJAQvpxj8 5wGBC4riWY6TzMYiCjcS5JfZIlbhcZe61ej3A48NVBSKCP1XKo0xbKuOHIQuMeeW wSaboBwRzJ9JdTXlq5UWfLvmjXDc/HCwk/N7cj021uECgYEA5KkQr3cSKrMIkYoO H0Vnkw1kYYGUjtTL00Nfdkv7uGMF122nyt1ND0gpdS0jgNx3LSEam/gY35UWEjGH i8DGD04k8pvKDUsl8cuYPcC3oce1lLCGAnw+aHPC5wtA829CLOLtBfxXIhVAI0tp ECosD/A63/m1LC19XolOd10/PC8CgYEA3yZChEYZupDGJFZltiy0ZgUarvD2Ss4N QiRVR+CmpBrOKZdD8q6uUuqWQN9Rw7kXm8LxAPYwiIDVjxjYALF+j7/9Q1oZyKuv eHJdMe4eRUeqeaIKbxnFfKwGZ5rj97jwPrkUCxio75KZhpOcDHMSgBCBtzW0XIZl gTeQYOshZQkCgYB5TK6LRnEesabj/gaL1DejrMEJcMIsGvqdceocSSaZo/4fUA5o 8YjFiJRlkrJ403ttN1h0UOJxCReSQzASlQr8Z4n2IWrILotMf6Kdb7R6YAUVgac1 fk9k/bPw+OlVujmyshbmy/w1GmzRzFlJt/Vz5w50bnULoH4XPmOfspmvBQKBgBcJ rihVzGY0eCBcQxfxuZYmxMB25BaI+1luwtcu3EVo9wvYMA2n9xtcWLLN23UncMaF 87ezswMEugeR+wrnSDezDISdkrfi8bSvqetzt/BTG8h+8DDUKk1avTaJCSwUDcmL 9gPHQfmp2uvH5X5riudpzNqLUtmSjnwurlszKzlxAoGAR8STlDJhNph+p3cF8k25 ydT1kypxnjzVG8CAV5/h3dUmc7j7gyV8NlWZfWacxMZWOBsrdVh0zhMNUPiLJaGd I1isOkmiN9JFYMMhHSnhPnTCIjmu6uBLxf8wotHAvzWOJPV7lUZbw21KIN3DS79F sGZ2QzGYn4inHG4UHClhZxU= -----END PRIVATE KEY----- MellonSPCertFile (sp_cert_file): pathname: "/etc/httpd/saml2/demo.cert" -----BEGIN CERTIFICATE----- MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= -----END CERTIFICATE----- MellonIdPPublicKeyFile (idp_public_key_file): file_data: NULL MellonIdPCAFile (idp_ca_file): file_data: NULL MellonIdPMetadataFile (idp_metadata): 1 items [ 0] Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress [ 0] Chain File file_data: NULL MellonIdPIgnore (idp_ignore): MellonSPentityId (sp_entity_id): (null) MellonOrganizationName (sp_org_name): 0 items MellonOrganizationDisplayName (sp_org_display_name): 0 items MellonOrganizationURL (sp_org_url): 0 items MellonSessionLength (session_length): -1 MellonNoCookieErrorPage (no_cookie_error_page): (null) MellonNoSuccessErrorPage (no_success_error_page): (null) MellonDefaultLoginPath (login_path): / MellonDiscoveryURL (discovery_url): (null) MellonProbeDiscoveryTimeout (probe_discovery_timeout): -1 MellonProbeDiscoveryIdP (probe_discovery_idp): 0 items MellonAuthnContextClassRef (authn_context_class_ref): 0 items MellonSubjectConfirmationDataAddressCheck (subject_confirmation_data_address_check): On MellonDoNotVerifyLogoutSignature (do_not_verify_logout_signature): 0 items MellonPostReplay (post_replay): On MellonECPSendIDPList (ecp_send_idplist): On enter function am_auth_mellon_user enter function am_handle_post_reply Loading SP Metadata pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient Loading IdP Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress SAMLResponse: https://rhsso.example.com:8443/auth/realms/ipa https://rhsso.example.com:8443/auth/realms/ipa 8OlYuzdYW/pu0OoNp9XFxRi1EETiNMZkgUpWOUJ/dG8= IuBZQ38BqtUc2Qbyy9BFj3x9BCfDQcoEoGFgOa//GFEwzOxunU9OxyZiod6cr1Z/0WgPeabX1GHJUwv//kf22FA8VLC1afErY4Yis9eUQUFUETyFRoFHjpou/rGz0NsEw4y8nTPN6p1je6jDM7Fo5cfSY8og6MY4rUdKOF2/rCCoFRJUC/UIFf8mUmOh8UIUNzPZqDc02Rw8nmGP2eHhHpWjDBalmzt+EL66tVqco3kRtQmPSIomkkJGh9vMGmHnu6n3k7I6SX8/h4bzuPurFB3eGhlxn9WKMQOdGphq4KKssij6yyZq2cM2fFLvZkrZSfLsDKjrpsf2YLC+Cmy5Ng== R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= oCD3moPiSZobQJCBgcFE/63qvoygunNFZdTYLc23HdOwj/ZqtQ6JgZI7lpefSdrq5RH61eMk/Gr4mtHmBOg5v06gu+Eh7d01bYlJYspwIf89ibxZS2/xGxnj0iqzJ7jVfK1e+WBnMhNdrwUaKFD8d4RmsoLH2kjoPgYow7gkqW1XVknVcfVXnLFiX8UqHITmVI+sr5v6k2/D7ZdMf6O/2/9ehoIxUSCkha3ZPKVbTJ9cAs6UEKBlR2ZqdY66aRnKfClyCueKu2+KT4k85VusYQYK5ltkbVkHrApjfIGtQlXgWSi7Gh+2q4MBh+U7c99Ynp7KlDtHXoBbup8ApbYE1Q== AQAB G-e292fc24-74d9-4979-9f81-2c26d85174de https://mellon.example.com/mellon/metadata urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ipausers openstack-users view-profile uma_authorization manage-account m5GHJo1Za6Aejg0O7ZpCZleaNS45qzST8/0qFORoGjfLyNm0+lcquA7L2j/puCuVPWJ2jS4Y8P62iWYbhnATo7IPO3srpcfrAZaPtVlDKVp0ra8knx73h7NNIrbkWS63TZZNRh5OU5MgJRShHEpKq8fbRanRlq1iLODveKgw64W3XYr7vXbVT26/2NrrA7bkPLbK7YUyglmYDHN9PUbEKxKwQiBLnKNvaQgvBZaBPTzYTiBidM1Jllp/mj8QzfwiXCIAS/28iVmDCmjkSN6j33nDDLYU/Cq3rIGOWI8uQiw2oh2PlZi49T3400ffz2L9Tucdm/kjkTbhwWB3b5A1YQ== 9agVoiDjgNINUO8G9L+D2vodXqbGgFyWfz2qmHTMEZLqmacSqNsX4bzyS+jE1yA9PR4vZXJsc8JwyQrFAyKcokbJHlIbB+BSUHaqd/5qH6BbA+mt48Er9aExwwEC0IHloIo8pg5WuNZ38+RATP5OVw+du3ZUQ22GWv918YaEOb7i1n36eNYiP9Fv+fCR6jtRaZBqFf/vvROv6ye1dVfDOhhZfflVBbhRKUTgng1eFIxuz/+BG8v+QnN1Hzo+4OMIgDxb4LuS9LSgJBpAyb/nazSJ6Qzmi1DNj3Mp4co/1iMVTdshF1gAOUBm0DWxCnH7pMX48VLMOqbhR1YOQrDUsqPBf/wFFbJKItUXCHi8U29tJnUaDfjfOMA8RRsfZ9wZ8mt3xAmeR+13VcZWgvTYD4Cjw8dXmFqQ5D71HtBGaVJzd9YCxsEDEMAmtkyeLrkBDQmGb6Jl5kR2WV22ZZcz885aIGwUYpG7u7K54O5/Y1s7dtKq9B7rnDv2yhNwm2LSlF59irPPiqfevM7B8uNuMXe8MVDde10f+d+tGyn4MdP1cim94UVThq0IBciiz28NMQBiI+q8tPmgCAqn3cgRx7u1hWZGf8q+v0bh5Db1igKrXzCNbOpD8kCkKv1nQEckilp8/WZjCH9LC7cLDXHfKorAWcy2qr6g/9XMdQcISXFGs9CZksXIt7lQl1ZUGrDKT3ejuWl9+kOD/H/ryCru+CbNl8UvIkPpiXRtPlYoaddhOqdfPA5xDdRpEbxpLjRQEOYFF1Sygub1S300ZJiOKa9py9bbsSSexBLDaK7qCH5f+tJt6fCLUkb9Eq0NCbwNQ1BLgc7Uhvhtb9+V4sSH0FQb7RxO0BSmaG1ZESCmTkwOra1BUgIISBQWI1YauzgeczypP3yFBVRXCIMxuPqi74hwwxg36VUc9JqaiUqXt3BFuHEdVMzkOdYuQ8FK6vxl2iV37oiG8yNbf9a32H2yR3v3Mcpz9ybicUKNTjSCJNP4ixCc9/+O8FdHwVIV6GcrPhlTbxjRdih0tWodcGYNqWt65YBNYsyExpOW7bS/nVaov/hMmWCv9FSg8/KK/ipAQxz2E/9V+SH5fv0KLfTQaaSy13XYBINm3xNcatr8xz6WXNkmf8AeHxQq8bSl/sAOeQnjn5H2S0s014l3ZfZ84RI5rI2bY5k7xWbnoCFr6e1jcA6UeKHB4zqes9azGxZ4A48VND1lNFUKt5XSxADLQjEH0nGxZczJMeYzHkrVqiUgtlPVxgDjm7TxfJDJGPumprmRxU3MYY8fr9E+3CCwAz9WJOxFdu1G/4cEo6zkMHhSmDddmSGcGGALbMFlMD0g0/o+P8GQ3pVEXd88bUH0U1Joanp0mJ9Tu6gS5LatPhAvbD6ucWKDvHXqRKYWy/N7txfSBrYbWkPcZrGesH3bVd8uWMgH61yg30Uq+K4/L9u9Wdtlu92aM/NTjNERGyXdxp+ZYrM3MEthnW8Oygt3/XI8stzml+HmAFvoDgpjTpkbY3prdu5Y+2FMCZVrq/qskpbT3n9aLjLnJVASzZUzwy/SuqzikH45Rstdm1himRksdG9cg2CIpQktXlD5cLfkTCfBuUYlr+MHYjOu8WMBNCXDeNdRgC9v/M/bqalQtOfGYlkK9NPpB8Ky2cKtykWdLDWdefbzXHM1Y2LEl0+ms+b0oZYe//61u8uvsjn5OpQ5/aNTM+ztQ5OsEPzn0skAkJONz6rD3YSZJuEp8pWHHEZkT4pF7+0aXiEJZfOrWSJPM7NgAjXIK93qomjZjHl6s3JodJzdycFSWeLdQjrIatu2PKhm1cSlwfhVhuumh7hMnDOyTdWNTgO2QH+y0ahudAviKYzHRIS9ad2103sWByxpRH4vRtVRcCfWPmQhtygn/3Gd2wHc2owXW9neUBASQJrNUIi3uDklftVAZwhMRCm6nGU5u2R7ywH1K2Ey6JNBp88bh4gn9dybFzYjpj9Ds+sCkqnm4B2O80x307vvKcQ/O5AEzF9ONFD5aypOVXlI1IAMSGraBA0oWeBeE8J6wTB0W3BSvF5PhuJYewu6pxrR9WEykkhJzN59fuOgjz6piJbgTjKSD1LYf4/ev1MxkfPOj611CF7XhhuZW94/3i0SCFTdkJdTXS4oepqBEsVYlaWGCg6Sh4Hznavz+LkeR3Z5aDc4KJfa2jTESxmFkIFgSglDq8TA+1XwCZo2mze2SjpPLEt5ZiIpX4F+RHiPZqikzshzVAeHoKH5rGOUx9/os+Fbvt/EWdpLkeMDRAGC4ptx76wBfrPJlH3k4LDBy8O7iIuwkdQ/XuV74wZSneW+jqpN+oUX+UcCnP6Hu8lZW/7Ip61i2cG+uD1hv0W/YF/9pomBQ2rEQ0hJVElJFpzpz38HriUbRkPiZyn467xec3GFm+8f9XINX8bcf4gmauV8f1hdfESegw0+pMdYFE6a62sWLJixCdOiZbkmkYXRiO7Bow8cCHLj8WKHe8EaUWuVOrJGvw8z+k5cYY0Opg6wG8bKN3Aeg8NFxr5RC9RFKt+3sYza28uCma24G+yHVPl6b4X04+q28oS6EmxW20VTz+uQGjAKxwB64aNr4lj+dqY41ZgLPVtrcrUpNtdtiqMKyIlyMVkNh24xEjCSEJ9RFDs1K4/GJa+1v2eM9QPA5WsN4kMIx5AgS1MIgYIL98tMH3CbXOjQTcGL2RbJDiICUQgXXLNWe+dDWfE1Lc8TGIHRZ6GmrYgF5yQx7PL7y2ZQblMY6nqEKvNPXcbVCSHWu0B15ZfEh/tpcuPWIU04Jq3KuVeEzJ/l3OsE+1jb9j+87ZjON+bezmxv6MHeziSKVoI2GJO0KL/HPTI/5RAMBVDdO0AkgLpDiatnrBvCUFXsvTVZt+h+fqQtjoTxaMmdlsK071RHqk47/8jXx1uFfA8LNzZsdZzqrQJXqh0paLmx9Sngki+TRgPfCzdpU2RNYXqn5qvaEn2dMCWZrIUMCHnHbNNqc4M0YIw869Nq/d35Wc+BDYDdDstkYaVxIV0oQIHaFb0Ewb96383kRJCKC1Gl9NIMwlxC+uJruXN3ApkyP731Mwkq7RX8sv6NI5h3UYhEwrlmvtnNlQ0S01W7qhbMfuEULT2sHc/NvFSMCtZAa2ipU+tSiaBpmElpZGyKJByF2XrxQDly84koxKQIR+vg7tabaRKQL4aIJQclRj1ewVei4iTE8NMML+ys4sqHYuH63e6iO6lBt306j52lDNBIV0ErLpLc5gDKgpfn8PbKcxkMc8v8pqCnZ48MVseNSRgkWKP2y5K6sZ6j2aT1i7/WHl3q0X6y5gTvDKmhKHzdEXR7cAdAfNZouh60dfSpGuN0y4cDn8y7nvzzW5w2TfAFV1SvwaBjIKqKMM3o6ZlEasqvcn7SwWtmViPJtlXvYNnM5idT4mR+r5SqfOqF3AQjKVG6fkaKVrucmZtWxiLp93CHdv4zRafwXr9RePjgeR9ix9cnb3PH+qbGIxYLt13gYpwj7eC7kYWKkHbVw0QLf6zm8loXFvXcQhnYbDb0xZDJdoHkBSBiEyoEQcr4Ps86XQHOJX1ez4GQ5d+fuNTkzoI7mts5sV/HW6kfqd/FMfHYH5D//2hYaluwhHXNhMDqHP0o7vLcg3zgRkhNV/csWcA4Vp8v2e7RE+13oLmREbtRa8mZmqQilAWzTjQAsM8Rp2CcoJGfRrdnZCS3t3ukrSNqJEyHjcreIRee82U+hhrdRsoj6kyVlLuhfv3PiNHHO7ockdwKPW/DNxKGii4qUCpkhaSLnsepXCWDk3hZLsS52WEqmNwA2x1y+KYsMiHUh7kYv5iSYWRyjY/eAWAS/dDcdzg7W0JFiefOb4CiHZvLsfv96GOqu6tQB2pb6nkhVH2CMRsCiZK0OqhII+AmB2RMqpbxvw8l5Vml9G4y+VLTlhwIp92bZ8nHI7j3khqn8dUGf+ZHAInt7XujFlP/p9uy2oV3dEPTPVZGxuQJgfNbGYYF5pUNotp831FL4MA4sUdLXa114JrRo9bWnDOXBrR+04ZUtpfaBehmKxMIVufy81fe2g7heBd/yoeRAkZAw6oUaLcgAWcGAUdR/Ozy121yCdroeeLqbm2nPlsGwDYCSwl0UhZKpYTAs16vvyata55LRPWjsHNs0tPmYtZQk/IOaV5q61Wt2ehsuqXnGmEkgd7Dr5kOokGjV0PRpC1P9NQ88WWeS/MeMGX1e0Ac8FYkJxpNwcdlwBwaKIMrOTrmOfuEibORIzTlh9mJQ99pJoL+FrE0S6Ye6UjAuwixvAgo2rY1JzeXkGv+x9hxBwtYkgY17snKT3lgv6aJxX25rx44EfqLnkAKqI3P/ZCCzixMcdGYILxB/7JAIAaoSNloTqEDyHUdpVpY/4wPMvVajn2O0WPEyk6CQJbzesjuF63Tz1igMqRY0YdJu9J1xRPqOo3m//1fS1rW3pofMCPY1FUCLQShL346t7u0OijOjSq/z3pfdWJ6DuLPAgWO4iLjAew3Qsch/KcDrSayEmwKo0fHQJEQr/EqYSebmRxUDqNkewEoDt85qZcLMu9kofbJRx7yItS/9Gbii1xJV1v9FghE9uAz9KM2DBxlt2oN7JbxbJOMm/h5aLLHTZpw4vm//GpUlZVrZlNJhJNw2QeLgq+zawDOulgUpuU+/G46H0uejnb4UHo/jwdOpEXnY7T5Omm9g1YPS22nzCxA4ile1j0fouRXfPpDcFkKsiIOj8feXoZahJSuxCTSQLrfnPpoacqtPCXx9Ons4V47TN3MsQvv/JBUxXNUhJPTI2d+SfZWHaqg8FoiLWFfRE8h24lsckxwYB6t1o80s4sQfqzUOC3nMl2/t+9d8Wvd8aa/S01lucD1Uweg4X6znzF+uNBm22FPaXWGKJar1OJeWxwDi7gx9Skco+rXWPNr+EWW7gLFzYaFMUdCwS58bGoMR3YCM5UnKUlVgVrlJzIKY1MvRdd+e2li7+LVssNkh2uzLBIlRH329rGLbkdtoRJ+3w67D8enw60xvJuh/unfEAA/81CfN+VH10yV3RvHtCgd+eFeUCh4wB2EKGXHEzufuvhEzzqJuu+nJwaKwAYzJBM4T9QoxZzmM5MUmqfc9zhaWWzYty+oBrca79lp+CL64EB+TLIKxf7WWt12aSHye9rNrSxSnkGR2b+plQWKMxN4lQWX0CgZDqim9BuItWgBNWUwMFwg9U0VZ6v+B3uNcAhkQJVSHurCY/ADkhkqU+j2HnNGupwAYNZv9W5/UuAD8yFpuP+JhtnTz+TGQzuy+B/cZcibZhFG2i5bAqrjxPGJQT/JNxQt8WkipdQfUdCNFhdh4HwD44RxKtBenpR51XXo5cbaQlhETFmaZzED9qf73OI5N6bPFv30WcqZpWEerWVT9hz5OuwTkIqKvtQgzBd/4qaAbMdeYb2F/eTxytKWYGbnarCcrasdkQS5IJfLYb2KyTm3hMc3EM+3/kF5OGszQQ1EaPQcsyI7GVmogRfNbitQ+qPTDjGJnotwwc5aLJpIa8pVeiFFCBfsEdqA5qIbS2NBc0A/Uv8e53L+fZy0uPpZfq4F7GO+qU/efa0+aVvW/aHJcyFJXvQHb2h0XjP7ljbfQB/5ypcP5cXS12at60w4hs7oJDfcQ30JHmeMIiZ9t2+bQwOn+M22KiVIHbjNRGUPfs1jwu5Cid69pqcRtSg2fxfpAH4h5B5PHybmeo2Q5kMMS1FS2ZUirwwfY9fk+q0pSLwq5fMfUooVxPXBgqtPYY4hMWU6A8a9cbUNsJZBSZi5mP1+1xRGAQ62xUCqOgGJt922/5YI2QNf9cWcKuFlsD2+RYLNgFHZK7OzCns8t2VaQZw/6rFURZKW+wfDY+bzYjmn/FgLHOGp5YxBhzwvIcOEAltqC3wWF6c2ehnZGUH3E+JVvaB8mDG1bWf9Gxby9cjKSIUcIS1CTxfboSSx3yYhr8qONLsYKkNZoVnAHks33M5RduRv8XVo51PnqVp7GENpyy2Q3tmQoGAhLz401vI3jhDGhXB7TFqCnQfN4VDnb3z7WuFVGz8P4iMdlIs7eDOM0NF3TgwFxoK+QdQh2DYP1f7hx6kXXON+/Bwo1II8VewMBPyS+/vRbD+zihEoVtuEG0k0h1oyfXuavmkzCgXr17efq+J8FC7FRT4npwUbqSyEOGW1fXtGYqUGeirL3SVGshmehujovVTKkJ0g8Vy/t5i5JKGXxxfhKxH151IrTIZ5oyDbL3ZbJ+b0OrgOS4HDPDBKB5RYqv8N5s8Ylnk0piKrk3nvnYA6j1V7Xu+HvwmfdrdpTcfWFMmy6K5cmxZWQmmS6uNUWBrfa303uMJmAjoZy9U/9qg1H/G+/TcTF7FhUZ+FdAUUGIns94Pjl1D+qeXA61TDRLtV40QHyKgcGXO0GBMBlOjIxdhNDdDMoqt9+xFoAml8Ue89hCYeDXDtPg+/T8a+/C+5CQ5FkD7ZEPAvaEYYeWUGC/HPAdVohtjnhTIigCqsil6+AxOFKaxV1R3Y2H35X3QDnCLow5MNfb0ck+7RtkOhIMqaDLWbF4/OcmNuW91Jchk6INIzLiwZnlSrBIK8He9SrhoE0Tz4+5dAqdvXCZu8IkWg31fxsLDutI9Fa0iFqaSrda/YVIEyp8qlMy2V3GEsOjhX/sPth4YAhTg2SgPQ+ncGrUCHtTCMpNJMqScJbeIWX2NWLsLOiSALc6WCQgALbm+AkpXFxCrxBcX77gENU7bMiSOqW45k1bHj0iPS737CMQ8KRMmdhZb+KNIbgk5HJZRxhPNWIQEub5tjyIxw8TiPN0IXsS+pLcTWFur1I11nplJMdIaho6OOycn/ww6uMsU/yTvBLDPTtHKK4ZVQIr4abDySfX915cjfGx61pTsH5Hs8hDFoqmUh3UUr/8NF9aMpuqZhuOYq2hi5DvIMiS8HQYGzW4GO4wE4fP2/nRZXecYOvVQi+FWTKwXtA3OduMotKj29QgCvRYXMRIyXkz4Mh+UvfUlEYtvy9NZaNboHTtoIJemQ2LCakIFxp0zk69sMsFm5c3hBgCxIXKVmJNCrO565/VA1kuP1qoJhekxBvyo85qSJfjCE4UsX8HcD+HXZqmYDkfbskcLwQkIzYLQus8+oWtRSIrOr1iGR1vTktq1WtRVZ+Af+alslzBZLOBcaNqx0XZRBdjZ4m2pSvlKihSn/9Za0iASX/oQcUyQ2xV6Bw843wONbEqB8ulXnpLI2RVvSdzBpj+yopD+f9TXZJf/qyNNa0sb+q/VTcG60I9xJ+S/pFnS/zIlHajuaS2heekN36nNP9i9q2nrBjMRb6bMF/sL/VBTACIsBxVaI5X1buB+8GqyGhUI0mbohmA5EnM7xOkcwGEdAi9pT9Zku1KWyk2g1nNbml1Q= am_new_request_session id=9cf3ebac4e542827e276dc064ce8c4e4 cookie_token="Name='mellon-cookie' Domain='mellon.example.com' Path='/'" am_cache_new created new session, id=9cf3ebac4e542827e276dc064ce8c4e4 at 2017-08-30T16:15:23Z cookie_token="Name='mellon-cookie' Domain='mellon.example.com' Path='/'" am_handle_session_expire failed to find Assertion.AuthnStatement.SessionNotOnOrAfter add_attributes name=groups value=ipausers add_attributes name=groups value=openstack-users add_attributes name=Role value=view-profile add_attributes name=Role value=uma_authorization add_attributes name=Role value=manage-account [APLOG_DEBUG auth_mellon_handler.c:549] The current LassoProfile object doesn't contain a LassoIdentity object. === Response === Status: 303 See Other(303) user: (null) auth_type=(null) Response Headers: Cache-Control: private, max-age=0, must-revalidate Set-Cookie: mellon-cookie=9cf3ebac4e542827e276dc064ce8c4e4; Version=1; Path=/; Domain=mellon.example.com; Location: https://mellon.example.com/saml-test/protected.html Content-Length: 258 Keep-Alive: timeout=5, max=100 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1 Response Error Headers: Environment: UNIQUE_ID: Wabkm2fzHRm5EyVgPZnqEAAAAAU HTTPS: on SSL_TLS_SNI: mellon.example.com ---------------------------------- New Request --------------------------------- GET - /saml-test/protected.html log_id: (null) server: name=/etc/httpd/conf.d/ssl.conf, scheme=https hostname=mellon.example.com port=443 pid: 21593, tid: 140251630954624 unparsed_uri: /saml-test/protected.html uri: /saml-test/protected.html path_info: filename: /var/www/html/saml-test/protected.html query args: (null) Request Headers: Host: mellon.example.com Connection: keep-alive Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 DNT: 1 Referer: https://rhsso.example.com:8443/auth/realms/ipa/login-actions/authenticate?code=qv8kqt2kFiT0YBmG8TIzcNzgxfFp6q_N15M5pS931Eo.caa7c606-3404-4961-8af9-ba27345d1f7b&execution=10aa0b63-d5d9-4960-8ad8-16720df6fc8e Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.8 Cookie: mellon-cookie=9cf3ebac4e542827e276dc064ce8c4e4 Mellon Directory Configuration for URL: /saml-test/protected.html MellonEnable (enable): auth MellonVariable (varname): cookie MellonSecureCookie (secure): Off MellonMergeEnvVars (merge_env_vars): (null) MellonEnvVarsIndexStart (env_vars_index_start): -1 MellonEnvVarsSetCount (env_vars_count_in_n): On MellonCookieDomain (cookie_domain): (null) MellonCookiePath (cookie_path): (null) MellonCond (cond): 0 items MellonSetEnv (envattr): 0 items MellonUser (userattr): NAME_ID MellonIdP (idpattr): IDP MellonSessionDump (dump_session): Off MellonSamlResponseDump (dump_saml_response): Off MellonEndpointPath (endpoint_path): /mellon/ MellonSPMetadataFile (sp_metadata_file): pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient MellonSPPrivateKeyFile (sp_private_key_file): pathname: "/etc/httpd/saml2/demo.key" -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHUXILDtuMxyFP 6OW81hqfj9z+gC7sxR6U8bRPI4gFiNjkNF+4HMyrKtVtfwA5WJ1tJ4IaZbg4fGap iCcWQ5tjjuZ7CpZ6XLekq63OGrkgZxNm8L+AjvREv78iR4Myc4x6ybN1LwQnV+oD F0i9SAEPnfJCaCA+1+DC3RKy7+xtK3SVimUWwAHmkNU6N0wCVSNYn1KgnxBSCPTx 2DifbyRl21SiokCfLSnktE++WBzRzldSxzMrkSfFoS7/uT232tbdw2NsJmHGLRGk myBrEC6BVhQ746NM1cLDtrOots7bNfZ3yfb3W1tpoKVOhM2se3OTKdidAQsaGlVu 7QIhBqinAgMBAAECggEBAJtU662WfJ9vqJRgCnpp2QG02iM0vl0jGbw1ybFLHXxC s9TUxCv1tcNHdGEK8p++YaFpgskTsMfEmzVPuDZvpa+m9BO7op3ll/CrIp5W0SNh cQtuX6/OuKrDTC9oz+QHjNk8S7DtXS1UJDkYckWg0cLb8qqx/z86eh0isKnmtLg2 H1+6L6mB9fcZldkcrU+kXT+dcDX85skMZAeBsrG4yaoX26AzVl8lEl2rJAQvpxj8 5wGBC4riWY6TzMYiCjcS5JfZIlbhcZe61ej3A48NVBSKCP1XKo0xbKuOHIQuMeeW wSaboBwRzJ9JdTXlq5UWfLvmjXDc/HCwk/N7cj021uECgYEA5KkQr3cSKrMIkYoO H0Vnkw1kYYGUjtTL00Nfdkv7uGMF122nyt1ND0gpdS0jgNx3LSEam/gY35UWEjGH i8DGD04k8pvKDUsl8cuYPcC3oce1lLCGAnw+aHPC5wtA829CLOLtBfxXIhVAI0tp ECosD/A63/m1LC19XolOd10/PC8CgYEA3yZChEYZupDGJFZltiy0ZgUarvD2Ss4N QiRVR+CmpBrOKZdD8q6uUuqWQN9Rw7kXm8LxAPYwiIDVjxjYALF+j7/9Q1oZyKuv eHJdMe4eRUeqeaIKbxnFfKwGZ5rj97jwPrkUCxio75KZhpOcDHMSgBCBtzW0XIZl gTeQYOshZQkCgYB5TK6LRnEesabj/gaL1DejrMEJcMIsGvqdceocSSaZo/4fUA5o 8YjFiJRlkrJ403ttN1h0UOJxCReSQzASlQr8Z4n2IWrILotMf6Kdb7R6YAUVgac1 fk9k/bPw+OlVujmyshbmy/w1GmzRzFlJt/Vz5w50bnULoH4XPmOfspmvBQKBgBcJ rihVzGY0eCBcQxfxuZYmxMB25BaI+1luwtcu3EVo9wvYMA2n9xtcWLLN23UncMaF 87ezswMEugeR+wrnSDezDISdkrfi8bSvqetzt/BTG8h+8DDUKk1avTaJCSwUDcmL 9gPHQfmp2uvH5X5riudpzNqLUtmSjnwurlszKzlxAoGAR8STlDJhNph+p3cF8k25 ydT1kypxnjzVG8CAV5/h3dUmc7j7gyV8NlWZfWacxMZWOBsrdVh0zhMNUPiLJaGd I1isOkmiN9JFYMMhHSnhPnTCIjmu6uBLxf8wotHAvzWOJPV7lUZbw21KIN3DS79F sGZ2QzGYn4inHG4UHClhZxU= -----END PRIVATE KEY----- MellonSPCertFile (sp_cert_file): pathname: "/etc/httpd/saml2/demo.cert" -----BEGIN CERTIFICATE----- MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= -----END CERTIFICATE----- MellonIdPPublicKeyFile (idp_public_key_file): file_data: NULL MellonIdPCAFile (idp_ca_file): file_data: NULL MellonIdPMetadataFile (idp_metadata): 1 items [ 0] Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress [ 0] Chain File file_data: NULL MellonIdPIgnore (idp_ignore): MellonSPentityId (sp_entity_id): (null) MellonOrganizationName (sp_org_name): 0 items MellonOrganizationDisplayName (sp_org_display_name): 0 items MellonOrganizationURL (sp_org_url): 0 items MellonSessionLength (session_length): -1 MellonNoCookieErrorPage (no_cookie_error_page): (null) MellonNoSuccessErrorPage (no_success_error_page): (null) MellonDefaultLoginPath (login_path): / MellonDiscoveryURL (discovery_url): (null) MellonProbeDiscoveryTimeout (probe_discovery_timeout): -1 MellonProbeDiscoveryIdP (probe_discovery_idp): 0 items MellonAuthnContextClassRef (authn_context_class_ref): 0 items MellonSubjectConfirmationDataAddressCheck (subject_confirmation_data_address_check): On MellonDoNotVerifyLogoutSignature (do_not_verify_logout_signature): 0 items MellonPostReplay (post_replay): On MellonECPSendIDPList (ecp_send_idplist): On enter function am_auth_mellon_user searching for session with key 9cf3ebac4e542827e276dc064ce8c4e4 (session) ... found. Session Cache Entry key: 9cf3ebac4e542827e276dc064ce8c4e4 name_id: G-e292fc24-74d9-4979-9f81-2c26d85174de expires: 2017-08-31T16:15:23Z access: 2017-08-30T16:15:23Z logged_in: True am_auth_mellon_user am_enable_auth, have valid session am_check_permissions succeeds === Response === Status: 200 OK(200) user: G-e292fc24-74d9-4979-9f81-2c26d85174de auth_type=Mellon Response Headers: Cache-Control: private, max-age=0, must-revalidate Last-Modified: Mon, 28 Aug 2017 15:15:18 GMT ETag: "4a-557d1c33a4519" Accept-Ranges: bytes Content-Length: 74 Keep-Alive: timeout=5, max=99 Connection: Keep-Alive Content-Type: text/html; charset=UTF-8 Response Error Headers: Cache-Control: private, max-age=0, must-revalidate Environment: UNIQUE_ID: Wabkm2fzHRm5EyVgPZnqEQAAAAU MELLON_NAME_ID: G-e292fc24-74d9-4979-9f81-2c26d85174de MELLON_NAME_ID_0: G-e292fc24-74d9-4979-9f81-2c26d85174de MELLON_groups: ipausers MELLON_groups_0: ipausers MELLON_groups_1: openstack-users MELLON_Role: view-profile MELLON_Role_0: view-profile MELLON_Role_1: uma_authorization MELLON_Role_2: manage-account MELLON_IDP: https://rhsso.example.com:8443/auth/realms/ipa MELLON_IDP_0: https://rhsso.example.com:8443/auth/realms/ipa HTTPS: on SSL_TLS_SNI: mellon.example.com ---------------------------------- New Request --------------------------------- GET - /favicon.ico log_id: (null) server: name=/etc/httpd/conf.d/ssl.conf, scheme=https hostname=mellon.example.com port=443 pid: 21593, tid: 140251630954624 unparsed_uri: /favicon.ico uri: /favicon.ico path_info: filename: /var/www/html/favicon.ico query args: (null) Request Headers: Host: mellon.example.com Connection: keep-alive User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36 Accept: image/webp,image/apng,image/*,*/*;q=0.8 DNT: 1 Referer: https://mellon.example.com/saml-test/protected.html Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.8 Cookie: mellon-cookie=9cf3ebac4e542827e276dc064ce8c4e4 Mellon Directory Configuration for URL: /favicon.ico MellonEnable (enable): info MellonVariable (varname): cookie MellonSecureCookie (secure): Off MellonMergeEnvVars (merge_env_vars): (null) MellonEnvVarsIndexStart (env_vars_index_start): -1 MellonEnvVarsSetCount (env_vars_count_in_n): On MellonCookieDomain (cookie_domain): (null) MellonCookiePath (cookie_path): (null) MellonCond (cond): 0 items MellonSetEnv (envattr): 0 items MellonUser (userattr): NAME_ID MellonIdP (idpattr): IDP MellonSessionDump (dump_session): Off MellonSamlResponseDump (dump_saml_response): Off MellonEndpointPath (endpoint_path): /mellon/ MellonSPMetadataFile (sp_metadata_file): pathname: "/etc/httpd/saml2/demo_sp_metadata.xml" MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= urn:oasis:names:tc:SAML:2.0:nameid-format:transient MellonSPPrivateKeyFile (sp_private_key_file): pathname: "/etc/httpd/saml2/demo.key" -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDHUXILDtuMxyFP 6OW81hqfj9z+gC7sxR6U8bRPI4gFiNjkNF+4HMyrKtVtfwA5WJ1tJ4IaZbg4fGap iCcWQ5tjjuZ7CpZ6XLekq63OGrkgZxNm8L+AjvREv78iR4Myc4x6ybN1LwQnV+oD F0i9SAEPnfJCaCA+1+DC3RKy7+xtK3SVimUWwAHmkNU6N0wCVSNYn1KgnxBSCPTx 2DifbyRl21SiokCfLSnktE++WBzRzldSxzMrkSfFoS7/uT232tbdw2NsJmHGLRGk myBrEC6BVhQ746NM1cLDtrOots7bNfZ3yfb3W1tpoKVOhM2se3OTKdidAQsaGlVu 7QIhBqinAgMBAAECggEBAJtU662WfJ9vqJRgCnpp2QG02iM0vl0jGbw1ybFLHXxC s9TUxCv1tcNHdGEK8p++YaFpgskTsMfEmzVPuDZvpa+m9BO7op3ll/CrIp5W0SNh cQtuX6/OuKrDTC9oz+QHjNk8S7DtXS1UJDkYckWg0cLb8qqx/z86eh0isKnmtLg2 H1+6L6mB9fcZldkcrU+kXT+dcDX85skMZAeBsrG4yaoX26AzVl8lEl2rJAQvpxj8 5wGBC4riWY6TzMYiCjcS5JfZIlbhcZe61ej3A48NVBSKCP1XKo0xbKuOHIQuMeeW wSaboBwRzJ9JdTXlq5UWfLvmjXDc/HCwk/N7cj021uECgYEA5KkQr3cSKrMIkYoO H0Vnkw1kYYGUjtTL00Nfdkv7uGMF122nyt1ND0gpdS0jgNx3LSEam/gY35UWEjGH i8DGD04k8pvKDUsl8cuYPcC3oce1lLCGAnw+aHPC5wtA829CLOLtBfxXIhVAI0tp ECosD/A63/m1LC19XolOd10/PC8CgYEA3yZChEYZupDGJFZltiy0ZgUarvD2Ss4N QiRVR+CmpBrOKZdD8q6uUuqWQN9Rw7kXm8LxAPYwiIDVjxjYALF+j7/9Q1oZyKuv eHJdMe4eRUeqeaIKbxnFfKwGZ5rj97jwPrkUCxio75KZhpOcDHMSgBCBtzW0XIZl gTeQYOshZQkCgYB5TK6LRnEesabj/gaL1DejrMEJcMIsGvqdceocSSaZo/4fUA5o 8YjFiJRlkrJ403ttN1h0UOJxCReSQzASlQr8Z4n2IWrILotMf6Kdb7R6YAUVgac1 fk9k/bPw+OlVujmyshbmy/w1GmzRzFlJt/Vz5w50bnULoH4XPmOfspmvBQKBgBcJ rihVzGY0eCBcQxfxuZYmxMB25BaI+1luwtcu3EVo9wvYMA2n9xtcWLLN23UncMaF 87ezswMEugeR+wrnSDezDISdkrfi8bSvqetzt/BTG8h+8DDUKk1avTaJCSwUDcmL 9gPHQfmp2uvH5X5riudpzNqLUtmSjnwurlszKzlxAoGAR8STlDJhNph+p3cF8k25 ydT1kypxnjzVG8CAV5/h3dUmc7j7gyV8NlWZfWacxMZWOBsrdVh0zhMNUPiLJaGd I1isOkmiN9JFYMMhHSnhPnTCIjmu6uBLxf8wotHAvzWOJPV7lUZbw21KIN3DS79F sGZ2QzGYn4inHG4UHClhZxU= -----END PRIVATE KEY----- MellonSPCertFile (sp_cert_file): pathname: "/etc/httpd/saml2/demo.cert" -----BEGIN CERTIFICATE----- MIIDDTCCAfWgAwIBAgIJALnqrR7yvGH5MA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNV BAMMEm1lbGxvbi5leGFtcGxlLmNvbTAeFw0xNzA4MjgxNTExNDlaFw0yMjA4Mjcx NTExNDlaMB0xGzAZBgNVBAMMEm1lbGxvbi5leGFtcGxlLmNvbTCCASIwDQYJKoZI hvcNAQEBBQADggEPADCCAQoCggEBAMdRcgsO24zHIU/o5bzWGp+P3P6ALuzFHpTx tE8jiAWI2OQ0X7gczKsq1W1/ADlYnW0nghpluDh8ZqmIJxZDm2OO5nsKlnpct6Sr rc4auSBnE2bwv4CO9ES/vyJHgzJzjHrJs3UvBCdX6gMXSL1IAQ+d8kJoID7X4MLd ErLv7G0rdJWKZRbAAeaQ1To3TAJVI1ifUqCfEFII9PHYOJ9vJGXbVKKiQJ8tKeS0 T75YHNHOV1LHMyuRJ8WhLv+5Pbfa1t3DY2wmYcYtEaSbIGsQLoFWFDvjo0zVwsO2 s6i2zts19nfJ9vdbW2mgpU6Ezax7c5Mp2J0BCxoaVW7tAiEGqKcCAwEAAaNQME4w HQYDVR0OBBYEFDBbq0pjLeMFPcBt7A++c90lSM5vMB8GA1UdIwQYMBaAFDBbq0pj LeMFPcBt7A++c90lSM5vMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB AFiIxqhW37Td/rD77N648feciigEk/GW4zsqxdx9MspnvSTfr0/lPPOaVhd/UGAw g+DwGOmqfisvl44wg07y+4T0NTDzvgkrT0ON5hyEBucFhSjPN+lhwWaH422URwUL cKTqkrnAk4Er4bSi1GhsV/2/Xv2ZYyJCcUeiwWQ2fEZXp4ke3IZPN0nYlajKzBTd Bv9YlynXKuO1hxBYDWQrrjpp1UZRKjJD2nLUsTi8oFuLhB/RwUMqXZ0nFuNoOkDQ XotXjsiL1KtqNW1k/oVtLwNP0trqqh9npWV+R3pDTckxIHQhOvs5VqQZANViH6mp YK53b9Bhr0TpIOKetFY68kQ= -----END CERTIFICATE----- MellonIdPPublicKeyFile (idp_public_key_file): file_data: NULL MellonIdPCAFile (idp_ca_file): file_data: NULL MellonIdPMetadataFile (idp_metadata): 1 items [ 0] Metadata pathname: "/etc/httpd/saml2/demo_keycloak_ipa_idp_metadata.xml" R2OGk9W0luNm_NtZbURWOrPlvFzSTDMimCVK5N1Mj5U MIIClTCCAX0CBgFeFdE9pDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANpcGEwHhcNMTcwODI0MTk1NDQ3WhcNMjcwODI0MTk1NjI3WjAOMQwwCgYDVQQDDANpcGEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgIPeag+JJmhtAkIGBwUT/req+jKC6c0Vl1Ngtzbcd07CP9mq1DomBkjuWl59J2urlEfrV4yT8avia0eYE6Dm/TqC74SHt3TVtiUliynAh/z2JvFlLb/EbGePSKrMnuNV8rV75YGcyE12vBRooUPx3hGaygsfaSOg+BijDuCSpbVdWSdVx9VecsWJfxSochOZUj6yvm/qTb8Ptl0x/o7/b/16GgjFRIKSFrdk8pVtMn1wCzpQQoGVHZmp1jrppGcp8KXIK54q7b4pPiTzlW6xhBgrmW2RtWQesCmN8ga1CVeBZKLsaH7argwGH5Ttz31iensqUO0degFu6nwCltgTVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFx8dl6RDle65q3IHIiGaL5fbJK5HxQiMXpk4N5riWQTP4g6xoTNAG4OFFUd4uRxt2ovdEdkbzhEy2lV4x626QdEfK5V9QKppupsTxTGA/4NMW9QCocAvFSpmYErmJIhfy6zzELoBK4Dpfcc3u1peHx2686msx6ExARF116d+5Xaps1dmPPy3yb2cCKzKbLhieqv+aLLrwz657ERUc4OnqEMEmmHFhHvPI7LRlS4AQ1/s1QlKcM9yqcu8WN3yKM/kuvZtZ0YTCSIl9W1b+I5v8wNoVFB22s7rfxs3DfJFaIImaTmRzaDX0MXgibEckrkigpO+anKe9B9z8CJdtlUHco= urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress [ 0] Chain File file_data: NULL MellonIdPIgnore (idp_ignore): MellonSPentityId (sp_entity_id): (null) MellonOrganizationName (sp_org_name): 0 items MellonOrganizationDisplayName (sp_org_display_name): 0 items MellonOrganizationURL (sp_org_url): 0 items MellonSessionLength (session_length): -1 MellonNoCookieErrorPage (no_cookie_error_page): (null) MellonNoSuccessErrorPage (no_success_error_page): (null) MellonDefaultLoginPath (login_path): / MellonDiscoveryURL (discovery_url): (null) MellonProbeDiscoveryTimeout (probe_discovery_timeout): -1 MellonProbeDiscoveryIdP (probe_discovery_idp): 0 items MellonAuthnContextClassRef (authn_context_class_ref): 0 items MellonSubjectConfirmationDataAddressCheck (subject_confirmation_data_address_check): On MellonDoNotVerifyLogoutSignature (do_not_verify_logout_signature): 0 items MellonPostReplay (post_replay): On MellonECPSendIDPList (ecp_send_idplist): On enter function am_auth_mellon_user searching for session with key 9cf3ebac4e542827e276dc064ce8c4e4 (session) ... found. Session Cache Entry key: 9cf3ebac4e542827e276dc064ce8c4e4 name_id: G-e292fc24-74d9-4979-9f81-2c26d85174de expires: 2017-08-31T16:15:23Z access: 2017-08-30T16:15:23Z logged_in: True am_check_permissions succeeds am_auth_mellon_user am_enable_info, have valid session === Response === Status: 404 Not Found(404) user: G-e292fc24-74d9-4979-9f81-2c26d85174de auth_type=Mellon Response Headers: Cache-Control: private, max-age=0, must-revalidate Content-Length: 209 Keep-Alive: timeout=5, max=98 Connection: Keep-Alive Content-Type: text/html; charset=iso-8859-1 Response Error Headers: Environment: UNIQUE_ID: WabknGfzHRm5EyVgPZnqEgAAAAU MELLON_NAME_ID: G-e292fc24-74d9-4979-9f81-2c26d85174de MELLON_NAME_ID_0: G-e292fc24-74d9-4979-9f81-2c26d85174de MELLON_groups: ipausers MELLON_groups_0: ipausers MELLON_groups_1: openstack-users MELLON_Role: view-profile MELLON_Role_0: view-profile MELLON_Role_1: uma_authorization MELLON_Role_2: manage-account MELLON_IDP: https://rhsso.example.com:8443/auth/realms/ipa MELLON_IDP_0: https://rhsso.example.com:8443/auth/realms/ipa HTTPS: on SSL_TLS_SNI: mellon.example.com ---- mod_auth_mellon-0.18.0/lasso_compat.h000066400000000000000000000042071410105036600176020ustar00rootroot00000000000000#ifdef HAVE_LASSO_UTILS_H #include #else #define lasso_assign_string(dest,src) \ { \ char *__tmp = g_strdup(src); \ lasso_release_string(dest); \ dest = __tmp; \ } #define lasso_release_string(dest) \ lasso_release_full(dest, g_free) #define lasso_release_full(dest, free_function) \ { \ if (dest) { \ free_function(dest); dest = NULL; \ } \ } #define lasso_check_type_equality(a,b) #define lasso_release_full2(dest, free_function, type) \ { \ lasso_check_type_equality(dest, type); \ if (dest) { \ free_function(dest); dest = NULL; \ } \ } #define lasso_release_list(dest) \ lasso_release_full2(dest, g_list_free, GList*) #define lasso_release_list_of_full(dest, free_function) \ { \ GList **__tmp = &(dest); \ if (*__tmp) { \ g_list_foreach(*__tmp, (GFunc)free_function, NULL); \ lasso_release_list(*__tmp); \ } \ } #define lasso_release_list_of_strings(dest) \ lasso_release_list_of_full(dest, g_free) #endif #ifndef LASSO_SAML2_ECP_PROFILE_WANT_AUTHN_SIGNED #define LASSO_SAML2_ECP_PROFILE_WANT_AUTHN_SIGNED "urn:oasis:names:tc:SAML:2.0:profiles:SSO:ecp:2.0:WantAuthnRequestsSigned" #endif #ifndef LASSO_SAML2_CONDITIONS_DELEGATION #define LASSO_SAML2_CONDITIONS_DELEGATION "urn:oasis:names:tc:SAML:2.0:conditions:delegation" #endif #ifndef LASSO_SAML_EXT_CHANNEL_BINDING #define LASSO_SAML_EXT_CHANNEL_BINDING "urn:oasis:names:tc:SAML:protocol:ext:channel-binding" #endif mod_auth_mellon-0.18.0/mellon_create_metadata.sh000077500000000000000000000051521410105036600217550ustar00rootroot00000000000000#!/usr/bin/env bash set -e PROG="$(basename "$0")" printUsage() { echo "Usage: $PROG ENTITY-ID ENDPOINT-URL" echo "" echo "Example:" echo " $PROG urn:someservice https://sp.example.org/mellon" echo "" } if [ "$#" -lt 2 ]; then printUsage exit 1 fi ENTITYID="$1" if [ -z "$ENTITYID" ]; then echo "$PROG: An entity ID is required." >&2 exit 1 fi BASEURL="$2" if [ -z "$BASEURL" ]; then echo "$PROG: The URL to the MellonEndpointPath is required." >&2 exit 1 fi if ! echo "$BASEURL" | grep -q '^https\?://'; then echo "$PROG: The URL must start with \"http://\" or \"https://\"." >&2 exit 1 fi HOST="$(echo "$BASEURL" | sed 's#^[a-z]*://\([^:/]*\).*#\1#')" BASEURL="$(echo "$BASEURL" | sed 's#/$##')" OUTFILE="$(echo "$ENTITYID" | sed 's/[^0-9A-Za-z.]/_/g' | sed 's/__*/_/g')" echo "Output files:" echo "Private key: $OUTFILE.key" echo "Certificate: $OUTFILE.cert" echo "Metadata: $OUTFILE.xml" echo echo "Host: $HOST" echo echo "Endpoints:" echo "SingleLogoutService: $BASEURL/logout" echo "AssertionConsumerService: $BASEURL/postResponse" echo # No files should not be readable by the rest of the world. umask 0077 TEMPLATEFILE="$(mktemp -t mellon_create_sp.XXXXXXXXXX)" cat >"$TEMPLATEFILE" </dev/null rm -f "$TEMPLATEFILE" CERT="$(grep -v '^-----' "$OUTFILE.cert")" cat >"$OUTFILE.xml" < $CERT
EOF umask 0777 chmod go+r "$OUTFILE.xml" chmod go+r "$OUTFILE.cert" mod_auth_mellon-0.18.0/mod_auth_mellon.c000066400000000000000000000207431410105036600202620ustar00rootroot00000000000000/* * * mod_auth_mellon.c: an authentication apache module * Copyright © 2003-2007 UNINETT (http://www.uninett.no/) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ #include "auth_mellon.h" #include #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(auth_mellon); #endif /* This function is called after the configuration of the server is parsed * (it's a post-config hook). * * It initializes the shared memory and the mutex which is used to protect * the shared memory. * * Parameters: * apr_pool_t *conf The configuration pool. Valid as long as this * configuration is valid. * apr_pool_t *log A pool for memory which is cleared after each read * through the config files. * apr_pool_t *tmp A pool for memory which will be destroyed after * all the post_config hooks are run. * server_rec *s The current server record. * * Returns: * OK on successful initialization, or !OK on failure. */ static int am_global_init(apr_pool_t *conf, apr_pool_t *log, apr_pool_t *tmp, server_rec *s) { apr_size_t mem_size; am_mod_cfg_rec *mod; int rv; const char userdata_key[] = "auth_mellon_init"; char buffer[512]; void *data; /* Apache tests loadable modules by loading them (as is the only way). * This has the effect that all modules are loaded and initialised twice, * and we just want to initialise shared memory and mutexes when the * module loads for real! * * To accomplish this, we store a piece of data as userdata in the * process pool the first time the function is run. This data can be * detected on all subsequent runs, and then we know that this isn't the * first time this function runs. */ apr_pool_userdata_get(&data, userdata_key, s->process->pool); if (!data) { /* This is the first time this function is run. */ apr_pool_userdata_set((const void *)1, userdata_key, apr_pool_cleanup_null, s->process->pool); return OK; } mod = am_get_mod_cfg(s); /* If the session store is initialized then we can't change it. */ if(mod->cache != NULL) { ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "auth_mellon session store already initialized -" " reinitialization skipped."); return OK; } /* Copy from the variables set by the configuration file into variables * which will be set only once. We do this to avoid confusion if the user * tries to change the parameters of the session store after it is * initialized. */ mod->init_cache_size = mod->cache_size; mod->init_lock_file = apr_pstrdup(conf, mod->lock_file); mod->init_entry_size = mod->entry_size; if (mod->init_entry_size < AM_CACHE_MIN_ENTRY_SIZE) { mod->init_entry_size = AM_CACHE_MIN_ENTRY_SIZE; } /* find out the memory size of the cache */ mem_size = mod->init_entry_size * mod->init_cache_size; /* Create the shared memory, exit if it fails. */ rv = apr_shm_create(&(mod->cache), mem_size, NULL, conf); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, "shm_create: Error [%d] \"%s\"", rv, apr_strerror(rv, buffer, sizeof(buffer))); return !OK; } /* Initialize the session table. */ am_cache_init(mod); /* Now create the mutex that we need for locking the shared memory, then * test for success. we really need this, so we exit on failure. */ rv = apr_global_mutex_create(&(mod->lock), mod->init_lock_file, APR_LOCK_DEFAULT, conf); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, "mutex_create: Error [%d] \"%s\"", rv, apr_strerror(rv, buffer, sizeof(buffer))); return !OK; } #ifdef AP_NEED_SET_MUTEX_PERMS /* On some platforms the mutex is implemented as a file. To allow child * processes running as a different user to open it, it is necessary to * change the permissions on it. */ rv = ap_unixd_set_global_mutex_perms(mod->lock); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_CRIT, rv, s, "Failed to set permissions on session table lock;" " check User and Group directives"); return rv; } #endif return OK; } /* This function is run when each child process of apache starts. * apr_global_mutex_child_init must be run on the session data mutex for * every child process of apache. * * Parameters: * apr_pool_t *p This pool is for data associated with this * child process. * server_rec *s The server record for the current server. * * Returns: * Nothing. */ static void am_child_init(apr_pool_t *p, server_rec *s) { am_mod_cfg_rec *m = am_get_mod_cfg(s); apr_status_t rv; CURLcode curl_res; /* Reinitialize the mutex for the child process. */ rv = apr_global_mutex_child_init(&(m->lock), m->init_lock_file, p); if (rv != APR_SUCCESS) { ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, "Child process could not connect to mutex"); } /* lasso_init() must be run before any other lasso-functions. */ lasso_init(); /* curl_global_init() should be called before any other curl * function. Relying on curl_easy_init() to call curl_global_init() * isn't thread safe. */ curl_res = curl_global_init(CURL_GLOBAL_SSL); if(curl_res != CURLE_OK) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "Failed to initialize curl library: %u", curl_res); } return; } static int am_create_request(request_rec *r) { am_req_cfg_rec *req_cfg; req_cfg = apr_pcalloc(r->pool, sizeof(am_req_cfg_rec)); req_cfg->cookie_value = NULL; #ifdef HAVE_ECP req_cfg->ecp_authn_req = false; #endif /* HAVE_ECP */ #ifdef ENABLE_DIAGNOSTICS req_cfg->diag_emitted = false; #endif ap_set_module_config(r->request_config, &auth_mellon_module, req_cfg); return OK; } static void register_hooks(apr_pool_t *p) { /* Our handler needs to run before mod_proxy so that it can properly * return ECP AuthnRequest messages when running as a reverse proxy. * See: https://github.com/Uninett/mod_auth_mellon/pull/196 */ static const char * const run_handler_before[]={ "mod_proxy.c", NULL }; ap_hook_access_checker(am_auth_mellon_user, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_check_user_id(am_check_uid, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_post_config(am_global_init, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_child_init(am_child_init, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_create_request(am_create_request, NULL, NULL, APR_HOOK_MIDDLE); /* Add the hook to handle requests to the mod_auth_mellon endpoint. * * This is APR_HOOK_FIRST because we do not expect nor require users * to add a SetHandler option for the endpoint. Instead, simply * setting MellonEndpointPath should be enough. * * Therefore this hook must run before any handler that may check * r->handler and decide that it is the only handler for this URL. */ ap_hook_handler(am_handler, NULL, run_handler_before, APR_HOOK_FIRST); #ifdef ENABLE_DIAGNOSTICS ap_hook_open_logs(am_diag_log_init,NULL,NULL,APR_HOOK_MIDDLE); ap_hook_log_transaction(am_diag_finalize_request,NULL,NULL,APR_HOOK_REALLY_LAST); #endif } module AP_MODULE_DECLARE_DATA auth_mellon_module = { STANDARD20_MODULE_STUFF, auth_mellon_dir_config, auth_mellon_dir_merge, auth_mellon_server_config, auth_mellon_srv_merge, auth_mellon_commands, register_hooks };