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