Implement ECP client functionality

Implement everything needed to support a SAMLv2 ECP client.

Re-implement lasso_ecp_process_authn_request_msg() and
lasso_ecp_process_response_msg() to use the Lasso XML serialization
subsystem with the ECP and PASO LassoNode's introduced earlier. This
replaces one-off explicit direct use of the libxml API with Lasso
common code. In the process provide support for 100% of the ECP and
PAOS SAMLv2 parameters, not just a subset. Include support for
receiving an IDPList from the SP in conjuction with selecting an IdP
known to the ECP client. Add extensive documentation.

Modify LassoSamlp2AuthnRequest to preserve it's original XML (enable
keep_xmlnode flag) so that when serializing the SOAP request the
LassoSamlp2AuthnRequest received from the SP is exactly duplicated.

Add the following internal static utility functions:

is_provider_in_sp_idplist()
is_idp_entry_in_entity_id_list()
intersect_sp_idplist_with_entity_id_list()

Add the following exported utility functions:

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()

Add the following members to the ECP class:

message_id
response_consumer_url
relaystate
issuer
provider_name
is_passive
sp_idp_list
known_sp_provided_idp_entries_supporting_ecp
known_idp_entity_ids_supporting_ecp

Signed-off-by: John Dennis <jdennis@redhat.com>
License: MIT
This commit is contained in:
John Dennis 2015-05-28 12:09:14 -04:00 committed by Benjamin Dauvergne
parent 75b0284c8e
commit 1c31736ded
5 changed files with 747 additions and 128 deletions

View File

@ -5757,6 +5757,12 @@ LASSO_IDWSF2_DSTREF_DATA_RESPONSE_GET_CLASS
<FILE>ecp</FILE>
<TITLE>LassoEcp</TITLE>
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

View File

@ -21,20 +21,74 @@
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
/*
* SAML2 Profile for ECP (Section 4.2) defines these steps for an ECP
* transaction
*
* 1. ECP issues HTTP Request to SP
* 2. SP issues <AuthnRequest> to ECP using PAOS
* 3. ECP determines IdP
* 4. ECP conveys <AuthnRequest> to IdP using SOAP
* 5. IdP identifies principal
* 6. IdP issues <Response> to ECP, targeted at SP using SOAP
* 7. ECP conveys <Response> 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 &lt;samlp:AuthnRequest&gt; to ECP using PAOS
* 3. ECP determines IdP
* 4. ECP conveys &lt;samlp:AuthnRequest&gt; to IdP using SOAP
* 5. IdP identifies principal
* 6. IdP issues &lt;samlp:Response&gt; to ECP, targeted at SP using SOAP
* 7. ECP conveys &lt;samlp:Response&gt; to SP using PAOS
* 8. SP grants or denies access to principal
*
*
*
*
**/
/**
* SECTION:ecp-sp
* @short_description: How to support ECP in an SP
*
*
* |[<!-- language="C" -->
* login = lasso_login_new(server);
* ]|
*/
/**
* SECTION:ecp-idp
* @short_description: How to support ECP in an IdP
*
*
* |[<!-- language="C" -->
* login = lasso_login_new(server);
* ]|
*/
#include "../xml/private.h"
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
#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

View File

@ -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 */

View File

@ -32,8 +32,7 @@ extern "C" {
struct _LassoEcpPrivate
{
xmlChar *messageID;
xmlChar *relay_state;
gboolean dispose_has_run;
};
#ifdef __cplusplus

View File

@ -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);