/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */ /* * GData Client * Copyright (C) Philip Withnall 2011, 2014 * * GData Client is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GData Client 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GData Client. If not, see . */ /** * SECTION:gdata-oauth2-authorizer * @short_description: GData OAuth 2.0 authorization interface * @stability: Stable * @include: gdata/gdata-oauth2-authorizer.h * * #GDataOAuth2Authorizer provides an implementation of the #GDataAuthorizer * interface for authentication and authorization using the * OAuth 2.0 * process, which is Google’s currently preferred authentication and * authorization process. * * OAuth 2.0 replaces the deprecated OAuth 1.0 and ClientLogin processes. One of * the main reasons for this is to allow two-factor authentication to be * supported, by moving the authentication interface to a web page under * Google’s control. * * The OAuth 2.0 process as implemented by Google follows the * OAuth 2.0 * protocol as specified by IETF in RFC 6749, with a few additions to * support scopes (implemented in libgdata by #GDataAuthorizationDomains), * locales and custom domains. Briefly, the process is initiated by building an * authentication URI (using gdata_oauth2_authorizer_build_authentication_uri()) * and opening it in the user’s web browser. The user authenticates and * authorizes the requested scopes on Google’s website, then an authorization * code is returned (via #GDataOAuth2Authorizer:redirect-uri) to the * application, which then converts the code into an access and refresh token * (using gdata_oauth2_authorizer_request_authorization()). The access token is * then attached to all future requests to the online service, and the refresh * token can be used in future (with gdata_authorizer_refresh_authorization()) * to refresh authorization after the access token expires. * * For an overview of the standard OAuth 2.0 flow, see * RFC 6749. * * Before an application can be authorized using OAuth 2.0, it must be * registered with * Google’s * Developer Console, and a client ID, client secret and redirect URI * retrieved. These must be built into your application, and knowledge of them * will allow any application to impersonate yours, so it is recommended that * they are kept secret (e.g. as a configure-time option). * * libgdata supports * incremental * authorization, where multiple #GDataOAuth2Authorizers can be used to * incrementally build up authorizations against multiple scopes. Typically, * you should use one #GDataOAuth2Authorizer per #GDataService your application * uses, limit the scope of each authorizer, and enable incremental * authorization when calling * gdata_oauth2_authorizer_build_authentication_uri(). * * Each access token is long lived, so reauthorization is rarely necessary with * #GDataOAuth2Authorizer. It is supported using * gdata_authorizer_refresh_authorization(). * * * Authenticating Asynchronously Using OAuth 2.0 * * GDataSomeService *service; * GDataOAuth2Authorizer *authorizer; * gchar *authentication_uri, *authorization_code; * * /* Create an authorizer and authenticate and authorize the service we're using, asynchronously. */ * authorizer = gdata_oauth2_authorizer_new ("some-client-id", "some-client-secret", * GDATA_OAUTH2_REDIRECT_URI_OOB, GDATA_TYPE_SOME_SERVICE); * authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (authorizer, NULL, FALSE); * * /* (Present the page at the authentication URI to the user, either in an embedded or stand-alone web browser, and * * ask them to grant access to the application and return the code Google gives them.) */ * authorization_code = ask_user_for_code (authentication_uri); * * gdata_oauth2_authorizer_request_authorization_async (authorizer, authorization_code, cancellable, * (GAsyncReadyCallback) request_authorization_cb, user_data); * * g_free (authentication_uri); * * /* Zero out the code before freeing. */ * if (token_secret != NULL) { * memset (authorization_code, 0, strlen (authorization_code)); * } * * g_free (authorization_code); * * /* Create a service object and link it with the authorizer */ * service = gdata_some_service_new (GDATA_AUTHORIZER (authorizer)); * * static void * request_authorization_cb (GDataOAuth2Authorizer *authorizer, GAsyncResult *async_result, gpointer user_data) * { * GError *error = NULL; * * if (gdata_oauth2_authorizer_request_authorization_finish (authorizer, async_result, &error) == FALSE) { * /* Notify the user of all errors except cancellation errors */ * if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { * g_error ("Authorization failed: %s", error->message); * } * * g_error_free (error); * return; * } * * /* (The client is now authenticated and authorized against the service. * * It can now proceed to execute queries on the service object which require the user to be authenticated.) */ * } * * g_object_unref (service); * g_object_unref (authorizer); * * * * Since: UNRELEASED */ #include #include #include #include #include "gdata-oauth2-authorizer.h" #include "gdata-private.h" static void authorizer_init (GDataAuthorizerInterface *iface); static void dispose (GObject *object); static void finalize (GObject *object); static void get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec); static void set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec); static void process_request (GDataAuthorizer *self, GDataAuthorizationDomain *domain, SoupMessage *message); static void sign_message_locked (GDataOAuth2Authorizer *self, SoupMessage *message, const gchar *access_token); static gboolean is_authorized_for_domain (GDataAuthorizer *self, GDataAuthorizationDomain *domain); static gboolean refresh_authorization (GDataAuthorizer *self, GCancellable *cancellable, GError **error); static void parse_grant_response (GDataOAuth2Authorizer *self, guint status, const gchar *reason_phrase, const gchar *response_body, gssize length, GError **error); static void parse_grant_error (GDataOAuth2Authorizer *self, guint status, const gchar *reason_phrase, const gchar *response_body, gssize length, GError **error); static void notify_timeout_cb (GObject *gobject, GParamSpec *pspec, GObject *self); struct _GDataOAuth2AuthorizerPrivate { SoupSession *session; /* owned */ GProxyResolver *proxy_resolver; /* owned */ gchar *client_id; /* owned */ gchar *redirect_uri; /* owned */ gchar *client_secret; /* owned */ gchar *locale; /* owned */ /* Mutex for access_token, refresh_token and authentication_domains. */ GMutex mutex; /* These are both non-NULL when authorised, and both NULL otherwise. */ gchar *access_token; /* owned */ gchar *refresh_token; /* owned */ /* Mapping from GDataAuthorizationDomain to itself; a set of domains for * which ->access_token is valid. */ GHashTable *authentication_domains; /* owned */ }; enum { PROP_CLIENT_ID = 1, PROP_REDIRECT_URI, PROP_CLIENT_SECRET, PROP_LOCALE, PROP_TIMEOUT, PROP_PROXY_RESOLVER, }; G_DEFINE_TYPE_WITH_CODE (GDataOAuth2Authorizer, gdata_oauth2_authorizer, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GDATA_TYPE_AUTHORIZER, authorizer_init)) static void gdata_oauth2_authorizer_class_init (GDataOAuth2AuthorizerClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); g_type_class_add_private (klass, sizeof (GDataOAuth2AuthorizerPrivate)); gobject_class->get_property = get_property; gobject_class->set_property = set_property; gobject_class->dispose = dispose; gobject_class->finalize = finalize; /** * GDataOAuth2Authorizer:client-id: * * A client ID for your application (see the * reference documentation). * * It is recommended that the ID is of the form * company name- * application name- * version ID. * * Since: UNRELEASED */ g_object_class_install_property (gobject_class, PROP_CLIENT_ID, g_param_spec_string ("client-id", "Client ID", "A client ID for your application.", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GDataOAuth2Authorizer:redirect-uri: * * Redirect URI to send the response from the authorisation request to. * This must either be %GDATA_OAUTH2_REDIRECT_URI_OOB, * %GDATA_OAUTH2_REDIRECT_URI_OOB_AUTO, or a * http://localhost URI with any port number (optionally) * specified. * * This URI is where the authorisation server will redirect the user * after they have completed interacting with the authentication page * (gdata_oauth2_authorizer_build_authentication_uri()). If it is * %GDATA_OAUTH2_REDIRECT_URI_OOB, a page will be returned in the user’s * browser with the authorisation code in its title and also embedded in * the page for the user to copy if it is not possible to automatically * extract the code from the page title. If it is * %GDATA_OAUTH2_REDIRECT_URI_OOB_AUTO, a similar page will be returned * with the authorisation code in its title, but without displaying the * code to the user — the user will simply be asked to close the page. * If it is a localhost URI, the authentication page will redirect to * that URI with the authorisation code appended as a code * query parameter. If the user denies the authentication request, the * authentication page will redirect to that URI with * error=access_denied appended as a query parameter. * * Note that the redirect URI used must match that registered in * Google’s Developer Console for your application. * * See the reference * documentation for details about choosing a redirect URI. * * Since: UNRELEASED */ g_object_class_install_property (gobject_class, PROP_REDIRECT_URI, g_param_spec_string ("redirect-uri", "Redirect URI", "Redirect URI to send the response from the authorisation request to.", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GDataOAuth2Authorizer:client-secret: * * Client secret provided by Google. This is unique for each application * and is accessible from Google’s Developer Console when registering * an application. It must be paired with the * #GDataOAuth2Authorizer:client-id. * * See the * reference * documentation for details. * * Since: UNRELEASED */ g_object_class_install_property (gobject_class, PROP_CLIENT_SECRET, g_param_spec_string ("client-secret", "Client secret", "Client secret provided by Google.", NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GDataOAuth2Authorizer:locale: * * The locale to use for network requests, in UNIX locale format. * (e.g. "en_GB", "cs", "de_DE".) Use %NULL for the default "C" locale * (typically "en_US"). * * This locale will be used by the server-side software to localise the * authentication and authorization pages at the URI returned by * gdata_oauth2_authorizer_build_authentication_uri(). * * The server-side behaviour is undefined if it doesn't support a given * locale. * * Since: UNRELEASED */ g_object_class_install_property (gobject_class, PROP_LOCALE, g_param_spec_string ("locale", "Locale", "The locale to use for network requests, in UNIX locale format.", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GDataOAuth2Authorizer:timeout: * * A timeout, in seconds, for network operations. If the timeout is * exceeded, the operation will be cancelled and * %GDATA_SERVICE_ERROR_NETWORK_ERROR will be returned. * * If the timeout is 0, operations will * never time out. * * Since: UNRELEASED */ g_object_class_install_property (gobject_class, PROP_TIMEOUT, g_param_spec_uint ("timeout", "Timeout", "A timeout, in seconds, for network operations.", 0, G_MAXUINT, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GDataOAuth2Authorizer:proxy-resolver: * * The #GProxyResolver used to determine a proxy URI. * * Since: UNRELEASED */ g_object_class_install_property (gobject_class, PROP_PROXY_RESOLVER, g_param_spec_object ("proxy-resolver", "Proxy Resolver", "A GProxyResolver used to determine a proxy URI.", G_TYPE_PROXY_RESOLVER, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); } static void authorizer_init (GDataAuthorizerInterface *iface) { iface->process_request = process_request; iface->is_authorized_for_domain = is_authorized_for_domain; /* We only implement the synchronous version, as GDataAuthorizer will * automatically wrap it in a thread for the asynchronous versions if * they’re not specifically implemented, which is fine for our needs. We * couldn’t do any better by implementing the asynchronous versions * ourselves. */ iface->refresh_authorization = refresh_authorization; } static void gdata_oauth2_authorizer_init (GDataOAuth2Authorizer *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_OAUTH2_AUTHORIZER, GDataOAuth2AuthorizerPrivate); /* Set up the authorizer's mutex */ g_mutex_init (&self->priv->mutex); self->priv->authentication_domains = g_hash_table_new_full (g_direct_hash, g_direct_equal, g_object_unref, NULL); /* Set up the session */ self->priv->session = _gdata_service_build_session (); /* Proxy the SoupSession’s timeout property. */ g_signal_connect (self->priv->session, "notify::timeout", (GCallback) notify_timeout_cb, self); /* Keep our GProxyResolver synchronized with SoupSession’s. */ g_object_bind_property (self->priv->session, "proxy-resolver", self, "proxy-resolver", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); } static void dispose (GObject *object) { GDataOAuth2AuthorizerPrivate *priv; priv = GDATA_OAUTH2_AUTHORIZER (object)->priv; g_clear_object (&priv->session); g_clear_object (&priv->proxy_resolver); /* Chain up to the parent class */ G_OBJECT_CLASS (gdata_oauth2_authorizer_parent_class)->dispose (object); } static void finalize (GObject *object) { GDataOAuth2AuthorizerPrivate *priv; priv = GDATA_OAUTH2_AUTHORIZER (object)->priv; g_free (priv->client_id); g_free (priv->client_secret); g_free (priv->redirect_uri); g_free (priv->locale); g_free (priv->access_token); g_free (priv->refresh_token); g_hash_table_unref (priv->authentication_domains); g_mutex_clear (&priv->mutex); /* Chain up to the parent class */ G_OBJECT_CLASS (gdata_oauth2_authorizer_parent_class)->finalize (object); } static void get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { GDataOAuth2Authorizer *self; GDataOAuth2AuthorizerPrivate *priv; self = GDATA_OAUTH2_AUTHORIZER (object); priv = self->priv; switch (property_id) { case PROP_CLIENT_ID: g_value_set_string (value, priv->client_id); break; case PROP_REDIRECT_URI: g_value_set_string (value, priv->redirect_uri); break; case PROP_CLIENT_SECRET: g_value_set_string (value, priv->client_secret); break; case PROP_LOCALE: g_value_set_string (value, priv->locale); break; case PROP_TIMEOUT: g_value_set_uint (value, gdata_oauth2_authorizer_get_timeout (self)); break; case PROP_PROXY_RESOLVER: g_value_set_object (value, gdata_oauth2_authorizer_get_proxy_resolver (self)); break; default: /* We don't have any other property... */ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { GDataOAuth2Authorizer *self; GDataOAuth2AuthorizerPrivate *priv; self = GDATA_OAUTH2_AUTHORIZER (object); priv = self->priv; switch (property_id) { /* Construct only. */ case PROP_CLIENT_ID: priv->client_id = g_value_dup_string (value); break; /* Construct only. */ case PROP_REDIRECT_URI: priv->redirect_uri = g_value_dup_string (value); break; /* Construct only. */ case PROP_CLIENT_SECRET: priv->client_secret = g_value_dup_string (value); break; case PROP_LOCALE: gdata_oauth2_authorizer_set_locale (self, g_value_get_string (value)); break; case PROP_TIMEOUT: gdata_oauth2_authorizer_set_timeout (self, g_value_get_uint (value)); break; case PROP_PROXY_RESOLVER: gdata_oauth2_authorizer_set_proxy_resolver (self, g_value_get_object (value)); break; default: /* We don't have any other property... */ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void process_request (GDataAuthorizer *self, GDataAuthorizationDomain *domain, SoupMessage *message) { GDataOAuth2AuthorizerPrivate *priv; priv = GDATA_OAUTH2_AUTHORIZER (self)->priv; /* Set the authorisation header */ g_mutex_lock (&priv->mutex); /* Sanity check */ g_assert ((priv->access_token == NULL) == (priv->refresh_token == NULL)); if (priv->access_token != NULL && g_hash_table_lookup (priv->authentication_domains, domain) != NULL) { sign_message_locked (GDATA_OAUTH2_AUTHORIZER (self), message, priv->access_token); } g_mutex_unlock (&priv->mutex); } static gboolean is_authorized_for_domain (GDataAuthorizer *self, GDataAuthorizationDomain *domain) { GDataOAuth2AuthorizerPrivate *priv; gpointer result; const gchar *access_token; priv = GDATA_OAUTH2_AUTHORIZER (self)->priv; g_mutex_lock (&priv->mutex); access_token = priv->access_token; result = g_hash_table_lookup (priv->authentication_domains, domain); g_mutex_unlock (&priv->mutex); /* Sanity check */ g_assert (result == NULL || result == domain); return (access_token != NULL && result != NULL); } /* Sign the message and add the Authorization header to it containing the * signature. * * Reference: https://developers.google.com/accounts/docs/OAuth2InstalledApp#callinganapi * * NOTE: This must be called with the mutex locked. */ static void sign_message_locked (GDataOAuth2Authorizer *self, SoupMessage *message, const gchar *access_token) { SoupURI *message_uri; /* unowned */ gchar *auth_header = NULL; /* owned */ g_return_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self)); g_return_if_fail (SOUP_IS_MESSAGE (message)); g_return_if_fail (access_token != NULL && *access_token != '\0'); /* Ensure that we’re using HTTPS: if not, we shouldn’t set the * Authorization header or we could be revealing the access * token to anyone snooping the connection, which would give * them the same rights as us on the user’s data. Generally a * bad thing to happen. */ message_uri = soup_message_get_uri (message); if (message_uri->scheme != SOUP_URI_SCHEME_HTTPS) { g_warning ("Not authorizing a non-HTTPS message with the " "user’s OAuth 2.0 access token as the connection " "isn’t secure."); return; } /* Add the authorisation header. */ auth_header = g_strdup_printf ("Bearer %s", access_token); soup_message_headers_append (message->request_headers, "Authorization", auth_header); g_free (auth_header); } static gboolean refresh_authorization (GDataAuthorizer *self, GCancellable *cancellable, GError **error) { /* See http://code.google.com/apis/accounts/docs/OAuth2.html#IAMoreToken */ GDataOAuth2AuthorizerPrivate *priv; SoupMessage *message = NULL; /* owned */ SoupURI *_uri = NULL; /* owned */ gchar *request_body; guint status; GError *child_error = NULL; g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), FALSE); priv = GDATA_OAUTH2_AUTHORIZER (self)->priv; g_mutex_lock (&priv->mutex); /* If we don’t have a refresh token, we can’t refresh the * authorisation. Do not set @error, as we haven’t been successfully * authorised previously. */ if (priv->refresh_token == NULL) { g_mutex_unlock (&priv->mutex); return FALSE; } /* Prepare the request */ request_body = soup_form_encode ("client_id", priv->client_id, "client_secret", priv->client_secret, "refresh_token", priv->refresh_token, "grant_type", "refresh_token", NULL); g_mutex_unlock (&priv->mutex); /* Build the message */ _uri = soup_uri_new ("https://accounts.google.com/o/oauth2/token"); soup_uri_set_port (_uri, _gdata_service_get_https_port ()); message = soup_message_new_from_uri (SOUP_METHOD_POST, _uri); soup_uri_free (_uri); soup_message_set_request (message, "application/x-www-form-urlencoded", SOUP_MEMORY_TAKE, request_body, strlen (request_body)); /* Send the message */ _gdata_service_actually_send_message (priv->session, message, cancellable, error); status = message->status_code; if (status == SOUP_STATUS_CANCELLED) { /* Cancelled (the error has already been set) */ g_object_unref (message); return FALSE; } else if (status != SOUP_STATUS_OK) { parse_grant_error (GDATA_OAUTH2_AUTHORIZER (self), status, message->reason_phrase, message->response_body->data, message->response_body->length, error); g_object_unref (message); return FALSE; } g_assert (message->response_body->data != NULL); /* Parse and handle the response */ parse_grant_response (GDATA_OAUTH2_AUTHORIZER (self), status, message->reason_phrase, message->response_body->data, message->response_body->length, &child_error); g_object_unref (message); if (child_error != NULL) { g_propagate_error (error, child_error); return FALSE; } return TRUE; } /** * gdata_oauth2_authorizer_new: * @client_id: your application’s client ID * @client_secret: your application’s client secret * @redirect_uri: authorisation redirect URI * @service_type: the #GType of a #GDataService subclass which the * #GDataOAuth2Authorizer will be used with * * Creates a new #GDataOAuth2Authorizer. The @client_id must be unique for your * application, and as registered with Google, and the @client_secret must be * paired with it. * * Return value: (transfer full): a new #GDataOAuth2Authorizer; unref with * g_object_unref() * * Since: UNRELEASED */ GDataOAuth2Authorizer * gdata_oauth2_authorizer_new (const gchar *client_id, const gchar *client_secret, const gchar *redirect_uri, GType service_type) { GList/**/ *domains; /* owned */ GDataOAuth2Authorizer *retval = NULL; /* owned */ g_return_val_if_fail (client_id != NULL && *client_id != '\0', NULL); g_return_val_if_fail (client_secret != NULL && *client_secret != '\0', NULL); g_return_val_if_fail (redirect_uri != NULL && *redirect_uri != '\0', NULL); g_return_val_if_fail (g_type_is_a (service_type, GDATA_TYPE_SERVICE), NULL); domains = gdata_service_get_authorization_domains (service_type); retval = gdata_oauth2_authorizer_new_for_authorization_domains (client_id, client_secret, redirect_uri, domains); g_list_free (domains); return retval; } /** * gdata_oauth2_authorizer_new_for_authorization_domains: * @client_id: your application’s client ID * @client_secret: your application’s client secret * @redirect_uri: authorisation redirect URI * @authorization_domains: (element-type GDataAuthorizationDomain) (transfer none): * a non-empty list of #GDataAuthorizationDomains to be authorized against by * the #GDataOAuth2Authorizer * * Creates a new #GDataOAuth2Authorizer. The @client_id must be unique for your * application, and as registered with Google, and the @client_secret must be * paired with it. * * Return value: (transfer full): a new #GDataOAuth2Authorizer; unref with * g_object_unref() * * Since: UNRELEASED */ GDataOAuth2Authorizer * gdata_oauth2_authorizer_new_for_authorization_domains (const gchar *client_id, const gchar *client_secret, const gchar *redirect_uri, GList *authorization_domains) { GList *i; GDataOAuth2Authorizer *authorizer; g_return_val_if_fail (client_id != NULL && *client_id != '\0', NULL); g_return_val_if_fail (client_secret != NULL && *client_secret != '\0', NULL); g_return_val_if_fail (redirect_uri != NULL && *redirect_uri != '\0', NULL); g_return_val_if_fail (authorization_domains != NULL, NULL); authorizer = GDATA_OAUTH2_AUTHORIZER (g_object_new (GDATA_TYPE_OAUTH2_AUTHORIZER, "client-id", client_id, "client-secret", client_secret, "redirect-uri", redirect_uri, NULL)); /* Register all the domains with the authorizer */ for (i = authorization_domains; i != NULL; i = i->next) { GDataAuthorizationDomain *domain; /* unowned */ g_return_val_if_fail (GDATA_IS_AUTHORIZATION_DOMAIN (i->data), NULL); /* We don’t have to lock the authoriser’s mutex here as no other * code has seen the authoriser yet */ domain = GDATA_AUTHORIZATION_DOMAIN (i->data); g_hash_table_insert (authorizer->priv->authentication_domains, g_object_ref (domain), domain); } return authorizer; } /** * gdata_oauth2_authorizer_build_authentication_uri: * @self: a #GDataOAuth2Authorizer * @login_hint: (nullable): optional e-mail address or sub identifier for the * user * @include_granted_scopes: %TRUE to enable incremental authorisation * * Build an authentication URI to open in the user’s web browser (or an embedded * browser widget). This will display an authentication page from Google, * including an authentication form and confirmation of the authorisation * domains being requested by this #GDataAuthorizer. The user will authenticate * in the browser, then an authorisation code will be returned via the * #GDataOAuth2Authorizer:redirect-uri, ready to be passed to * gdata_oauth2_authorizer_request_authorization(). * * If @login_hint is non-%NULL, it will be passed to the server as a hint of * which user is attempting to authenticate, which can be used to pre-fill the * e-mail address box in the authentication form. * * If @include_granted_scopes is %TRUE, the authentication request will * automatically include all authorisation domains previously granted to this * user/application pair, allowing for incremental authentication — asking for * permissions as needed, rather than all in one large bundle at the first * opportunity. If @include_granted_scopes is %FALSE, incremental authentication * will not be enabled, and only the domains passed to the * #GDataOAuth2Authorizer constructor will eventually be authenticated. * See the * reference * documentation for more details. * * Return value: (transfer full): the authentication URI to open in a web * browser; free with g_free() * * Since: UNRELEASED */ gchar * gdata_oauth2_authorizer_build_authentication_uri (GDataOAuth2Authorizer *self, const gchar *login_hint, gboolean include_granted_scopes) { GDataOAuth2AuthorizerPrivate *priv; GString *uri = NULL; /* owned */ GDataAuthorizationDomain *domain; /* unowned */ GHashTableIter iter; gboolean is_first = TRUE; g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL); priv = self->priv; g_mutex_lock (&priv->mutex); /* Build and memoise the URI. * * Reference: https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl */ g_assert (g_hash_table_size (priv->authentication_domains) > 0); uri = g_string_new ("https://accounts.google.com/o/oauth2/auth" "?response_type=code" "&client_id="); g_string_append_uri_escaped (uri, priv->client_id, NULL, TRUE); g_string_append (uri, "&redirect_uri="); g_string_append_uri_escaped (uri, priv->redirect_uri, NULL, TRUE); g_string_append (uri, "&scope="); /* Add the scopes of all our domains */ g_hash_table_iter_init (&iter, priv->authentication_domains); while (g_hash_table_iter_next (&iter, (gpointer *) &domain, NULL)) { const gchar *scope; if (!is_first) { /* Delimiter */ g_string_append (uri, "%20"); } scope = gdata_authorization_domain_get_scope (domain); g_string_append_uri_escaped (uri, scope, NULL, TRUE); is_first = FALSE; } if (login_hint != NULL && *login_hint != '\0') { g_string_append (uri, "&login_hint="); g_string_append_uri_escaped (uri, login_hint, NULL, TRUE); } if (priv->locale != NULL) { g_string_append (uri, "&hl="); g_string_append_uri_escaped (uri, priv->locale, NULL, TRUE); } if (include_granted_scopes) { g_string_append (uri, "&include_granted_scopes=true"); } else { g_string_append (uri, "&include_granted_scopes=false"); } g_mutex_unlock (&priv->mutex); return g_string_free (uri, FALSE); } /* NOTE: This has to be thread safe, as it can be called from * refresh_authorization() at any time. * * Reference: https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse */ static void parse_grant_response (GDataOAuth2Authorizer *self, guint status, const gchar *reason_phrase, const gchar *response_body, gssize length, GError **error) { GDataOAuth2AuthorizerPrivate *priv; JsonParser *parser = NULL; /* owned */ JsonNode *root_node; /* unowned */ JsonObject *root_object; /* unowned */ const gchar *access_token = NULL, *refresh_token = NULL; GError *child_error = NULL; priv = self->priv; /* Parse the successful response */ parser = json_parser_new (); json_parser_load_from_data (parser, response_body, length, &child_error); if (child_error != NULL) { g_clear_error (&child_error); g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); goto done; } /* Extract the access token, TTL and refresh token */ root_node = json_parser_get_root (parser); if (JSON_NODE_HOLDS_OBJECT (root_node) == FALSE) { g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); goto done; } root_object = json_node_get_object (root_node); if (json_object_has_member (root_object, "access_token")) { access_token = json_object_get_string_member (root_object, "access_token"); } if (json_object_has_member (root_object, "refresh_token")) { refresh_token = json_object_get_string_member (root_object, "refresh_token"); } /* Always require an access token. */ if (access_token == NULL || *access_token == '\0') { g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); access_token = NULL; refresh_token = NULL; goto done; } /* Only require a refresh token if this is the first authentication. * See the documentation for refreshing authentication: * https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh */ if ((refresh_token == NULL || *refresh_token == '\0') && priv->refresh_token == NULL) { g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); access_token = NULL; refresh_token = NULL; goto done; } done: /* Postconditions. */ g_assert ((refresh_token == NULL) || (access_token != NULL)); g_assert ((child_error != NULL) == (access_token == NULL)); /* Update state. */ g_mutex_lock (&priv->mutex); g_free (priv->access_token); priv->access_token = g_strdup (access_token); if (refresh_token != NULL) { g_free (priv->refresh_token); priv->refresh_token = g_strdup (refresh_token); } g_mutex_unlock (&priv->mutex); if (child_error != NULL) { g_propagate_error (error, child_error); } g_object_unref (parser); } /* NOTE: This has to be thread safe, as it can be called from * refresh_authorization() at any time. * * There is no reference for this, because Google apparently don’t deem it * necessary to document. * * Example response: * HTTP/1.1 400 Bad Request * Content-Type: application/json * * { * "error" : "invalid_grant" * } */ static void parse_grant_error (GDataOAuth2Authorizer *self, guint status, const gchar *reason_phrase, const gchar *response_body, gssize length, GError **error) { JsonParser *parser = NULL; /* owned */ JsonNode *root_node; /* unowned */ JsonObject *root_object; /* unowned */ const gchar *error_code = NULL; GError *child_error = NULL; /* Parse the error response */ parser = json_parser_new (); json_parser_load_from_data (parser, response_body, length, &child_error); if (child_error != NULL) { g_clear_error (&child_error); g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); goto done; } /* Extract the error code. */ root_node = json_parser_get_root (parser); if (JSON_NODE_HOLDS_OBJECT (root_node) == FALSE) { g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); goto done; } root_object = json_node_get_object (root_node); if (json_object_has_member (root_object, "error")) { error_code = json_object_get_string_member (root_object, "error"); } /* Always require an error_code. */ if (error_code == NULL || *error_code == '\0') { g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); error_code = NULL; goto done; } /* Parse the error code. */ if (strcmp (error_code, "invalid_grant") == 0) { g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_FORBIDDEN, _("Access was denied by the user or server.")); } else { /* Unknown error code. */ g_set_error_literal (&child_error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_PROTOCOL_ERROR, _("The server returned a malformed response.")); } done: /* Postconditions. */ g_assert (child_error != NULL); if (child_error != NULL) { g_propagate_error (error, child_error); } g_object_unref (parser); } /** * gdata_oauth2_authorizer_request_authorization: * @self: a #GDataOAuth2Authorizer * @authorization_code: code returned from the authentication page * @cancellable: (allow-none): a #GCancellable, or %NULL * @error: return location for a #GError, or %NULL * * Request an authorisation code from the user’s web browser is converted to * authorisation (access and refresh) tokens. This is the final step in the * authentication process; once complete, the #GDataOAuth2Authorizer should be * fully authorised for its domains. * * On failure, %GDATA_SERVICE_ERROR_FORBIDDEN will be returned if the user or * server denied the authorisation request. %GDATA_SERVICE_ERROR_PROTOCOL_ERROR * will be returned if the server didn’t follow the expected protocol. * %G_IO_ERROR_CANCELLED will be returned if the operation was cancelled using * @cancellable. * * Return value: %TRUE on success, %FALSE otherwise * * Since: UNRELEASED */ gboolean gdata_oauth2_authorizer_request_authorization (GDataOAuth2Authorizer *self, const gchar *authorization_code, GCancellable *cancellable, GError **error) { GDataOAuth2AuthorizerPrivate *priv; SoupMessage *message = NULL; /* owned */ SoupURI *_uri = NULL; /* owned */ gchar *request_body = NULL; /* owned */ guint status; GError *child_error = NULL; g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), FALSE); g_return_val_if_fail (authorization_code != NULL && *authorization_code != '\0', FALSE); g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); priv = self->priv; /* Prepare the request. * * Reference: https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse */ request_body = soup_form_encode ("client_id", priv->client_id, "client_secret", priv->client_secret, "code", authorization_code, "redirect_uri", priv->redirect_uri, "grant_type", "authorization_code", NULL); /* Build the message */ _uri = soup_uri_new ("https://accounts.google.com/o/oauth2/token"); soup_uri_set_port (_uri, _gdata_service_get_https_port ()); message = soup_message_new_from_uri (SOUP_METHOD_POST, _uri); soup_uri_free (_uri); soup_message_set_request (message, "application/x-www-form-urlencoded", SOUP_MEMORY_TAKE, request_body, strlen (request_body)); request_body = NULL; /* Send the message */ _gdata_service_actually_send_message (priv->session, message, cancellable, error); status = message->status_code; if (status == SOUP_STATUS_CANCELLED) { /* Cancelled (the error has already been set) */ g_object_unref (message); return FALSE; } else if (status != SOUP_STATUS_OK) { parse_grant_error (self, status, message->reason_phrase, message->response_body->data, message->response_body->length, error); g_object_unref (message); return FALSE; } g_assert (message->response_body->data != NULL); /* Parse and handle the response */ parse_grant_response (self, status, message->reason_phrase, message->response_body->data, message->response_body->length, &child_error); g_object_unref (message); if (child_error != NULL) { g_propagate_error (error, child_error); return FALSE; } return TRUE; } static void request_authorization_thread (GSimpleAsyncResult *result, GDataOAuth2Authorizer *authorizer, GCancellable *cancellable) { GError *error = NULL; const gchar *authorization_code; authorization_code = g_simple_async_result_get_op_res_gpointer (result); if (gdata_oauth2_authorizer_request_authorization (authorizer, authorization_code, cancellable, &error) == FALSE) { g_simple_async_result_set_from_error (result, error); g_error_free (error); } } /** * gdata_oauth2_authorizer_request_authorization_async: * @self: a #GDataOAuth2Authorizer * @authorization_code: code returned from the authentication page * @cancellable: (allow-none): an optional #GCancellable, or %NULL * @callback: a #GAsyncReadyCallback to call when authorization is finished * @user_data: (closure): data to pass to the @callback function * * Asynchronous version of gdata_oauth2_authorizer_request_authorization(). * * Since: UNRELEASED */ void gdata_oauth2_authorizer_request_authorization_async (GDataOAuth2Authorizer *self, const gchar *authorization_code, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *result; g_return_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self)); g_return_if_fail (authorization_code != NULL && *authorization_code != '\0'); g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); result = g_simple_async_result_new (G_OBJECT (self), callback, user_data, gdata_oauth2_authorizer_request_authorization_async); g_simple_async_result_set_op_res_gpointer (result, g_strdup (authorization_code), (GDestroyNotify) g_free); g_simple_async_result_run_in_thread (result, (GSimpleAsyncThreadFunc) request_authorization_thread, G_PRIORITY_DEFAULT, cancellable); g_object_unref (result); } /** * gdata_oauth2_authorizer_request_authorization_finish: * @self: a #GDataOAuth2Authorizer * @async_result: a #GAsyncResult * @error: a #GError, or %NULL * * Finishes an asynchronous authorization operation started with * gdata_oauth2_authorizer_request_authorization_async(). * * Return value: %TRUE if authorization was successful, %FALSE otherwise * * Since: UNRELEASED */ gboolean gdata_oauth2_authorizer_request_authorization_finish (GDataOAuth2Authorizer *self, GAsyncResult *async_result, GError **error) { GSimpleAsyncResult *result; g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), FALSE); g_return_val_if_fail (G_IS_ASYNC_RESULT (async_result), FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); result = G_SIMPLE_ASYNC_RESULT (async_result); g_warn_if_fail (g_simple_async_result_is_valid (async_result, G_OBJECT (self), gdata_oauth2_authorizer_request_authorization_async)); if (g_simple_async_result_propagate_error (result, error)) { return FALSE; } return TRUE; } /** * gdata_oauth2_authorizer_get_client_id: * @self: a #GDataOAuth2Authorizer * * Returns the authorizer's client ID, #GDataOAuth2Authorizer:client-id, as * specified on constructing the #GDataOAuth2Authorizer. * * Return value: the authorizer's client ID * * Since: UNRELEASED */ const gchar * gdata_oauth2_authorizer_get_client_id (GDataOAuth2Authorizer *self) { g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL); return self->priv->client_id; } /** * gdata_oauth2_authorizer_get_redirect_uri: * @self: a #GDataOAuth2Authorizer * * Returns the authorizer’s redirect URI, #GDataOAuth2Authorizer:redirect-uri, * as specified on constructing the #GDataOAuth2Authorizer. * * Return value: the authorizer’s redirect URI * * Since: UNRELEASED */ const gchar * gdata_oauth2_authorizer_get_redirect_uri (GDataOAuth2Authorizer *self) { g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL); return self->priv->redirect_uri; } /** * gdata_oauth2_authorizer_get_client_secret: * @self: a #GDataOAuth2Authorizer * * Returns the authorizer's client secret, #GDataOAuth2Authorizer:client-secret, * as specified on constructing the #GDataOAuth2Authorizer. * * Return value: the authorizer's client secret * * Since: UNRELEASED */ const gchar * gdata_oauth2_authorizer_get_client_secret (GDataOAuth2Authorizer *self) { g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL); return self->priv->client_secret; } /** * gdata_oauth2_authorizer_get_locale: * @self: a #GDataOAuth2Authorizer * * Returns the locale currently being used for network requests, or %NULL if the * locale is the default. * * Return value: (allow-none): the current locale * * Since: UNRELEASED */ const gchar * gdata_oauth2_authorizer_get_locale (GDataOAuth2Authorizer *self) { g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL); return self->priv->locale; } /** * gdata_oauth2_authorizer_set_locale: * @self: a #GDataOAuth2Authorizer * @locale: (allow-none): the new locale in UNIX locale format, or %NULL for the * default locale * * Set the locale used for network requests to @locale, given in standard UNIX * locale format. See #GDataOAuth2Authorizer:locale for more details. * * Note that while it’s possible to change the locale after sending network * requests (i.e. calling gdata_oauth2_authorizer_build_authentication_uri() for * the first time), it is unsupported, as the server-side software may behave * unexpectedly. The only supported use of this method is after creation of the * authorizer, but before any network requests are made. * * Since: UNRELEASED */ void gdata_oauth2_authorizer_set_locale (GDataOAuth2Authorizer *self, const gchar *locale) { g_return_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self)); if (g_strcmp0 (locale, self->priv->locale) == 0) { /* Already has this value */ return; } g_free (self->priv->locale); self->priv->locale = g_strdup (locale); g_object_notify (G_OBJECT (self), "locale"); } static void notify_timeout_cb (GObject *gobject, GParamSpec *pspec, GObject *self) { g_object_notify (self, "timeout"); } /** * gdata_oauth2_authorizer_get_timeout: * @self: a #GDataOAuth2Authorizer * * Gets the #GDataOAuth2Authorizer:timeout property; the network timeout, in * seconds. * * Return value: the timeout, or 0 * * Since: UNRELEASED */ guint gdata_oauth2_authorizer_get_timeout (GDataOAuth2Authorizer *self) { guint timeout; g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), 0); g_object_get (self->priv->session, SOUP_SESSION_TIMEOUT, &timeout, NULL); return timeout; } /** * gdata_oauth2_authorizer_set_timeout: * @self: a #GDataOAuth2Authorizer * @timeout: the timeout, or 0 * * Sets the #GDataOAuth2Authorizer:timeout property; the network timeout, in * seconds. * * If @timeout is 0, network operations will never * time out. * * Since: UNRELEASED */ void gdata_oauth2_authorizer_set_timeout (GDataOAuth2Authorizer *self, guint timeout) { g_return_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self)); if (timeout == gdata_oauth2_authorizer_get_timeout (self)) { return; } g_object_set (self->priv->session, SOUP_SESSION_TIMEOUT, timeout, NULL); } /** * gdata_oauth2_authorizer_get_proxy_resolver: * @self: a #GDataOAuth2Authorizer * * Gets the #GProxyResolver on the #GDataOAuth2Authorizer's #SoupSession. * * Return value: (transfer none) (allow-none): a #GProxyResolver, or %NULL * * Since: UNRELEASED */ GProxyResolver * gdata_oauth2_authorizer_get_proxy_resolver (GDataOAuth2Authorizer *self) { g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL); return self->priv->proxy_resolver; } /** * gdata_oauth2_authorizer_set_proxy_resolver: * @self: a #GDataOAuth2Authorizer * @proxy_resolver: (allow-none): a #GProxyResolver, or %NULL * * Sets the #GProxyResolver on the #SoupSession used internally by the given * #GDataOAuth2Authorizer. * * Since: UNRELEASED */ void gdata_oauth2_authorizer_set_proxy_resolver (GDataOAuth2Authorizer *self, GProxyResolver *proxy_resolver) { g_return_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self)); g_return_if_fail (proxy_resolver == NULL || G_IS_PROXY_RESOLVER (proxy_resolver)); if (proxy_resolver != NULL) { g_object_ref (proxy_resolver); } g_clear_object (&self->priv->proxy_resolver); self->priv->proxy_resolver = proxy_resolver; g_object_notify (G_OBJECT (self), "proxy-resolver"); }