diff --git a/docs/reference/lasso/lasso-sections.txt b/docs/reference/lasso/lasso-sections.txt index c849b303..4d822505 100644 --- a/docs/reference/lasso/lasso-sections.txt +++ b/docs/reference/lasso/lasso-sections.txt @@ -5757,6 +5757,12 @@ LASSO_IDWSF2_DSTREF_DATA_RESPONSE_GET_CLASS ecp LassoEcp LassoEcp +lasso_ecp_is_provider_in_sp_idplist +lasso_ecp_is_idp_entry_known_idp_supporting_ecp +lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp +lasso_ecp_has_sp_idplist +lasso_ecp_get_endpoint_url_by_entity_id +lasso_ecp_process_sp_idp_list lasso_ecp_new lasso_ecp_process_authn_request_msg lasso_ecp_process_response_msg diff --git a/lasso/saml-2.0/ecp.c b/lasso/saml-2.0/ecp.c index 1bd6bf06..ddece31a 100644 --- a/lasso/saml-2.0/ecp.c +++ b/lasso/saml-2.0/ecp.c @@ -21,20 +21,74 @@ * along with this program; if not, see . */ +/* + * 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 + */ + /** * SECTION:ecp * @short_description: Enhanced Client or Proxy Profile (SAMLv2) * + * # Introduction + * + * The #LassoEcp object is used to implement a SAMLv2 ECP client. + * If you want to support ECP in a SP see [ecp-sp]. + * If you want to support ECP in a IdP see [ecp-idp]. + * + * # ECP Operational Steps + * + * SAML2 Profile for ECP (Section 4.2) defines these steps for an ECP + * transaction + * + * 1. ECP issues HTTP Request to SP + * 2. SP issues <samlp:AuthnRequest> to ECP using PAOS + * 3. ECP determines IdP + * 4. ECP conveys <samlp:AuthnRequest> to IdP using SOAP + * 5. IdP identifies principal + * 6. IdP issues <samlp:Response> to ECP, targeted at SP using SOAP + * 7. ECP conveys <samlp:Response> to SP using PAOS + * 8. SP grants or denies access to principal + * + * + * + * **/ +/** + * SECTION:ecp-sp + * @short_description: How to support ECP in an SP + * + * + * |[ + * login = lasso_login_new(server); + * ]| + */ + +/** + * SECTION:ecp-idp + * @short_description: How to support ECP in an IdP + * + * + * |[ + * login = lasso_login_new(server); + * ]| + */ + #include "../xml/private.h" #include #include -#include "providerprivate.h" #include "profileprivate.h" -#include "../id-ff/providerprivate.h" -#include "../id-ff/identityprivate.h" #include "../id-ff/serverprivate.h" #include "ecpprivate.h" @@ -42,6 +96,33 @@ #include "ecp.h" #include "../utils.h" +#include "../xml/soap-1.1/soap_envelope.h" +#include "../xml/soap-1.1/soap_header.h" +#include "../xml/soap-1.1/soap_body.h" +#include "../xml/soap-1.1/soap_fault.h" +#include "../xml/misc_text_node.h" +#include "../xml/paos_request.h" +#include "../xml/paos_response.h" +#include "../xml/ecp/ecp_request.h" +#include "../xml/ecp/ecp_response.h" +#include "../xml/ecp/ecp_relaystate.h" +#include "../xml/lib_authn_request.h" +#include "../xml/saml-2.0/samlp2_response.h" +#include "../xml/saml-2.0/samlp2_authn_request.h" + +/*****************************************************************************/ +/* Prototypes */ +/*****************************************************************************/ + +static gboolean +is_provider_in_sp_idplist(GList *idp_list, const gchar *entity_id); + +static gboolean +is_idp_entry_in_entity_id_list(GList *entity_id_list, const LassoSamlp2IDPEntry *idp_entry); + +static GList * +intersect_sp_idplist_with_entity_id_list(GList *sp_provided_idp_entries, GList *known_idp_entity_ids_supporting_ecp); + /*****************************************************************************/ /* public methods */ /*****************************************************************************/ @@ -59,12 +140,259 @@ lasso_ecp_destroy(LassoEcp *ecp) lasso_node_destroy(LASSO_NODE(ecp)); } +/** + * lasso_ecp_is_provider_in_sp_idplist: + * @ecp: a #LassoEcp + * @entity_id: EntityID to check if member of #LassoEcp.IDPList + * + * Check to see if the provider with @entity_id is in the + * ecp IDPList returned by the SP. + * + * Return value: TRUE if @entity_id is in #LassoEcp.IDPList, FALSE otherwise + */ +gboolean +lasso_ecp_is_provider_in_sp_idplist(LassoEcp *ecp, const gchar *entity_id) { + return is_provider_in_sp_idplist(ecp->sp_idp_list->IDPEntry, entity_id); +} + +/** + * lasso_ecp_is_idp_entry_known_idp_supporting_ecp: + * @ecp: a #LassoEcp + * @idp_entry: #LassoSamlp2IDPEntry to check if member of @entity_id_list + * + * Check to see if the @idp_entry is in the @entity_id_list + * + * + * Return value: TRUE if @entity_id is in @idp_list, FALSE otherwise + */ +gboolean +lasso_ecp_is_idp_entry_known_idp_supporting_ecp(LassoEcp *ecp, const LassoSamlp2IDPEntry *idp_entry) { + return is_idp_entry_in_entity_id_list(ecp->known_idp_entity_ids_supporting_ecp, idp_entry); +} + +/** + * lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp: + * @ecp: a #LassoEcp + * + * The SP may provide a list of #LassoSamlp2IDPEntry + * (#LassoEcp.sp_idp_list) which it trusts. The ECP client + * has a list of IDP EntityID's it knows supports ECP + * (#LassoEcp.known_idp_entity_ids_supporting_ecp). The set of + * possible IDP's which can service the SP's authn request are the + * interesection of these two lists (the IDP's the SP approves and + * IDP's the ECP knows about). This find the common members between + * the two lists and assign them to + * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp. + */ +void +lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp(LassoEcp *ecp) +{ + lasso_assign_new_list_of_strings(ecp->known_sp_provided_idp_entries_supporting_ecp, + intersect_sp_idplist_with_entity_id_list(ecp->sp_idp_list ? ecp->sp_idp_list->IDPEntry : NULL, + ecp->known_idp_entity_ids_supporting_ecp)); +} + +/** + * lasso_ecp_has_sp_idplist: + * @ecp: a #LassoEcp + * + * Returns TRUE if the SP provided an IDP List, FALSE otherwise. + */ +gboolean +lasso_ecp_has_sp_idplist(LassoEcp *ecp) +{ + return ecp->sp_idp_list && ecp->sp_idp_list->IDPEntry != NULL; +} + +/** + * lasso_ecp_get_endpoint_url_by_entity_id: + * @ecp: a #LassoEcp + * @entity_id: the EntityID of the IdP + * + * Returns the SingleSignOnService SOAP endpoint URL for the specified + * @entity_id. If the provider cannot be found or if the provider does + * not have a matching endpoint NULL will be returned. + * + * Returns: url (must be freed by caller) + */ +gchar * +lasso_ecp_get_endpoint_url_by_entity_id(LassoEcp *ecp, const gchar *entity_id) +{ + LassoProfile *profile; + + profile = LASSO_PROFILE(ecp); + + return lasso_server_get_endpoint_url_by_id(profile->server, entity_id, + "SingleSignOnService SOAP"); +} + +/** + * lasso_ecp_process_sp_idp_list: + * @ecp: a #LassoEcp + * + * The SP may optionally send a list of IdP's it trusts in ecp:IDPList. + * The ecp:IDPList may not be complete if the IDPList.GetComplete is + * non-NULL. If so the IDPList.GetComplete is a URL where a complete + * IDPList may be fetched. + * + * Whenever the IDPList is updated this function needs to be called + * because it sets the + * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp and the + * default IdP URL (#LassoProfile.msg_url). + * + * The #LassoEcp client has a list of IdP's it knows supports ECP + * (#LassoEcp.known_idp_entity_ids_supporting_ecp). The set of IdP's + * available to select from should be those in common between SP + * provided IdP list and those known by this ECP client to support + * ECP. + * + * This routine sets the + * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp list to the + * common members (e.g. intersection) of the SP provided IdP list and + * the list of known IdP's supporting ECP. + * + * A default IdP will be selected and it's endpoint URL will be + * assigned to #LassoProfile.msg_url. + * + * If the SP provided an IDP list then the default URL will be taken + * from first IDPEntry in + * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp otherwise + * it will be taken from #LassoEcp.known_idp_entity_ids_supporting_ecp. + * + */ +int +lasso_ecp_process_sp_idp_list(LassoEcp *ecp, const LassoSamlp2IDPList *sp_idp_list) +{ + int rc = 0; + LassoProfile *profile; + gchar *provider_id = NULL; + gchar *url; + + profile = LASSO_PROFILE(ecp); + + lasso_assign_gobject(ecp->sp_idp_list, sp_idp_list); + + /* Build a list of IdP's which are common between the SP and those we know support ECP */ + lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp(ecp); + + /* Select a default IdP */ + provider_id = NULL; + if (lasso_ecp_has_sp_idplist(ecp)) { + /* Select first IDP provided by SP that is in our IDP list */ + if (ecp->known_sp_provided_idp_entries_supporting_ecp) { + provider_id = ((LassoSamlp2IDPEntry*)ecp->known_sp_provided_idp_entries_supporting_ecp->data)->ProviderID; + } + } + if (!provider_id) { + /* Select first IDP from our IDP list */ + if (ecp->known_idp_entity_ids_supporting_ecp) { + provider_id = ecp->known_idp_entity_ids_supporting_ecp->data; + } + } + + /* If we have a default IdP assign it's ECP URL to the profile->msg_url */ + lasso_release_string(profile->msg_url) + if (provider_id) { + url = lasso_ecp_get_endpoint_url_by_entity_id(ecp, provider_id); + lasso_assign_new_string(profile->msg_url, url); + } + return rc; +} + /*****************************************************************************/ /* private methods */ /*****************************************************************************/ static LassoNodeClass *parent_class = NULL; +/** + * compare_idp_entry_to_entity_id: + * + * Helper function for is_provider_in_sp_idplist(). + */ +static gboolean +compare_idp_entry_to_entity_id(gconstpointer a, gconstpointer b) +{ + const LassoSamlp2IDPEntry *idp_entry = LASSO_SAMLP2_IDP_ENTRY(a); + const gchar *entity_id = b; + + return g_strcmp0(idp_entry->ProviderID, entity_id); +} + +/** + * is_provider_in_sp_idplist: + * @idp_list: GList of LassoSamlp2IDPEntry + * @entity_id: EntityID to check if member of @idp_list + * + * Check if the provider with @entity_id is in the #idp_list. + * + * Return value: TRUE if @entity_id is in @idp_list, FALSE otherwise + */ +static gboolean +is_provider_in_sp_idplist(GList *idp_list, const gchar *entity_id) { + return g_list_find_custom(idp_list, entity_id, compare_idp_entry_to_entity_id) == NULL ? FALSE : TRUE; +} + +/** + * compare_entity_id_to_idp_entry: + * + * Helper function for is_idp_entry_in_entity_id_list(). + */ +static gboolean +compare_entity_id_to_idp_entry(gconstpointer a, gconstpointer b) +{ + const gchar *entity_id = a; + const LassoSamlp2IDPEntry *idp_entry = LASSO_SAMLP2_IDP_ENTRY(b); + + return g_strcmp0(entity_id, idp_entry->ProviderID); +} + +/** + * is_idp_entry_in_entity_id_list: + * @entity_id_list: #GList of entity id's + * @idp_entry: #LassoSamlp2IDPEntry to check if member of @entity_id_list + * + * Check if the provider with @entity_id is in the #idp_list. + * + * Return value: TRUE if @entity_id is in @idp_list, FALSE otherwise + */ +static gboolean +is_idp_entry_in_entity_id_list(GList *entity_id_list, const LassoSamlp2IDPEntry *idp_entry) { + return g_list_find_custom(entity_id_list, idp_entry, compare_entity_id_to_idp_entry) == NULL ? FALSE : TRUE; +} + +/* + * intersect_sp_idplist_with_entity_id_list: + * @sp_provided_idp_entries: #GList of #LassoSamlp2IDPEntry + * @known_idp_entity_ids_supporting_ecp: #GList of entity id's + * + * The SP may provide a list of #LassoSamlp2IDPEntry which it + * trusts. The ECP client has a list of IDP EntityID's it knows + * supports ECP. The set of possible IDP's which can service the SP's + * authn request are the interesection of these two lists (the IDP's + * the SP approves and IDP's the ECP knows about). This function + * accepts the SP's IDPEntry list and returns a new list containing + * only those the ECP client knows about. The returned list must be + * freed with lasso_release_list_of_gobjects(). + * + * Return value: GList of #LassoSamlp2IDPEntry + * (caller must free with lasso_release_list_of_gobjects()) + */ +static GList * +intersect_sp_idplist_with_entity_id_list(GList *sp_provided_idp_entries, GList *known_idp_entity_ids_supporting_ecp) +{ + GList *i; + GList *new_list = NULL; + + lasso_foreach(i, sp_provided_idp_entries) { + LassoSamlp2IDPEntry *idp_entry = i->data; + if (is_idp_entry_in_entity_id_list(known_idp_entity_ids_supporting_ecp, idp_entry)) { + lasso_list_add_gobject(new_list, idp_entry); + } + } + return new_list; +} + /*****************************************************************************/ /* overridden parent class methods */ /*****************************************************************************/ @@ -74,15 +402,20 @@ dispose(GObject *object) { LassoEcp *ecp = LASSO_ECP(object); - if (ecp->private_data->messageID) { - xmlFree(ecp->private_data->messageID); - ecp->private_data->messageID = NULL; + if (ecp->private_data->dispose_has_run) { + return; } + ecp->private_data->dispose_has_run = TRUE; - if (ecp->private_data->relay_state) { - xmlFree(ecp->private_data->relay_state); - ecp->private_data->relay_state = NULL; - } + lasso_release_string(ecp->assertion_consumer_url); + lasso_release_string(ecp->message_id); + lasso_release_string(ecp->response_consumer_url); + lasso_release_string(ecp->relaystate); + lasso_release_gobject(ecp->issuer); + lasso_release_string(ecp->provider_name); + lasso_release_gobject(ecp->sp_idp_list); + lasso_release_list_of_gobjects(ecp->known_sp_provided_idp_entries_supporting_ecp); + lasso_release_list_of_strings(ecp->known_idp_entity_ids_supporting_ecp); G_OBJECT_CLASS(parent_class)->dispose(G_OBJECT(ecp)); } @@ -92,7 +425,6 @@ finalize(GObject *object) { LassoEcp *ecp = LASSO_ECP(object); lasso_release(ecp->private_data); - ecp->private_data = NULL; G_OBJECT_CLASS(parent_class)->finalize(object); } @@ -105,10 +437,6 @@ static void instance_init(LassoEcp *ecp) { ecp->private_data = g_new0(LassoEcpPrivate, 1); - ecp->private_data->messageID = NULL; - ecp->private_data->relay_state = NULL; - - ecp->assertionConsumerURL = NULL; } static void @@ -123,151 +451,413 @@ class_init(LassoEcpClass *klass) G_OBJECT_CLASS(klass)->dispose = dispose; G_OBJECT_CLASS(klass)->finalize = finalize; } - +/** + * lasso_ecp_process_authn_request_msg: + * @ecp: this #LassoEcp object + * @authn_request_msg: the PAOS authn request received from the SP + * + * This function implements the following ECP step: + * ECP Step 3, ECP determines IdP + * ECP Step 4, parse SP PAOS Authn request, build SOAP for IdP + * + * This is to be used in an ECP client. The @authn_request_msg is the + * SOAP PAOS message received from the SP in response to a resource + * request with an HTTP Accept header indicating PAOS support. + * + * The following actions are implemented: + * + * * Extract the samlp:AuthnRequest from the SOAP body and build a + * new SOAP message containing the samlp:AuthnRequest which will + * be forwarded to the IdP. This new SOAP message is stored in the + * #LassoProfile.msg_body. + * + * * Parse the SOAP header which will contain a paos:Request, a + * ecp:Request and optionally a ecp:RelayState. Some of the data + * in these headers need to be preserved for later processing steps. + * + * 1. The paos:Request.responseConsumerURL is copied to the + * #LassoEcp.response_consumer_url. This is necessary because the + * ECP client MUST assure it matches the + * ecp:Response.AssertionConsumerServiceURL returned by the IdP to + * prevent man-in-the-middle attacks. It must also match the + * samlp:AuthnRequest.AssertionConsumerServiceURL. + * + * 2. If the paos:Request contained a messageID it is copied to + * #LassoEcp.message_id so it can be returned in the subsequent + * paos:Response.refToMessageID. This allows a provider to + * correlate messages. + * + * 3. If an ecp:RelayState is present it is copied to + * #LassoEcp.relaystate. This is necessary because in step 7 when + * the ECP responds to the SP it must include RelayState provided in + * the request. + * + * * In addition the following items are copied to the #LassoEcp for + * informational purposes: + * + * * #LassoEcp.issuer = ecp:Request.Issuer + * + * * #LassoEcp.provider_name = ecp:Request.ProviderName + * + * * #LassoEcp.is_passive = ecp:Request.IsPassive + * + * * #LassoEcp.sp_idp_list = ecp:Request.IDPList + * + * # IdP Selection + * + * In Step 3. The ECP must determine the IdP to forward the + * AuthnRequest to. There are two sets of IdP's which come into + * play. The ECP client has a set of IdP's it knows about because + * their metadata has been loaded into the #LassoServer object. The SP + * may optionally send a list of IdP's in the ecp:Request that it + * trusts. + * + * The selected IdP *must* be one of the IdP's loaded into the + * #LassoServer object from metadata because the IdP endpoints must be + * known. Furthermore the IdP *must* support the SingleSignOnService + * using the SOAP binding. Therefore the known IdP's are filtered for + * those that match this criteria and a list of their EntityID's are + * assigned to #LassoEcp.known_idp_entity_ids_supporting_ecp. The + * selected IdP *must* be a member of this list. + * + * The SP may optionally send a list of IdP's it trusts. If the SP + * sends an IDPList the selected IdP should be a member of this list + * and from above we know it must also be a member of the + * #LassoEcp.known_idp_entity_ids_supporting_ecp. Therefore the + * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp list is set + * to the common members (e.g. intersection) of the SP provided IdP + * list and the list of known IdP's supporting ECP. + * + * When making an IdP selection if the SP provided an IdP List (use + * #LassoEcp.lasso_ecp_has_sp_idplist()) then it should be selected + * from the #LassoEcp.known_sp_provided_idp_entries_supporting_ecp + * list. Otherwise the IdP should be selected from + * #LassoEcp.known_idp_entity_ids_supporting_ecp. + * + * A default IdP will be selected using the above logic by picking the + * first IdP in the appropriate list, it's endpoint URL will be + * assigned to #LassoProfile.msg_url. The above processing is + * implemented by #LassoEcp.lasso_ecp_process_sp_idp_list() and if the + * SP IDPList is updated this routine should be called. + * + * A note about the 3 IdP lists. The #LassoEcp.sp_idp_list.IDPList + * and #LassoEcp.known_sp_provided_idp_entries_supporting_ecp are + * #GList's of #LassoSamlp2IDPEntry object which have a ProviderID, + * Name, and Loc attribute. You may wish to use this SP provided + * information when making a decision or presenting in a user + * interface that allows a user to make a choice. The + * #LassoEcp.known_idp_entity_ids_supporting_ecp is a #GList of + * EntityID strings. + * + * Given the EntityID of an IdP you can get the ECP endpoint by + * calling #LassoEcp.lasso_ecp_get_endpoint_url_by_entity_id() + * + * # Results + * + * After a successful return from this call you are ready to complete + * Step 4. and forward the request the IdP. + * + * The URL to send to the request to will be #LassoProfile.msg_url (if + * you accept the default IdP) and the body of the message to post + * will be #LassoProfile.msg_body. + * + * + * # Side Effects + * + * After a successful return the #LassoEcp object will be updated with: + * + * * ecp->response_consumer_url = paos_request->responseConsumerURL + * * ecp->message_id = paos_request->messageID + * * ecp->relaystate = ecp_relaystate->RelayState + * * ecp->issuer = ecp_request->Issue + * * ecp->provider_name = ecp_request->ProviderName + * * ecp->is_passive = ecp_request->IsPassive + * * ecp->known_idp_entity_ids_supporting_ecp + * * ecp->sp_idp_list = ecp_request->IDPList + * * ecp->known_sp_provided_idp_entries_supporting_ecp + * + */ int lasso_ecp_process_authn_request_msg(LassoEcp *ecp, const char *authn_request_msg) { - xmlDoc *doc; - xmlXPathContext *xpathCtx; - xmlXPathObject *xpathObj; - xmlNode *xmlnode; + int rc = 0; + LassoSoapEnvelope *envelope = NULL; + LassoSoapHeader *header = NULL; + LassoSoapBody *body = NULL; + LassoPaosRequest *paos_request = NULL; + LassoEcpRequest *ecp_request = NULL; + LassoEcpRelayState *ecp_relaystate = NULL; + LassoSamlp2AuthnRequest *authn_request = NULL; + GList *i; LassoProfile *profile; - LassoProvider *remote_provider; g_return_val_if_fail(LASSO_IS_ECP(ecp), LASSO_PARAM_ERROR_BAD_TYPE_OR_NULL_OBJ); g_return_val_if_fail(authn_request_msg != NULL, LASSO_PARAM_ERROR_INVALID_VALUE); profile = LASSO_PROFILE(ecp); - doc = lasso_xml_parse_memory(authn_request_msg, strlen(authn_request_msg)); - xpathCtx = xmlXPathNewContext(doc); + /* Get the SOAP envelope */ + lasso_extract_node_or_fail(envelope, lasso_soap_envelope_new_from_message(authn_request_msg), + SOAP_ENVELOPE, LASSO_PROFILE_ERROR_INVALID_SOAP_MSG); - xmlXPathRegisterNs(xpathCtx, (xmlChar*)"ecp", (xmlChar*)LASSO_ECP_HREF); - xpathObj = xmlXPathEvalExpression((xmlChar*)"//ecp:RelayState", xpathCtx); - if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr) { - xmlnode = xpathObj->nodesetval->nodeTab[0]; - ecp->private_data->relay_state = xmlNodeGetContent(xmlnode); - } - xmlXPathFreeObject(xpathObj); + /* Get the SOAP body */ + lasso_extract_node_or_fail(body, envelope->Body, SOAP_BODY, + LASSO_SOAP_ERROR_MISSING_BODY); + goto_cleanup_if_fail_with_rc(body->any && LASSO_IS_NODE(body->any->data), + LASSO_SOAP_ERROR_MISSING_BODY); + lasso_extract_node_or_fail(authn_request, body->any->data, SAMLP2_AUTHN_REQUEST, + LASSO_ECP_ERROR_MISSING_AUTHN_REQUEST); - xmlXPathRegisterNs(xpathCtx, (xmlChar*)"paos", (xmlChar*)LASSO_PAOS_HREF); - xpathObj = xmlXPathEvalExpression((xmlChar*)"//paos:Request", xpathCtx); - if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr) { - ecp->private_data->messageID = xmlGetProp( - xpathObj->nodesetval->nodeTab[0], (xmlChar*)"messageID"); - } - xmlXPathFreeObject(xpathObj); + /* Get the SOAP header */ + lasso_extract_node_or_fail(header, envelope->Header, SOAP_HEADER, + LASSO_SOAP_ERROR_MISSING_HEADER); + goto_cleanup_if_fail_with_rc(header->Other && LASSO_IS_NODE(header->Other->data), + LASSO_SOAP_ERROR_MISSING_HEADER); - xmlXPathRegisterNs(xpathCtx, (xmlChar*)"s", (xmlChar*)LASSO_SOAP_ENV_HREF); - xpathObj = xmlXPathEvalExpression((xmlChar*)"//s:Header", xpathCtx); - if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr) { - xmlnode = xpathObj->nodesetval->nodeTab[0]; - xmlUnlinkNode(xmlnode); - xmlFreeNode(xmlnode); - } - xmlXPathFreeObject(xpathObj); - xmlXPathFreeContext(xpathCtx); - xpathCtx = NULL; - xpathObj = NULL; + /* + * Get the following header elements: + * * paos:Request (required) + * * ecp:Request (required) + * * ecp:RelayState (optional) + */ + lasso_foreach(i, header->Other) { + if (!paos_request && LASSO_IS_PAOS_REQUEST(i->data)) { + paos_request = (LassoPaosRequest *)i->data; + } else if (!ecp_request && LASSO_IS_ECP_REQUEST(i->data)) { + ecp_request = (LassoEcpRequest *)i->data; + } else if (!ecp_relaystate && LASSO_IS_ECP_RELAYSTATE(i->data)) { + ecp_relaystate = (LassoEcpRelayState *)i->data; + } - xmlnode = xmlDocGetRootElement(doc); - lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body, - lasso_xmlnode_to_string(xmlnode, 0, 0)) - lasso_release_doc(doc); - - profile->remote_providerID = lasso_server_get_first_providerID_by_role(profile->server, LASSO_PROVIDER_ROLE_IDP); - if (profile->remote_providerID == NULL) { - return critical_error(LASSO_SERVER_ERROR_PROVIDER_NOT_FOUND); + if (ecp_relaystate && ecp_request && paos_request) break; } - remote_provider = lasso_server_get_provider(profile->server, profile->remote_providerID); - if (LASSO_IS_PROVIDER(remote_provider) == FALSE) { - return critical_error(LASSO_SERVER_ERROR_PROVIDER_NOT_FOUND); + goto_cleanup_if_fail_with_rc(paos_request, LASSO_PAOS_ERROR_MISSING_REQUEST); + goto_cleanup_if_fail_with_rc(ecp_request, LASSO_ECP_ERROR_MISSING_REQUEST); + + /* Copy data for later use */ + if (paos_request->responseConsumerURL) { + lasso_assign_string(ecp->response_consumer_url, paos_request->responseConsumerURL); + } else { + goto_cleanup_with_rc(LASSO_PAOS_ERROR_MISSING_RESPONSE_CONSUMER_URL); } - profile->msg_url = lasso_provider_get_metadata_one(remote_provider, - "SingleSignOnService SOAP"); - if (profile->msg_url == NULL) { - return critical_error(LASSO_PROFILE_ERROR_UNKNOWN_PROFILE_URL); + if (paos_request->messageID) { + lasso_assign_string(ecp->message_id, paos_request->messageID); } - return 0; + if (ecp_relaystate) { + lasso_assign_string(ecp->relaystate, ecp_relaystate->RelayState); + } + + lasso_assign_gobject(ecp->issuer, ecp_request->Issuer); + lasso_assign_string(ecp->provider_name, ecp_request->ProviderName); + ecp->is_passive = ecp_request->IsPassive; + + /* + * Build a SOAP envelope whose body contains the original + * AuthnRequest received from the SP. The obvious solution is to + * serialize into XML the LassoSamlp2AuthnRequest LassoNode that + * was serialized from XML when we parsed the PAOS request + * (e.g. lasso_node_export_to_soap(LASSO_NODE(authn_request))) but + * that won't work because XML serialization is not symmetric. + * Serializing from XML into a LassoNode and then serializing the + * LassoNode back into XML does not produce the originial XML + * content. This is mostly due to the presence of signatures. In + * order to forward the *exact* same XML AuthnRequest we received + * from the SP to the IdP we mark the LassoSamlp2AuthnRequest with + * a flag indicating it's xmlNode needs to be preserved + * (e.g. keep_xmlnode = TRUE). We copy the xmlNode into a special + * LassoNode (LassoMiscTextNode) which is capable of preserving + * the exact xmlNode thus insuring no modification was made to the + * content. + * + * We assign the SOAP message to the profile->msg_body so it's + * available for transmitting to the IdP. + */ + + { + xmlNodePtr xml; + LassoMiscTextNode *misc; + + xml = lasso_node_get_original_xmlnode(LASSO_NODE(authn_request)); + + misc = lasso_misc_text_node_new_with_xml_node(xml); + lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body, + lasso_node_export_to_soap(LASSO_NODE(misc))); + lasso_release_gobject(misc); + } + + + /* Set up for IdP selection, build IdP lists, make default IdP choice */ + + /* Filter our server's list of IdP's to only include those that support ECP */ + ecp->known_idp_entity_ids_supporting_ecp = lasso_server_get_filtered_provider_list( + profile->server, LASSO_PROVIDER_ROLE_IDP, LASSO_MD_PROTOCOL_TYPE_SINGLE_SIGN_ON, + LASSO_HTTP_METHOD_SOAP); + + /* Update the IdP lists and select a default URL */ + lasso_ecp_process_sp_idp_list(ecp, ecp_request->IDPList); + + cleanup: + lasso_release_gobject(envelope); + + return rc; } +/** + * lasso_ecp_process_response_msg: + * @ecp: this #LassoEcp object + * @response_msg: the SOAP response from the IdP + * + * + * The function implements ECP Step 7; parse IdP SOAP response and + * build PAOS response for SP. + * + * See SAML Profile Section 4.2.4.5 PAOS Response Header Block: ECP to SP + * + * This is to be used in an ECP client. The @response_msg parameter + * contains the SOAP response from the IdP. We extract the ECP Header + * Block and body from it. We will generate a new PAOS message to send + * to the SP, the SOAP header will contain a paos:Response. If we + * received a paos:Request.MessageID in Step. 4 from the SP then we + * will copy it back to the paos:Response.refToMessageID. If we + * received a RelayState we will add that to the SOAP header as well. + * + * To prevent a man-in-the-middle attack we verify the + * responseConsumerURL we received in Step 4 matches the + * ecp:Response.AssertionConsumerServiceURL we just received back from + * the IdP. If they do not match we return a + * #LASSO_ECP_ERROR_ASSERTION_CONSUMER_URL_MISMATCH error and set the + * #LassoProvider.msg_body to the appropriate SOAP fault. + * + * The new PAOS message for the SP we are buiding contains the IdP + * response in the new SOAP body and the new SOAP headers will contain + * a paso:Response and optionally an ecp:RelayState. + * + * After a successful return from this call you are ready to complete + * Step 7. and forward the response to the SP. + * + * The PASO message is assigned to the #LassoProvider.msg_body and + * the desination URL is assigned to the #LassoProvider.msg_url. + * + * # Side Effects + * + * After a successful return the #LassoEcp object will be updated with: + * + * * ecp->assertion_consumer_url = ecp_response->AssertionConsumerServiceURL + * * ecp.profile.msg_url = ecp->assertion_consumer_url + * * ecp.profile.msg_body_url = PAOS response to SP + */ int lasso_ecp_process_response_msg(LassoEcp *ecp, const char *response_msg) { - xmlDoc *doc; - xmlXPathContext *xpathCtx; - xmlXPathObject *xpathObj; - xmlNode *new_envelope, *header, *paos_response, *ecp_relay_state; - xmlNode *body = NULL; - xmlNs *soap_env_ns, *ecp_ns; + int rc = 0; + LassoSoapEnvelope *envelope = NULL; + LassoSoapHeader *header = NULL; + LassoSoapBody *body = NULL; + LassoPaosResponse *paos_response = NULL; + LassoEcpResponse *ecp_response = NULL; + LassoEcpRelayState *ecp_relaystate = NULL; + LassoSamlp2Response *samlp2_response = NULL; + GList *i; + GList *headers = NULL; g_return_val_if_fail(LASSO_IS_ECP(ecp), LASSO_PARAM_ERROR_BAD_TYPE_OR_NULL_OBJ); g_return_val_if_fail(response_msg != NULL, LASSO_PARAM_ERROR_INVALID_VALUE); - doc = lasso_xml_parse_memory(response_msg, strlen(response_msg)); - xpathCtx = xmlXPathNewContext(doc); - xmlXPathRegisterNs(xpathCtx, (xmlChar*)"s", (xmlChar*)LASSO_SOAP_ENV_HREF); - xpathObj = xmlXPathEvalExpression((xmlChar*)"//s:Body", xpathCtx); - if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr) { - body = xmlCopyNode(xpathObj->nodesetval->nodeTab[0], 1); - } - xmlXPathFreeObject(xpathObj); + /* Get the SOAP envelope */ + lasso_extract_node_or_fail(envelope, lasso_soap_envelope_new_from_message(response_msg), + SOAP_ENVELOPE, LASSO_PROFILE_ERROR_INVALID_SOAP_MSG); - xmlXPathRegisterNs(xpathCtx, (xmlChar*)"ecp", (xmlChar*)LASSO_ECP_HREF); - xpathObj = xmlXPathEvalExpression((xmlChar*)"//ecp:Response", xpathCtx); - if (xpathObj && xpathObj->nodesetval && xpathObj->nodesetval->nodeNr) { - ecp->assertionConsumerURL = (char*)xmlGetProp( - xpathObj->nodesetval->nodeTab[0], (xmlChar*)"AssertionConsumerURL"); - } - xmlXPathFreeObject(xpathObj); - xmlXPathFreeContext(xpathCtx); - xpathCtx = NULL; - xpathObj = NULL; + /* Get the SOAP body */ + lasso_extract_node_or_fail(body, envelope->Body, SOAP_BODY, + LASSO_SOAP_ERROR_MISSING_BODY); + goto_cleanup_if_fail_with_rc(body->any && LASSO_IS_NODE(body->any->data), + LASSO_SOAP_ERROR_MISSING_BODY); + lasso_extract_node_or_fail(samlp2_response, body->any->data, SAMLP2_RESPONSE, + LASSO_ECP_ERROR_MISSING_SAML_RESPONSE); - new_envelope = xmlNewNode(NULL, (xmlChar*)"Envelope"); - xmlSetNs(new_envelope, xmlNewNs(new_envelope, - (xmlChar*)LASSO_SOAP_ENV_HREF, (xmlChar*)LASSO_SOAP_ENV_PREFIX)); - xmlNewNs(new_envelope, - (xmlChar*)LASSO_SAML_ASSERTION_HREF, (xmlChar*)LASSO_SAML_ASSERTION_PREFIX); - header = xmlNewTextChild(new_envelope, NULL, (xmlChar*)"Header", NULL); + /* Get the SOAP header */ + lasso_extract_node_or_fail(header, envelope->Header, SOAP_HEADER, + LASSO_SOAP_ERROR_MISSING_HEADER); + goto_cleanup_if_fail_with_rc(header->Other && LASSO_IS_NODE(header->Other->data), + LASSO_SOAP_ERROR_MISSING_HEADER); - /* PAOS request header block */ - soap_env_ns = xmlNewNs(new_envelope, - (xmlChar*)LASSO_SOAP_ENV_HREF, (xmlChar*)LASSO_SOAP_ENV_PREFIX); - paos_response = xmlNewNode(NULL, (xmlChar*)"Response"); - xmlSetNs(paos_response, xmlNewNs(paos_response, - (xmlChar*)LASSO_PAOS_HREF, (xmlChar*)LASSO_PAOS_PREFIX)); - xmlSetNsProp(paos_response, soap_env_ns, (xmlChar*)"mustUnderstand", (xmlChar*)"1"); - xmlSetNsProp(paos_response, soap_env_ns, (xmlChar*)"actor", - (xmlChar*)LASSO_SOAP_ENV_ACTOR); - if (ecp->private_data->messageID) { - xmlSetNsProp(paos_response, soap_env_ns, (xmlChar*)"refToMessageID", - (xmlChar*)ecp->private_data->messageID); - } - xmlAddChild(header, paos_response); + /* + * Get the following header elements: + * * ecp:Response (required) + */ + lasso_foreach(i, header->Other) { + if (!ecp_response && LASSO_IS_ECP_RESPONSE(i->data)) { + ecp_response = (LassoEcpResponse *)i->data; + } - /* ECP relay state block */ - if (ecp->private_data->relay_state) { - ecp_relay_state = xmlNewNode(NULL, (xmlChar*)"RelayState"); - xmlNodeSetContent(ecp_relay_state, (xmlChar*)ecp->private_data->relay_state); - ecp_ns = xmlNewNs(ecp_relay_state, (xmlChar*)LASSO_ECP_HREF, - (xmlChar*)LASSO_ECP_PREFIX); - xmlSetNs(ecp_relay_state, ecp_ns); - xmlSetNsProp(ecp_relay_state, soap_env_ns, (xmlChar*)"mustUnderstand", - (xmlChar*)"1"); - xmlSetNsProp(ecp_relay_state, soap_env_ns, (xmlChar*)"actor", - (xmlChar*)LASSO_SOAP_ENV_ACTOR); - xmlAddChild(header, ecp_relay_state); + if (ecp_response) break; } - xmlAddChild(new_envelope, body); - lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body, - lasso_xmlnode_to_string(new_envelope, 0, 0)) - lasso_release_doc(doc); - return 0; + goto_cleanup_if_fail_with_rc(ecp_response, LASSO_ECP_ERROR_MISSING_RESPONSE); + + lasso_assign_string(ecp->assertion_consumer_url, ecp_response->AssertionConsumerServiceURL); + + /* + * The ECP MUST confirm the ecp:Response + * AssertionConsumerServiceURL corresponds to the paos:Request + * responseConsumerURL. Since the responseConsumerServiceURL MAY + * be relative and the AssertionConsumerServiceURL is absolute + * some processing/normalization may be required. + * + * If the values do not match the ECP MUST generate a SOAP fault + * and MUST not return the SAML response. + */ + + if (lasso_strisnotequal(ecp->response_consumer_url, ecp_response->AssertionConsumerServiceURL)) { + goto_cleanup_with_rc(LASSO_ECP_ERROR_ASSERTION_CONSUMER_URL_MISMATCH); + } + + /* Generate SOAP headers */ + paos_response = LASSO_PAOS_RESPONSE(lasso_paos_response_new(ecp->message_id)); + lasso_list_add_new_gobject(headers, paos_response); + if (ecp->relaystate) { + ecp_relaystate = LASSO_ECP_RELAYSTATE(lasso_ecp_relay_state_new(ecp->relaystate)); + lasso_list_add_new_gobject(headers, ecp_relaystate); + } + + /* + * Create a SOAP document and assign it to the LassoEcp->msg_body. + * See comment in lasso_ecp_process_authn_request_msg() where the + * profile->msg_body is assigned for an explanation of what is + * being done here. + */ + { + xmlNodePtr xml; + LassoMiscTextNode *misc; + + xml = lasso_node_get_original_xmlnode(LASSO_NODE(samlp2_response)); + + misc = lasso_misc_text_node_new_with_xml_node(xml); + + lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body, + lasso_node_export_to_soap_with_headers(LASSO_NODE(misc), + headers)); + lasso_release_gobject(misc); + } + + /* Set the destination URL for the the PAOS response */ + lasso_assign_string(LASSO_PROFILE(ecp)->msg_url, ecp->response_consumer_url); + + cleanup: + if (rc) { + LassoSoapFault *fault = NULL; + + fault = lasso_soap_fault_new_full(LASSO_SOAP_FAULT_CODE_CLIENT, lasso_strerror(rc)); + lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body, lasso_node_export_to_soap(LASSO_NODE(fault))); + } + + lasso_release_list_of_gobjects(headers); + lasso_release_gobject(envelope); + + return rc; } GType diff --git a/lasso/saml-2.0/ecp.h b/lasso/saml-2.0/ecp.h index 19bcac55..18eb54a3 100644 --- a/lasso/saml-2.0/ecp.h +++ b/lasso/saml-2.0/ecp.h @@ -31,6 +31,7 @@ extern "C" { #include "../xml/xml.h" #include "../id-ff/profile.h" +#include "../xml//saml-2.0/samlp2_idp_list.h" #define LASSO_TYPE_ECP (lasso_ecp_get_type()) #define LASSO_ECP(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LASSO_TYPE_ECP, LassoEcp)) @@ -47,7 +48,16 @@ struct _LassoEcp { LassoProfile parent; /*< public >*/ - gchar *assertionConsumerURL; + gchar *assertion_consumer_url; + gchar *message_id; + gchar *response_consumer_url; + gchar *relaystate; + LassoSaml2NameID *issuer; + gchar *provider_name; + gboolean is_passive; + LassoSamlp2IDPList *sp_idp_list; + GList *known_sp_provided_idp_entries_supporting_ecp; /* of LassoSamlp2IDPEntry */ + GList *known_idp_entity_ids_supporting_ecp; /* of strings */ /*< private >*/ LassoEcpPrivate *private_data; @@ -69,6 +79,19 @@ LASSO_EXPORT lasso_error_t lasso_ecp_process_response_msg(LassoEcp *ecp, LASSO_EXPORT void lasso_ecp_destroy(LassoEcp *ecp); +LASSO_EXPORT gboolean lasso_ecp_is_provider_in_sp_idplist(LassoEcp *ecp, const gchar *entity_id); + +LASSO_EXPORT gboolean lasso_ecp_is_idp_entry_known_idp_supporting_ecp(LassoEcp *ecp, const LassoSamlp2IDPEntry *idp_entry); + +LASSO_EXPORT void lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp(LassoEcp *ecp); + +LASSO_EXPORT gboolean lasso_ecp_has_sp_idplist(LassoEcp *ecp); + +LASSO_EXPORT gchar *lasso_ecp_get_endpoint_url_by_entity_id(LassoEcp *ecp, const gchar *entity_id); + +LASSO_EXPORT int lasso_ecp_process_sp_idp_list(LassoEcp *ecp, const LassoSamlp2IDPList *sp_idp_list); + + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/lasso/saml-2.0/ecpprivate.h b/lasso/saml-2.0/ecpprivate.h index ea318a32..cce24e70 100644 --- a/lasso/saml-2.0/ecpprivate.h +++ b/lasso/saml-2.0/ecpprivate.h @@ -32,8 +32,7 @@ extern "C" { struct _LassoEcpPrivate { - xmlChar *messageID; - xmlChar *relay_state; + gboolean dispose_has_run; }; #ifdef __cplusplus diff --git a/lasso/xml/saml-2.0/samlp2_authn_request.c b/lasso/xml/saml-2.0/samlp2_authn_request.c index 409b4fb1..f782df50 100644 --- a/lasso/xml/saml-2.0/samlp2_authn_request.c +++ b/lasso/xml/saml-2.0/samlp2_authn_request.c @@ -112,6 +112,7 @@ class_init(LassoSamlp2AuthnRequestClass *klass) parent_class = g_type_class_peek_parent(klass); nclass->node_data = g_new0(LassoNodeClassData, 1); + nclass->node_data->keep_xmlnode = TRUE; lasso_node_class_set_nodename(nclass, "AuthnRequest"); lasso_node_class_set_ns(nclass, LASSO_SAML2_PROTOCOL_HREF, LASSO_SAML2_PROTOCOL_PREFIX); lasso_node_class_add_snippets(nclass, schema_snippets);