diff --git a/docs/simplesamlphp-errorhandling.txt b/docs/simplesamlphp-errorhandling.txt new file mode 100644 index 0000000000000000000000000000000000000000..0b5cc602b6d08826cae1f08a95544abedc57862e --- /dev/null +++ b/docs/simplesamlphp-errorhandling.txt @@ -0,0 +1,227 @@ +Exception and error handling in simpleSAMLphp +============================================= + +<!-- + This file is written in Markdown syntax. + For more information about how to use the Markdown syntax, read here: + http://daringfireball.net/projects/markdown/syntax +--> + + * Version: `$Id$` + +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: + + <?php + $id = $_REQUEST['StateId']; + $state = SimpleSAML_Auth_State::loadState($id, 'somestage...'); + SimpleSAML_Auth_State::throwException($state, + new SimpleSAML_Error_Exception('Something is wrong...')); + ?> + +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_saml2_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_saml2_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_saml2_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. + +