Exception and error handling in simpleSAMLphp ============================================= This document describes the way errors and exceptions are handled in authentication sources and authentication processing filters. The basic goal is to be able to throw an exception during authentication, and then have that exception transported back to the SP in a way that the SP understands. This means that internal simpleSAMLphp exceptions must be mapped to transport specific error codes for the various transports that are supported by simpleSAMLphp. E.g.: When a `SimpleSAML_Error_NoPassive` error is thrown by an authentication processing filter in a SAML 2.0 IdP, we want to map that exception to the `urn:oasis:names:tc:SAML:2.0:status:NoPassive` status code. That status code should then be returned to the SP. Throwing exceptions ------------------- How you throw an exception depends on where you want to throw it from. The simplest case is if you want to throw it during the `authenticate()`-method in an authentication module or during the `process()`-method in a processing filter. In those methods, you can just throw an exception: public function process(&$state) { if ($state['something'] === FALSE) { throw new SimpleSAML_Error_Exception('Something is wrong...'); } } Exceptions thrown at this stage will be caught and delivered to the appropriate error handler. If you want to throw an exception outside of those methods, i.e. after you have done a redirect, you need to use the `SimpleSAML_Auth_State::throwException()` function: The `SimpleSAML_Auth_State::throwException` function will then transfer your exception to the appropriate error handler. ### Note Note that we use the `SimpleSAML_Error_Exception` class in both cases. This is because the delivery of the exception may require a redirect to a different web page. In those cases, the exception needs to be serialized. The normal `Exception` class in PHP isn't always serializable. If you throw an exception that isn't a subclass of the `SimpleSAML_Error_Exception` class, your exception will be converted to an instance of `SimpleSAML_Error_UnserializableException`. The `SimpleSAML_Auth_State::throwException` function does not accept any exceptions that does not subclass the `SimpleSAML_Error_Exception` class. Returning specific SAML 2 errors -------------------------------- By default, all thrown exceptions will be converted to a generic SAML 2 error. In some cases, you may want to convert the exception to a specific SAML 2 status code. For example, the `SimpleSAML_Error_NoPassive` exception should be converted to a SAML 2 status code with the following properties: * The top-level status code should be `urn:oasis:names:tc:SAML:2.0:status:Responder`. * The second-level status code should be `urn:oasis:names:tc:SAML:2.0:status:NoPassive`. * The status message should contain the cause of the exception. The `sspmod_saml_Error` class represents SAML 2 errors. It represents a SAML 2 status code with three elements: the top-level status code, the second-level status code and the status message. The second-level status code and the status message is optional, and can be `NULL`. The `sspmod_saml_Error` class contains a helper function named `fromException`. The `fromException()` function is used by `www/saml2/idp/SSOService.php` to return SAML 2 errors to the SP. The function contains a list which maps various exceptions to specific SAML 2 errors. If it is unable to convert the exception, it will return a generic SAML 2 error describing the original exception in its status message. To return a specific SAML 2 error, you should: * Create a new exception class for your error. This exception class must subclass `SimpleSAML_Error_Exception`. * Add that exception to the list in `fromException()`. * Consider adding the exception to `toException()` in the same file. (See the next section.) ### Note While it is possible to throw SAML 2 errors directly from within authentication sources and processing filters, this practice is discouraged. Throwing SAML 2 errors will tie your code directly to the SAML 2 protocol, and it may be more difficult to use with other protocols. Converting SAML 2 errors to normal exceptions --------------------------------------------- On the SP side, we want to convert SAML 2 errors to simpleSAMLphp exceptions again. This is handled by the `toException()` method in `sspmod_saml_Error`. The assertion consumer script of the SAML 2 authentication source (`modules/saml2/sp/acs.php`) uses this method. The result is that generic exceptions are thrown from that authentication source. For example, `NoPassive` errors will be converted back to instances of `SimpleSAML_Error_NoPassive`. Other protocols --------------- The error handling code has not yet been added to other protocols, but the framework should be easy to adapt for other protocols. To eventually support other protocols was a goal when designing this framework. Technical details ----------------------- This section attempts to describe the internals of the error handling framework. ### `SimpleSAML_Error_Exception` The `SimpleSAML_Error_Exception` class extends the normal PHP `Exception` class. It makes the exceptions serializable by overriding the `__sleep()` method. The `__sleep()` method returns all variables in the class which should be serialized when saving the class. To make sure that the class is serializable, we remove the `$trace` variable from the serialization. The `$trace` variable contains the full stack trace to the point where the exception was instantiated. This can be a problem, since the stack trace also contains the parameters to the function calls. If one of the parameters in unserializable, serialization of the exception will fail. Since preserving the stack trace can be useful for debugging, we save a variant of the stack trace in the `$backtrace` variable. This variable can be accessed through the `getBacktrace()` method. It returns an array with one line of text for each function call in the stack, ending on the point where the exception was created. #### Note Since we lose the original `$trace` variable during serialization, PHP will fill it with a new stack trace when the exception is unserialized. This may be confusing since the new stack trace leads into the `unserialize()` function. It is therefore recommended to use the getBacktrace() method. ### `SimpleSAML_Auth_State` There are two methods in this class that deals with exceptions: * `throwException($state, $exception)`, which throws an exception. * `loadExceptionState($id)`, which restores a state containing an exception. #### `throwException` This method delivers the exception to the code that initialized the exception handling in the authentication state. That would be `SimpleSAML_Auth_Default` for authtentication sources, and `www/saml2/idp/SSOService.php` for processing filters. To configure how and where the exception should be delivered, there are two fields in the state-array which can be set: * `SimpleSAML_Auth_State::EXCEPTION_HANDLER_FUNC`, in which case the exception will be delivered by a function call to the function specified in that field. * `SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL`, in which case the exception will be delivered by a redirect to the URL specified in that field. If the exception is delivered by a function call, the function will be called with two parameters: The exception and the state array. If the exception is delivered by a redirect, SimpleSAML_Auth_State will save the exception in a field in the state array, pass a parameter with the id of the state array to the URL. The `SimpleSAML_Auth_State::EXCEPTION_PARAM` constant contains the name of that parameter, while the `SimpleSAML_Auth_State::EXCEPTION_DATA` constant holds the name of the field where the exception is saved. #### `loadException` To retrieve the exception, the application should check for the state parameter in the request, and then retrieve the state array by calling `SimpleSAML_Auth_State::loadExceptionState()`. The exception can be located in a field named `SimpleSAML_Auth_State::EXCEPTION_DATA`. The following code illustrates this behaviour: if (array_key_exists(SimpleSAML_Auth_State::EXCEPTION_PARAM, $_REQUEST)) { $state = SimpleSAML_Auth_State::loadExceptionState(); $exception = $state[SimpleSAML_Auth_State::EXCEPTION_DATA]; /* Process exception. */ } ### `SimpleSAML_Auth_Default` This class accepts an `$errorURL` parameter to the `initLogin()` function. This parameter is stored in the `SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL` of the state array. Exceptions thrown by the authentication source will be delivered to that URL. It also wraps the call to the `authenticate()` function inside a try-catch block. Any exceptions thrown during that function call will be delivered to the URL specified in the `$errorURL` parameter. This is done for consistency, since `SimpleSAML_Auth_Default` never transfers control back to the caller by returning. ### `SimpleSAML_Auth_ProcessingChain` This class requires the caller to add the error handler to the state array before calling the `processState()` function. Exceptions thrown by the processing filters will be delivered directly to the caller of `processState()` if possible. However, if one of the filters in the processing chain redirected the user away from the caller, exceptions will be delivered through the error handler saved in the state array. This is the same behaviour as normal processing filters. The result will be delivered directly if it is possible, but if not, it will be delivered through a redirect. The code for handling this becomes something like: if (array_key_exists(SimpleSAML_Auth_State::EXCEPTION_PARAM, $_REQUEST)) { $state = SimpleSAML_Auth_State::loadExceptionState(); $exception = $state[SimpleSAML_Auth_State::EXCEPTION_DATA]; /* Handle exception... */ [...] } $procChain = [...]; $state = array( 'ReturnURL' => SimpleSAML_Utilities::selfURLNoQuery(), SimpleSAML_Auth_State::EXCEPTION_HANDLER_URL => SimpleSAML_Utilities::selfURLNoQuery(), [...], ) try { $procChain->processState($state); } catch (SimpleSAML_Error_Exception $e) { /* Handle exception. */ [...]; } #### Note An exception which isn't a subclass of `SimpleSAML_Error_Exception` will be converted to the `SimpleSAML_Error_UnserializedException` class. This happens regardless of whether the exception is delivered directly or through the error handler. This is done to be consistent in what the application receives - now it will always receive the same exception, regardless of whether it is delivered directly or through a redirect. Custom error show function -------------------------- Optional custom error show function, called from SimpleSAML_Error_Error::show, is defined with 'errors.show_function' in config.php. Example code for this function, which implements the same functionality as SimpleSAML_Error_Error::show, looks something like: public static function show(SimpleSAML_Configuration $config, array $data) { $t = new SimpleSAML_XHTML_Template($config, 'error.php', 'errors'); $t->data = array_merge($t->data, $data); $t->show(); exit; }