1 /* $Id$
2  *
3  * Lasso - A free implementation of the Liberty Alliance specifications.
4  *
5  * Copyright (C) 2004-2007 Entr'ouvert
6  * http://lasso.entrouvert.org
7  *
8  * Authors: See AUTHORS file in top-level directory.
9  *
10  * This program is free software; you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation; either version 2 of the License, or
13  * (at your option) any later version.
14  *
15  * This program is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with this program; if not, see <http://www.gnu.org/licenses/>.
22  */
23 
24 /*
25  * SAML2 Profile for ECP (Section 4.2) defines these steps for an ECP
26  * transaction
27  *
28  * 1. ECP issues HTTP Request to SP
29  * 2. SP issues <AuthnRequest> to ECP using PAOS
30  * 3. ECP determines IdP
31  * 4. ECP conveys <AuthnRequest> to IdP using SOAP
32  * 5. IdP identifies principal
33  * 6. IdP issues <Response> to ECP, targeted at SP using SOAP
34  * 7. ECP conveys <Response> to SP using PAOS
35  * 8. SP grants or denies access to principal
36  */
37 
38 /**
39  * SECTION:ecp
40  * @short_description: Enhanced Client or Proxy Profile (SAMLv2)
41  *
42  * # Introduction
43  *
44  * The #LassoEcp object is used to implement a SAMLv2 ECP client.
45  * If you want to support ECP in a SP see [ecp-sp].
46  * If you want to support ECP in a IdP see [ecp-idp].
47  *
48  * # ECP Operational Steps
49  *
50  * SAML2 Profile for ECP (Section 4.2) defines these steps for an ECP
51  * transaction
52  *
53  * 1. ECP issues HTTP Request to SP
54  * 2. SP issues &lt;samlp:AuthnRequest&gt; to ECP using PAOS
55  * 3. ECP determines IdP
56  * 4. ECP conveys &lt;samlp:AuthnRequest&gt; to IdP using SOAP
57  * 5. IdP identifies principal
58  * 6. IdP issues &lt;samlp:Response&gt; to ECP, targeted at SP using SOAP
59  * 7. ECP conveys &lt;samlp:Response&gt; to SP using PAOS
60  * 8. SP grants or denies access to principal
61  *
62  *
63  *
64  *
65  **/
66 
67 /**
68  * SECTION:ecp-sp
69  * @short_description: How to support ECP in an SP
70  *
71  *
72  * |[<!-- language="C" -->
73  * login = lasso_login_new(server);
74  * ]|
75  */
76 
77 /**
78  * SECTION:ecp-idp
79  * @short_description: How to support ECP in an IdP
80  *
81  *
82  * |[<!-- language="C" -->
83  * login = lasso_login_new(server);
84  * ]|
85  */
86 
87 #include "../xml/private.h"
88 #include <libxml/xpath.h>
89 #include <libxml/xpathInternals.h>
90 
91 #include "profileprivate.h"
92 #include "../id-ff/serverprivate.h"
93 
94 #include "ecpprivate.h"
95 
96 #include "ecp.h"
97 #include "../utils.h"
98 
99 #include "../xml/soap-1.1/soap_envelope.h"
100 #include "../xml/soap-1.1/soap_header.h"
101 #include "../xml/soap-1.1/soap_body.h"
102 #include "../xml/soap-1.1/soap_fault.h"
103 #include "../xml/misc_text_node.h"
104 #include "../xml/paos_request.h"
105 #include "../xml/paos_response.h"
106 #include "../xml/ecp/ecp_request.h"
107 #include "../xml/ecp/ecp_response.h"
108 #include "../xml/ecp/ecp_relaystate.h"
109 #include "../xml/lib_authn_request.h"
110 #include "../xml/saml-2.0/samlp2_response.h"
111 #include "../xml/saml-2.0/samlp2_authn_request.h"
112 
113 /*****************************************************************************/
114 /* Prototypes                                                                */
115 /*****************************************************************************/
116 
117 static gboolean
118 is_provider_in_sp_idplist(GList *idp_list, const gchar *entity_id);
119 
120 static gboolean
121 is_idp_entry_in_entity_id_list(GList *entity_id_list, const LassoSamlp2IDPEntry *idp_entry);
122 
123 static GList *
124 intersect_sp_idplist_with_entity_id_list(GList *sp_provided_idp_entries, GList *known_idp_entity_ids_supporting_ecp);
125 
126 /*****************************************************************************/
127 /* public methods                                                            */
128 /*****************************************************************************/
129 
130 /**
131  * lasso_ecp_destroy:
132  * @ecp: a #LassoEcp
133  *
134  * Destroys a #LassoEcp object
135  *
136  **/
137 void
lasso_ecp_destroy(LassoEcp * ecp)138 lasso_ecp_destroy(LassoEcp *ecp)
139 {
140 	lasso_node_destroy(LASSO_NODE(ecp));
141 }
142 
143 /**
144  * lasso_ecp_is_provider_in_sp_idplist:
145  * @ecp: a #LassoEcp
146  * @entity_id: EntityID to check if member of #LassoEcp.IDPList
147  *
148  * Check to see if the provider with @entity_id is in the
149  * ecp IDPList returned by the SP.
150  *
151  * Return value: TRUE if @entity_id is in #LassoEcp.IDPList, FALSE otherwise
152  */
153 gboolean
lasso_ecp_is_provider_in_sp_idplist(LassoEcp * ecp,const gchar * entity_id)154 lasso_ecp_is_provider_in_sp_idplist(LassoEcp *ecp, const gchar *entity_id) {
155 	return is_provider_in_sp_idplist(ecp->sp_idp_list->IDPEntry, entity_id);
156 }
157 
158 /**
159  * lasso_ecp_is_idp_entry_known_idp_supporting_ecp:
160  * @ecp: a #LassoEcp
161  * @idp_entry: #LassoSamlp2IDPEntry to check if member of @entity_id_list
162  *
163  * Check to see if the @idp_entry is in the @entity_id_list
164  *
165  *
166  * Return value: TRUE if @entity_id is in @idp_list, FALSE otherwise
167  */
168 gboolean
lasso_ecp_is_idp_entry_known_idp_supporting_ecp(LassoEcp * ecp,const LassoSamlp2IDPEntry * idp_entry)169 lasso_ecp_is_idp_entry_known_idp_supporting_ecp(LassoEcp *ecp, const LassoSamlp2IDPEntry *idp_entry) {
170 	return is_idp_entry_in_entity_id_list(ecp->known_idp_entity_ids_supporting_ecp, idp_entry);
171 }
172 
173 /**
174  * lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp:
175  * @ecp: a #LassoEcp
176  *
177  * The SP may provide a list of #LassoSamlp2IDPEntry
178  * (#LassoEcp.sp_idp_list) which it trusts. The ECP client
179  * has a list of IDP EntityID's it knows supports ECP
180  * (#LassoEcp.known_idp_entity_ids_supporting_ecp).  The set of
181  * possible IDP's which can service the SP's authn request are the
182  * interesection of these two lists (the IDP's the SP approves and
183  * IDP's the ECP knows about). This find the common members between
184  * the two lists and assign them to
185  * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp.
186  */
187 void
lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp(LassoEcp * ecp)188 lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp(LassoEcp *ecp)
189 {
190 	lasso_assign_new_list_of_strings(ecp->known_sp_provided_idp_entries_supporting_ecp,
191 		intersect_sp_idplist_with_entity_id_list(ecp->sp_idp_list ? ecp->sp_idp_list->IDPEntry : NULL,
192 												 ecp->known_idp_entity_ids_supporting_ecp));
193 }
194 
195 /**
196  * lasso_ecp_has_sp_idplist:
197  * @ecp: a #LassoEcp
198  *
199  * Returns TRUE if the SP provided an IDP List, FALSE otherwise.
200  */
201 gboolean
lasso_ecp_has_sp_idplist(LassoEcp * ecp)202 lasso_ecp_has_sp_idplist(LassoEcp *ecp)
203 {
204 	return ecp->sp_idp_list && ecp->sp_idp_list->IDPEntry != NULL;
205 }
206 
207 /**
208  * lasso_ecp_get_endpoint_url_by_entity_id:
209  * @ecp: a #LassoEcp
210  * @entity_id: the EntityID of the IdP
211  *
212  * Returns the SingleSignOnService SOAP endpoint URL for the specified
213  * @entity_id. If the provider cannot be found or if the provider does
214  * not have a matching endpoint NULL will be returned.
215  *
216  * Returns: url (must be freed by caller)
217  */
218 gchar *
lasso_ecp_get_endpoint_url_by_entity_id(LassoEcp * ecp,const gchar * entity_id)219 lasso_ecp_get_endpoint_url_by_entity_id(LassoEcp *ecp, const gchar *entity_id)
220 {
221 	LassoProfile *profile;
222 
223 	profile = LASSO_PROFILE(ecp);
224 
225 	return lasso_server_get_endpoint_url_by_id(profile->server, entity_id,
226 											   "SingleSignOnService SOAP");
227 }
228 
229 /**
230  * lasso_ecp_process_sp_idp_list:
231  * @ecp: a #LassoEcp
232  *
233  * The SP may optionally send a list of IdP's it trusts in ecp:IDPList.
234  * The ecp:IDPList may not be complete if the IDPList.GetComplete is
235  * non-NULL. If so the IDPList.GetComplete is a URL where a complete
236  * IDPList may be fetched.
237  *
238  * Whenever the IDPList is updated this function needs to be called
239  * because it sets the
240  * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp and the
241  * default IdP URL (#LassoProfile.msg_url).
242  *
243  * The #LassoEcp client has a list of IdP's it knows supports ECP
244  * (#LassoEcp.known_idp_entity_ids_supporting_ecp). The set of IdP's
245  * available to select from should be those in common between SP
246  * provided IdP list and those known by this ECP client to support
247  * ECP.
248  *
249  * This routine sets the
250  * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp list to the
251  * common members (e.g. intersection) of the SP provided IdP list and
252  * the list of known IdP's supporting ECP.
253  *
254  * A default IdP will be selected and it's endpoint URL will be
255  * assigned to #LassoProfile.msg_url.
256  *
257  * If the SP provided an IDP list then the default URL will be taken
258  * from first IDPEntry in
259  * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp otherwise
260  * it will be taken from #LassoEcp.known_idp_entity_ids_supporting_ecp.
261  *
262  */
263 int
lasso_ecp_process_sp_idp_list(LassoEcp * ecp,const LassoSamlp2IDPList * sp_idp_list)264 lasso_ecp_process_sp_idp_list(LassoEcp *ecp, const LassoSamlp2IDPList *sp_idp_list)
265 {
266 	int rc = 0;
267 	LassoProfile *profile;
268 	gchar *provider_id = NULL;
269 	gchar *url;
270 
271 	profile = LASSO_PROFILE(ecp);
272 
273 	lasso_assign_gobject(ecp->sp_idp_list, sp_idp_list);
274 
275 	/* Build a list of IdP's which are common between the SP and those we know support ECP */
276 	lasso_ecp_set_known_sp_provided_idp_entries_supporting_ecp(ecp);
277 
278 	/* Select a default IdP */
279 	provider_id = NULL;
280 	if (lasso_ecp_has_sp_idplist(ecp)) {
281 		/* Select first IDP provided by SP that is in our IDP list */
282 		if (ecp->known_sp_provided_idp_entries_supporting_ecp) {
283 			provider_id = ((LassoSamlp2IDPEntry*)ecp->known_sp_provided_idp_entries_supporting_ecp->data)->ProviderID;
284 		}
285 	}
286 	if (!provider_id) {
287 		/* Select first IDP from our IDP list */
288 		if (ecp->known_idp_entity_ids_supporting_ecp) {
289 			provider_id = ecp->known_idp_entity_ids_supporting_ecp->data;
290 		}
291 	}
292 
293 	/* If we have a default IdP assign it's ECP URL to the profile->msg_url */
294 	lasso_release_string(profile->msg_url)
295 	if (provider_id) {
296 		url = lasso_ecp_get_endpoint_url_by_entity_id(ecp, provider_id);
297 		lasso_assign_new_string(profile->msg_url, url);
298 	}
299 	return rc;
300 }
301 
302 /*****************************************************************************/
303 /* private methods                                                           */
304 /*****************************************************************************/
305 
306 static LassoNodeClass *parent_class = NULL;
307 
308 /**
309  * compare_idp_entry_to_entity_id:
310  *
311  * Helper function for is_provider_in_sp_idplist().
312  */
313 static gboolean
compare_idp_entry_to_entity_id(gconstpointer a,gconstpointer b)314 compare_idp_entry_to_entity_id(gconstpointer a, gconstpointer b)
315 {
316 	const LassoSamlp2IDPEntry *idp_entry = LASSO_SAMLP2_IDP_ENTRY(a);
317 	const gchar *entity_id = b;
318 
319 	return g_strcmp0(idp_entry->ProviderID, entity_id);
320 }
321 
322 /**
323  * is_provider_in_sp_idplist:
324  * @idp_list: GList of LassoSamlp2IDPEntry
325  * @entity_id: EntityID to check if member of @idp_list
326  *
327  * Check if the provider with @entity_id is in the #idp_list.
328  *
329  * Return value: TRUE if @entity_id is in @idp_list, FALSE otherwise
330  */
331 static gboolean
is_provider_in_sp_idplist(GList * idp_list,const gchar * entity_id)332 is_provider_in_sp_idplist(GList *idp_list, const gchar *entity_id) {
333 	return g_list_find_custom(idp_list, entity_id, compare_idp_entry_to_entity_id) == NULL ? FALSE : TRUE;
334 }
335 
336 /**
337  * compare_entity_id_to_idp_entry:
338  *
339  * Helper function for is_idp_entry_in_entity_id_list().
340  */
341 static gboolean
compare_entity_id_to_idp_entry(gconstpointer a,gconstpointer b)342 compare_entity_id_to_idp_entry(gconstpointer a, gconstpointer b)
343 {
344 	const gchar *entity_id = a;
345 	const LassoSamlp2IDPEntry *idp_entry = LASSO_SAMLP2_IDP_ENTRY(b);
346 
347 	return g_strcmp0(entity_id, idp_entry->ProviderID);
348 }
349 
350 /**
351  * is_idp_entry_in_entity_id_list:
352  * @entity_id_list: #GList of entity id's
353  * @idp_entry: #LassoSamlp2IDPEntry to check if member of @entity_id_list
354  *
355  * Check if the provider with @entity_id is in the #idp_list.
356  *
357  * Return value: TRUE if @entity_id is in @idp_list, FALSE otherwise
358  */
359 static gboolean
is_idp_entry_in_entity_id_list(GList * entity_id_list,const LassoSamlp2IDPEntry * idp_entry)360 is_idp_entry_in_entity_id_list(GList *entity_id_list, const LassoSamlp2IDPEntry *idp_entry) {
361 	return g_list_find_custom(entity_id_list, idp_entry, compare_entity_id_to_idp_entry) == NULL ? FALSE : TRUE;
362 }
363 
364 /*
365  * intersect_sp_idplist_with_entity_id_list:
366  * @sp_provided_idp_entries: #GList of #LassoSamlp2IDPEntry
367  * @known_idp_entity_ids_supporting_ecp: #GList of entity id's
368  *
369  * The SP may provide a list of #LassoSamlp2IDPEntry which it
370  * trusts. The ECP client has a list of IDP EntityID's it knows
371  * supports ECP.  The set of possible IDP's which can service the SP's
372  * authn request are the interesection of these two lists (the IDP's
373  * the SP approves and IDP's the ECP knows about). This function
374  * accepts the SP's IDPEntry list and returns a new list containing
375  * only those the ECP client knows about. The returned list must be
376  * freed with lasso_release_list_of_gobjects().
377  *
378  * Return value: GList of #LassoSamlp2IDPEntry
379  * (caller must free with lasso_release_list_of_gobjects())
380  */
381 static GList *
intersect_sp_idplist_with_entity_id_list(GList * sp_provided_idp_entries,GList * known_idp_entity_ids_supporting_ecp)382 intersect_sp_idplist_with_entity_id_list(GList *sp_provided_idp_entries, GList *known_idp_entity_ids_supporting_ecp)
383 {
384 	GList *i;
385 	GList *new_list = NULL;
386 
387 	lasso_foreach(i, sp_provided_idp_entries) {
388 		LassoSamlp2IDPEntry *idp_entry = i->data;
389 		if (is_idp_entry_in_entity_id_list(known_idp_entity_ids_supporting_ecp, idp_entry)) {
390 			lasso_list_add_gobject(new_list, idp_entry);
391 		}
392 	}
393 	return new_list;
394 }
395 
396 /*****************************************************************************/
397 /* overridden parent class methods                                           */
398 /*****************************************************************************/
399 
400 static void
dispose(GObject * object)401 dispose(GObject *object)
402 {
403 	LassoEcp *ecp = LASSO_ECP(object);
404 
405 	if (ecp->private_data->dispose_has_run) {
406 		return;
407 	}
408 	ecp->private_data->dispose_has_run = TRUE;
409 
410 	lasso_release_string(ecp->assertion_consumer_url);
411 	lasso_release_string(ecp->message_id);
412 	lasso_release_string(ecp->response_consumer_url);
413 	lasso_release_string(ecp->relaystate);
414 	lasso_release_gobject(ecp->issuer);
415 	lasso_release_string(ecp->provider_name);
416 	lasso_release_gobject(ecp->sp_idp_list);
417 	lasso_release_list_of_gobjects(ecp->known_sp_provided_idp_entries_supporting_ecp);
418 	lasso_release_list_of_strings(ecp->known_idp_entity_ids_supporting_ecp);
419 
420 	G_OBJECT_CLASS(parent_class)->dispose(G_OBJECT(ecp));
421 }
422 
423 static void
finalize(GObject * object)424 finalize(GObject *object)
425 {
426 	LassoEcp *ecp = LASSO_ECP(object);
427 	lasso_release(ecp->private_data);
428 
429 	G_OBJECT_CLASS(parent_class)->finalize(object);
430 }
431 
432 /*****************************************************************************/
433 /* instance and class init functions                                         */
434 /*****************************************************************************/
435 
436 static void
instance_init(LassoEcp * ecp)437 instance_init(LassoEcp *ecp)
438 {
439 	ecp->private_data = g_new0(LassoEcpPrivate, 1);
440 }
441 
442 static void
class_init(LassoEcpClass * klass,void * unused G_GNUC_UNUSED)443 class_init(LassoEcpClass *klass, void *unused G_GNUC_UNUSED)
444 {
445 	LassoNodeClass *nclass = LASSO_NODE_CLASS(klass);
446 	parent_class = g_type_class_peek_parent(klass);
447 
448 	nclass->node_data = g_new0(LassoNodeClassData, 1);
449 	lasso_node_class_set_nodename(nclass, "Ecp");
450 	lasso_node_class_set_ns(nclass, LASSO_LASSO_HREF, LASSO_LASSO_PREFIX);
451 	G_OBJECT_CLASS(klass)->dispose = dispose;
452 	G_OBJECT_CLASS(klass)->finalize = finalize;
453 }
454 /**
455  * lasso_ecp_process_authn_request_msg:
456  * @ecp: this #LassoEcp object
457  * @authn_request_msg: the PAOS authn request received from the SP
458  *
459  * This function implements the following ECP step:
460  * ECP Step 3, ECP determines IdP
461  * ECP Step 4, parse SP PAOS Authn request, build SOAP for IdP
462  *
463  * This is to be used in an ECP client. The @authn_request_msg is the
464  * SOAP PAOS message received from the SP in response to a resource
465  * request with an HTTP Accept header indicating PAOS support.
466  *
467  * The following actions are implemented:
468  *
469  * * Extract the samlp:AuthnRequest from the SOAP body and build a
470  *   new SOAP message containing the samlp:AuthnRequest which will
471  *   be forwarded to the IdP. This new SOAP message is stored in the
472  *   #LassoProfile.msg_body.
473  *
474  * * Parse the SOAP header which will contain a paos:Request, a
475  *   ecp:Request and optionally a ecp:RelayState. Some of the data
476  *   in these headers need to be preserved for later processing steps.
477  *
478  *   1. The paos:Request.responseConsumerURL is copied to the
479  *      #LassoEcp.response_consumer_url. This is necessary because the
480  *      ECP client MUST assure it matches the
481  *      ecp:Response.AssertionConsumerServiceURL returned by the IdP to
482  *      prevent man-in-the-middle attacks. It must also match the
483  *      samlp:AuthnRequest.AssertionConsumerServiceURL.
484  *
485  *   2. If the paos:Request contained a messageID it is copied to
486  *      #LassoEcp.message_id so it can be returned in the subsequent
487  *      paos:Response.refToMessageID. This allows a provider to
488  *      correlate messages.
489  *
490  *   3. If an ecp:RelayState is present it is copied to
491  *      #LassoEcp.relaystate. This is necessary because in step 7 when
492  *      the ECP responds to the SP it must include RelayState provided in
493  *      the request.
494  *
495  * * In addition the following items are copied to the #LassoEcp for
496  *   informational purposes:
497  *
498  *   * #LassoEcp.issuer = ecp:Request.Issuer
499  *
500  *   * #LassoEcp.provider_name = ecp:Request.ProviderName
501  *
502  *   * #LassoEcp.is_passive = ecp:Request.IsPassive
503  *
504  *   * #LassoEcp.sp_idp_list = ecp:Request.IDPList
505  *
506  * # IdP Selection
507  *
508  * In Step 3. The ECP must determine the IdP to forward the
509  * AuthnRequest to. There are two sets of IdP's which come into
510  * play. The ECP client has a set of IdP's it knows about because
511  * their metadata has been loaded into the #LassoServer object. The SP
512  * may optionally send a list of IdP's in the ecp:Request that it
513  * trusts.
514  *
515  * The selected IdP *must* be one of the IdP's loaded into the
516  * #LassoServer object from metadata because the IdP endpoints must be
517  * known. Furthermore the IdP *must* support the SingleSignOnService
518  * using the SOAP binding. Therefore the known IdP's are filtered for
519  * those that match this criteria and a list of their EntityID's are
520  * assigned to #LassoEcp.known_idp_entity_ids_supporting_ecp. The
521  * selected IdP *must* be a member of this list.
522  *
523  * The SP may optionally send a list of IdP's it trusts. If the SP
524  * sends an IDPList the selected IdP should be a member of this list
525  * and from above we know it must also be a member of the
526  * #LassoEcp.known_idp_entity_ids_supporting_ecp. Therefore the
527  * #LassoEcp.known_sp_provided_idp_entries_supporting_ecp list is set
528  * to the common members (e.g. intersection) of the SP provided IdP
529  * list and the list of known IdP's supporting ECP.
530  *
531  * When making an IdP selection if the SP provided an IdP List (use
532  * #LassoEcp.lasso_ecp_has_sp_idplist()) then it should be selected
533  * from the #LassoEcp.known_sp_provided_idp_entries_supporting_ecp
534  * list. Otherwise the IdP should be selected from
535  * #LassoEcp.known_idp_entity_ids_supporting_ecp.
536  *
537  * A default IdP will be selected using the above logic by picking the
538  * first IdP in the appropriate list, it's endpoint URL will be
539  * assigned to #LassoProfile.msg_url. The above processing is
540  * implemented by #LassoEcp.lasso_ecp_process_sp_idp_list() and if the
541  * SP IDPList is updated this routine should be called.
542  *
543  * A note about the 3 IdP lists. The #LassoEcp.sp_idp_list.IDPList
544  * and #LassoEcp.known_sp_provided_idp_entries_supporting_ecp are
545  * #GList's of #LassoSamlp2IDPEntry object which have a ProviderID,
546  * Name, and Loc attribute. You may wish to use this SP provided
547  * information when making a decision or presenting in a user
548  * interface that allows a user to make a choice. The
549  * #LassoEcp.known_idp_entity_ids_supporting_ecp is a #GList of
550  * EntityID strings.
551  *
552  * Given the EntityID of an IdP you can get the ECP endpoint by
553  * calling #LassoEcp.lasso_ecp_get_endpoint_url_by_entity_id()
554  *
555  * # Results
556  *
557  * After a successful return from this call you are ready to complete
558  * Step 4. and forward the request the IdP.
559  *
560  * The URL to send to the request to will be #LassoProfile.msg_url (if
561  * you accept the default IdP) and the body of the message to post
562  * will be #LassoProfile.msg_body.
563  *
564  *
565  * # Side Effects
566  *
567  * After a successful return the #LassoEcp object will be updated with:
568  *
569  * * ecp->response_consumer_url = paos_request->responseConsumerURL
570  * * ecp->message_id = paos_request->messageID
571  * * ecp->relaystate = ecp_relaystate->RelayState
572  * * ecp->issuer = ecp_request->Issue
573  * * ecp->provider_name = ecp_request->ProviderName
574  * * ecp->is_passive = ecp_request->IsPassive
575  * * ecp->known_idp_entity_ids_supporting_ecp
576  * * ecp->sp_idp_list = ecp_request->IDPList
577  * * ecp->known_sp_provided_idp_entries_supporting_ecp
578  *
579  */
580 int
lasso_ecp_process_authn_request_msg(LassoEcp * ecp,const char * authn_request_msg)581 lasso_ecp_process_authn_request_msg(LassoEcp *ecp, const char *authn_request_msg)
582 {
583 	int rc = 0;
584 	LassoSoapEnvelope *envelope = NULL;
585 	LassoSoapHeader *header = NULL;
586 	LassoSoapBody *body = NULL;
587 	LassoPaosRequest *paos_request = NULL;
588 	LassoEcpRequest *ecp_request = NULL;
589 	LassoEcpRelayState *ecp_relaystate = NULL;
590 	LassoSamlp2AuthnRequest *authn_request = NULL;
591 	GList *i;
592 	LassoProfile *profile;
593 
594 	g_return_val_if_fail(LASSO_IS_ECP(ecp), LASSO_PARAM_ERROR_BAD_TYPE_OR_NULL_OBJ);
595 	g_return_val_if_fail(authn_request_msg != NULL, LASSO_PARAM_ERROR_INVALID_VALUE);
596 
597 	profile = LASSO_PROFILE(ecp);
598 
599 	/* Get the SOAP envelope */
600 	lasso_extract_node_or_fail(envelope, lasso_soap_envelope_new_from_message(authn_request_msg),
601 							   SOAP_ENVELOPE, LASSO_PROFILE_ERROR_INVALID_SOAP_MSG);
602 
603 	/* Get the SOAP body */
604 	lasso_extract_node_or_fail(body, envelope->Body, SOAP_BODY,
605 							   LASSO_SOAP_ERROR_MISSING_BODY);
606 	goto_cleanup_if_fail_with_rc(body->any && LASSO_IS_NODE(body->any->data),
607 								 LASSO_SOAP_ERROR_MISSING_BODY);
608 	lasso_extract_node_or_fail(authn_request, body->any->data, SAMLP2_AUTHN_REQUEST,
609 							   LASSO_ECP_ERROR_MISSING_AUTHN_REQUEST);
610 
611 	/* Get the SOAP header */
612 	lasso_extract_node_or_fail(header, envelope->Header, SOAP_HEADER,
613 							   LASSO_SOAP_ERROR_MISSING_HEADER);
614 	goto_cleanup_if_fail_with_rc(header->Other && LASSO_IS_NODE(header->Other->data),
615 								 LASSO_SOAP_ERROR_MISSING_HEADER);
616 
617 	/*
618 	 * Get the following header elements:
619 	 *   * paos:Request (required)
620 	 *   * ecp:Request (required)
621 	 *   * ecp:RelayState (optional)
622 	 */
623 	lasso_foreach(i, header->Other) {
624 		if (!paos_request && LASSO_IS_PAOS_REQUEST(i->data)) {
625 			paos_request = (LassoPaosRequest *)i->data;
626 		} else if (!ecp_request && LASSO_IS_ECP_REQUEST(i->data)) {
627 			ecp_request = (LassoEcpRequest *)i->data;
628 		} else if (!ecp_relaystate && LASSO_IS_ECP_RELAYSTATE(i->data)) {
629 			ecp_relaystate = (LassoEcpRelayState *)i->data;
630 		}
631 
632 		if (ecp_relaystate && ecp_request && paos_request) break;
633 	}
634 
635 	goto_cleanup_if_fail_with_rc(paos_request, LASSO_PAOS_ERROR_MISSING_REQUEST);
636 	goto_cleanup_if_fail_with_rc(ecp_request, LASSO_ECP_ERROR_MISSING_REQUEST);
637 
638     /* Copy data for later use */
639 	if (paos_request->responseConsumerURL) {
640 		lasso_assign_string(ecp->response_consumer_url, paos_request->responseConsumerURL);
641 	} else {
642 		goto_cleanup_with_rc(LASSO_PAOS_ERROR_MISSING_RESPONSE_CONSUMER_URL);
643 	}
644 
645 	if (paos_request->messageID) {
646 		lasso_assign_string(ecp->message_id, paos_request->messageID);
647 	}
648 
649 	if (ecp_relaystate) {
650 		lasso_assign_string(ecp->relaystate, ecp_relaystate->RelayState);
651 	}
652 
653 	lasso_assign_gobject(ecp->issuer, ecp_request->Issuer);
654 	lasso_assign_string(ecp->provider_name, ecp_request->ProviderName);
655 	ecp->is_passive = ecp_request->IsPassive;
656 
657 	/*
658 	 * Build a SOAP envelope whose body contains the original
659 	 * AuthnRequest received from the SP. The obvious solution is to
660 	 * serialize into XML the LassoSamlp2AuthnRequest LassoNode that
661 	 * was serialized from XML when we parsed the PAOS request
662 	 * (e.g. lasso_node_export_to_soap(LASSO_NODE(authn_request))) but
663 	 * that won't work because XML serialization is not symmetric.
664 	 * Serializing from XML into a LassoNode and then serializing the
665 	 * LassoNode back into XML does not produce the originial XML
666 	 * content. This is mostly due to the presence of signatures. In
667 	 * order to forward the *exact* same XML AuthnRequest we received
668 	 * from the SP to the IdP we mark the LassoSamlp2AuthnRequest with
669 	 * a flag indicating it's xmlNode needs to be preserved
670 	 * (e.g. keep_xmlnode = TRUE). We copy the xmlNode into a special
671 	 * LassoNode (LassoMiscTextNode) which is capable of preserving
672 	 * the exact xmlNode thus insuring no modification was made to the
673 	 * content.
674      *
675      * We assign the SOAP message to the profile->msg_body so it's
676      * available for transmitting to the IdP.
677      */
678 
679 	{
680 		xmlNodePtr xml;
681 		LassoMiscTextNode *misc;
682 
683 		xml = lasso_node_get_original_xmlnode(LASSO_NODE(authn_request));
684 
685 		misc = lasso_misc_text_node_new_with_xml_node(xml);
686 		lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body,
687 								lasso_node_export_to_soap(LASSO_NODE(misc)));
688 		lasso_release_gobject(misc);
689 	}
690 
691 
692 	/* Set up for IdP selection, build IdP lists, make default IdP choice */
693 
694 	/* Filter our server's list of IdP's to only include those that support ECP */
695 	ecp->known_idp_entity_ids_supporting_ecp = lasso_server_get_filtered_provider_list(
696 		profile->server, LASSO_PROVIDER_ROLE_IDP, LASSO_MD_PROTOCOL_TYPE_SINGLE_SIGN_ON,
697 		LASSO_HTTP_METHOD_SOAP);
698 
699 	/* Update the IdP lists and select a default URL */
700 	lasso_ecp_process_sp_idp_list(ecp, ecp_request->IDPList);
701 
702  cleanup:
703 	lasso_release_gobject(envelope);
704 
705 	return rc;
706 }
707 
708 /**
709  * lasso_ecp_process_response_msg:
710  * @ecp: this #LassoEcp object
711  * @response_msg: the SOAP response from the IdP
712  *
713  *
714  * The function implements ECP Step 7; parse IdP SOAP response and
715  * build PAOS response for SP.
716  *
717  * See SAML Profile Section 4.2.4.5 PAOS Response Header Block: ECP to SP
718  *
719  * This is to be used in an ECP client. The @response_msg parameter
720  * contains the SOAP response from the IdP. We extract the ECP Header
721  * Block and body from it. We will generate a new PAOS message to send
722  * to the SP, the SOAP header will contain a paos:Response. If we
723  * received a paos:Request.MessageID in Step. 4 from the SP then we
724  * will copy it back to the paos:Response.refToMessageID. If we
725  * received a RelayState we will add that to the SOAP header as well.
726  *
727  * To prevent a man-in-the-middle attack we verify the
728  * responseConsumerURL we received in Step 4 matches the
729  * ecp:Response.AssertionConsumerServiceURL we just received back from
730  * the IdP. If they do not match we return a
731  * #LASSO_ECP_ERROR_ASSERTION_CONSUMER_URL_MISMATCH error and set the
732  * #LassoProvider.msg_body to the appropriate SOAP fault.
733  *
734  * The new PAOS message for the SP we are buiding contains the IdP
735  * response in the new SOAP body and the new SOAP headers will contain
736  * a paso:Response and optionally an ecp:RelayState.
737  *
738  * After a successful return from this call you are ready to complete
739  * Step 7. and forward the response to the SP.
740  *
741  * The PASO message is assigned to the #LassoProvider.msg_body and
742  * the desination URL is assigned to the #LassoProvider.msg_url.
743  *
744  * # Side Effects
745  *
746  * After a successful return the #LassoEcp object will be updated with:
747  *
748  * * ecp->assertion_consumer_url = ecp_response->AssertionConsumerServiceURL
749  * * ecp.profile.msg_url = ecp->assertion_consumer_url
750  * * ecp.profile.msg_body_url = PAOS response to SP
751  */
752 int
lasso_ecp_process_response_msg(LassoEcp * ecp,const char * response_msg)753 lasso_ecp_process_response_msg(LassoEcp *ecp, const char *response_msg)
754 {
755 	int rc = 0;
756 	LassoSoapEnvelope *envelope = NULL;
757 	LassoSoapHeader *header = NULL;
758 	LassoSoapBody *body = NULL;
759 	LassoPaosResponse *paos_response = NULL;
760 	LassoEcpResponse *ecp_response = NULL;
761 	LassoEcpRelayState *ecp_relaystate = NULL;
762 	LassoSamlp2Response *samlp2_response = NULL;
763 	GList *i;
764 	GList *headers = NULL;
765 
766 	g_return_val_if_fail(LASSO_IS_ECP(ecp), LASSO_PARAM_ERROR_BAD_TYPE_OR_NULL_OBJ);
767 	g_return_val_if_fail(response_msg != NULL, LASSO_PARAM_ERROR_INVALID_VALUE);
768 
769 	/* Get the SOAP envelope */
770 	lasso_extract_node_or_fail(envelope, lasso_soap_envelope_new_from_message(response_msg),
771 							   SOAP_ENVELOPE, LASSO_PROFILE_ERROR_INVALID_SOAP_MSG);
772 
773 	/* Get the SOAP body */
774 	lasso_extract_node_or_fail(body, envelope->Body, SOAP_BODY,
775 							   LASSO_SOAP_ERROR_MISSING_BODY);
776 	goto_cleanup_if_fail_with_rc(body->any && LASSO_IS_NODE(body->any->data),
777 								 LASSO_SOAP_ERROR_MISSING_BODY);
778 	lasso_extract_node_or_fail(samlp2_response, body->any->data, SAMLP2_RESPONSE,
779 							   LASSO_ECP_ERROR_MISSING_SAML_RESPONSE);
780 
781 	/* Get the SOAP header */
782 	lasso_extract_node_or_fail(header, envelope->Header, SOAP_HEADER,
783 							   LASSO_SOAP_ERROR_MISSING_HEADER);
784 	goto_cleanup_if_fail_with_rc(header->Other && LASSO_IS_NODE(header->Other->data),
785 								 LASSO_SOAP_ERROR_MISSING_HEADER);
786 
787 	/*
788 	 * Get the following header elements:
789 	 *   * ecp:Response (required)
790 	 */
791 	lasso_foreach(i, header->Other) {
792 		if (!ecp_response && LASSO_IS_ECP_RESPONSE(i->data)) {
793 			ecp_response = (LassoEcpResponse *)i->data;
794 		}
795 
796 		if (ecp_response) break;
797 	}
798 
799 	goto_cleanup_if_fail_with_rc(ecp_response, LASSO_ECP_ERROR_MISSING_RESPONSE);
800 
801 	lasso_assign_string(ecp->assertion_consumer_url, ecp_response->AssertionConsumerServiceURL);
802 
803 	/*
804 	 * The ECP MUST confirm the ecp:Response
805      * AssertionConsumerServiceURL corresponds to the paos:Request
806      * responseConsumerURL. Since the responseConsumerServiceURL MAY
807      * be relative and the AssertionConsumerServiceURL is absolute
808      * some processing/normalization may be required.
809      *
810      * If the values do not match the ECP MUST generate a SOAP fault
811      * and MUST not return the SAML response.
812 	 */
813 
814 	if (lasso_strisnotequal(ecp->response_consumer_url, ecp_response->AssertionConsumerServiceURL)) {
815 		goto_cleanup_with_rc(LASSO_ECP_ERROR_ASSERTION_CONSUMER_URL_MISMATCH);
816 	}
817 
818 	/* Generate SOAP headers */
819 	paos_response = LASSO_PAOS_RESPONSE(lasso_paos_response_new(ecp->message_id));
820 	lasso_list_add_new_gobject(headers, paos_response);
821 	if (ecp->relaystate) {
822 		ecp_relaystate = LASSO_ECP_RELAYSTATE(lasso_ecp_relay_state_new(ecp->relaystate));
823 		lasso_list_add_new_gobject(headers, ecp_relaystate);
824 	}
825 
826 	/*
827 	 * Create a SOAP document and assign it to the LassoEcp->msg_body.
828 	 * See comment in lasso_ecp_process_authn_request_msg() where the
829 	 * profile->msg_body is assigned for an explanation of what is
830 	 * being done here.
831 	 */
832 	{
833 		xmlNodePtr xml;
834 		LassoMiscTextNode *misc;
835 
836 		xml = lasso_node_get_original_xmlnode(LASSO_NODE(samlp2_response));
837 
838 		misc = lasso_misc_text_node_new_with_xml_node(xml);
839 
840 		lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body,
841 								lasso_node_export_to_soap_with_headers(LASSO_NODE(misc),
842 																	   headers));
843 		lasso_release_gobject(misc);
844 	}
845 
846 	/* Set the destination URL for the the PAOS response */
847 	lasso_assign_string(LASSO_PROFILE(ecp)->msg_url, ecp->response_consumer_url);
848 
849  cleanup:
850 	if (rc) {
851 		LassoSoapFault *fault = NULL;
852 
853 		fault = lasso_soap_fault_new_full(LASSO_SOAP_FAULT_CODE_CLIENT, lasso_strerror(rc));
854 		lasso_assign_new_string(LASSO_PROFILE(ecp)->msg_body, lasso_node_export_to_soap(LASSO_NODE(fault)));
855 	}
856 
857 	lasso_release_list_of_gobjects(headers);
858 	lasso_release_gobject(envelope);
859 
860 	return rc;
861 }
862 
863 GType
lasso_ecp_get_type()864 lasso_ecp_get_type()
865 {
866 	static GType this_type = 0;
867 
868 	if (!this_type) {
869 		static const GTypeInfo this_info = {
870 			sizeof (LassoEcpClass),
871 			NULL,
872 			NULL,
873 			(GClassInitFunc) class_init,
874 			NULL,
875 			NULL,
876 			sizeof(LassoEcp),
877 			0,
878 			(GInstanceInitFunc) instance_init,
879 			NULL
880 		};
881 
882 		this_type = g_type_register_static(LASSO_TYPE_PROFILE,
883 				"LassoEcp", &this_info, 0);
884 	}
885 	return this_type;
886 }
887 
888 /**
889  * lasso_ecp_new
890  *
891  * Creates a new #LassoEcp.
892  *
893  * Return value: a newly created #LassoEcp object; or NULL if an error
894  *     occured
895  **/
896 LassoEcp*
lasso_ecp_new(LassoServer * server)897 lasso_ecp_new(LassoServer *server)
898 {
899 	LassoEcp *ecp;
900 
901 	ecp = g_object_new(LASSO_TYPE_ECP, NULL);
902 	LASSO_PROFILE(ecp)->server = g_object_ref(server);
903 
904 	return ecp;
905 }
906