1 /*
2  * e-gdata-oauth2-authorizer.c
3  *
4  * This library is free software: you can redistribute it and/or modify it
5  * under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation.
7  *
8  * This library is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
11  * for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this library. If not, see <http://www.gnu.org/licenses/>.
15  *
16  */
17 
18 #include "evolution-data-server-config.h"
19 
20 #include <time.h>
21 
22 #include "e-gdata-oauth2-authorizer.h"
23 
24 #ifdef HAVE_LIBGDATA
25 
26 #include <gdata/gdata.h>
27 
28 #define EXPIRY_INVALID ((time_t) -1)
29 
30 struct _EGDataOAuth2AuthorizerPrivate {
31 	GWeakRef source;
32 	GType service_type;
33 
34 	/* These members are protected by the global mutex. */
35 	gchar *access_token;
36 	time_t expiry;
37 	GHashTable *authorization_domains;
38 	ENamedParameters *credentials;
39 };
40 
41 enum {
42 	PROP_0,
43 	PROP_SERVICE_TYPE,
44 	PROP_SOURCE
45 };
46 
47 /* GDataAuthorizer methods must be thread-safe. */
48 static GMutex mutex;
49 
50 /* Forward Declarations */
51 static void e_gdata_oauth2_authorizer_interface_init (GDataAuthorizerInterface *iface);
52 
G_DEFINE_TYPE_WITH_CODE(EGDataOAuth2Authorizer,e_gdata_oauth2_authorizer,G_TYPE_OBJECT,G_ADD_PRIVATE (EGDataOAuth2Authorizer)G_IMPLEMENT_INTERFACE (GDATA_TYPE_AUTHORIZER,e_gdata_oauth2_authorizer_interface_init))53 G_DEFINE_TYPE_WITH_CODE (EGDataOAuth2Authorizer, e_gdata_oauth2_authorizer, G_TYPE_OBJECT,
54 	G_ADD_PRIVATE (EGDataOAuth2Authorizer)
55 	G_IMPLEMENT_INTERFACE (GDATA_TYPE_AUTHORIZER, e_gdata_oauth2_authorizer_interface_init))
56 
57 static gboolean
58 e_gdata_oauth2_authorizer_is_authorized (GDataAuthorizer *authorizer,
59 					 GDataAuthorizationDomain *domain)
60 {
61 	EGDataOAuth2Authorizer *oauth2_authorizer;
62 
63 	/* This MUST be called with the mutex already locked. */
64 
65 	if (!domain)
66 		return TRUE;
67 
68 	oauth2_authorizer = E_GDATA_OAUTH2_AUTHORIZER (authorizer);
69 
70 	return g_hash_table_contains (oauth2_authorizer->priv->authorization_domains, domain);
71 }
72 
73 static void
e_gdata_oauth2_authorizer_set_service_type(EGDataOAuth2Authorizer * authorizer,GType service_type)74 e_gdata_oauth2_authorizer_set_service_type (EGDataOAuth2Authorizer *authorizer,
75 					    GType service_type)
76 {
77 	g_return_if_fail (service_type != 0);
78 
79 	authorizer->priv->service_type = service_type;
80 }
81 
82 static void
e_gdata_oauth2_authorizer_set_source(EGDataOAuth2Authorizer * authorizer,ESource * source)83 e_gdata_oauth2_authorizer_set_source (EGDataOAuth2Authorizer *authorizer,
84 				      ESource *source)
85 {
86 	g_return_if_fail (E_IS_SOURCE (source));
87 
88 	g_weak_ref_set (&authorizer->priv->source, source);
89 }
90 
91 static void
e_gdata_oauth2_authorizer_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)92 e_gdata_oauth2_authorizer_set_property (GObject *object,
93 					guint property_id,
94 					const GValue *value,
95 					GParamSpec *pspec)
96 {
97 	switch (property_id) {
98 		case PROP_SERVICE_TYPE:
99 			e_gdata_oauth2_authorizer_set_service_type (
100 				E_GDATA_OAUTH2_AUTHORIZER (object),
101 				g_value_get_gtype (value));
102 			return;
103 
104 		case PROP_SOURCE:
105 			e_gdata_oauth2_authorizer_set_source (
106 				E_GDATA_OAUTH2_AUTHORIZER (object),
107 				g_value_get_object (value));
108 			return;
109 	}
110 
111 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
112 }
113 
114 static void
e_gdata_oauth2_authorizer_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)115 e_gdata_oauth2_authorizer_get_property (GObject *object,
116 					guint property_id,
117 					GValue *value,
118 					GParamSpec *pspec)
119 {
120 	switch (property_id) {
121 		case PROP_SERVICE_TYPE:
122 			g_value_set_gtype (
123 				value,
124 				e_gdata_oauth2_authorizer_get_service_type (
125 				E_GDATA_OAUTH2_AUTHORIZER (object)));
126 			return;
127 
128 		case PROP_SOURCE:
129 			g_value_take_object (
130 				value,
131 				e_gdata_oauth2_authorizer_ref_source (
132 				E_GDATA_OAUTH2_AUTHORIZER (object)));
133 			return;
134 	}
135 
136 	G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
137 }
138 
139 static void
e_gdata_oauth2_authorizer_dispose(GObject * object)140 e_gdata_oauth2_authorizer_dispose (GObject *object)
141 {
142 	EGDataOAuth2Authorizer *oauth2_authorizer;
143 
144 	oauth2_authorizer = E_GDATA_OAUTH2_AUTHORIZER (object);
145 
146 	g_weak_ref_set (&oauth2_authorizer->priv->source, NULL);
147 
148 	g_hash_table_remove_all (oauth2_authorizer->priv->authorization_domains);
149 
150 	/* Chain up to parent's method. */
151 	G_OBJECT_CLASS (e_gdata_oauth2_authorizer_parent_class)->dispose (object);
152 }
153 
154 static void
e_gdata_oauth2_authorizer_finalize(GObject * object)155 e_gdata_oauth2_authorizer_finalize (GObject *object)
156 {
157 	EGDataOAuth2Authorizer *oauth2_authorizer;
158 
159 	oauth2_authorizer = E_GDATA_OAUTH2_AUTHORIZER (object);
160 
161 	g_free (oauth2_authorizer->priv->access_token);
162 
163 	g_hash_table_destroy (oauth2_authorizer->priv->authorization_domains);
164 	g_weak_ref_clear (&oauth2_authorizer->priv->source);
165 
166 	e_named_parameters_free (oauth2_authorizer->priv->credentials);
167 
168 	/* Chain up to parent's method. */
169 	G_OBJECT_CLASS (e_gdata_oauth2_authorizer_parent_class)->finalize (object);
170 }
171 
172 static void
e_gdata_oauth2_authorizer_constructed(GObject * object)173 e_gdata_oauth2_authorizer_constructed (GObject *object)
174 {
175 	EGDataOAuth2Authorizer *oauth2_authorizer;
176 	GList *domains, *link;
177 
178 	/* Chain up to parent's method. */
179 	G_OBJECT_CLASS (e_gdata_oauth2_authorizer_parent_class)->constructed (object);
180 
181 	oauth2_authorizer = E_GDATA_OAUTH2_AUTHORIZER (object);
182 
183 	domains = gdata_service_get_authorization_domains (oauth2_authorizer->priv->service_type);
184 	for (link = domains; link; link = g_list_next (link)) {
185 		g_hash_table_add (
186 			oauth2_authorizer->priv->authorization_domains,
187 			g_object_ref (domains->data));
188 	}
189 
190 	g_list_free (domains);
191 }
192 
193 static void
e_gdata_oauth2_authorizer_process_request(GDataAuthorizer * authorizer,GDataAuthorizationDomain * domain,SoupMessage * message)194 e_gdata_oauth2_authorizer_process_request (GDataAuthorizer *authorizer,
195 					   GDataAuthorizationDomain *domain,
196 					   SoupMessage *message)
197 {
198 	EGDataOAuth2Authorizer *oauth2_authorizer;
199 	gchar *authorization;
200 
201 	oauth2_authorizer = E_GDATA_OAUTH2_AUTHORIZER (authorizer);
202 
203 	g_mutex_lock (&mutex);
204 
205 	if (!e_gdata_oauth2_authorizer_is_authorized (authorizer, domain) ||
206 	    e_gdata_oauth2_authorizer_is_expired (oauth2_authorizer))
207 		goto exit;
208 
209 	/* We can't add an Authorization header without an access token.
210 	 * Let the request fail.  GData should refresh us if it gets back
211 	 * a "401 Authorization required" response from Google, and then
212 	 * automatically retry the request. */
213 	if (!oauth2_authorizer->priv->access_token)
214 		goto exit;
215 
216 	authorization = g_strdup_printf ("OAuth %s", oauth2_authorizer->priv->access_token);
217 
218 	/* Use replace here, not append, to make sure
219 	 * there's only one "Authorization" header. */
220 	soup_message_headers_replace (
221 		message->request_headers,
222 		"Authorization", authorization);
223 
224 	g_free (authorization);
225 
226 exit:
227 	g_mutex_unlock (&mutex);
228 }
229 
230 static gboolean
e_gdata_oauth2_authorizer_is_authorized_for_domain(GDataAuthorizer * authorizer,GDataAuthorizationDomain * domain)231 e_gdata_oauth2_authorizer_is_authorized_for_domain (GDataAuthorizer *authorizer,
232 						    GDataAuthorizationDomain *domain)
233 {
234 	gboolean authorized;
235 
236 	g_mutex_lock (&mutex);
237 
238 	authorized = e_gdata_oauth2_authorizer_is_authorized (authorizer, domain);
239 
240 	g_mutex_unlock (&mutex);
241 
242 	return authorized;
243 }
244 
245 static gboolean
e_gdata_oauth2_authorizer_refresh_authorization(GDataAuthorizer * authorizer,GCancellable * cancellable,GError ** error)246 e_gdata_oauth2_authorizer_refresh_authorization (GDataAuthorizer *authorizer,
247 						 GCancellable *cancellable,
248 						 GError **error)
249 {
250 	EGDataOAuth2Authorizer *oauth2_authorizer;
251 	ESource *source;
252 	gchar *access_token = NULL;
253 	gint expires_in_seconds = -1;
254 	gboolean success = FALSE;
255 
256 	oauth2_authorizer = E_GDATA_OAUTH2_AUTHORIZER (authorizer);
257 	source = e_gdata_oauth2_authorizer_ref_source (oauth2_authorizer);
258 	g_return_val_if_fail (source != NULL, FALSE);
259 
260 	g_mutex_lock (&mutex);
261 
262 	success = e_source_get_oauth2_access_token_sync (source, cancellable,
263 		&access_token, &expires_in_seconds, error);
264 
265 	/* Returned token is the same, thus no refresh happened, thus rather fail. */
266 	if (access_token && g_strcmp0 (access_token, oauth2_authorizer->priv->access_token) == 0) {
267 		g_free (access_token);
268 		access_token = NULL;
269 		success = FALSE;
270 	}
271 
272 	g_free (oauth2_authorizer->priv->access_token);
273 	oauth2_authorizer->priv->access_token = access_token;
274 
275 	if (success && expires_in_seconds > 0)
276 		oauth2_authorizer->priv->expiry = time (NULL) + expires_in_seconds - 1;
277 	else
278 		oauth2_authorizer->priv->expiry = EXPIRY_INVALID;
279 
280 	g_mutex_unlock (&mutex);
281 
282 	g_object_unref (source);
283 
284 	return success && access_token;
285 }
286 
287 static void
e_gdata_oauth2_authorizer_class_init(EGDataOAuth2AuthorizerClass * class)288 e_gdata_oauth2_authorizer_class_init (EGDataOAuth2AuthorizerClass *class)
289 {
290 	GObjectClass *object_class;
291 
292 	object_class = G_OBJECT_CLASS (class);
293 	object_class->set_property = e_gdata_oauth2_authorizer_set_property;
294 	object_class->get_property = e_gdata_oauth2_authorizer_get_property;
295 	object_class->dispose = e_gdata_oauth2_authorizer_dispose;
296 	object_class->finalize = e_gdata_oauth2_authorizer_finalize;
297 	object_class->constructed = e_gdata_oauth2_authorizer_constructed;
298 
299 	g_object_class_install_property (
300 		object_class,
301 		PROP_SERVICE_TYPE,
302 		g_param_spec_gtype (
303 			"service-type",
304 			"Service Type",
305 			"The service type for which this authorization will be used",
306 			GDATA_TYPE_SERVICE,
307 			G_PARAM_READWRITE |
308 			G_PARAM_CONSTRUCT_ONLY |
309 			G_PARAM_STATIC_STRINGS));
310 
311 	g_object_class_install_property (
312 		object_class,
313 		PROP_SOURCE,
314 		g_param_spec_object (
315 			"source",
316 			"Source",
317 			"The data source to authenticate",
318 			E_TYPE_SOURCE,
319 			G_PARAM_READWRITE |
320 			G_PARAM_CONSTRUCT_ONLY |
321 			G_PARAM_STATIC_STRINGS));
322 }
323 
324 static void
e_gdata_oauth2_authorizer_interface_init(GDataAuthorizerInterface * iface)325 e_gdata_oauth2_authorizer_interface_init (GDataAuthorizerInterface *iface)
326 {
327 	iface->process_request = e_gdata_oauth2_authorizer_process_request;
328 	iface->is_authorized_for_domain = e_gdata_oauth2_authorizer_is_authorized_for_domain;
329 	iface->refresh_authorization = e_gdata_oauth2_authorizer_refresh_authorization;
330 }
331 
332 static void
e_gdata_oauth2_authorizer_init(EGDataOAuth2Authorizer * oauth2_authorizer)333 e_gdata_oauth2_authorizer_init (EGDataOAuth2Authorizer *oauth2_authorizer)
334 {
335 	oauth2_authorizer->priv = e_gdata_oauth2_authorizer_get_instance_private (oauth2_authorizer);
336 	oauth2_authorizer->priv->authorization_domains = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, NULL);
337 	oauth2_authorizer->priv->expiry = EXPIRY_INVALID;
338 	g_weak_ref_init (&oauth2_authorizer->priv->source, NULL);
339 }
340 
341 #else /* HAVE_LIBGDATA */
342 
343 /* Define a fake object, thus GObject introspection code is happy even when
344    libgdata support was disabled. */
G_DEFINE_TYPE(EGDataOAuth2Authorizer,e_gdata_oauth2_authorizer,G_TYPE_OBJECT)345 G_DEFINE_TYPE (EGDataOAuth2Authorizer, e_gdata_oauth2_authorizer, G_TYPE_OBJECT)
346 
347 static void
348 e_gdata_oauth2_authorizer_class_init (EGDataOAuth2AuthorizerClass *class)
349 {
350 }
351 
352 static void
e_gdata_oauth2_authorizer_init(EGDataOAuth2Authorizer * oauth2_authorizer)353 e_gdata_oauth2_authorizer_init (EGDataOAuth2Authorizer *oauth2_authorizer)
354 {
355 }
356 
357 #endif /* HAVE_LIBGDATA */
358 
359 /**
360  * e_gdata_oauth2_authorizer_supported:
361  *
362  * Returns: Whether the #EGDataOAuth2Authorizer is supported, which
363  *    means whether evolution-data-server had been compiled with libgdata.
364  *
365  * Since: 3.28
366  **/
367 gboolean
e_gdata_oauth2_authorizer_supported(void)368 e_gdata_oauth2_authorizer_supported (void)
369 {
370 #ifdef HAVE_LIBGDATA
371 	return TRUE;
372 #else
373 	return FALSE;
374 #endif
375 }
376 
377 /**
378  * e_gdata_oauth2_authorizer_new:
379  * @source: an #ESource
380  * @service_type: a #GDataService type descendant
381  *
382  * Creates a new #EGDataOAuth2Authorizer for the given @source
383  * and @service_type. The function always returns %NULL when
384  * e_gdata_oauth2_authorizer_supported() returns %FALSE.
385  *
386  * Returns: (transfer full): a new #EGDataOAuth2Authorizer, or %NULL when
387  *    the #EGDataOAuth2Authorizer is not supported.
388  *
389  * Since: 3.28
390  **/
391 EGDataOAuth2Authorizer *
e_gdata_oauth2_authorizer_new(ESource * source,GType service_type)392 e_gdata_oauth2_authorizer_new (ESource *source,
393 			       GType service_type)
394 {
395 	g_return_val_if_fail (E_IS_SOURCE (source), NULL);
396 
397 #ifdef HAVE_LIBGDATA
398 	return g_object_new (E_TYPE_GDATA_OAUTH2_AUTHORIZER,
399 		"service-type", service_type,
400 		"source", source,
401 		NULL);
402 #else
403 	return NULL;
404 #endif
405 }
406 
407 /**
408  * e_gdata_oauth2_authorizer_ref_source:
409  * @oauth2_authorizer: an #EGDataOAuth2Authorizer
410  *
411  * Returns: (transfer full): an #ESource, for which the @oauth2_authorizer
412  *    had been created, or %NULL. Free returned non-NULL object with g_object_unref(),
413  *    when done with it.
414  *
415  * See: e_gdata_oauth2_authorizer_supported()
416  *
417  * Since: 3.28
418  **/
419 ESource *
e_gdata_oauth2_authorizer_ref_source(EGDataOAuth2Authorizer * oauth2_authorizer)420 e_gdata_oauth2_authorizer_ref_source (EGDataOAuth2Authorizer *oauth2_authorizer)
421 {
422 #ifdef HAVE_LIBGDATA
423 	g_return_val_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (oauth2_authorizer), NULL);
424 
425 	return g_weak_ref_get (&oauth2_authorizer->priv->source);
426 #else
427 	return NULL;
428 #endif
429 }
430 
431 /**
432  * e_gdata_oauth2_authorizer_get_service_type:
433  * @oauth2_authorizer: an #EGDataOAuth2Authorizer
434  *
435  * Returns: a service %GType, for which the @oauth2_authorizer had been created.
436  *
437  * See: e_gdata_oauth2_authorizer_supported()
438  *
439  * Since: 3.28
440  **/
441 GType
e_gdata_oauth2_authorizer_get_service_type(EGDataOAuth2Authorizer * oauth2_authorizer)442 e_gdata_oauth2_authorizer_get_service_type (EGDataOAuth2Authorizer *oauth2_authorizer)
443 {
444 #ifdef HAVE_LIBGDATA
445 	g_return_val_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (oauth2_authorizer), (GType) 0);
446 
447 	return oauth2_authorizer->priv->service_type;
448 #else
449 	return (GType) 0;
450 #endif
451 }
452 
453 /**
454  * e_gdata_oauth2_authorizer_set_credentials:
455  * @oauth2_authorizer: an #EGDataOAuth2Authorizer
456  * @credentials: (nullable): credentials to set, or %NULL
457  *
458  * Updates internally stored credentials, used to get access token.
459  *
460  * See: e_gdata_oauth2_authorizer_supported()
461  *
462  * Since: 3.28
463  **/
464 void
e_gdata_oauth2_authorizer_set_credentials(EGDataOAuth2Authorizer * oauth2_authorizer,const ENamedParameters * credentials)465 e_gdata_oauth2_authorizer_set_credentials (EGDataOAuth2Authorizer *oauth2_authorizer,
466 					   const ENamedParameters *credentials)
467 {
468 #ifdef HAVE_LIBGDATA
469 	g_return_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (oauth2_authorizer));
470 
471 	g_mutex_lock (&mutex);
472 
473 	e_named_parameters_free (oauth2_authorizer->priv->credentials);
474 	if (credentials)
475 		oauth2_authorizer->priv->credentials = e_named_parameters_new_clone (credentials);
476 	else
477 		oauth2_authorizer->priv->credentials = NULL;
478 
479 	g_free (oauth2_authorizer->priv->access_token);
480 	oauth2_authorizer->priv->access_token = NULL;
481 
482 	oauth2_authorizer->priv->expiry = EXPIRY_INVALID;
483 
484 	g_mutex_unlock (&mutex);
485 #endif
486 }
487 
488 /**
489  * e_gdata_oauth2_authorizer_clone_credentials:
490  * @oauth2_authorizer: an #EGDataOAuth2Authorizer
491  *
492  * Returns: (transfer full) (nullable): A copy of currently stored credentials,
493  *    or %NULL, when none are set. Free the returned structure with
494  *    e_named_parameters_free(), when no longer needed.
495  *
496  * See: e_gdata_oauth2_authorizer_supported()
497  *
498  * Since: 3.28
499  **/
500 ENamedParameters *
e_gdata_oauth2_authorizer_clone_credentials(EGDataOAuth2Authorizer * oauth2_authorizer)501 e_gdata_oauth2_authorizer_clone_credentials (EGDataOAuth2Authorizer *oauth2_authorizer)
502 {
503 #ifdef HAVE_LIBGDATA
504 	ENamedParameters *credentials = NULL;
505 
506 	g_return_val_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (oauth2_authorizer), NULL);
507 
508 	g_mutex_lock (&mutex);
509 
510 	if (oauth2_authorizer->priv->credentials)
511 		credentials = e_named_parameters_new_clone (oauth2_authorizer->priv->credentials);
512 
513 	g_mutex_unlock (&mutex);
514 
515 	return credentials;
516 #else
517 	return NULL;
518 #endif
519 }
520 
521 /**
522  * e_gdata_oauth2_authorizer_is_expired:
523  * @oauth2_authorizer: an #EGDataOAuth2Authorizer
524  *
525  * Returns: Whether the internally stored token is expired.
526  *
527  * See: e_gdata_oauth2_authorizer_supported()
528  *
529  * Since: 3.28
530  **/
531 gboolean
e_gdata_oauth2_authorizer_is_expired(EGDataOAuth2Authorizer * oauth2_authorizer)532 e_gdata_oauth2_authorizer_is_expired (EGDataOAuth2Authorizer *oauth2_authorizer)
533 {
534 #ifdef HAVE_LIBGDATA
535 	g_return_val_if_fail (E_IS_GDATA_OAUTH2_AUTHORIZER (oauth2_authorizer), TRUE);
536 
537 	return oauth2_authorizer->priv->expiry == EXPIRY_INVALID ||
538 	       oauth2_authorizer->priv->expiry <= time (NULL);
539 #else
540 	return TRUE;
541 #endif
542 }
543