diff options
Diffstat (limited to 'src/goabackend/goaoauth2provider.c')
-rw-r--r-- | src/goabackend/goaoauth2provider.c | 1473 |
1 files changed, 1473 insertions, 0 deletions
diff --git a/src/goabackend/goaoauth2provider.c b/src/goabackend/goaoauth2provider.c new file mode 100644 index 0000000..74cc6c1 --- /dev/null +++ b/src/goabackend/goaoauth2provider.c @@ -0,0 +1,1473 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n-lib.h> +#include <stdlib.h> + +#include <rest/oauth2-proxy.h> +#include <webkit/webkit.h> +#include <json-glib/json-glib.h> + +#include "goaprovider.h" +#include "goaoauth2provider.h" + +/** + * SECTION:goaoauth2provider + * @title: GoaOAuth2Provider + * @short_description: Abstract base class for OAuth 2.0 providers + * + * #GoaOAuth2Provider is an abstract base class for <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15">OAuth + * 2.0</ulink> based providers. + * + * Subclasses must implement + * #GoaOAuth2ProviderClass.get_authorization_uri, + * #GoaOAuth2ProviderClass.get_token_uri, + * #GoaOAuth2ProviderClass.get_redirect_uri, + * #GoaOAuth2ProviderClass.get_scope, + * #GoaOAuth2ProviderClass.get_client_id, + * #GoaOAuth2ProviderClass.get_client_secret and + * #GoaOAuth2ProviderClass.get_identity_sync methods. + * + * Additionally, the + * #GoaProviderClass.get_provider_type, + * #GoaProviderClass.get_name, + * #GoaProviderClass.build_object (this should chain up to its + * parent class) methods must be implemented. + * + * Note that the #GoaProviderClass.add_account, + * #GoaProviderClass.refresh_account and + * #GoaProviderClass.ensure_credentials_sync methods do not + * need to be implemented - this type implements these methods.. + */ + +G_LOCK_DEFINE_STATIC (provider_lock); + +G_DEFINE_ABSTRACT_TYPE (GoaOAuth2Provider, goa_oauth2_provider, GOA_TYPE_PROVIDER); + +static gboolean +is_authorization_error (GError *error) +{ + gboolean ret; + + g_return_val_if_fail (error != NULL, FALSE); + + ret = FALSE; + if (error->domain == REST_PROXY_ERROR || error->domain == SOUP_HTTP_ERROR) + { + if (SOUP_STATUS_IS_CLIENT_ERROR (error->code)) + ret = TRUE; + } + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_oauth2_provider_get_use_external_browser_default (GoaOAuth2Provider *provider) +{ + return FALSE; +} + +/** + * goa_oauth2_provider_get_use_external_browser: + * @provider: A #GoaOAuth2Provider. + * + * Returns whether an external browser (the users default browser) + * should be used for user interaction. + * + * If an external browser is used, then the dialogs presented in + * goa_provider_add_account() and + * goa_provider_refresh_account() will show a simple "Paste + * authorization code here" instructions along with an entry and + * button. + * + * This is a virtual method where the default implementation returns + * %FALSE. + * + * Returns: %TRUE if the user interaction should happen in an external + * browser, %FALSE to use an embedded browser widget. + */ +gboolean +goa_oauth2_provider_get_use_external_browser (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), FALSE); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_use_external_browser (provider); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gchar * +goa_oauth2_provider_build_authorization_uri_default (GoaOAuth2Provider *provider, + const gchar *authorization_uri, + const gchar *escaped_redirect_uri, + const gchar *escaped_client_id, + const gchar *escaped_scope) +{ + return g_strdup_printf ("%s" + "?response_type=code" + "&redirect_uri=%s" + "&client_id=%s" + "&scope=%s", + authorization_uri, + escaped_redirect_uri, + escaped_client_id, + escaped_scope); +} + +/** + * goa_oauth2_provider_build_authorization_uri: + * @provider: A #GoaOAuth2Provider. + * @authorization_uri: An authorization URI. + * @escaped_redirect_uri: An escaped redirect URI + * @escaped_client_id: An escaped client id + * @escaped_scope: The escaped scope. + * + * Builds the URI that can be opened in a web browser (or embedded web + * browser widget) to start authenticating an user. + * + * The default implementation just returns the expected URI + * (e.g. <literal>http://example.com/dialog/oauth2?response_type=code&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&client_id=foo&scope=email%20stuff</literal>) + * - override (and chain up) if you e.g. need to to pass additional + * parameters. + * + * The @authorization_uri, @escaped_redirect_uri, @escaped_client_id + * and @escaped_scope parameters originate from the result of the + * the goa_oauth2_provider_get_authorization_uri(), goa_oauth2_provider_get_redirect_uri(), goa_oauth2_provider_get_client_id() + * and goa_oauth2_provider_get_scope() methods with the latter + * three escaped using g_uri_escape_string(). + * + * Returns: (transfer full): An authorization URI that must be freed with g_free(). + */ +gchar * +goa_oauth2_provider_build_authorization_uri (GoaOAuth2Provider *provider, + const gchar *authorization_uri, + const gchar *escaped_redirect_uri, + const gchar *escaped_client_id, + const gchar *escaped_scope) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + g_return_val_if_fail (authorization_uri != NULL, NULL); + g_return_val_if_fail (escaped_redirect_uri != NULL, NULL); + g_return_val_if_fail (escaped_client_id != NULL, NULL); + g_return_val_if_fail (escaped_scope != NULL, NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->build_authorization_uri (provider, + authorization_uri, + escaped_redirect_uri, + escaped_client_id, + escaped_scope); +} + +/** + * goa_oauth2_provider_get_authorization_uri: + * @provider: A #GoaOAuth2Provider. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-2.1">authorization + * endpoint</ulink> used for authenticating the user and obtaining + * authorization. + * + * You should not include any parameters in the returned URI. If you + * need to include additional parameters than the standard ones, + * override #GoaOAuth2ProviderClass.build_authorization_uri - + * see goa_oauth2_provider_build_authorization_uri() for more + * details. + * + * This is a pure virtual method - a subclass must provide an + * implementation. + * + * Returns: (transfer none): A string owned by @provider - do not free. + */ +const gchar * +goa_oauth2_provider_get_authorization_uri (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_authorization_uri (provider); +} + +/** + * goa_oauth2_provider_get_token_uri: + * @provider: A #GoaOAuth2Provider. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-2.2">token + * endpoint</ulink> used for obtaining an access token. + * + * You should not include any parameters in the returned URI. + * + * This is a pure virtual method - a subclass must provide an + * implementation. + * + * Returns: (transfer none): A string owned by @provider - do not free. + */ +const gchar * +goa_oauth2_provider_get_token_uri (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_token_uri (provider); +} + +/** + * goa_oauth2_provider_get_redirect_uri: + * @provider: A #GoaOAuth2Provider. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-2.1.1">redirect_uri</ulink> + * used when requesting authorization. + * + * This is a pure virtual method - a subclass must provide an + * implementation. + * + * Returns: (transfer none): A string owned by @provider - do not free. + */ +const gchar * +goa_oauth2_provider_get_redirect_uri (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_redirect_uri (provider); +} + +/** + * goa_oauth2_provider_get_scope: + * @provider: A #GoaOAuth2Provider. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-2.1.1">scope</ulink> + * used when requesting authorization. + * + * This is a pure virtual method - a subclass must provide an + * implementation. + * + * Returns: (transfer none): A string owned by @provider - do not free. + */ +const gchar * +goa_oauth2_provider_get_scope (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_scope (provider); +} + +/** + * goa_oauth2_provider_get_client_id: + * @provider: A #GoaOAuth2Provider. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-3">client_id</ulink> + * identifying the client. + * + * This is a pure virtual method - a subclass must provide an + * implementation. + * + * Returns: (transfer none): A string owned by @provider - do not free. + */ +const gchar * +goa_oauth2_provider_get_client_id (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_client_id (provider); +} + +/** + * goa_oauth2_provider_get_client_secret: + * @provider: A #GoaOAuth2Provider. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-3">client_secret</ulink> + * associated with the client. + * + * This is a pure virtual method - a subclass must provide an + * implementation. + * + * Returns: (transfer none): A string owned by @provider - do not free. + */ +const gchar * +goa_oauth2_provider_get_client_secret (GoaOAuth2Provider *provider) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_client_secret (provider); +} + +/** + * goa_oauth2_provider_get_identity_sync: + * @provider: A #GoaOAuth2Provider. + * @access_token: A valid OAuth 2.0 access token. + * @out_name: (out): Return location for name or %NULL. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @error: Return location for @error or %NULL. + * + * Method that returns the identity corresponding to + * @access_token. + * + * The identity is needed because all authentication happens out of + * band. The only requirement is that the returned identity is unique + * - for example, for #GoaGoogleProvider the returned identity + * is the email address, for #GoaFacebookProvider it's the user + * name. In addition to the identity, an implementation also returns a + * <emphasis>name</emphasis> that is more suitable for presentation + * (the identity could be a GUID for example) and doesn't have to be + * unique. + * + * The calling thread is blocked while the identity is obtained. + * + * Returns: The identity or %NULL if error is set. The returned string + * must be freed with g_free(). + */ +gchar * +goa_oauth2_provider_get_identity_sync (GoaOAuth2Provider *provider, + const gchar *access_token, + gchar **out_name, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + g_return_val_if_fail (access_token != NULL, NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + return GOA_OAUTH2_PROVIDER_GET_CLASS (provider)->get_identity_sync (provider, access_token, out_name, cancellable, error); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gchar * +get_tokens_sync (GoaOAuth2Provider *provider, + const gchar *authorization_code, + const gchar *refresh_token, + gchar **out_refresh_token, + gint *out_access_token_expires_in, + GCancellable *cancellable, + GError **error) +{ + RestProxy *proxy; + RestProxyCall *call; + gchar *ret; + guint status_code; + gchar *ret_access_token; + gint ret_access_token_expires_in; + gchar *ret_refresh_token; + const gchar *payload; + gsize payload_length; + + ret = NULL; + ret_access_token = NULL; + ret_access_token_expires_in = 0; + ret_refresh_token = NULL; + + proxy = rest_proxy_new (goa_oauth2_provider_get_token_uri (provider), FALSE); + call = rest_proxy_new_call (proxy); + rest_proxy_call_set_method (call, "POST"); + rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded"); + if (refresh_token != NULL) + { + /* Swell, we have a refresh code - just use that */ + rest_proxy_call_add_param (call, "client_id", goa_oauth2_provider_get_client_id (provider)); + rest_proxy_call_add_param (call, "client_secret", goa_oauth2_provider_get_client_secret (provider)); + rest_proxy_call_add_param (call, "grant_type", "refresh_token"); + rest_proxy_call_add_param (call, "refresh_token", refresh_token); + } + else + { + /* No refresh code.. request an access token using the authorization code instead */ + rest_proxy_call_add_param (call, "client_id", goa_oauth2_provider_get_client_id (provider)); + rest_proxy_call_add_param (call, "client_secret", goa_oauth2_provider_get_client_secret (provider)); + rest_proxy_call_add_param (call, "grant_type", "authorization_code"); + rest_proxy_call_add_param (call, "code", authorization_code); + rest_proxy_call_add_param (call, "redirect_uri", goa_oauth2_provider_get_redirect_uri (provider)); + } + + /* TODO: cancellable support? */ + if (!rest_proxy_call_sync (call, error)) + goto out; + + status_code = rest_proxy_call_get_status_code (call); + if (status_code != 200) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Expected status 200 when requesting access token, instead got status %d (%s)"), + status_code, + rest_proxy_call_get_status_message (call)); + goto out; + } + + payload = rest_proxy_call_get_payload (call); + payload_length = rest_proxy_call_get_payload_length (call); + /* some older OAuth2 implementations does not return json - handle that too */ + if (g_str_has_prefix (payload, "access_token=")) + { + GHashTable *hash; + const gchar *expires_in_str; + hash = soup_form_decode (payload); + ret_access_token = g_strdup (g_hash_table_lookup (hash, "access_token")); + if (ret_access_token == NULL) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Didn't find access_token in non-JSON data")); + g_hash_table_unref (hash); + goto out; + } + /* refresh_token is optional */ + ret_refresh_token = g_hash_table_lookup (hash, "refresh_token"); + /* expires_in is optional */ + expires_in_str = g_hash_table_lookup (hash, "expires_in"); + /* sometimes "expires_in" appears as "expires" */ + if (expires_in_str == NULL) + expires_in_str = g_hash_table_lookup (hash, "expires"); + if (expires_in_str != NULL) + ret_access_token_expires_in = atoi (expires_in_str); + g_hash_table_unref (hash); + } + else + { + GError *local_error; + JsonParser *parser; + JsonObject *object; + + local_error = NULL; + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, payload, payload_length, &local_error)) + { + g_prefix_error (error, _("Error parsing response as JSON: ")); + g_object_unref (parser); + goto out; + } + object = json_node_get_object (json_parser_get_root (parser)); + ret_access_token = g_strdup (json_object_get_string_member (object, "access_token")); + if (ret_access_token == NULL) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Didn't find access_token in JSON data")); + goto out; + } + /* refresh_token is optional... */ + if (json_object_has_member (object, "refresh_token")) + ret_refresh_token = g_strdup (json_object_get_string_member (object, "refresh_token")); + if (json_object_has_member (object, "expires_in")) + ret_access_token_expires_in = json_object_get_int_member (object, "expires_in"); + g_object_unref (parser); + } + + ret = ret_access_token; + ret_access_token = NULL; + if (out_access_token_expires_in != NULL) + *out_access_token_expires_in = ret_access_token_expires_in; + if (out_refresh_token != NULL) + { + *out_refresh_token = ret_refresh_token; + ret_refresh_token = NULL; + } + + out: + g_free (ret_access_token); + g_free (ret_refresh_token); + if (call != NULL) + g_object_unref (call); + if (proxy != NULL) + g_object_unref (proxy); + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ +typedef struct +{ + GoaOAuth2Provider *provider; + GtkDialog *dialog; + GError *error; + GMainLoop *loop; + + gchar *authorization_code; + gchar *access_token; + gint access_token_expires_in; + gchar *refresh_token; + + gchar *identity; + gchar *name; +} IdentifyData; + +static gboolean +on_web_view_navigation_policy_decision_requested (WebKitWebView *webView, + WebKitWebFrame *frame, + WebKitNetworkRequest *request, + WebKitWebNavigationAction *navigation_action, + WebKitWebPolicyDecision *policy_decision, + gpointer user_data) +{ + IdentifyData *data = user_data; + const gchar *redirect_uri; + const gchar *requested_uri; + + /* TODO: use oauth2_proxy_extract_access_token() */ + + requested_uri = webkit_network_request_get_uri (request); + //g_debug ("requested_uri is %s", requested_uri); + redirect_uri = goa_oauth2_provider_get_redirect_uri (data->provider); + if (g_str_has_prefix (requested_uri, redirect_uri)) + { + SoupMessage *message; + SoupURI *uri; + GHashTable *key_value_pairs; + + message = webkit_network_request_get_message (request); + uri = soup_message_get_uri (message); + key_value_pairs = soup_form_decode (uri->query); + + data->authorization_code = g_strdup (g_hash_table_lookup (key_value_pairs, "code")); + if (data->authorization_code != NULL) + { + gtk_dialog_response (data->dialog, GTK_RESPONSE_OK); + } + else + { + g_set_error (&data->error, + GOA_ERROR, + GOA_ERROR_NOT_AUTHORIZED, + _("Authorization response was \"%s\""), + (const gchar *) g_hash_table_lookup (key_value_pairs, "error")); + gtk_dialog_response (data->dialog, GTK_RESPONSE_CLOSE); + } + g_hash_table_unref (key_value_pairs); + webkit_web_policy_decision_ignore (policy_decision); + return TRUE; /* ignore the request */ + } + else + { + return FALSE; /* make default behavior apply */ + } +} + +static void +on_entry_changed (GtkEditable *editable, + gpointer user_data) +{ + IdentifyData *data = user_data; + gboolean sensitive; + + g_free (data->authorization_code); + data->authorization_code = g_strdup (gtk_entry_get_text (GTK_ENTRY (editable))); + sensitive = data->authorization_code != NULL && (strlen (data->authorization_code) > 0); + gtk_dialog_set_response_sensitive (data->dialog, GTK_RESPONSE_OK, sensitive); +} + +static gboolean +get_tokens_and_identity (GoaOAuth2Provider *provider, + GtkDialog *dialog, + GtkBox *vbox, + gchar **out_authorization_code, + gchar **out_access_token, + gint *out_access_token_expires_in, + gchar **out_refresh_token, + gchar **out_identity, + gchar **out_name, + GError **error) +{ + gboolean ret; + gchar *url; + IdentifyData data; + gchar *escaped_redirect_uri; + gchar *escaped_client_id; + gchar *escaped_scope; + + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), FALSE); + g_return_val_if_fail (GTK_IS_DIALOG (dialog), FALSE); + g_return_val_if_fail (GTK_IS_BOX (vbox), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + ret = FALSE; + escaped_redirect_uri = NULL; + escaped_client_id = NULL; + escaped_scope = NULL; + + /* TODO: check with NM whether we're online, if not - return error */ + + memset (&data, '\0', sizeof (IdentifyData)); + data.provider = provider; + data.loop = g_main_loop_new (NULL, FALSE); + + /* TODO: use oauth2_proxy_build_login_url_full() */ + escaped_redirect_uri = g_uri_escape_string (goa_oauth2_provider_get_redirect_uri (provider), NULL, TRUE); + escaped_client_id = g_uri_escape_string (goa_oauth2_provider_get_client_id (provider), NULL, TRUE); + escaped_scope = g_uri_escape_string (goa_oauth2_provider_get_scope (provider), NULL, TRUE); + url = goa_oauth2_provider_build_authorization_uri (provider, + goa_oauth2_provider_get_authorization_uri (provider), + escaped_redirect_uri, + escaped_client_id, + escaped_scope); + //g_debug ("url = %s", url); + + if (goa_oauth2_provider_get_use_external_browser (provider)) + { + GtkWidget *label; + GtkWidget *entry; + gchar *escaped_url; + gchar *markup; + + escaped_url = g_markup_escape_text (url, -1); + markup = g_strdup_printf (_("Paste authorization code obtained from the <a href=\"%s\">authorization page</a>:"), + escaped_url); + g_free (escaped_url); + + label = gtk_label_new (NULL); + gtk_label_set_markup (GTK_LABEL (label), markup); + g_free (markup); + gtk_box_pack_start (GTK_BOX (vbox), label, FALSE, TRUE, 0); + entry = gtk_entry_new (); + gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE); + gtk_box_pack_start (GTK_BOX (vbox), entry, FALSE, TRUE, 0); + gtk_widget_grab_focus (entry); + gtk_widget_show_all (GTK_WIDGET (vbox)); + + gtk_dialog_add_button (dialog, GTK_STOCK_OK, GTK_RESPONSE_OK); + gtk_dialog_set_default_response (dialog, GTK_RESPONSE_OK); + gtk_dialog_set_response_sensitive (dialog, GTK_RESPONSE_OK, FALSE); + g_signal_connect (entry, "changed", G_CALLBACK (on_entry_changed), &data); + + if (!gtk_show_uri (NULL, + url, + GDK_CURRENT_TIME, + &data.error)) + { + goto out; + } + } + else + { + GtkWidget *scrolled_window; + GtkWidget *web_view; + SoupSession *webkit_soup_session; + SoupCookieJar *cookie_jar; + + /* Ensure we use an empty non-persistent cookie to avoid login + * credentials being reused... + */ + webkit_soup_session = webkit_get_default_session (); + soup_session_remove_feature_by_type (webkit_soup_session, SOUP_TYPE_COOKIE_JAR); + cookie_jar = soup_cookie_jar_new (); + soup_session_add_feature (webkit_soup_session, SOUP_SESSION_FEATURE (cookie_jar)); + g_object_unref (cookie_jar); + + /* TODO: we might need to add some more web browser UI to make this + * work... + */ + web_view = webkit_web_view_new (); + webkit_web_view_load_uri (WEBKIT_WEB_VIEW (web_view), url); + g_signal_connect (web_view, + "navigation-policy-decision-requested", + G_CALLBACK (on_web_view_navigation_policy_decision_requested), + &data); + + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_widget_set_size_request (scrolled_window, 500, 400); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_container_add (GTK_CONTAINER (scrolled_window), web_view); + gtk_container_add (GTK_CONTAINER (vbox), scrolled_window); + gtk_widget_show_all (scrolled_window); + } + data.dialog = dialog; + gtk_dialog_run (GTK_DIALOG (dialog)); + if (data.authorization_code == NULL) + { + if (data.error == NULL) + { + g_set_error (&data.error, + GOA_ERROR, + GOA_ERROR_DIALOG_DISMISSED, + _("Dialog was dismissed")); + } + goto out; + } + g_assert (data.error == NULL); + + gtk_widget_hide (GTK_WIDGET (dialog)); + + /* OK, we now have the authorization code... now we need to get the + * email address (to e.g. check if the account already exists on + * @client).. for that we need to get a (short-lived) access token + * and a refresh_token + */ + + /* TODO: run in worker thread */ + data.access_token = get_tokens_sync (provider, + data.authorization_code, + NULL, /* refresh_token */ + &data.refresh_token, + &data.access_token_expires_in, + NULL, /* GCancellable */ + error); + if (data.access_token == NULL) + { + g_prefix_error (&data.error, _("Error getting an Access Token: ")); + goto out; + } + + /* TODO: run in worker thread */ + data.identity = goa_oauth2_provider_get_identity_sync (provider, + data.access_token, + &data.name, + NULL, /* TODO: GCancellable */ + error); + if (data.identity == NULL) + { + g_prefix_error (&data.error, _("Error getting identity: ")); + goto out; + } + + ret = TRUE; + + out: + if (ret) + { + g_warn_if_fail (data.error == NULL); + if (out_authorization_code != NULL) + *out_authorization_code = g_strdup (data.authorization_code); + if (out_access_token != NULL) + *out_access_token = g_strdup (data.access_token); + if (out_access_token_expires_in != NULL) + *out_access_token_expires_in = data.access_token_expires_in; + if (out_refresh_token != NULL) + *out_refresh_token = g_strdup (data.refresh_token); + if (out_identity != NULL) + *out_identity = g_strdup (data.identity); + if (out_name != NULL) + *out_name = g_strdup (data.name); + } + else + { + g_warn_if_fail (data.error != NULL); + g_propagate_error (error, data.error); + } + + g_free (data.identity); + g_free (data.name); + g_free (url); + + g_free (data.authorization_code); + if (data.loop != NULL) + g_main_loop_unref (data.loop); + g_free (data.access_token); + g_free (data.refresh_token); + g_free (escaped_redirect_uri); + g_free (escaped_client_id); + g_free (escaped_scope); + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +typedef struct +{ + GError *error; + GMainLoop *loop; + gchar *account_object_path; +} AddData; + +static void +add_account_cb (GoaManager *manager, + GAsyncResult *res, + gpointer user_data) +{ + AddData *data = user_data; + goa_manager_call_add_account_finish (manager, + &data->account_object_path, + res, + &data->error); + g_main_loop_quit (data->loop); +} + +static gint64 +duration_to_abs_usec (gint duration_sec) +{ + gint64 ret; + GTimeVal now; + + g_get_current_time (&now); + ret = ((gint64) now.tv_sec) * 1000L * 1000L + ((gint64) now.tv_usec); + ret += ((gint64) duration_sec) * 1000L * 1000L; + return ret; +} + +static gint +abs_usec_to_duration (gint64 abs_usec) +{ + gint64 ret; + GTimeVal now; + + g_get_current_time (&now); + ret = abs_usec - (((gint64) now.tv_sec) * 1000L * 1000L + ((gint64) now.tv_usec)); + ret /= 1000L * 1000L; + return ret; +} + +static GoaObject * +goa_oauth2_provider_add_account (GoaProvider *_provider, + GoaClient *client, + GtkDialog *dialog, + GtkBox *vbox, + GError **error) +{ + GoaOAuth2Provider *provider = GOA_OAUTH2_PROVIDER (_provider); + GoaObject *ret; + gchar *authorization_code; + gchar *access_token; + gint access_token_expires_in; + gchar *refresh_token; + gchar *identity; + gchar *name; + GList *accounts; + GList *l; + AddData data; + GVariantBuilder builder; + + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + g_return_val_if_fail (GOA_IS_CLIENT (client), NULL); + g_return_val_if_fail (GTK_IS_DIALOG (dialog), NULL); + g_return_val_if_fail (GTK_IS_BOX (vbox), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + ret = NULL; + authorization_code = NULL; + access_token = NULL; + refresh_token = NULL; + identity = NULL; + name = NULL; + accounts = NULL; + + memset (&data, '\0', sizeof (AddData)); + data.loop = g_main_loop_new (NULL, FALSE); + + if (!get_tokens_and_identity (provider, + dialog, + vbox, + &authorization_code, + &access_token, + &access_token_expires_in, + &refresh_token, + &identity, + &name, + &data.error)) + goto out; + + /* OK, got the identity... see if there's already an account + * of this type with the given identity + */ + accounts = goa_client_get_accounts (client); + for (l = accounts; l != NULL; l = l->next) + { + GoaObject *object = GOA_OBJECT (l->data); + GoaAccount *account; + GoaOAuth2Based *oauth2_based; + const gchar *identity_from_object; + + account = goa_object_peek_account (object); + oauth2_based = goa_object_peek_oauth2_based (object); + if (oauth2_based == NULL) + continue; + + if (g_strcmp0 (goa_account_get_provider_type (account), + goa_provider_get_provider_type (GOA_PROVIDER (provider))) != 0) + continue; + + identity_from_object = goa_oauth2_based_get_identity (oauth2_based); + if (g_strcmp0 (identity_from_object, identity) == 0) + { + g_set_error (&data.error, + GOA_ERROR, + GOA_ERROR_ACCOUNT_EXISTS, + _("There is already an account for the identity %s"), + identity); + goto out; + } + } + + /* we want the GoaClient to update before this method returns (so it + * can create a proxy for the new object) so run the mainloop while + * waiting for this to complete + */ + goa_manager_call_add_account (goa_client_get_manager (client), + goa_provider_get_provider_type (GOA_PROVIDER (provider)), + name, /* Name */ + g_variant_new_parsed ("{'Identity': %s}", + identity), + NULL, /* GCancellable* */ + (GAsyncReadyCallback) add_account_cb, + &data); + g_main_loop_run (data.loop); + if (data.error != NULL) + goto out; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&builder, "{sv}", "authorization_code", g_variant_new_string (authorization_code)); + g_variant_builder_add (&builder, "{sv}", "access_token", g_variant_new_string (access_token)); + if (access_token_expires_in > 0) + g_variant_builder_add (&builder, "{sv}", "access_token_expires_at", + g_variant_new_int64 (duration_to_abs_usec (access_token_expires_in))); + if (refresh_token != NULL) + g_variant_builder_add (&builder, "{sv}", "refresh_token", g_variant_new_string (refresh_token)); + /* TODO: run in worker thread */ + if (!goa_provider_store_credentials_sync (GOA_PROVIDER (provider), + identity, + g_variant_builder_end (&builder), + NULL, /* GCancellable */ + &data.error)) + goto out; + + ret = GOA_OBJECT (g_dbus_object_manager_get_object (goa_client_get_object_manager (client), + data.account_object_path)); + + out: + if (data.error != NULL) + { + g_propagate_error (error, data.error); + g_assert (ret == NULL); + } + else + { + g_assert (ret != NULL); + } + + g_list_foreach (accounts, (GFunc) g_object_unref, NULL); + g_list_free (accounts); + g_free (identity); + g_free (name); + g_free (refresh_token); + g_free (access_token); + g_free (authorization_code); + g_free (data.account_object_path); + if (data.loop != NULL) + g_main_loop_unref (data.loop); + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_oauth2_provider_refresh_account (GoaProvider *_provider, + GoaClient *client, + GoaObject *object, + GtkWindow *parent, + GError **error) +{ + GoaOAuth2Provider *provider = GOA_OAUTH2_PROVIDER (_provider); + GtkWidget *dialog; + gchar *authorization_code; + gchar *access_token; + gint access_token_expires_in; + gchar *refresh_token; + gchar *identity; + const gchar *existing_identity; + GVariantBuilder builder; + gboolean ret; + + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), FALSE); + g_return_val_if_fail (GOA_IS_CLIENT (client), FALSE); + g_return_val_if_fail (GOA_IS_OBJECT (object), FALSE); + g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + authorization_code = NULL; + access_token = NULL; + refresh_token = NULL; + identity = NULL; + + dialog = gtk_dialog_new_with_buttons (NULL, + parent, + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CANCEL, GTK_RESPONSE_REJECT, + NULL); + gtk_container_set_border_width (GTK_CONTAINER (dialog), 12); + gtk_window_set_resizable (GTK_WINDOW (dialog), FALSE); + gtk_widget_show_all (dialog); + + if (!get_tokens_and_identity (provider, + GTK_DIALOG (dialog), + GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dialog))), + &authorization_code, + &access_token, + &access_token_expires_in, + &refresh_token, + &identity, + NULL, /* out_name */ + error)) + goto out; + + existing_identity = goa_oauth2_based_get_identity (goa_object_peek_oauth2_based (object)); + if (g_strcmp0 (identity, existing_identity) != 0) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Was asked to login as %s, but logged in as %s"), + existing_identity, + identity); + goto out; + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&builder, "{sv}", "authorization_code", g_variant_new_string (authorization_code)); + g_variant_builder_add (&builder, "{sv}", "access_token", g_variant_new_string (access_token)); + if (access_token_expires_in > 0) + g_variant_builder_add (&builder, "{sv}", "access_token_expires_at", + g_variant_new_int64 (duration_to_abs_usec (access_token_expires_in))); + if (refresh_token != NULL) + g_variant_builder_add (&builder, "{sv}", "refresh_token", g_variant_new_string (refresh_token)); + if (!goa_provider_store_credentials_sync (GOA_PROVIDER (provider), + identity, + g_variant_builder_end (&builder), + NULL, /* GCancellable */ + error)) + goto out; + + goa_account_call_ensure_credentials (goa_object_peek_account (object), + NULL, /* GCancellable */ + NULL, NULL); /* callback, user_data */ + + ret = TRUE; + + out: + gtk_widget_destroy (dialog); + + g_free (identity); + g_free (access_token); + g_free (authorization_code); + g_free (refresh_token); + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +free_mutex (GMutex *mutex) +{ + g_mutex_free (mutex); +} + +/** + * goa_oauth2_provider_get_access_token_sync: + * @provider: A #GoaOAuth2Provider. + * @object: A #GoaObject. + * @force_refresh: If set to %TRUE, forces a refresh of the access token, if possible. + * @out_access_token_expires_in: (out): Return location for how many seconds the returned token is valid for (0 if unknown) or %NULL. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @error: Return location for error or %NULL. + * + * Synchronously gets an access token for @object. The calling thread + * is blocked while the operation is pending. + * + * The resulting token is typically read from the local cache so most + * of the time only a local roundtrip to the storage for the token + * cache (e.g. <command>gnome-keyring-daemon</command>) is + * needed. However, the operation may involve refreshing the token + * with the service provider so a full network round-trip may be + * needed. + * + * Note that multiple calls are serialized to avoid multiple + * outstanding requests to the service provider. + * + * This operation may fail if e.g. unable to refresh the credentials + * or if network connectivity is not available. Note that even if a + * token is returned, the returned token isn't guaranteed to work - + * use goa_provider_ensure_credentials_sync() if you need + * stronger guarantees. + * + * Returns: The access token or %NULL if error is set. The returned + * string must be freed with g_free(). + */ +gchar * +goa_oauth2_provider_get_access_token_sync (GoaOAuth2Provider *provider, + GoaObject *object, + gboolean force_refresh, + gint *out_access_token_expires_in, + GCancellable *cancellable, + GError **error) +{ + const gchar *identity; + GVariant *credentials; + GVariantIter iter; + const gchar *key; + GVariant *value; + gchar *authorization_code; + gchar *access_token; + gint access_token_expires_in; + gchar *refresh_token; + gchar *old_refresh_token; + gboolean success; + GVariantBuilder builder; + gchar *ret; + GMutex *lock; + + g_return_val_if_fail (GOA_IS_OAUTH2_PROVIDER (provider), NULL); + g_return_val_if_fail (GOA_IS_OBJECT (object), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + ret = NULL; + credentials = NULL; + authorization_code = NULL; + access_token = NULL; + refresh_token = NULL; + old_refresh_token = NULL; + access_token_expires_in = 0; + success = FALSE; + + /* provider_lock is too coarse, use a per-object lock instead */ + G_LOCK (provider_lock); + lock = g_object_get_data (G_OBJECT (object), "-goa-oauth2-provider-get-access-token-lock"); + if (lock == NULL) + { + lock = g_mutex_new (); + g_object_set_data_full (G_OBJECT (object), + "-goa-oauth2-provider-get-access-token-lock", + lock, + (GDestroyNotify) free_mutex); + } + G_UNLOCK (provider_lock); + + g_mutex_lock (lock); + + /* First, get the credentials from the keyring */ + identity = goa_oauth2_based_get_identity (goa_object_peek_oauth2_based (object)); + credentials = goa_provider_lookup_credentials_sync (GOA_PROVIDER (provider), + identity, + cancellable, + error); + if (credentials == NULL) + { + if (error != NULL) + { + g_prefix_error (error, _("Credentials not found in keyring (%s, %d): "), + g_quark_to_string ((*error)->domain), (*error)->code); + (*error)->domain = GOA_ERROR; + (*error)->code = GOA_ERROR_NOT_AUTHORIZED; + } + goto out; + } + + g_variant_iter_init (&iter, credentials); + while (g_variant_iter_next (&iter, "{&sv}", &key, &value)) + { + if (g_strcmp0 (key, "access_token") == 0) + access_token = g_variant_dup_string (value, NULL); + else if (g_strcmp0 (key, "access_token_expires_at") == 0) + access_token_expires_in = abs_usec_to_duration (g_variant_get_int64 (value)); + else if (g_strcmp0 (key, "refresh_token") == 0) + refresh_token = g_variant_dup_string (value, NULL); + else if (g_strcmp0 (key, "authorization_code") == 0) + authorization_code = g_variant_dup_string (value, NULL); + g_variant_unref (value); + } + + if (access_token == NULL) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_NOT_AUTHORIZED, + _("Credentials does not contain access_token")); + goto out; + } + + /* if we can't refresh the token, just return it no matter what */ + if (refresh_token == NULL) + { + g_debug ("Returning locally cached credentials that cannot be refreshed"); + success = TRUE; + goto out; + } + + /* If access_token is still "fresh enough" (e.g. more than ten + * minutes of life left in it), just return it unless we've been + * asked to forcibly refresh it + */ + if (!force_refresh && access_token_expires_in > 10*60) + { + g_debug ("Returning locally cached credentials (expires in %d seconds)", access_token_expires_in); + success = TRUE; + goto out; + } + + g_debug ("Refreshing locally cached credentials (expires in %d seconds, force_refresh=%d)", access_token_expires_in, force_refresh); + + /* Otherwise, refresh it */ + old_refresh_token = refresh_token; refresh_token = NULL; + g_free (access_token); access_token = NULL; + access_token = get_tokens_sync (provider, + authorization_code, + old_refresh_token, + &refresh_token, + &access_token_expires_in, + cancellable, + error); + if (access_token == NULL) + { + if (error != NULL) + { + g_prefix_error (error, _("Failed to refresh access token (%s, %d): "), + g_quark_to_string ((*error)->domain), (*error)->code); + (*error)->code = is_authorization_error (*error) ? GOA_ERROR_NOT_AUTHORIZED : GOA_ERROR_FAILED; + (*error)->domain = GOA_ERROR; + } + goto out; + } + + /* It's not a sure thing we get a new refresh_token, so use our old + * old if we didn't get a new one + */ + if (refresh_token == NULL) + { + refresh_token = old_refresh_token; + old_refresh_token = NULL; + } + + /* Good. Now update the keyring with the refreshed credentials */ + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&builder, "{sv}", "authorization_code", g_variant_new_string (authorization_code)); + g_variant_builder_add (&builder, "{sv}", "access_token", g_variant_new_string (access_token)); + if (access_token_expires_in > 0) + g_variant_builder_add (&builder, "{sv}", "access_token_expires_at", + g_variant_new_int64 (duration_to_abs_usec (access_token_expires_in))); + if (refresh_token != NULL) + g_variant_builder_add (&builder, "{sv}", "refresh_token", g_variant_new_string (refresh_token)); + + identity = goa_oauth2_based_get_identity (goa_object_peek_oauth2_based (object)); + if (!goa_provider_store_credentials_sync (GOA_PROVIDER (provider), + identity, + g_variant_builder_end (&builder), + cancellable, + error)) + { + if (error != NULL) + { + g_prefix_error (error, _("Error storing credentials in keyring (%s, %d): "), + g_quark_to_string ((*error)->domain), (*error)->code); + (*error)->domain = GOA_ERROR; + (*error)->code = GOA_ERROR_NOT_AUTHORIZED; + } + goto out; + } + + success = TRUE; + + out: + if (success) + { + ret = access_token; access_token = NULL; + g_assert (ret != NULL); + if (out_access_token_expires_in != NULL) + *out_access_token_expires_in = access_token_expires_in; + } + g_free (authorization_code); + g_free (access_token); + g_free (refresh_token); + g_free (old_refresh_token); + if (credentials != NULL) + g_variant_unref (credentials); + + g_mutex_unlock (lock); + + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean on_handle_get_access_token (GoaOAuth2Based *object, + GDBusMethodInvocation *invocation, + gpointer user_data); + +static gboolean +goa_oauth2_provider_build_object (GoaProvider *provider, + GoaObjectSkeleton *object, + GKeyFile *key_file, + const gchar *group, + GError **error) +{ + GoaOAuth2Based *oauth2_based; + gchar *identity; + + identity = NULL; + + oauth2_based = goa_object_get_oauth2_based (GOA_OBJECT (object)); + if (oauth2_based != NULL) + goto out; + + oauth2_based = goa_oauth2_based_skeleton_new (); + /* Ensure D-Bus method invocations run in their own thread */ + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (oauth2_based), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + goa_object_skeleton_set_oauth2_based (object, oauth2_based); + g_signal_connect (oauth2_based, + "handle-get-access-token", + G_CALLBACK (on_handle_get_access_token), + NULL); + + identity = g_key_file_get_string (key_file, group, "Identity", NULL); + if (identity == NULL) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "No Identity key"); + goto out; + } + goa_oauth2_based_set_identity (oauth2_based, identity); + + out: + g_object_unref (oauth2_based); + g_free (identity); + return TRUE; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_oauth2_provider_ensure_credentials_sync (GoaProvider *_provider, + GoaObject *object, + gint *out_expires_in, + GCancellable *cancellable, + GError **error) +{ + GoaOAuth2Provider *provider = GOA_OAUTH2_PROVIDER (_provider); + gboolean ret; + gchar *access_token; + gint access_token_expires_in; + gchar *identity; + gboolean force_refresh; + + ret = FALSE; + access_token = NULL; + identity = NULL; + force_refresh = FALSE; + + again: + access_token = goa_oauth2_provider_get_access_token_sync (provider, + object, + force_refresh, + &access_token_expires_in, + cancellable, + error); + if (access_token == NULL) + goto out; + + identity = goa_oauth2_provider_get_identity_sync (provider, + access_token, + NULL, /* out_name */ + cancellable, + error); + if (identity == NULL) + { + /* OK, try again, with forcing the locally cached credentials to be refreshed */ + if (!force_refresh) + { + force_refresh = TRUE; + g_free (access_token); access_token = NULL; + goto again; + } + else + { + goto out; + } + } + + /* TODO: maybe check with the identity we have */ + ret = TRUE; + if (out_expires_in != NULL) + *out_expires_in = access_token_expires_in; + + out: + g_free (identity); + g_free (access_token); + return ret; +} + + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +goa_oauth2_provider_init (GoaOAuth2Provider *client) +{ +} + +static void +goa_oauth2_provider_class_init (GoaOAuth2ProviderClass *klass) +{ + GoaProviderClass *provider_class; + + provider_class = GOA_PROVIDER_CLASS (klass); + provider_class->add_account = goa_oauth2_provider_add_account; + provider_class->refresh_account = goa_oauth2_provider_refresh_account; + provider_class->build_object = goa_oauth2_provider_build_object; + provider_class->ensure_credentials_sync = goa_oauth2_provider_ensure_credentials_sync; + + klass->build_authorization_uri = goa_oauth2_provider_build_authorization_uri_default; + klass->get_use_external_browser = goa_oauth2_provider_get_use_external_browser_default; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/* runs in a thread dedicated to handling @invocation */ +static gboolean +on_handle_get_access_token (GoaOAuth2Based *interface, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GoaObject *object; + GoaAccount *account; + GoaProvider *provider; + GError *error; + gchar *access_token; + gint access_token_expires_in; + + /* TODO: maybe log what app is requesting access */ + + access_token = NULL; + + object = GOA_OBJECT (g_dbus_interface_get_object (G_DBUS_INTERFACE (interface))); + account = goa_object_peek_account (object); + provider = goa_provider_get_for_provider_type (goa_account_get_provider_type (account)); + + error = NULL; + access_token = goa_oauth2_provider_get_access_token_sync (GOA_OAUTH2_PROVIDER (provider), + object, + FALSE, /* force_refresh */ + &access_token_expires_in, + NULL, /* GCancellable* */ + &error); + if (access_token == NULL) + { + g_dbus_method_invocation_return_gerror (invocation, error); + g_error_free (error); + } + else + { + goa_oauth2_based_complete_get_access_token (interface, + invocation, + access_token, + access_token_expires_in); + } + g_free (access_token); + g_object_unref (provider); + return TRUE; /* invocation was handled */ +} |