2 * @file sipe-webticket.c
6 * Copyright (C) 2011-12 SIPE Project <http://sipe.sourceforge.net/>
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 * Specification references:
26 * - [MS-OCAUTHWS]: http://msdn.microsoft.com/en-us/library/ff595592.aspx
27 * - MS Tech-Ed Europe 2010 "UNC310: Microsoft Lync 2010 Technology Explained"
28 * http://ecn.channel9.msdn.com/o9/te/Europe/2010/pptx/unc310.pptx
36 #include "sipe-common.h"
37 #include "sipe-backend.h"
38 #include "sipe-core.h"
39 #include "sipe-core-private.h"
40 #include "sipe-digest.h"
43 #include "sipe-webticket.h"
44 #include "sipe-utils.h"
47 struct webticket_queued_data
{
48 sipe_webticket_callback
*callback
;
49 gpointer callback_data
;
52 struct webticket_callback_data
{
54 const gchar
*service_port
;
55 gchar
*service_auth_uri
;
57 gchar
*webticket_negotiate_uri
;
58 gchar
*webticket_fedbearer_uri
;
60 gboolean tried_fedbearer
;
61 gboolean requires_signing
;
65 TOKEN_STATE_FEDERATION
,
66 TOKEN_STATE_FED_BEARER
,
69 struct sipe_tls_random entropy
;
71 sipe_webticket_callback
*callback
;
72 gpointer callback_data
;
74 struct sipe_svc_session
*session
;
79 struct webticket_token
{
85 struct sipe_webticket
{
89 gchar
*webticket_adfs_uri
;
91 gboolean retrieved_realminfo
;
94 void sipe_webticket_free(struct sipe_core_private
*sipe_private
)
96 struct sipe_webticket
*webticket
= sipe_private
->webticket
;
100 g_free(webticket
->webticket_adfs_uri
);
101 if (webticket
->pending
)
102 g_hash_table_destroy(webticket
->pending
);
103 if (webticket
->cache
)
104 g_hash_table_destroy(webticket
->cache
);
106 sipe_private
->webticket
= NULL
;
109 static void free_token(gpointer data
)
111 struct webticket_token
*wt
= data
;
112 g_free(wt
->auth_uri
);
117 static void sipe_webticket_init(struct sipe_core_private
*sipe_private
)
119 struct sipe_webticket
*webticket
;
121 if (sipe_private
->webticket
)
124 sipe_private
->webticket
= webticket
= g_new0(struct sipe_webticket
, 1);
126 webticket
->cache
= g_hash_table_new_full(g_str_hash
,
130 webticket
->pending
= g_hash_table_new(g_str_hash
,
134 /* takes ownership of "token" */
135 static void cache_token(struct sipe_core_private
*sipe_private
,
136 const gchar
*service_uri
,
137 const gchar
*auth_uri
,
141 struct webticket_token
*wt
= g_new0(struct webticket_token
, 1);
142 wt
->auth_uri
= g_strdup(auth_uri
);
144 wt
->expires
= expires
;
145 g_hash_table_insert(sipe_private
->webticket
->cache
,
146 g_strdup(service_uri
),
150 static const struct webticket_token
*cache_hit(struct sipe_core_private
*sipe_private
,
151 const gchar
*service_uri
)
153 const struct webticket_token
*wt
;
155 sipe_webticket_init(sipe_private
);
157 /* make sure a cached Web Ticket is still valid for 60 seconds */
158 wt
= g_hash_table_lookup(sipe_private
->webticket
->cache
,
160 if (wt
&& (wt
->expires
< time(NULL
) + 60)) {
161 SIPE_DEBUG_INFO("cache_hit: cached token for URI %s has expired",
169 /* frees just the main request data, when this is called "queued" is cleared */
170 static void callback_data_free(struct webticket_callback_data
*wcd
)
173 sipe_tls_free_random(&wcd
->entropy
);
174 g_free(wcd
->webticket_negotiate_uri
);
175 g_free(wcd
->webticket_fedbearer_uri
);
176 g_free(wcd
->service_auth_uri
);
177 g_free(wcd
->service_uri
);
182 static void queue_request(struct webticket_callback_data
*wcd
,
183 sipe_webticket_callback
*callback
,
184 gpointer callback_data
)
186 struct webticket_queued_data
*wqd
= g_new0(struct webticket_queued_data
, 1);
188 wqd
->callback
= callback
;
189 wqd
->callback_data
= callback_data
;
191 wcd
->queued
= g_slist_prepend(wcd
->queued
, wqd
);
194 static void callback_execute(struct sipe_core_private
*sipe_private
,
195 struct webticket_callback_data
*wcd
,
196 const gchar
*auth_uri
,
197 const gchar
*wsse_security
,
198 const gchar
*failure_msg
)
200 GSList
*entry
= wcd
->queued
;
202 /* complete main request */
203 wcd
->callback(sipe_private
,
210 /* complete queued requests */
212 struct webticket_queued_data
*wqd
= entry
->data
;
214 SIPE_DEBUG_INFO("callback_execute: completing queue request URI %s (Auth URI %s)",
215 wcd
->service_uri
, auth_uri
);
216 wqd
->callback(sipe_private
,
226 g_slist_free(wcd
->queued
);
228 /* drop request from pending hash */
229 g_hash_table_remove(sipe_private
->webticket
->pending
,
233 static gchar
*extract_raw_xml_attribute(const gchar
*xml
,
236 gchar
*attr_start
= g_strdup_printf("%s=\"", name
);
238 const gchar
*start
= strstr(xml
, attr_start
);
241 const gchar
*value
= start
+ strlen(attr_start
);
242 const gchar
*end
= strchr(value
, '"');
244 data
= g_strndup(value
, end
- value
);
252 static gchar
*generate_timestamp(const gchar
*raw
,
253 const gchar
*lifetime_tag
)
255 gchar
*lifetime
= sipe_xml_extract_raw(raw
, lifetime_tag
, FALSE
);
256 gchar
*timestamp
= NULL
;
258 timestamp
= g_strdup_printf("<wsu:Timestamp xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\" wsu:Id=\"timestamp\">%s</wsu:Timestamp>",
264 static gchar
*generate_fedbearer_wsse(const gchar
*raw
)
266 gchar
*timestamp
= generate_timestamp(raw
, "wst:Lifetime");
267 gchar
*keydata
= sipe_xml_extract_raw(raw
, "EncryptedData", TRUE
);
268 gchar
*wsse_security
= NULL
;
270 if (timestamp
&& keydata
) {
271 SIPE_DEBUG_INFO_NOFORMAT("generate_fedbearer_wsse: found timestamp & keydata");
272 wsse_security
= g_strconcat(timestamp
, keydata
, NULL
);
277 return(wsse_security
);
280 static gchar
*generate_federation_wsse(const gchar
*raw
)
282 gchar
*timestamp
= generate_timestamp(raw
, "t:Lifetime");
283 gchar
*keydata
= sipe_xml_extract_raw(raw
, "saml:Assertion", TRUE
);
284 gchar
*wsse_security
= NULL
;
286 if (timestamp
&& keydata
) {
287 SIPE_DEBUG_INFO_NOFORMAT("generate_federation_wsse: found timestamp & keydata");
288 wsse_security
= g_strconcat(timestamp
, keydata
, NULL
);
293 return(wsse_security
);
296 static gchar
*generate_sha1_proof_wsse(const gchar
*raw
,
297 struct sipe_tls_random
*entropy
,
300 gchar
*timestamp
= generate_timestamp(raw
, "Lifetime");
301 gchar
*keydata
= sipe_xml_extract_raw(raw
, "saml:Assertion", TRUE
);
302 gchar
*wsse_security
= NULL
;
304 if (timestamp
&& keydata
) {
305 gchar
*expires_string
= sipe_xml_extract_raw(timestamp
,
310 gchar
*assertionID
= extract_raw_xml_attribute(keydata
,
316 * http://docs.oasis-open.org/ws-sx/ws-trust/200512/CK/PSHA1:
318 * "The key is computed using P_SHA1() from the TLS sepcification to generate
319 * a bit stream using entropy from both sides. The exact form is:
321 * key = P_SHA1(Entropy_REQ, Entropy_RES)"
323 gchar
*entropy_res_base64
= sipe_xml_extract_raw(raw
, "BinarySecret", FALSE
);
324 gsize entropy_res_length
;
325 guchar
*entropy_response
= g_base64_decode(entropy_res_base64
,
326 &entropy_res_length
);
327 guchar
*key
= sipe_tls_p_sha1(entropy
->buffer
,
332 g_free(entropy_response
);
333 g_free(entropy_res_base64
);
335 SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found timestamp & keydata");
337 if (assertionID
&& key
) {
338 /* same as SIPE_DIGEST_HMAC_SHA1_LENGTH */
339 guchar digest
[SIPE_DIGEST_SHA1_LENGTH
];
344 SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found assertionID and successfully computed the key");
346 /* Digest over reference element (#timestamp -> wsu:Timestamp) */
347 sipe_digest_sha1((guchar
*) timestamp
,
350 base64
= g_base64_encode(digest
,
351 SIPE_DIGEST_SHA1_LENGTH
);
353 /* XML-Sig: SignedInfo for reference element */
354 signed_info
= g_strdup_printf("<SignedInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">"
355 "<CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>"
356 "<SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#hmac-sha1\"/>"
357 "<Reference URI=\"#timestamp\">"
359 "<Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>"
361 "<DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
362 "<DigestValue>%s</DigestValue>"
368 /* XML-Sig: SignedInfo in canonical form */
369 canon
= sipe_xml_exc_c14n(signed_info
);
375 /* calculate signature */
376 sipe_digest_hmac_sha1(key
, entropy
->length
,
380 base64
= g_base64_encode(digest
,
381 SIPE_DIGEST_HMAC_SHA1_LENGTH
);
383 /* XML-Sig: Signature from SignedInfo + Key */
384 signature
= g_strdup_printf("<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">"
386 " <SignatureValue>%s</SignatureValue>"
388 " <wsse:SecurityTokenReference wsse:TokenType=\"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1\">"
389 " <wsse:KeyIdentifier ValueType=\"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID\">%s</wsse:KeyIdentifier>"
390 " </wsse:SecurityTokenReference>"
399 wsse_security
= g_strconcat(timestamp
,
411 /* token doesn't require signature */
412 SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found timestamp & keydata, no signing required");
413 wsse_security
= g_strconcat(timestamp
,
419 if (expires_string
) {
420 *expires
= sipe_utils_str_to_time(expires_string
);
421 g_free(expires_string
);
427 return(wsse_security
);
430 static gboolean
initiate_fedbearer(struct sipe_core_private
*sipe_private
,
431 struct webticket_callback_data
*wcd
);
432 static void webticket_token(struct sipe_core_private
*sipe_private
,
436 gpointer callback_data
)
438 struct webticket_callback_data
*wcd
= callback_data
;
439 gboolean failed
= TRUE
;
442 switch (wcd
->token_state
) {
443 case TOKEN_STATE_NONE
:
444 SIPE_DEBUG_INFO_NOFORMAT("webticket_token: ILLEGAL STATE - should not happen...");
447 case TOKEN_STATE_SERVICE
: {
448 /* WebTicket for Web Service */
450 gchar
*wsse_security
= generate_sha1_proof_wsse(raw
,
451 wcd
->requires_signing
? &wcd
->entropy
: NULL
,
455 /* cache takes ownership of wsse_security */
456 cache_token(sipe_private
,
458 wcd
->service_auth_uri
,
461 callback_execute(sipe_private
,
463 wcd
->service_auth_uri
,
471 case TOKEN_STATE_FEDERATION
: {
472 /* WebTicket from ADFS for federated authentication */
473 gchar
*wsse_security
= generate_federation_wsse(raw
);
477 SIPE_DEBUG_INFO("webticket_token: received valid SOAP message from ADFS %s",
480 if (sipe_svc_webticket_lmc_federated(sipe_private
,
483 wcd
->webticket_fedbearer_uri
,
486 wcd
->token_state
= TOKEN_STATE_FED_BEARER
;
488 /* callback data passed down the line */
491 g_free(wsse_security
);
496 case TOKEN_STATE_FED_BEARER
: {
497 /* WebTicket for federated authentication */
498 gchar
*wsse_security
= generate_fedbearer_wsse(raw
);
502 SIPE_DEBUG_INFO("webticket_token: received valid SOAP message from service %s",
505 if (sipe_svc_webticket(sipe_private
,
507 wcd
->webticket_fedbearer_uri
,
509 wcd
->service_auth_uri
,
513 wcd
->token_state
= TOKEN_STATE_SERVICE
;
515 /* callback data passed down the line */
518 g_free(wsse_security
);
523 /* end of: switch (wcd->token_state) { */
527 /* Retry with federated authentication? */
528 if (wcd
->webticket_fedbearer_uri
&& !wcd
->tried_fedbearer
) {
529 SIPE_DEBUG_INFO("webticket_token: anonymous authentication to service %s failed, retrying with federated authentication",
532 if (initiate_fedbearer(sipe_private
, wcd
)) {
533 /* callback data passed down the line */
541 gchar
*failure_msg
= NULL
;
544 failure_msg
= sipe_xml_data(sipe_xml_child(soap_body
,
545 "Body/Fault/Detail/error/internalerror/text"));
546 /* XML data can end in 
 */
547 g_strstrip(failure_msg
);
550 callback_execute(sipe_private
,
557 callback_data_free(wcd
);
561 static gboolean
fedbearer_authentication(struct sipe_core_private
*sipe_private
,
562 struct webticket_callback_data
*wcd
)
564 struct sipe_webticket
*webticket
= sipe_private
->webticket
;
567 if (webticket
->webticket_adfs_uri
) {
568 if ((success
= sipe_svc_webticket_adfs(sipe_private
,
570 webticket
->webticket_adfs_uri
,
573 wcd
->token_state
= TOKEN_STATE_FEDERATION
;
575 if ((success
= sipe_svc_webticket_lmc(sipe_private
,
577 wcd
->webticket_fedbearer_uri
,
580 wcd
->token_state
= TOKEN_STATE_FED_BEARER
;
583 /* If TRUE then callback data has been passed down the line */
587 static void realminfo(struct sipe_core_private
*sipe_private
,
589 SIPE_UNUSED_PARAMETER
const gchar
*raw
,
591 gpointer callback_data
)
593 struct sipe_webticket
*webticket
= sipe_private
->webticket
;
594 struct webticket_callback_data
*wcd
= callback_data
;
596 /* Only try retrieving of RealmInfo once */
597 webticket
->retrieved_realminfo
= TRUE
;
600 /* detect ADFS setup. See also:
602 * http://en.wikipedia.org/wiki/Active_Directory_Federation_Services
604 * NOTE: this is based on observed behaviour.
605 * It is unkown if this is documented somewhere...
607 SIPE_DEBUG_INFO("realminfo: data for user %s retrieved successfully",
608 sipe_private
->username
);
610 webticket
->webticket_adfs_uri
= sipe_xml_data(sipe_xml_child(realminfo
,
614 if (webticket
->webticket_adfs_uri
)
615 SIPE_DEBUG_INFO("realminfo: ADFS setup detected: %s",
616 webticket
->webticket_adfs_uri
);
618 SIPE_DEBUG_INFO_NOFORMAT("realminfo: no RealmInfo found or no ADFS setup detected - try direct login");
620 if (!fedbearer_authentication(sipe_private
, wcd
)) {
621 callback_execute(sipe_private
,
626 callback_data_free(wcd
);
630 static gboolean
initiate_fedbearer(struct sipe_core_private
*sipe_private
,
631 struct webticket_callback_data
*wcd
)
635 if (sipe_private
->webticket
->retrieved_realminfo
) {
636 /* skip retrieval and go to authentication */
637 success
= fedbearer_authentication(sipe_private
, wcd
);
639 success
= sipe_svc_realminfo(sipe_private
,
645 wcd
->tried_fedbearer
= TRUE
;
650 static void webticket_metadata(struct sipe_core_private
*sipe_private
,
652 SIPE_UNUSED_PARAMETER
const gchar
*raw
,
654 gpointer callback_data
)
656 struct webticket_callback_data
*wcd
= callback_data
;
659 const sipe_xml
*node
;
661 SIPE_DEBUG_INFO("webticket_metadata: metadata for service %s retrieved successfully",
664 /* Authentication ports accepted by WebTicket Service */
665 for (node
= sipe_xml_child(metadata
, "service/port");
667 node
= sipe_xml_twin(node
)) {
668 const gchar
*auth_uri
= sipe_xml_attribute(sipe_xml_child(node
,
673 if (sipe_strcase_equal(sipe_xml_attribute(node
, "name"),
674 "WebTicketServiceWinNegotiate")) {
675 SIPE_DEBUG_INFO("webticket_metadata: WebTicket Windows Negotiate Auth URI %s", auth_uri
);
676 g_free(wcd
->webticket_negotiate_uri
);
677 wcd
->webticket_negotiate_uri
= g_strdup(auth_uri
);
678 } else if (sipe_strcase_equal(sipe_xml_attribute(node
, "name"),
680 SIPE_DEBUG_INFO("webticket_metadata: WebTicket FedBearer Auth URI %s", auth_uri
);
681 g_free(wcd
->webticket_fedbearer_uri
);
682 wcd
->webticket_fedbearer_uri
= g_strdup(auth_uri
);
687 if (wcd
->webticket_negotiate_uri
|| wcd
->webticket_fedbearer_uri
) {
690 /* Entropy: 256 random bits */
691 if (!wcd
->entropy
.buffer
)
692 sipe_tls_fill_random(&wcd
->entropy
, 256);
694 if (wcd
->webticket_negotiate_uri
) {
695 /* Try Negotiate authentication first */
697 success
= sipe_svc_webticket(sipe_private
,
699 wcd
->webticket_negotiate_uri
,
701 wcd
->service_auth_uri
,
705 wcd
->token_state
= TOKEN_STATE_SERVICE
;
707 success
= initiate_fedbearer(sipe_private
,
712 /* callback data passed down the line */
719 callback_execute(sipe_private
,
724 callback_data_free(wcd
);
728 static void service_metadata(struct sipe_core_private
*sipe_private
,
730 SIPE_UNUSED_PARAMETER
const gchar
*raw
,
732 gpointer callback_data
)
734 struct webticket_callback_data
*wcd
= callback_data
;
737 const sipe_xml
*node
;
738 gchar
*policy
= g_strdup_printf("%s_policy", wcd
->service_port
);
739 gchar
*ticket_uri
= NULL
;
741 SIPE_DEBUG_INFO("webservice_metadata: metadata for service %s retrieved successfully",
744 /* WebTicket policies accepted by Web Service */
745 for (node
= sipe_xml_child(metadata
, "Policy");
747 node
= sipe_xml_twin(node
)) {
748 if (sipe_strcase_equal(sipe_xml_attribute(node
, "Id"),
751 SIPE_DEBUG_INFO_NOFORMAT("webservice_metadata: WebTicket policy found");
753 ticket_uri
= sipe_xml_data(sipe_xml_child(node
,
754 "ExactlyOne/All/EndorsingSupportingTokens/Policy/IssuedToken/Issuer/Address"));
756 /* this token type requires signing */
757 wcd
->requires_signing
= TRUE
;
759 /* try alternative token type */
760 ticket_uri
= sipe_xml_data(sipe_xml_child(node
,
761 "ExactlyOne/All/SignedSupportingTokens/Policy/IssuedToken/Issuer/Address"));
764 SIPE_DEBUG_INFO("webservice_metadata: WebTicket URI %s", ticket_uri
);
773 /* Authentication ports accepted by Web Service */
774 for (node
= sipe_xml_child(metadata
, "service/port");
776 node
= sipe_xml_twin(node
)) {
777 if (sipe_strcase_equal(sipe_xml_attribute(node
, "name"),
778 wcd
->service_port
)) {
779 const gchar
*auth_uri
;
781 SIPE_DEBUG_INFO_NOFORMAT("webservice_metadata: authentication port found");
783 auth_uri
= sipe_xml_attribute(sipe_xml_child(node
,
787 SIPE_DEBUG_INFO("webservice_metadata: Auth URI %s", auth_uri
);
789 if (sipe_svc_metadata(sipe_private
,
794 /* Remember for later */
795 wcd
->service_auth_uri
= g_strdup(auth_uri
);
797 /* callback data passed down the line */
809 callback_execute(sipe_private
,
814 callback_data_free(wcd
);
818 gboolean
sipe_webticket_request(struct sipe_core_private
*sipe_private
,
819 struct sipe_svc_session
*session
,
820 const gchar
*base_uri
,
821 const gchar
*port_name
,
822 sipe_webticket_callback
*callback
,
823 gpointer callback_data
)
825 const struct webticket_token
*wt
= cache_hit(sipe_private
, base_uri
);
828 /* cache hit for this URI? */
830 SIPE_DEBUG_INFO("sipe_webticket_request: using cached token for URI %s (Auth URI %s)",
831 base_uri
, wt
->auth_uri
);
832 callback(sipe_private
,
840 GHashTable
*pending
= sipe_private
->webticket
->pending
;
841 struct webticket_callback_data
*wcd
= g_hash_table_lookup(pending
,
844 /* is there already a pending request for this URI? */
846 SIPE_DEBUG_INFO("sipe_webticket_request: pending request found for URI %s - queueing",
848 queue_request(wcd
, callback
, callback_data
);
851 wcd
= g_new0(struct webticket_callback_data
, 1);
853 ret
= sipe_svc_metadata(sipe_private
,
860 wcd
->service_uri
= g_strdup(base_uri
);
861 wcd
->service_port
= port_name
;
862 wcd
->callback
= callback
;
863 wcd
->callback_data
= callback_data
;
864 wcd
->session
= session
;
865 wcd
->token_state
= TOKEN_STATE_NONE
;
866 g_hash_table_insert(pending
,
867 wcd
->service_uri
, /* borrowed */