=========================================
Writing a SAML2 service Provider in PHP
=========================================

:author Nicolas Clapiès
:contact: nclapies@entrouvert.com
:date: $Date$
:revision: $Revision$
:copyright: Copyright © 2004-2007 Entr'ouvert

.. contents:: Table of Contents
.. section-numbering::


Lasso PHP Binding Basics
========================

Lasso functions are available from the Lasso PHP extension.

There are two ways to load this extension.

This first one is to add the line::

  extension = lasso.so

in your ``php.ini`` configuration file, which can be found in something like
``/etc/php4/apache2/php.ini`` (if you're using apache2 and php4 on Debian,
otherwise you may need to adapt the path to your local configuration).


The other way is to load it dynamically, like::

  if (!extension_loaded('lasso')) {
      $prefix = (PHP_SHLIB_SUFFIX == 'dll') ? 'php_' : '';
      dl($prefix . 'lasso.' . PHP_SHLIB_SUFFIX);
  }

You can easily include this code every time you need lasso.


The first thing to do is to call ``lasso_init()``. Similarly, the last thing
should be to call ``lasso_shutdown()``.

Once ``lasso_init()`` is called. The smallest and useless Lasso project will
therefore be::

  lasso_init();
  print("Hello world.\n");
  lasso_shutdown();

If your PHP code is used in HTML script environment, it could be difficult to
call ``lasso_shutdown()``, this is not mandatory.


SAML2 and Lasso profiles
========================

Lasso provides the necessary functions to implement SAML2 profiles,
as defined in the `SAML2 Profiles Specification`_. They
are:

- Single Sign-On
- Single Logout
- Name Identifier Management

Each profile maps to a Lasso object : Single Sign-on profile is implemented by
``LassoLogin`` object, Single Logout profile is implemented by ``LassoLogout``
object.

Those are initialised with data known about identity and service providers,
available in a ``LassoServer`` object.

The ``LassoServer`` object may be created as follows::

  lasso_init();
  $server = new LassoServer("sp-saml2-metadata.xml", "sp-private-key.pem",
                              NULL, "sp-crt.pem");
  $server->addProvider(LASSO_PROVIDER_ROLE_IDP, "idp-saml2-metadata.xml",
                       "idp-public-key.pem", "ca-crt.pem");
  lasso_shutdown();

- ``sp-saml2-metadata.xml`` is the Liberty metadata file for the service provider
- ``idp-saml2-metadata.xml`` is the Liberty metadata file for the identity provider
- ``sp-private-key.pem`` is the service provider private key; used to sign
  documents
- ``sp-crt.pem`` is the service provider certificate; sent inside signed
  documents
- ``idp-public-key.pem`` is the identity provider public key; used to verify
  signature in documents sent by the identity provider
- ``ca-crt.pem`` is the certificate of the certification authority used by the
  identity provider.

It is of course possible to have several calls to the ``addProvider`` method of
an instantiated ``LassoServer`` object if there are more than one identity provider.

.. note:: Figures in the previously referred Binding and Profiles specification
          document are quite helpful in figuring out the message passing.

Serialisation
-------------

``LassoServer`` objects can be serialised into a XML formatted string::

  $dump = $server->dump();

It is then really easy to get back properly constructed objects::
  
  $server = LassoServer::newFromDump($dump);

.. warning:: The server dump only contains the file names, not the actual file
             contents.  Files should not be moved afterwards.


Liberty Metadata Files
======================

Providers are described by metadata containing an ``entityID`` and various
normative URLs for supported profiles ::

  <?xml version="1.0"?>
  <EntityDescriptor 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="http://sp.example.com/liberty/metadata">

    <SPSSODescriptor
      AuthnRequestsSigned="true"
      protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">

      <KeyDescriptor use="signing">
          <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
              <ds:KeyValue></ds:KeyValue>
          </ds:KeyInfo>
      </KeyDescriptor>

      <SingleLogoutService
          Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
          Location="https://sp.example.com/liberty/singleLogoutSoap" />

      <ManageNameIDService
          Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP"
          Location="https://sp.example.com/liberty/nameIdManagmentSoap" />

      <AssertionConsumerService index="0" isDefault="true" 
          Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact"
          Location="https://sp.example.com/liberty/assertionConsumerServiceArtifact" />

    </SPSSODescriptor>

    <Organization>
      <OrganizationName xml:lang="en"></OrganizationName>
    </Organization>

  </EntityDescriptor>

``SingleLogoutService`` entry allows to define method and url supported where
service provider wants to receive single logout requests. In previous metadata
example, service provider receives SOAP single logout requests on the
url ``https://sp.example.com/liberty/singleLogoutSoap``.

``ManageNameIDService`` entry allows to define method and url supported where
service provider wants to receive name id management requests. In previous metadata
example, service provider receives SOAP name id management requests on the
url ``http://sp.example.com/liberty/nameIdManagmentSoap``.

``AssertionConsumerService`` entry allows to define method and url supported where
service provider wants to receive assertion consumer responses. In previous metadata
example, service provider consume assertions responses on the
url ``https://sp.example.com/liberty/assertionConsumerServicePost``.


Single Sign-On and Federation Profile
=====================================

.. warning:: The source code presented in this section has for sole purpose
             to explain the different steps necessary to implement this
	     profile; they notably lack proper error checking.  See `Proper
	     Error Checking`_ for details on error checking.


As a first step the user points its browser to the service provider to the
login URL; the service provider must then respond with an HTTP 302 Redirect
response, pointing the user browser to the identity provider single sign on
service.

.. note:: the login URL is not normative; any name will do.


``$server`` is a instantiated ``LassoServer`` as seen earlier and
``$idpEntityId`` is a string with the idp entity Id (the string must
match a entityID defined in the metadata file).

::



  $entityIdList = $server->providerIds;
  $idpEntityId = $entityIdList->getItem(0);

  $lassoLogin = new LassoLogin($server);
  $lassoLogin->initAuthnRequest($idpEntityId, LASSO_HTTP_METHOD_REDIRECT);

  $lassoRequest = $lassoLogin->request;
  $lassoRequest->ProtocolBinding = LASSO_SAML2_METADATA_BINDING_ARTIFACT;

  $lassoNameIdPolicy = $lassoRequest->NameIDPolicy;
  $lassoNameIdPolicy->Format = LASSO_SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT;
  $lassoNameIdPolicy->AllowCreate = TRUE;

  $lassoLogin->buildAuthnRequestMsg();


You can now redirect the user to the URL defined in ``$lassoLogin->msgUrl``; for
example::
  
  header("Location: ".$lassoLogin->msgUrl);


The user then logs in on the identity provider which ultimately redirects back
to the service provider; to the assertion consumer URL. A SAML artifact is
passed in the query parameter.

.. note:: the assertion consumer URL is defined by Liberty; it must be declared
   in the ``AssertionConsumerServiceURL`` element of the metadata file.

::

  $lassoLogin = new LassoLogin($server);
  $lassoLogin->initRequest($query_string, LASSO_HTTP_METHOD_ARTIFACT_GET);
  $lassoLogin->buildRequestMsg();
  
The service provider must check this artifact using a SOAP request to the
identity provider.  The URL is ``$lassoLogin->msgUrl`` while the
request is ``$lassoLogin->msgBody``.  The request must succeed with
an HTTP 200 status code; let's consider its content is put in the ``$answer``,
the next statement would be::

  $lassoLogin->processResponseMsg($answer);

The users are defined by a ``nameIdentifier`` (accessible through
``$lassoLogin->nameIdentifier``).  Those typically map to users
and sessions in some database on the service provider.  If existing; the
session should probably contains a ``session_dump`` element and the user a
``identity_dump`` element.  See `Database Considerations`_ below for more
information.

It is now time to get them out of the database and apply them to the ``login``
object.

::

  if ($session_dump != NULL) {
      $lassoLogin->setSessionFromDump($session_dump);
  }
  if ($identity_dump != NULL) {
      $lassoLogin->setIdentityFromDump($identity_dump);
  }
  $lassoLogin->acceptSso();

After ``lassoLogin->acceptSso()`` the session and the identity are updated (or
created) and should then be saved.  If the identity has not recognised by the
service provider an account will probably have to be created on the service
provider; this is a good opportunity to ask the user for more information.

You can get respective dumps like this::

  if ($lassoLogin->isIdentityDirty) {
      $lassoIdentity = $lassoLogin->identity;
      $lassoIdentityDump = $lassoIdentity->dump();
  }

  if ($lassoLogin->isSessionDirty) {
      $lassoSession = $lassoLogin->session;
      $lassoSessionDump = $lassoSession->dump();
  }

  /* code to store $identity_dump and $session_dump */


A success web page can be displayed.


Single Logout Profile
=====================

There are different single logout profiles; some initiated on the identity
provider, others initiated on the service provider, using either HTTP redirects
or SOAP requests.

This part is about a logout using SOAP and initiated on the service provider.

::

  $lassoLogout = new LassoLogout($server);


Identity and session dumps should be restored to prepare the logout request.

::

  if ($session_dump != NULL) {
      $lassoLogout->setSessionFromDump($session_dump);
  }
  if ($identity_dump != NULL) {
      $lassoLogout->setIdentityFromDump($identity_dump);
  }

  $lassoLogout->initRequest($idpProviderId, LASSO_HTTP_METHOD_SOAP);
  $lassoLogout->buildRequestMsg();


The service provider must then make a SOAP request to the identity provider;
``$msgUrl`` and ``$msgBody``.  You should then pass the answer to Lasso::

  $lassoLogout->processResponseMsg($answer));

And save back session and user dump; the process is similar as the one at the
end of the single sign on profile.


Proper Error Checking
=====================

Most Lasso functions raise PHP error (fatal) or warning (non-fatal).

It is strongly advised to code an user error handler::

  function userErrorHandler($errno, $errmsg, $filename, $linenum, $vars) {
      print("No: ".$errno."  -  ".$errmsg." at ".$filename.", line: ".$linenum."\n");
  }

and to set up the script to use it::

  set_error_handler("userErrorHandler");

Most Lasso functions returns 0 on success and a negative number on failure.  It
is strongly advised to check this return code on each call. If the error raise a
PHP warning, the code resume after the call to the error handler function.

::

  $lrv = $lassoLogin->processResponseMsg($responseMsg);
  if ($lrv > 0) {
      print("Lasso Error: ".$lrv."\n");
      /* handling error; most probably bailing out */
  }


Database Considerations
=======================

Lasso has been designed to let the service provider keep on using existing
databases.  Typically there is already a table describing users; just add an
identity dump column to the existing table:

=======    ========================================    ==============
User Id    existing data (name, address...)            Identity dump
=======    ========================================    ==============
1          ...                                         <Identity> ...
2          ...                                         <Identity> ...
=======    ========================================    ==============

Mapping between existing users and name identifiers sent by the identity
provider can be done with a simple table.

===============    =======
Name Identifier    User Id
===============    =======
AQWWRRS...         1
CGFASDE...         2
YYSSSDS...         1
===============    =======

.. note:: A separate table is needed because one user Id could map
          to several name identifiers; in case there are several identity
	  providers.

Sessions are also commonly stored in databases; just add a session dump column
to the existing session table:

==========    =================    =============
Session Id    misc session data    Session dump
==========    =================    =============
6744066       ...                  <Session> ...
3338824       ...                  <Session> ...
==========    =================    =============

Likewise sessions should be mapped to name identifiers.

===============    ==========
Name Identifier    Session Id
===============    ==========
AQWWRRS...         3338824
===============    ==========


API Reference
=============

- LassoLogin_
- LassoLogout_
- LassoIdentity_
- LassoServer_
- LassoSession_


.. _SAML2 Profiles Specification:
   http://docs.oasis-open.org/security/saml/v2.0/saml-profiles-2.0-os.pdf

.. _LassoLogin: /documentation/api-reference/lassologin.html
.. _LassoLogout: /documentation/api-reference/lassologout.html
.. _LassoIdentity: /documentation/api-reference/lassoidentity.html
.. _LassoServer: /documentation/api-reference/lassoserver.html
.. _LassoSession: /documentation/api-reference/lassosession.html

