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 time_t adfs_token_expires
;
93 gboolean retrieved_realminfo
;
96 void sipe_webticket_free(struct sipe_core_private
*sipe_private
)
98 struct sipe_webticket
*webticket
= sipe_private
->webticket
;
102 g_free(webticket
->webticket_adfs_uri
);
103 g_free(webticket
->adfs_token
);
104 if (webticket
->pending
)
105 g_hash_table_destroy(webticket
->pending
);
106 if (webticket
->cache
)
107 g_hash_table_destroy(webticket
->cache
);
109 sipe_private
->webticket
= NULL
;
112 static void free_token(gpointer data
)
114 struct webticket_token
*wt
= data
;
115 g_free(wt
->auth_uri
);
120 static void sipe_webticket_init(struct sipe_core_private
*sipe_private
)
122 struct sipe_webticket
*webticket
;
124 if (sipe_private
->webticket
)
127 sipe_private
->webticket
= webticket
= g_new0(struct sipe_webticket
, 1);
129 webticket
->cache
= g_hash_table_new_full(g_str_hash
,
133 webticket
->pending
= g_hash_table_new(g_str_hash
,
137 /* takes ownership of "token" */
138 static void cache_token(struct sipe_core_private
*sipe_private
,
139 const gchar
*service_uri
,
140 const gchar
*auth_uri
,
144 struct webticket_token
*wt
= g_new0(struct webticket_token
, 1);
145 wt
->auth_uri
= g_strdup(auth_uri
);
147 wt
->expires
= expires
;
148 g_hash_table_insert(sipe_private
->webticket
->cache
,
149 g_strdup(service_uri
),
153 static const struct webticket_token
*cache_hit(struct sipe_core_private
*sipe_private
,
154 const gchar
*service_uri
)
156 const struct webticket_token
*wt
;
158 sipe_webticket_init(sipe_private
);
160 /* make sure a cached Web Ticket is still valid for 60 seconds */
161 wt
= g_hash_table_lookup(sipe_private
->webticket
->cache
,
163 if (wt
&& (wt
->expires
< time(NULL
) + 60)) {
164 SIPE_DEBUG_INFO("cache_hit: cached token for URI %s has expired",
172 /* frees just the main request data, when this is called "queued" is cleared */
173 static void callback_data_free(struct webticket_callback_data
*wcd
)
176 sipe_tls_free_random(&wcd
->entropy
);
177 g_free(wcd
->webticket_negotiate_uri
);
178 g_free(wcd
->webticket_fedbearer_uri
);
179 g_free(wcd
->service_auth_uri
);
180 g_free(wcd
->service_uri
);
185 static void queue_request(struct webticket_callback_data
*wcd
,
186 sipe_webticket_callback
*callback
,
187 gpointer callback_data
)
189 struct webticket_queued_data
*wqd
= g_new0(struct webticket_queued_data
, 1);
191 wqd
->callback
= callback
;
192 wqd
->callback_data
= callback_data
;
194 wcd
->queued
= g_slist_prepend(wcd
->queued
, wqd
);
197 static void callback_execute(struct sipe_core_private
*sipe_private
,
198 struct webticket_callback_data
*wcd
,
199 const gchar
*auth_uri
,
200 const gchar
*wsse_security
,
201 const gchar
*failure_msg
)
203 GSList
*entry
= wcd
->queued
;
205 /* complete main request */
206 wcd
->callback(sipe_private
,
213 /* complete queued requests */
215 struct webticket_queued_data
*wqd
= entry
->data
;
217 SIPE_DEBUG_INFO("callback_execute: completing queue request URI %s (Auth URI %s)",
218 wcd
->service_uri
, auth_uri
);
219 wqd
->callback(sipe_private
,
229 g_slist_free(wcd
->queued
);
231 /* drop request from pending hash */
232 g_hash_table_remove(sipe_private
->webticket
->pending
,
236 static gchar
*extract_raw_xml_attribute(const gchar
*xml
,
239 gchar
*attr_start
= g_strdup_printf("%s=\"", name
);
241 const gchar
*start
= strstr(xml
, attr_start
);
244 const gchar
*value
= start
+ strlen(attr_start
);
245 const gchar
*end
= strchr(value
, '"');
247 data
= g_strndup(value
, end
- value
);
255 static gchar
*generate_timestamp(const gchar
*raw
,
256 const gchar
*lifetime_tag
)
258 gchar
*lifetime
= sipe_xml_extract_raw(raw
, lifetime_tag
, FALSE
);
259 gchar
*timestamp
= NULL
;
261 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>",
267 static gchar
*generate_fedbearer_wsse(const gchar
*raw
)
269 gchar
*timestamp
= generate_timestamp(raw
, "wst:Lifetime");
270 gchar
*keydata
= sipe_xml_extract_raw(raw
, "EncryptedData", TRUE
);
271 gchar
*wsse_security
= NULL
;
273 if (timestamp
&& keydata
) {
274 SIPE_DEBUG_INFO_NOFORMAT("generate_fedbearer_wsse: found timestamp & keydata");
275 wsse_security
= g_strconcat(timestamp
, keydata
, NULL
);
280 return(wsse_security
);
283 static void generate_federation_wsse(struct sipe_webticket
*webticket
,
286 gchar
*timestamp
= generate_timestamp(raw
, "t:Lifetime");
287 gchar
*keydata
= sipe_xml_extract_raw(raw
, "saml:Assertion", TRUE
);
290 /* clear old ADFS token */
291 g_free(webticket
->adfs_token
);
292 webticket
->adfs_token
= NULL
;
294 if (timestamp
&& keydata
) {
295 gchar
*expires_string
= sipe_xml_extract_raw(timestamp
,
299 if (expires_string
) {
301 SIPE_DEBUG_INFO("generate_federation_wsse: found timestamp & keydata, expires %s",
304 /* cache ADFS token */
305 webticket
->adfs_token
= g_strconcat(timestamp
,
308 webticket
->adfs_token_expires
= sipe_utils_str_to_time(expires_string
);
309 g_free(expires_string
);
317 static gchar
*generate_sha1_proof_wsse(const gchar
*raw
,
318 struct sipe_tls_random
*entropy
,
321 gchar
*timestamp
= generate_timestamp(raw
, "Lifetime");
322 gchar
*keydata
= sipe_xml_extract_raw(raw
, "saml:Assertion", TRUE
);
323 gchar
*wsse_security
= NULL
;
325 if (timestamp
&& keydata
) {
326 gchar
*expires_string
= sipe_xml_extract_raw(timestamp
,
331 gchar
*assertionID
= extract_raw_xml_attribute(keydata
,
337 * http://docs.oasis-open.org/ws-sx/ws-trust/200512/CK/PSHA1:
339 * "The key is computed using P_SHA1() from the TLS sepcification to generate
340 * a bit stream using entropy from both sides. The exact form is:
342 * key = P_SHA1(Entropy_REQ, Entropy_RES)"
344 gchar
*entropy_res_base64
= sipe_xml_extract_raw(raw
, "BinarySecret", FALSE
);
345 gsize entropy_res_length
;
346 guchar
*entropy_response
= g_base64_decode(entropy_res_base64
,
347 &entropy_res_length
);
348 guchar
*key
= sipe_tls_p_sha1(entropy
->buffer
,
353 g_free(entropy_response
);
354 g_free(entropy_res_base64
);
356 SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found timestamp & keydata");
358 if (assertionID
&& key
) {
359 /* same as SIPE_DIGEST_HMAC_SHA1_LENGTH */
360 guchar digest
[SIPE_DIGEST_SHA1_LENGTH
];
365 SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found assertionID and successfully computed the key");
367 /* Digest over reference element (#timestamp -> wsu:Timestamp) */
368 sipe_digest_sha1((guchar
*) timestamp
,
371 base64
= g_base64_encode(digest
,
372 SIPE_DIGEST_SHA1_LENGTH
);
374 /* XML-Sig: SignedInfo for reference element */
375 signed_info
= g_strdup_printf("<SignedInfo xmlns=\"http://www.w3.org/2000/09/xmldsig#\">"
376 "<CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>"
377 "<SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#hmac-sha1\"/>"
378 "<Reference URI=\"#timestamp\">"
380 "<Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>"
382 "<DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>"
383 "<DigestValue>%s</DigestValue>"
389 /* XML-Sig: SignedInfo in canonical form */
390 canon
= sipe_xml_exc_c14n(signed_info
);
396 /* calculate signature */
397 sipe_digest_hmac_sha1(key
, entropy
->length
,
401 base64
= g_base64_encode(digest
,
402 SIPE_DIGEST_HMAC_SHA1_LENGTH
);
404 /* XML-Sig: Signature from SignedInfo + Key */
405 signature
= g_strdup_printf("<Signature xmlns=\"http://www.w3.org/2000/09/xmldsig#\">"
407 " <SignatureValue>%s</SignatureValue>"
409 " <wsse:SecurityTokenReference wsse:TokenType=\"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1\">"
410 " <wsse:KeyIdentifier ValueType=\"http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID\">%s</wsse:KeyIdentifier>"
411 " </wsse:SecurityTokenReference>"
420 wsse_security
= g_strconcat(timestamp
,
432 /* token doesn't require signature */
433 SIPE_DEBUG_INFO_NOFORMAT("generate_sha1_proof_wsse: found timestamp & keydata, no signing required");
434 wsse_security
= g_strconcat(timestamp
,
440 if (expires_string
) {
441 *expires
= sipe_utils_str_to_time(expires_string
);
442 g_free(expires_string
);
448 return(wsse_security
);
451 static gboolean
federated_authentication(struct sipe_core_private
*sipe_private
,
452 struct webticket_callback_data
*wcd
);
453 static gboolean
initiate_fedbearer(struct sipe_core_private
*sipe_private
,
454 struct webticket_callback_data
*wcd
);
455 static void webticket_token(struct sipe_core_private
*sipe_private
,
459 gpointer callback_data
)
461 struct webticket_callback_data
*wcd
= callback_data
;
462 gboolean failed
= TRUE
;
465 switch (wcd
->token_state
) {
466 case TOKEN_STATE_NONE
:
467 SIPE_DEBUG_INFO_NOFORMAT("webticket_token: ILLEGAL STATE - should not happen...");
470 case TOKEN_STATE_SERVICE
: {
471 /* WebTicket for Web Service */
473 gchar
*wsse_security
= generate_sha1_proof_wsse(raw
,
474 wcd
->requires_signing
? &wcd
->entropy
: NULL
,
478 /* cache takes ownership of wsse_security */
479 cache_token(sipe_private
,
481 wcd
->service_auth_uri
,
484 callback_execute(sipe_private
,
486 wcd
->service_auth_uri
,
494 case TOKEN_STATE_FEDERATION
:
495 /* WebTicket from ADFS for federated authentication */
496 generate_federation_wsse(sipe_private
->webticket
,
499 if (sipe_private
->webticket
->adfs_token
) {
501 SIPE_DEBUG_INFO("webticket_token: received valid SOAP message from ADFS %s",
504 if (federated_authentication(sipe_private
,
506 /* callback data passed down the line */
512 case TOKEN_STATE_FED_BEARER
: {
513 /* WebTicket for federated authentication */
514 gchar
*wsse_security
= generate_fedbearer_wsse(raw
);
518 SIPE_DEBUG_INFO("webticket_token: received valid SOAP message from service %s",
521 if (sipe_svc_webticket(sipe_private
,
523 wcd
->webticket_fedbearer_uri
,
525 wcd
->service_auth_uri
,
529 wcd
->token_state
= TOKEN_STATE_SERVICE
;
531 /* callback data passed down the line */
534 g_free(wsse_security
);
539 /* end of: switch (wcd->token_state) { */
543 /* Retry with federated authentication? */
544 if (wcd
->webticket_fedbearer_uri
&& !wcd
->tried_fedbearer
) {
545 SIPE_DEBUG_INFO("webticket_token: anonymous authentication to service %s failed, retrying with federated authentication",
548 if (initiate_fedbearer(sipe_private
, wcd
)) {
549 /* callback data passed down the line */
557 gchar
*failure_msg
= NULL
;
560 failure_msg
= sipe_xml_data(sipe_xml_child(soap_body
,
561 "Body/Fault/Detail/error/internalerror/text"));
562 /* XML data can end in 
 */
563 g_strstrip(failure_msg
);
566 callback_execute(sipe_private
,
573 callback_data_free(wcd
);
577 static gboolean
federated_authentication(struct sipe_core_private
*sipe_private
,
578 struct webticket_callback_data
*wcd
)
582 if ((success
= sipe_svc_webticket_lmc_federated(sipe_private
,
584 sipe_private
->webticket
->adfs_token
,
585 wcd
->webticket_fedbearer_uri
,
588 wcd
->token_state
= TOKEN_STATE_FED_BEARER
;
590 /* If TRUE then callback data has been passed down the line */
594 static gboolean
fedbearer_authentication(struct sipe_core_private
*sipe_private
,
595 struct webticket_callback_data
*wcd
)
597 struct sipe_webticket
*webticket
= sipe_private
->webticket
;
600 /* make sure a cached ADFS token is still valid for 60 seconds */
601 if (webticket
->adfs_token
&&
602 (webticket
->adfs_token_expires
>= time(NULL
) + 60)) {
604 SIPE_DEBUG_INFO_NOFORMAT("fedbearer_authentication: reusing cached ADFS token");
605 success
= federated_authentication(sipe_private
, wcd
);
607 } else if (webticket
->webticket_adfs_uri
) {
608 if ((success
= sipe_svc_webticket_adfs(sipe_private
,
610 webticket
->webticket_adfs_uri
,
613 wcd
->token_state
= TOKEN_STATE_FEDERATION
;
615 if ((success
= sipe_svc_webticket_lmc(sipe_private
,
617 wcd
->webticket_fedbearer_uri
,
620 wcd
->token_state
= TOKEN_STATE_FED_BEARER
;
623 /* If TRUE then callback data has been passed down the line */
627 static void realminfo(struct sipe_core_private
*sipe_private
,
629 SIPE_UNUSED_PARAMETER
const gchar
*raw
,
631 gpointer callback_data
)
633 struct sipe_webticket
*webticket
= sipe_private
->webticket
;
634 struct webticket_callback_data
*wcd
= callback_data
;
636 /* Only try retrieving of RealmInfo once */
637 webticket
->retrieved_realminfo
= TRUE
;
640 /* detect ADFS setup. See also:
642 * http://en.wikipedia.org/wiki/Active_Directory_Federation_Services
644 * NOTE: this is based on observed behaviour.
645 * It is unkown if this is documented somewhere...
647 SIPE_DEBUG_INFO("realminfo: data for user %s retrieved successfully",
648 sipe_private
->username
);
650 webticket
->webticket_adfs_uri
= sipe_xml_data(sipe_xml_child(realminfo
,
654 if (webticket
->webticket_adfs_uri
)
655 SIPE_DEBUG_INFO("realminfo: ADFS setup detected: %s",
656 webticket
->webticket_adfs_uri
);
658 SIPE_DEBUG_INFO_NOFORMAT("realminfo: no RealmInfo found or no ADFS setup detected - try direct login");
660 if (!fedbearer_authentication(sipe_private
, wcd
)) {
661 callback_execute(sipe_private
,
666 callback_data_free(wcd
);
670 static gboolean
initiate_fedbearer(struct sipe_core_private
*sipe_private
,
671 struct webticket_callback_data
*wcd
)
675 if (sipe_private
->webticket
->retrieved_realminfo
) {
676 /* skip retrieval and go to authentication */
677 success
= fedbearer_authentication(sipe_private
, wcd
);
679 success
= sipe_svc_realminfo(sipe_private
,
685 wcd
->tried_fedbearer
= TRUE
;
690 static void webticket_metadata(struct sipe_core_private
*sipe_private
,
692 SIPE_UNUSED_PARAMETER
const gchar
*raw
,
694 gpointer callback_data
)
696 struct webticket_callback_data
*wcd
= callback_data
;
699 const sipe_xml
*node
;
701 SIPE_DEBUG_INFO("webticket_metadata: metadata for service %s retrieved successfully",
704 /* Authentication ports accepted by WebTicket Service */
705 for (node
= sipe_xml_child(metadata
, "service/port");
707 node
= sipe_xml_twin(node
)) {
708 const gchar
*auth_uri
= sipe_xml_attribute(sipe_xml_child(node
,
713 if (sipe_strcase_equal(sipe_xml_attribute(node
, "name"),
714 "WebTicketServiceWinNegotiate")) {
715 SIPE_DEBUG_INFO("webticket_metadata: WebTicket Windows Negotiate Auth URI %s", auth_uri
);
716 g_free(wcd
->webticket_negotiate_uri
);
717 wcd
->webticket_negotiate_uri
= g_strdup(auth_uri
);
718 } else if (sipe_strcase_equal(sipe_xml_attribute(node
, "name"),
720 SIPE_DEBUG_INFO("webticket_metadata: WebTicket FedBearer Auth URI %s", auth_uri
);
721 g_free(wcd
->webticket_fedbearer_uri
);
722 wcd
->webticket_fedbearer_uri
= g_strdup(auth_uri
);
727 if (wcd
->webticket_negotiate_uri
|| wcd
->webticket_fedbearer_uri
) {
730 /* Entropy: 256 random bits */
731 if (!wcd
->entropy
.buffer
)
732 sipe_tls_fill_random(&wcd
->entropy
, 256);
734 if (wcd
->webticket_negotiate_uri
) {
735 /* Try Negotiate authentication first */
737 success
= sipe_svc_webticket(sipe_private
,
739 wcd
->webticket_negotiate_uri
,
741 wcd
->service_auth_uri
,
745 wcd
->token_state
= TOKEN_STATE_SERVICE
;
747 success
= initiate_fedbearer(sipe_private
,
752 /* callback data passed down the line */
759 callback_execute(sipe_private
,
764 callback_data_free(wcd
);
768 static void service_metadata(struct sipe_core_private
*sipe_private
,
770 SIPE_UNUSED_PARAMETER
const gchar
*raw
,
772 gpointer callback_data
)
774 struct webticket_callback_data
*wcd
= callback_data
;
777 const sipe_xml
*node
;
778 gchar
*policy
= g_strdup_printf("%s_policy", wcd
->service_port
);
779 gchar
*ticket_uri
= NULL
;
781 SIPE_DEBUG_INFO("webservice_metadata: metadata for service %s retrieved successfully",
784 /* WebTicket policies accepted by Web Service */
785 for (node
= sipe_xml_child(metadata
, "Policy");
787 node
= sipe_xml_twin(node
)) {
788 if (sipe_strcase_equal(sipe_xml_attribute(node
, "Id"),
791 SIPE_DEBUG_INFO_NOFORMAT("webservice_metadata: WebTicket policy found");
793 ticket_uri
= sipe_xml_data(sipe_xml_child(node
,
794 "ExactlyOne/All/EndorsingSupportingTokens/Policy/IssuedToken/Issuer/Address"));
796 /* this token type requires signing */
797 wcd
->requires_signing
= TRUE
;
799 /* try alternative token type */
800 ticket_uri
= sipe_xml_data(sipe_xml_child(node
,
801 "ExactlyOne/All/SignedSupportingTokens/Policy/IssuedToken/Issuer/Address"));
804 SIPE_DEBUG_INFO("webservice_metadata: WebTicket URI %s", ticket_uri
);
813 /* Authentication ports accepted by Web Service */
814 for (node
= sipe_xml_child(metadata
, "service/port");
816 node
= sipe_xml_twin(node
)) {
817 if (sipe_strcase_equal(sipe_xml_attribute(node
, "name"),
818 wcd
->service_port
)) {
819 const gchar
*auth_uri
;
821 SIPE_DEBUG_INFO_NOFORMAT("webservice_metadata: authentication port found");
823 auth_uri
= sipe_xml_attribute(sipe_xml_child(node
,
827 SIPE_DEBUG_INFO("webservice_metadata: Auth URI %s", auth_uri
);
829 if (sipe_svc_metadata(sipe_private
,
834 /* Remember for later */
835 wcd
->service_auth_uri
= g_strdup(auth_uri
);
837 /* callback data passed down the line */
849 callback_execute(sipe_private
,
854 callback_data_free(wcd
);
858 gboolean
sipe_webticket_request(struct sipe_core_private
*sipe_private
,
859 struct sipe_svc_session
*session
,
860 const gchar
*base_uri
,
861 const gchar
*port_name
,
862 sipe_webticket_callback
*callback
,
863 gpointer callback_data
)
865 const struct webticket_token
*wt
= cache_hit(sipe_private
, base_uri
);
868 /* cache hit for this URI? */
870 SIPE_DEBUG_INFO("sipe_webticket_request: using cached token for URI %s (Auth URI %s)",
871 base_uri
, wt
->auth_uri
);
872 callback(sipe_private
,
880 GHashTable
*pending
= sipe_private
->webticket
->pending
;
881 struct webticket_callback_data
*wcd
= g_hash_table_lookup(pending
,
884 /* is there already a pending request for this URI? */
886 SIPE_DEBUG_INFO("sipe_webticket_request: pending request found for URI %s - queueing",
888 queue_request(wcd
, callback
, callback_data
);
891 wcd
= g_new0(struct webticket_callback_data
, 1);
893 ret
= sipe_svc_metadata(sipe_private
,
900 wcd
->service_uri
= g_strdup(base_uri
);
901 wcd
->service_port
= port_name
;
902 wcd
->callback
= callback
;
903 wcd
->callback_data
= callback_data
;
904 wcd
->session
= session
;
905 wcd
->token_state
= TOKEN_STATE_NONE
;
906 g_hash_table_insert(pending
,
907 wcd
->service_uri
, /* borrowed */