[libgdata] core: Add a GDataOAuth2Authorizer for OAuth 2.0 support



commit c54b759085dfeb5f59aea7d1ac620620958ac29e
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sun Jul 3 21:37:49 2011 +0100

    core: Add a GDataOAuth2Authorizer for OAuth 2.0 support
    
    This adds a new GDataAuthorizer subclass to support OAuth 2.0
    authorisation, which is needed for the Google Tasks unit tests.
    
    This includes support for all Google OAuth 2.0 features (note that
    Google’s implementation of OAuth 2.0 is not entirely standard, so this
    OAuth 2.0 authoriser cannot be used outside of Google’s services). It
    includes full unit tests too.
    
    New API:
     • GDATA_OAUTH2_REDIRECT_URI_OOB
     • GDATA_OAUTH2_REDIRECT_URI_OOB_AUTO
     • GDataOAuth2Authorizer
    
    https://bugzilla.gnome.org/show_bug.cgi?id=646285

 Makefile.am                                        |    5 +-
 docs/reference/gdata-docs.xml                      |    1 +
 docs/reference/gdata-sections.txt                  |   34 +
 gdata/gdata-authorizer.c                           |   10 +-
 gdata/gdata-client-login-authorizer.c              |    2 +
 gdata/gdata-oauth1-authorizer.c                    |    5 +-
 gdata/gdata-oauth2-authorizer.c                    | 1485 ++++++++++++++++++++
 gdata/gdata-oauth2-authorizer.h                    |  128 ++
 gdata/gdata.h                                      |    1 +
 gdata/gdata.symbols                                |   16 +
 gdata/tests/Makefile.am                            |   11 +
 gdata/tests/oauth2-authorizer.c                    | 1030 ++++++++++++++
 ...th2-authorizer-refresh-authorization-authorized |   34 +
 .../oauth2-authorizer-request-authorization-async  |   35 +
 ...zer-request-authorization-async-bad-credentials |   31 +
 .../oauth2-authorizer-request-authorization-sync   |   35 +
 ...izer-request-authorization-sync-bad-credentials |   31 +
 .../setup-oauth2-authorizer-data-authenticated     |   35 +
 18 files changed, 2922 insertions(+), 7 deletions(-)
---
diff --git a/Makefile.am b/Makefile.am
index 9940f4a..e5c5d07 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -191,7 +191,9 @@ gdata_headers = \
        gdata/gdata-authorizer.h        \
        gdata/gdata-authorization-domain.h      \
        gdata/gdata-client-login-authorizer.h   \
-       gdata/gdata-oauth1-authorizer.h
+       gdata/gdata-oauth1-authorizer.h \
+       gdata/gdata-oauth2-authorizer.h \
+       $(NULL)
 
 if ENABLE_GOA
 gdata_headers += \
@@ -366,6 +368,7 @@ gdata_sources = \
        gdata/gdata-authorization-domain.c      \
        gdata/gdata-client-login-authorizer.c   \
        gdata/gdata-oauth1-authorizer.c         \
+       gdata/gdata-oauth2-authorizer.c         \
        \
        gdata/atom/gdata-author.c       \
        gdata/atom/gdata-category.c     \
diff --git a/docs/reference/gdata-docs.xml b/docs/reference/gdata-docs.xml
index 926d795..2e4e077 100644
--- a/docs/reference/gdata-docs.xml
+++ b/docs/reference/gdata-docs.xml
@@ -56,6 +56,7 @@
                        <xi:include href="xml/gdata-client-login-authorizer.xml"/>
                        <xi:include href="xml/gdata-goa-authorizer.xml"/>
                        <xi:include href="xml/gdata-oauth1-authorizer.xml"/>
+                       <xi:include href="xml/gdata-oauth2-authorizer.xml"/>
                </chapter>
 
                <chapter>
diff --git a/docs/reference/gdata-sections.txt b/docs/reference/gdata-sections.txt
index 7f935c8..2dce568 100644
--- a/docs/reference/gdata-sections.txt
+++ b/docs/reference/gdata-sections.txt
@@ -2758,3 +2758,37 @@ GDATA_TYPE_FREEBASE_SEARCH_RESULT
 <SUBSECTION Private>
 GDataFreebaseSearchResultPrivate
 </SECTION>
+
+<SECTION>
+<FILE>gdata-oauth2-authorizer</FILE>
+<TITLE>GDataOAuth2Authorizer</TITLE>
+GDATA_OAUTH2_REDIRECT_URI_OOB
+GDATA_OAUTH2_REDIRECT_URI_OOB_AUTO
+GDataOAuth2Authorizer
+GDataOAuth2AuthorizerClass
+gdata_oauth2_authorizer_new
+gdata_oauth2_authorizer_new_for_authorization_domains
+gdata_oauth2_authorizer_build_authentication_uri
+gdata_oauth2_authorizer_request_authorization
+gdata_oauth2_authorizer_request_authorization_async
+gdata_oauth2_authorizer_request_authorization_finish
+gdata_oauth2_authorizer_get_client_id
+gdata_oauth2_authorizer_get_redirect_uri
+gdata_oauth2_authorizer_get_client_secret
+gdata_oauth2_authorizer_get_locale
+gdata_oauth2_authorizer_set_locale
+gdata_oauth2_authorizer_get_timeout
+gdata_oauth2_authorizer_set_timeout
+gdata_oauth2_authorizer_get_proxy_resolver
+gdata_oauth2_authorizer_set_proxy_resolver
+<SUBSECTION Standard>
+GDATA_TYPE_OAUTH2_AUTHORIZER
+GDATA_OAUTH2_AUTHORIZER
+GDATA_OAUTH2_AUTHORIZER_CLASS
+GDATA_IS_OAUTH2_AUTHORIZER
+GDATA_IS_OAUTH2_AUTHORIZER_CLASS
+GDATA_OAUTH2_AUTHORIZER_GET_CLASS
+gdata_oauth2_authorizer_get_type
+<SUBSECTION Private>
+GDataOAuth2AuthorizerPrivate
+</SECTION>
diff --git a/gdata/gdata-authorizer.c b/gdata/gdata-authorizer.c
index a7c144e..f41878c 100644
--- a/gdata/gdata-authorizer.c
+++ b/gdata/gdata-authorizer.c
@@ -1,7 +1,7 @@
 /* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
 /*
  * GData Client
- * Copyright (C) Philip Withnall 2011 <philip tecnocode co uk>
+ * Copyright (C) Philip Withnall 2011, 2014 <philip tecnocode co uk>
  *
  * GData Client is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -24,7 +24,7 @@
  * @include: gdata/gdata-authorizer.h
  *
  * The #GDataAuthorizer interface provides a uniform way to implement authentication and authorization 
processes for use by #GDataService<!-- -->s.
- * Client code will construct a new #GDataAuthorizer instance of their choosing, such as 
#GDataClientLoginAuthorizer or #GDataOAuth1Authorizer, for
+ * Client code will construct a new #GDataAuthorizer instance of their choosing, such as 
#GDataClientLoginAuthorizer or #GDataOAuth2Authorizer, for
  * the #GDataService<!-- -->s which will be used by the client, then authenticates and authorizes with the 
#GDataAuthorizer instead of the
  * #GDataService. The #GDataService then uses the #GDataAuthorizer to authorize individual network requests 
using whatever authorization token was
  * returned to the #GDataAuthorizer by the Google Accounts service.
@@ -41,10 +41,12 @@
  *  <listitem>#GDataClientLoginAuthorizer for
  *    <ulink type="http" 
url="http://code.google.com/apis/accounts/docs/AuthForInstalledApps.html";>ClientLogin</ulink> 
(deprecated)</listitem>
  *  <listitem>#GDataOAuth1Authorizer for
- *    <ulink type="http" url="http://code.google.com/apis/accounts/docs/OAuthForInstalledApps.html";>OAuth 
1.0</ulink> (preferred)</listitem>
+ *    <ulink type="http" url="http://code.google.com/apis/accounts/docs/OAuthForInstalledApps.html";>OAuth 
1.0</ulink> (deprecated)</listitem>
+ *  <listitem>#GDataOAuth2Authorizer for
+ *    <ulink type="http" url="https://developers.google.com/accounts/docs/OAuth2InstalledApp";>OAuth 
2.0</ulink> (preferred)</listitem>
  * </itemizedlist>
  *
- * It is quite possible for clients to write their own #GDataAuthorizer implementation. For example, if a 
client uses OAuth 2.0 and handles
+ * It is quite possible for clients to write their own #GDataAuthorizer implementation. For example, if a 
client already uses OAuth 2.0 and handles
  * authentication itself, it may want to use its own #GDataAuthorizer implementation which simply exposes 
the client's existing access token to
  * libgdata and does nothing more.
  *
diff --git a/gdata/gdata-client-login-authorizer.c b/gdata/gdata-client-login-authorizer.c
index 7185271..4a7fb4d 100644
--- a/gdata/gdata-client-login-authorizer.c
+++ b/gdata/gdata-client-login-authorizer.c
@@ -31,6 +31,8 @@
  * two-factor authentication enabled has to use a service-specific one-time password instead if a client is 
authenticating with
  * #GDataClientLoginAuthorizer. More documentation about this is
  * <ulink type="http" 
url="http://www.google.com/support/accounts/bin/static.py?page=guide.cs&guide=1056283&topic=1056286";>available
 online</ulink>.
+ * Note that newer services cannot be authenticated against using ClientLogin,
+ * and a #GDataOAuth2Authorizer must be used instead.
  *
  * The ClientLogin process is a simple one whereby the user's Google Account username and password are sent 
over an HTTPS connection to the Google
  * Account servers (when gdata_client_login_authorizer_authenticate() is called), which return an 
authorization token. This token is then attached to
diff --git a/gdata/gdata-oauth1-authorizer.c b/gdata/gdata-oauth1-authorizer.c
index 30ef600..2ec3beb 100644
--- a/gdata/gdata-oauth1-authorizer.c
+++ b/gdata/gdata-oauth1-authorizer.c
@@ -24,8 +24,9 @@
  * @include: gdata/gdata-oauth1-authorizer.h
  *
  * #GDataOAuth1Authorizer provides an implementation of the #GDataAuthorizer interface for authentication 
and authorization using the
- * <ulink type="http" url="http://code.google.com/apis/accounts/docs/OAuthForInstalledApps.html";>OAuth 
1.0</ulink> process, which is Google's
- * currently preferred authentication and authorization process, though OAuth 2.0 will be transitioned to in 
future.
+ * <ulink type="http" url="http://code.google.com/apis/accounts/docs/OAuthForInstalledApps.html";>OAuth 
1.0</ulink> process,
+ * which was preferred by Google until OAuth 2.0 was released — it is now
+ * preferred to use #GDataOAuth2Authorizer.
  *
  * OAuth 1.0 replaces the deprecated ClientLogin process. 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.
diff --git a/gdata/gdata-oauth2-authorizer.c b/gdata/gdata-oauth2-authorizer.c
new file mode 100644
index 0000000..faae215
--- /dev/null
+++ b/gdata/gdata-oauth2-authorizer.c
@@ -0,0 +1,1485 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2011, 2014 <philip tecnocode co uk>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SECTION:gdata-oauth2-authorizer
+ * @short_description: GData OAuth 2.0 authorization interface
+ * @stability: Unstable
+ * @include: gdata/gdata-oauth2-authorizer.h
+ *
+ * #GDataOAuth2Authorizer provides an implementation of the #GDataAuthorizer
+ * interface for authentication and authorization using the
+ * <ulink type="http" url="https://developers.google.com/accounts/docs/OAuth2InstalledApp";>OAuth 2.0</ulink>
+ * 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
+ * <ulink type="http" url="http://tools.ietf.org/html/rfc6749";>OAuth 2.0
+ * protocol as specified by IETF in RFC 6749</ulink>, 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
+ * <ulink type="http" url="http://tools.ietf.org/html/rfc6749#section-1.2";>RFC 6749</ulink>.
+ *
+ * Before an application can be authorized using OAuth 2.0, it must be
+ * registered with
+ * <ulink type="http" url="https://console.developers.google.com/project";>Google’s
+ * Developer Console</ulink>, 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
+ * <ulink type="http" 
url="https://developers.google.com/accounts/docs/OAuth2InstalledApp#incrementalAuth";>incremental
+ * authorization</ulink>, 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().
+ *
+ * <example>
+ *     <title>Authenticating Asynchronously Using OAuth 2.0</title>
+ *     <programlisting>
+ *     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);
+ *     </programlisting>
+ * </example>
+ *
+ * Since: UNRELEASED
+ */
+
+#include <config.h>
+#include <string.h>
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#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
+        * <ulink url="https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse"; 
type="http">reference documentation</ulink>).
+        *
+        * It is recommended that the ID is of the form
+        * <literal><replaceable>company name</replaceable>-
+        * <replaceable>application name</replaceable>-
+        * <replaceable>version ID</replaceable></literal>.
+        *
+        * 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
+        * <code>http://localhost</code> 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>code</code>
+        * query parameter. If the user denies the authentication request, the
+        * authentication page will redirect to that URI with
+        * <code>error=access_denied</code> 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 <ulink type="http" 
url="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi";>reference
+        * documentation</ulink> 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
+        * <ulink url="https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse"; 
type="http">reference
+        * documentation</ulink> 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 <code class="literal">0</code>, 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/*<unowned GDataAuthorizationDomain>*/ *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
+ * <ulink type="http" 
url="https://developers.google.com/accounts/docs/OAuth2WebServer#incrementalAuth";>reference
+ * documentation</ulink> 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);
+
+       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 <code class="literal">0</code>
+ *
+ * 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 <code class="literal">0</code>
+ *
+ * Sets the #GDataOAuth2Authorizer:timeout property; the network timeout, in
+ * seconds.
+ *
+ * If @timeout is <code class="literal">0</code>, 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");
+}
diff --git a/gdata/gdata-oauth2-authorizer.h b/gdata/gdata-oauth2-authorizer.h
new file mode 100644
index 0000000..04cbe51
--- /dev/null
+++ b/gdata/gdata-oauth2-authorizer.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2011, 2014 <philip tecnocode co uk>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef GDATA_OAUTH2_AUTHORIZER_H
+#define GDATA_OAUTH2_AUTHORIZER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "gdata-authorizer.h"
+
+G_BEGIN_DECLS
+
+/**
+ * GDATA_OAUTH2_REDIRECT_URI_OOB:
+ *
+ * OAuth 2 redirect URI for out-of-band authorisation code transfer, where the
+ * user is shown the authorisation code and asked to copy it.
+ *
+ * See
+ * <ulink type="http" 
url="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi";>reference
+ * documentation</ulink> for details.
+ *
+ * Since: UNRELEASED
+ */
+#define GDATA_OAUTH2_REDIRECT_URI_OOB "urn:ietf:wg:oauth:2.0:oob"
+
+/**
+ * GDATA_OAUTH2_REDIRECT_URI_OOB_AUTO:
+ *
+ * OAuth 2 redirect URI for out-of-band authorisation code transfer, where the
+ * user is not shown the authorisation code or asked to copy it.
+ *
+ * See
+ * <ulink type="http" 
url="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi";>reference
+ * documentation</ulink> for details.
+ *
+ * Since: UNRELEASED
+ */
+#define GDATA_OAUTH2_REDIRECT_URI_OOB_AUTO "urn:ietf:wg:oauth:2.0:oob:auto"
+
+#define GDATA_TYPE_OAUTH2_AUTHORIZER           (gdata_oauth2_authorizer_get_type ())
+#define GDATA_OAUTH2_AUTHORIZER(o)             (G_TYPE_CHECK_INSTANCE_CAST ((o), 
GDATA_TYPE_OAUTH2_AUTHORIZER, GDataOAuth2Authorizer))
+#define GDATA_OAUTH2_AUTHORIZER_CLASS(k)       (G_TYPE_CHECK_CLASS_CAST((k), GDATA_TYPE_OAUTH2_AUTHORIZER, 
GDataOAuth2AuthorizerClass))
+#define GDATA_IS_OAUTH2_AUTHORIZER(o)          (G_TYPE_CHECK_INSTANCE_TYPE ((o), 
GDATA_TYPE_OAUTH2_AUTHORIZER))
+#define GDATA_IS_OAUTH2_AUTHORIZER_CLASS(k)    (G_TYPE_CHECK_CLASS_TYPE ((k), GDATA_TYPE_OAUTH2_AUTHORIZER))
+#define GDATA_OAUTH2_AUTHORIZER_GET_CLASS(o)   (G_TYPE_INSTANCE_GET_CLASS ((o), 
GDATA_TYPE_OAUTH2_AUTHORIZER, GDataOAuth2AuthorizerClass))
+
+typedef struct _GDataOAuth2AuthorizerPrivate   GDataOAuth2AuthorizerPrivate;
+
+/**
+ * GDataOAuth2Authorizer:
+ *
+ * All the fields in the #GDataOAuth2Authorizer structure are private and should never be accessed directly.
+ *
+ * Since: UNRELEASED
+ */
+typedef struct {
+       /*< private >*/
+       GObject parent;
+       GDataOAuth2AuthorizerPrivate *priv;
+} GDataOAuth2Authorizer;
+
+/**
+ * GDataOAuth2AuthorizerClass:
+ *
+ * All the fields in the #GDataOAuth2AuthorizerClass structure are private and should never be accessed 
directly.
+ *
+ * Since: UNRELEASED
+ */
+typedef struct {
+       /*< private >*/
+       GObjectClass parent;
+} GDataOAuth2AuthorizerClass;
+
+GType gdata_oauth2_authorizer_get_type (void) G_GNUC_CONST;
+
+GDataOAuth2Authorizer *gdata_oauth2_authorizer_new (const gchar *client_id,
+                                                    const gchar *client_secret,
+                                                    const gchar *redirect_uri,
+                                                    GType service_type) G_GNUC_WARN_UNUSED_RESULT 
G_GNUC_MALLOC;
+GDataOAuth2Authorizer *gdata_oauth2_authorizer_new_for_authorization_domains (const gchar *client_id,
+                                                                              const gchar *client_secret,
+                                                                              const gchar *redirect_uri,
+                                                                              GList *authorization_domains) 
G_GNUC_WARN_UNUSED_RESULT G_GNUC_MALLOC;
+
+gchar *gdata_oauth2_authorizer_build_authentication_uri (GDataOAuth2Authorizer *self,
+                                                         const gchar *login_hint,
+                                                         gboolean include_granted_scopes) G_GNUC_PURE;
+
+gboolean gdata_oauth2_authorizer_request_authorization (GDataOAuth2Authorizer *self, const gchar 
*authorization_code,
+                                                        GCancellable *cancellable, GError **error);
+void gdata_oauth2_authorizer_request_authorization_async (GDataOAuth2Authorizer *self, const gchar 
*authorization_code,
+                                                          GCancellable *cancellable, GAsyncReadyCallback 
callback, gpointer user_data);
+gboolean gdata_oauth2_authorizer_request_authorization_finish (GDataOAuth2Authorizer *self, GAsyncResult 
*async_result, GError **error);
+
+const gchar *gdata_oauth2_authorizer_get_client_id (GDataOAuth2Authorizer *self) G_GNUC_PURE;
+const gchar *gdata_oauth2_authorizer_get_redirect_uri (GDataOAuth2Authorizer *self) G_GNUC_PURE;
+const gchar *gdata_oauth2_authorizer_get_client_secret (GDataOAuth2Authorizer *self) G_GNUC_PURE;
+
+const gchar *gdata_oauth2_authorizer_get_locale (GDataOAuth2Authorizer *self) G_GNUC_PURE;
+void gdata_oauth2_authorizer_set_locale (GDataOAuth2Authorizer *self, const gchar *locale);
+
+guint gdata_oauth2_authorizer_get_timeout (GDataOAuth2Authorizer *self) G_GNUC_PURE;
+void gdata_oauth2_authorizer_set_timeout (GDataOAuth2Authorizer *self, guint timeout);
+
+GProxyResolver *gdata_oauth2_authorizer_get_proxy_resolver (GDataOAuth2Authorizer *self) G_GNUC_PURE;
+void gdata_oauth2_authorizer_set_proxy_resolver (GDataOAuth2Authorizer *self, GProxyResolver 
*proxy_resolver);
+
+G_END_DECLS
+
+#endif /* !GDATA_OAUTH2_AUTHORIZER_H */
diff --git a/gdata/gdata.h b/gdata/gdata.h
index 158a765..fe7a4c3 100644
--- a/gdata/gdata.h
+++ b/gdata/gdata.h
@@ -39,6 +39,7 @@
 #include <gdata/gdata-authorization-domain.h>
 #include <gdata/gdata-client-login-authorizer.h>
 #include <gdata/gdata-oauth1-authorizer.h>
+#include <gdata/gdata-oauth2-authorizer.h>
 #ifdef GOA_API_IS_SUBJECT_TO_CHANGE
 /* You need to define GOA_API_IS_SUBJECT_TO_CHANGE in order to use the GOA authoriser. */
 #include <gdata/gdata-goa-authorizer.h>
diff --git a/gdata/gdata.symbols b/gdata/gdata.symbols
index bba24ec..8854a65 100644
--- a/gdata/gdata.symbols
+++ b/gdata/gdata.symbols
@@ -1094,3 +1094,19 @@ gdata_freebase_result_error_quark
 gdata_freebase_result_get_type
 gdata_freebase_result_new
 gdata_freebase_result_dup_variant
+gdata_oauth2_authorizer_get_type
+gdata_oauth2_authorizer_new
+gdata_oauth2_authorizer_new_for_authorization_domains
+gdata_oauth2_authorizer_build_authentication_uri
+gdata_oauth2_authorizer_request_authorization
+gdata_oauth2_authorizer_request_authorization_async
+gdata_oauth2_authorizer_request_authorization_finish
+gdata_oauth2_authorizer_get_client_id
+gdata_oauth2_authorizer_get_redirect_uri
+gdata_oauth2_authorizer_get_client_secret
+gdata_oauth2_authorizer_get_locale
+gdata_oauth2_authorizer_set_locale
+gdata_oauth2_authorizer_get_timeout
+gdata_oauth2_authorizer_set_timeout
+gdata_oauth2_authorizer_get_proxy_resolver
+gdata_oauth2_authorizer_set_proxy_resolver
diff --git a/gdata/tests/Makefile.am b/gdata/tests/Makefile.am
index ce64ab5..af68480 100644
--- a/gdata/tests/Makefile.am
+++ b/gdata/tests/Makefile.am
@@ -57,6 +57,7 @@ TESTS = \
        authorization \
        client-login-authorizer \
        oauth1-authorizer \
+       oauth2-authorizer \
        $(NULL)
 
 # FIXME: Temporarily disabled until https://bugzilla.gnome.org/show_bug.cgi?id=705303 is fixed.
@@ -284,6 +285,16 @@ EXTRA_DIST += \
        traces/oauth1-authorizer/setup-oauth1-authorizer-data-authenticated \
        traces/oauth1-authorizer/setup-oauth1-authorizer-interactive-data \
        \
+       traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-authorized \
+       traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-unauthorized \
+       traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async \
+       traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-bad-credentials \
+       traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-cancellation \
+       traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync \
+       traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-bad-credentials \
+       traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-cancellation \
+       traces/oauth2-authorizer/setup-oauth2-authorizer-data-authenticated \
+       \
        traces/picasaweb/authentication \
        traces/picasaweb/authentication-async \
        traces/picasaweb/authentication-async-cancellation \
diff --git a/gdata/tests/oauth2-authorizer.c b/gdata/tests/oauth2-authorizer.c
new file mode 100644
index 0000000..d1e8bcf
--- /dev/null
+++ b/gdata/tests/oauth2-authorizer.c
@@ -0,0 +1,1030 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+/*
+ * GData Client
+ * Copyright (C) Philip Withnall 2011, 2014 <philip tecnocode co uk>
+ *
+ * GData Client 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.1 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with GData Client.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+#include <gdata/gdata.h>
+
+#include "common.h"
+
+static GThread *main_thread = NULL;
+static UhmServer *mock_server = NULL;
+
+#undef CLIENT_ID  /* from common.h */
+
+#define CLIENT_ID "352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com"
+#define CLIENT_SECRET "-fA4pHQJxR3zJ-FyAMPQsikg"
+#define REDIRECT_URI "urn:ietf:wg:oauth:2.0:oob"
+
+static void
+test_oauth2_authorizer_constructor (void)
+{
+       GDataOAuth2Authorizer *authorizer;
+
+       authorizer = gdata_oauth2_authorizer_new (CLIENT_ID, CLIENT_SECRET,
+                                                 REDIRECT_URI,
+                                                 GDATA_TYPE_TASKS_SERVICE);
+
+       g_assert (authorizer != NULL);
+       g_assert (GDATA_IS_OAUTH2_AUTHORIZER (authorizer));
+       g_assert (GDATA_IS_AUTHORIZER (authorizer));
+
+       g_object_unref (authorizer);
+}
+
+static void
+test_oauth2_authorizer_constructor_for_domains (void)
+{
+       GDataOAuth2Authorizer *authorizer;
+       GDataAuthorizationDomain *domain;
+       GList *domains;
+
+       /* Try with standard domains first */
+       domains = gdata_service_get_authorization_domains (GDATA_TYPE_TASKS_SERVICE);
+       authorizer = gdata_oauth2_authorizer_new_for_authorization_domains (CLIENT_ID, CLIENT_SECRET,
+                                                                           REDIRECT_URI, domains);
+       g_list_free (domains);
+
+       g_assert (authorizer != NULL);
+       g_assert (GDATA_IS_OAUTH2_AUTHORIZER (authorizer));
+       g_assert (GDATA_IS_AUTHORIZER (authorizer));
+
+       g_object_unref (authorizer);
+
+       /* Try again with a custom domain. Note that, as in test_authorization_domain_properties() this 
should not normally happen in client code. */
+       domain = GDATA_AUTHORIZATION_DOMAIN (g_object_new (GDATA_TYPE_AUTHORIZATION_DOMAIN,
+                                                          "service-name", "test",
+                                                          "scope", "test",
+                                                          NULL));
+
+       domains = g_list_prepend (NULL, domain);
+       authorizer = gdata_oauth2_authorizer_new_for_authorization_domains (CLIENT_ID, CLIENT_SECRET,
+                                                                           REDIRECT_URI, domains);
+       g_list_free (domains);
+
+       g_assert (authorizer != NULL);
+       g_assert (GDATA_IS_OAUTH2_AUTHORIZER (authorizer));
+       g_assert (GDATA_IS_AUTHORIZER (authorizer));
+
+       g_object_unref (authorizer);
+       g_object_unref (domain);
+}
+
+typedef struct {
+       GDataOAuth2Authorizer *authorizer;
+
+       guint locale_notification_count;
+       gulong locale_signal_handler;
+       guint proxy_resolver_notification_count;
+       gulong proxy_resolver_signal_handler;
+       guint timeout_notification_count;
+       gulong timeout_signal_handler;
+} OAuth2AuthorizerData;
+
+/* Used to count that exactly the right number of notify signals are emitted when setting properties */
+static void
+notify_cb (GObject *object, GParamSpec *pspec, guint *notification_count)
+{
+       /* Check we're running in the main thread */
+       g_assert (g_thread_self () == main_thread);
+
+       /* Increment the notification count */
+       *notification_count = *notification_count + 1;
+}
+
+static void
+connect_to_oauth2_authorizer (OAuth2AuthorizerData *data)
+{
+       /* Connect to notifications from the object to verify they're only emitted the correct number of 
times */
+       data->locale_signal_handler = g_signal_connect (data->authorizer, "notify::locale", (GCallback) 
notify_cb,
+                                                       &(data->locale_notification_count));
+       data->proxy_resolver_signal_handler = g_signal_connect (data->authorizer, "notify::proxy-resolver", 
(GCallback) notify_cb,
+                                                               &(data->proxy_resolver_notification_count));
+       data->timeout_signal_handler = g_signal_connect (data->authorizer, "notify::timeout", (GCallback) 
notify_cb,
+                                                        &(data->timeout_notification_count));
+}
+
+static void
+set_up_oauth2_authorizer_data (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       data->authorizer = gdata_oauth2_authorizer_new (CLIENT_ID,
+                                                       CLIENT_SECRET,
+                                                       REDIRECT_URI,
+                                                       GDATA_TYPE_TASKS_SERVICE);
+       connect_to_oauth2_authorizer (data);
+}
+
+static void
+set_up_oauth2_authorizer_data_multiple_domains (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       GList *authorization_domains = NULL;
+
+       authorization_domains = g_list_prepend (authorization_domains, 
gdata_tasks_service_get_primary_authorization_domain ());
+       authorization_domains = g_list_prepend (authorization_domains, 
gdata_picasaweb_service_get_primary_authorization_domain ());
+       data->authorizer = gdata_oauth2_authorizer_new_for_authorization_domains (CLIENT_ID, CLIENT_SECRET,
+                                                                                 REDIRECT_URI, 
authorization_domains);
+       g_list_free (authorization_domains);
+
+       connect_to_oauth2_authorizer (data);
+}
+
+static void
+set_up_oauth2_authorizer_data_locale (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       data->authorizer = gdata_oauth2_authorizer_new (CLIENT_ID,
+                                                       CLIENT_SECRET,
+                                                       REDIRECT_URI,
+                                                       GDATA_TYPE_TASKS_SERVICE);
+       gdata_oauth2_authorizer_set_locale (data->authorizer, "en_GB");
+       connect_to_oauth2_authorizer (data);
+}
+
+static void
+set_up_oauth2_authorizer_data_authenticated (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gboolean *skip_test = (gboolean*) user_data;
+       gchar *authentication_uri, *authorisation_code;
+
+       gdata_test_mock_server_start_trace (mock_server, "setup-oauth2-authorizer-data-authenticated");
+
+       /* Chain up */
+       set_up_oauth2_authorizer_data (data, NULL);
+
+       /* Get an authentication URI. */
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (data->authorizer, NULL, FALSE);
+       g_assert (authentication_uri != NULL);
+
+       /* Get the authorisation code off the user. */
+       if (uhm_server_get_enable_online (mock_server)) {
+               authorisation_code = gdata_test_query_user_for_verifier (authentication_uri);
+
+               g_free (authentication_uri);
+
+               if (authorisation_code == NULL) {
+                       *skip_test = TRUE;
+                       goto skip_test;
+               }
+       } else {
+               /* Hard-coded default to match the trace file. */
+               authorisation_code = g_strdup 
("4/GeYb_3HkYh4vyephp-lbvzQs1GAb.YtXAxmx-uJ0eoiIBeO6P2m9iH6kvkQI");
+               g_free (authentication_uri);
+       }
+
+       /* Authorise the token. */
+       g_assert (gdata_oauth2_authorizer_request_authorization (data->authorizer, authorisation_code, NULL, 
NULL) == TRUE);
+
+skip_test:
+       g_free (authorisation_code);
+
+       uhm_server_end_trace (mock_server);
+}
+
+static void
+tear_down_oauth2_authorizer_data (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       /* Clean up signal handlers */
+       g_signal_handler_disconnect (data->authorizer, data->timeout_signal_handler);
+       g_signal_handler_disconnect (data->authorizer, data->proxy_resolver_signal_handler);
+       g_signal_handler_disconnect (data->authorizer, data->locale_signal_handler);
+
+       g_object_unref (data->authorizer);
+}
+
+/* Test getting and setting the client-id property */
+static void
+test_oauth2_authorizer_properties_client_id (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gchar *client_id;
+
+       /* Verifying the normal state of the property in a newly-constructed instance of 
GDataOAuth2Authorizer */
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_client_id (data->authorizer), ==, CLIENT_ID);
+
+       g_object_get (data->authorizer, "client-id", &client_id, NULL);
+       g_assert_cmpstr (client_id, ==, CLIENT_ID);
+       g_free (client_id);
+}
+
+/* Test getting and setting the client-id property */
+static void
+test_oauth2_authorizer_properties_client_secret (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gchar *client_secret;
+
+       /* Verifying the normal state of the property in a newly-constructed instance of 
GDataOAuth2Authorizer */
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_client_secret (data->authorizer), ==, CLIENT_SECRET);
+
+       g_object_get (data->authorizer, "client-secret", &client_secret, NULL);
+       g_assert_cmpstr (client_secret, ==, CLIENT_SECRET);
+       g_free (client_secret);
+}
+
+/* Test getting and setting the client-id property */
+static void
+test_oauth2_authorizer_properties_redirect_uri (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gchar *redirect_uri;
+
+       /* Verifying the normal state of the property in a newly-constructed instance of 
GDataOAuth2Authorizer */
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_redirect_uri (data->authorizer), ==, REDIRECT_URI);
+
+       g_object_get (data->authorizer, "redirect-uri", &redirect_uri, NULL);
+       g_assert_cmpstr (redirect_uri, ==, REDIRECT_URI);
+       g_free (redirect_uri);
+}
+
+/* Test getting and setting the locale property */
+static void
+test_oauth2_authorizer_properties_locale (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gchar *locale;
+
+       /* Verifying the normal state of the property in a newly-constructed instance of 
GDataOAuth2Authorizer */
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_locale (data->authorizer), ==, NULL);
+
+       g_object_get (data->authorizer, "locale", &locale, NULL);
+       g_assert_cmpstr (locale, ==, NULL);
+       g_free (locale);
+
+       g_assert_cmpuint (data->locale_notification_count, ==, 0);
+
+       /* Check setting it works and emits a notification */
+       gdata_oauth2_authorizer_set_locale (data->authorizer, "en");
+
+       g_assert_cmpuint (data->locale_notification_count, ==, 1);
+
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_locale (data->authorizer), ==, "en");
+
+       g_object_get (data->authorizer, "locale", &locale, NULL);
+       g_assert_cmpstr (locale, ==, "en");
+       g_free (locale);
+
+       /* Check setting it to the same value is a no-op */
+       gdata_oauth2_authorizer_set_locale (data->authorizer, "en");
+       g_assert_cmpuint (data->locale_notification_count, ==, 1);
+
+       /* Check setting it back to NULL works */
+       gdata_oauth2_authorizer_set_locale (data->authorizer, NULL);
+
+       g_assert_cmpuint (data->locale_notification_count, ==, 2);
+
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_locale (data->authorizer), ==, NULL);
+
+       g_object_get (data->authorizer, "locale", &locale, NULL);
+       g_assert_cmpstr (locale, ==, NULL);
+       g_free (locale);
+
+       /* Test that setting it using g_object_set() works */
+       g_object_set (data->authorizer, "locale", "de", NULL);
+       g_assert_cmpstr (gdata_oauth2_authorizer_get_locale (data->authorizer), ==, "de");
+}
+
+/* Test getting and setting the timeout property */
+static void
+test_oauth2_authorizer_properties_timeout (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       guint timeout;
+
+       /* Verifying the normal state of the property in a newly-constructed instance of 
GDataOAuth2Authorizer */
+       g_assert_cmpuint (gdata_oauth2_authorizer_get_timeout (data->authorizer), ==, 0);
+
+       g_object_get (data->authorizer, "timeout", &timeout, NULL);
+       g_assert_cmpuint (timeout, ==, 0);
+
+       g_assert_cmpuint (data->timeout_notification_count, ==, 0);
+
+       /* Check setting it works and emits a notification */
+       gdata_oauth2_authorizer_set_timeout (data->authorizer, 30);
+
+       g_assert_cmpuint (data->timeout_notification_count, ==, 1);
+
+       g_assert_cmpuint (gdata_oauth2_authorizer_get_timeout (data->authorizer), ==, 30);
+
+       g_object_get (data->authorizer, "timeout", &timeout, NULL);
+       g_assert_cmpuint (timeout, ==, 30);
+
+       /* Check setting it to the same value is a no-op */
+       gdata_oauth2_authorizer_set_timeout (data->authorizer, 30);
+       g_assert_cmpuint (data->timeout_notification_count, ==, 1);
+
+       /* Check setting it back to 0 works */
+       gdata_oauth2_authorizer_set_timeout (data->authorizer, 0);
+
+       g_assert_cmpuint (data->timeout_notification_count, ==, 2);
+
+       g_assert_cmpuint (gdata_oauth2_authorizer_get_timeout (data->authorizer), ==, 0);
+
+       g_object_get (data->authorizer, "timeout", &timeout, NULL);
+       g_assert_cmpuint (timeout, ==, 0);
+
+       /* Test that setting it using g_object_set() works */
+       g_object_set (data->authorizer, "timeout", 15, NULL);
+       g_assert_cmpuint (gdata_oauth2_authorizer_get_timeout (data->authorizer), ==, 15);
+}
+
+/* Test getting and setting the proxy-resolver property */
+static void
+test_oauth2_authorizer_properties_proxy_resolver (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       GProxyResolver *old_proxy_resolver, *proxy_resolver, *new_proxy_resolver;
+
+       /* Verifying the normal state of the property in a newly-constructed instance of 
GDataOAuth2Authorizer.
+        * Since the resolver comes from the SoupSession, we don’t know whether it’s initially NULL. */
+       old_proxy_resolver = gdata_oauth2_authorizer_get_proxy_resolver (data->authorizer);
+
+       g_object_get (data->authorizer, "proxy-resolver", &proxy_resolver, NULL);
+       g_assert (proxy_resolver == old_proxy_resolver);
+
+       g_assert_cmpuint (data->proxy_resolver_notification_count, ==, 0);
+
+       /* Check setting it works and emits a notification */
+       new_proxy_resolver = g_object_ref (g_proxy_resolver_get_default ());
+       gdata_oauth2_authorizer_set_proxy_resolver (data->authorizer, new_proxy_resolver);
+
+       g_assert_cmpuint (data->proxy_resolver_notification_count, ==, 1);
+
+       g_assert (gdata_oauth2_authorizer_get_proxy_resolver (data->authorizer) != NULL);
+       g_assert (gdata_oauth2_authorizer_get_proxy_resolver (data->authorizer) == new_proxy_resolver);
+
+       g_object_get (data->authorizer, "proxy-resolver", &proxy_resolver, NULL);
+       g_assert (proxy_resolver != NULL);
+       g_assert (gdata_oauth2_authorizer_get_proxy_resolver (data->authorizer) == new_proxy_resolver);
+       g_object_unref (proxy_resolver);
+
+       g_object_unref (new_proxy_resolver);
+
+       /* Check setting it back to NULL works */
+       gdata_oauth2_authorizer_set_proxy_resolver (data->authorizer, NULL);
+
+       g_assert_cmpuint (data->proxy_resolver_notification_count, ==, 2);
+
+       g_assert (gdata_oauth2_authorizer_get_proxy_resolver (data->authorizer) == NULL);
+
+       g_object_get (data->authorizer, "proxy-resolver", &proxy_resolver, NULL);
+       g_assert (proxy_resolver == NULL);
+
+       /* Test that setting it using g_object_set() works */
+       new_proxy_resolver = g_object_ref (g_proxy_resolver_get_default ());
+       g_object_set (data->authorizer, "proxy-resolver", new_proxy_resolver, NULL);
+       g_object_unref (new_proxy_resolver);
+
+       g_assert (gdata_oauth2_authorizer_get_proxy_resolver (data->authorizer) != NULL);
+}
+
+/* Test that gdata_authorizer_refresh_authorization() is a no-op when
+ * unauthenticated. */
+static void
+test_oauth2_authorizer_refresh_authorization_unauthenticated (OAuth2AuthorizerData *data, gconstpointer 
user_data)
+{
+       gboolean *skip_test = (gboolean*) user_data;
+       GError *error = NULL;
+
+       gdata_test_mock_server_start_trace (mock_server, 
"oauth2-authorizer-refresh-authorization-unauthorized");
+
+       /* Skip the test if the user's requested */
+       if (skip_test != NULL && *skip_test == TRUE) {
+               return;
+       }
+
+       g_assert (gdata_authorizer_refresh_authorization (GDATA_AUTHORIZER (data->authorizer), NULL, &error) 
== FALSE);
+       g_assert_no_error (error);
+       g_clear_error (&error);
+
+       uhm_server_end_trace (mock_server);
+}
+
+/* Test that gdata_authorizer_refresh_authorization() works when authenticated. */
+static void
+test_oauth2_authorizer_refresh_authorization_authenticated (OAuth2AuthorizerData *data, gconstpointer 
user_data)
+{
+       gboolean *skip_test = (gboolean*) user_data;
+       GError *error = NULL;
+
+       gdata_test_mock_server_start_trace (mock_server, 
"oauth2-authorizer-refresh-authorization-authorized");
+
+       /* Skip the test if the user's requested */
+       if (skip_test != NULL && *skip_test == TRUE) {
+               return;
+       }
+
+       g_assert (gdata_authorizer_refresh_authorization (GDATA_AUTHORIZER (data->authorizer), NULL, &error) 
== TRUE);
+       g_assert_no_error (error);
+       g_clear_error (&error);
+
+       uhm_server_end_trace (mock_server);
+}
+
+/* Test that processing a request with a NULL domain will not change the request. */
+static void
+test_oauth2_authorizer_process_request_null (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       SoupMessage *message;
+       SoupMessageHeadersIter iter;
+       guint header_count = 0;
+       const gchar *name, *value;
+
+       /* Create a new message with an empty set of request headers */
+       message = soup_message_new (SOUP_METHOD_GET, "https://example.com/";);
+
+       /* Process the message */
+       gdata_authorizer_process_request (GDATA_AUTHORIZER (data->authorizer), NULL, message);
+
+       /* Check that the set of request headers is still empty */
+       soup_message_headers_iter_init (&iter, message->request_headers);
+
+       while (soup_message_headers_iter_next (&iter, &name, &value) == TRUE) {
+               header_count++;
+       }
+
+       g_assert_cmpuint (header_count, ==, 0);
+
+       g_object_unref (message);
+}
+
+/* Test that processing a request with an authorizer which hasn't been authenticated yet will not change the 
request. */
+static void
+test_oauth2_authorizer_process_request_unauthenticated (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       SoupMessage *message;
+       SoupMessageHeadersIter iter;
+       guint header_count = 0;
+       const gchar *name, *value;
+
+       /* Create a new message with an empty set of request headers */
+       message = soup_message_new (SOUP_METHOD_GET, "https://example.com/";);
+
+       /* Process the message */
+       gdata_authorizer_process_request (GDATA_AUTHORIZER (data->authorizer), 
gdata_tasks_service_get_primary_authorization_domain (), message);
+
+       /* Check that the set of request headers is still empty */
+       soup_message_headers_iter_init (&iter, message->request_headers);
+
+       while (soup_message_headers_iter_next (&iter, &name, &value) == TRUE) {
+               header_count++;
+       }
+
+       g_assert_cmpuint (header_count, ==, 0);
+
+       g_object_unref (message);
+}
+
+/* Test that processing a request with an authorizer which has been authenticated will change the request. */
+static void
+test_oauth2_authorizer_process_request_authenticated (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gboolean *skip_test = (gboolean*) user_data;
+       SoupMessage *message;
+       SoupMessageHeadersIter iter;
+       guint header_count = 0;
+       const gchar *name, *value;
+
+       /* Skip the test if the user's requested */
+       if (skip_test != NULL && *skip_test == TRUE) {
+               return;
+       }
+
+       /* Create a new message with an empty set of request headers */
+       message = soup_message_new (SOUP_METHOD_GET, "https://example.com/";);
+
+       /* Process the message */
+       gdata_authorizer_process_request (GDATA_AUTHORIZER (data->authorizer), 
gdata_tasks_service_get_primary_authorization_domain (), message);
+
+       /* Check that at least one new header has been set */
+       soup_message_headers_iter_init (&iter, message->request_headers);
+
+       while (soup_message_headers_iter_next (&iter, &name, &value) == TRUE) {
+               header_count++;
+       }
+
+       g_assert_cmpuint (header_count, >, 0);
+
+       g_object_unref (message);
+}
+
+/* Test that building an authentication URI works correctly */
+static void
+test_oauth2_authorizer_build_authentication_uri_default (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gchar *authentication_uri;
+
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (data->authorizer, NULL, FALSE);
+       g_assert (authentication_uri != NULL && *authentication_uri != '\0');
+
+       g_test_message ("Building an authentication URI gave “%s”.",
+                       authentication_uri);
+
+       g_free (authentication_uri);
+}
+
+/* Test that building an authentication URI with a login hint works
+ * correctly. */
+static void
+test_oauth2_authorizer_build_authentication_uri_hint (OAuth2AuthorizerData *data, gconstpointer user_data)
+{
+       gchar *authentication_uri;
+
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (data->authorizer, "test user 
gmail com", FALSE);
+       g_assert (authentication_uri != NULL && *authentication_uri != '\0');
+
+       g_test_message ("Building an authentication URI gave “%s”.",
+                       authentication_uri);
+
+       g_free (authentication_uri);
+}
+
+/* Test that buildig an authentication URI with a login hint and incremental
+ * authentication works correctly. */
+static void
+test_oauth2_authorizer_build_authentication_uri_incremental (OAuth2AuthorizerData *data, gconstpointer 
user_data)
+{
+       gchar *authentication_uri;
+
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (data->authorizer, "test user 
gmail com", TRUE);
+       g_assert (authentication_uri != NULL && *authentication_uri != '\0');
+
+       g_test_message ("Building an authentication URI gave “%s”.",
+                       authentication_uri);
+
+       g_free (authentication_uri);
+}
+
+typedef struct {
+       OAuth2AuthorizerData parent;
+       gchar *authorisation_code;
+} OAuth2AuthorizerInteractiveData;
+
+/* NOTE: Any consumer of this data has to check for (data->authorisation_code == NULL) and skip the test in 
that case */
+static void
+set_up_oauth2_authorizer_interactive_data (OAuth2AuthorizerInteractiveData *data, gconstpointer user_data)
+{
+       gchar *authentication_uri;
+
+       /* Chain up */
+       set_up_oauth2_authorizer_data ((OAuth2AuthorizerData*) data, user_data);
+
+       /* Get an authentication URI */
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (data->parent.authorizer, NULL, 
FALSE);
+       g_assert (authentication_uri != NULL);
+
+       /* Wait for the user to retrieve and enter the authorisation code. */
+       if (uhm_server_get_enable_online (mock_server)) {
+               data->authorisation_code = gdata_test_query_user_for_verifier (authentication_uri);
+       } else {
+               /* Hard-coded default to match the trace file. */
+               data->authorisation_code = g_strdup ((const gchar *) user_data);
+       }
+
+       g_free (authentication_uri);
+}
+
+static void
+set_up_oauth2_authorizer_interactive_data_bad_credentials (OAuth2AuthorizerInteractiveData *data, 
gconstpointer user_data)
+{
+       gchar *authentication_uri;
+
+       /* Chain up */
+       set_up_oauth2_authorizer_data ((OAuth2AuthorizerData*) data, user_data);
+
+       /* Get an authentication URI */
+       authentication_uri = gdata_oauth2_authorizer_build_authentication_uri (data->parent.authorizer, NULL, 
FALSE);
+       g_assert (authentication_uri != NULL);
+
+       /* Give a bogus authorisation code. */
+       data->authorisation_code = g_strdup ("test");
+
+       g_free (authentication_uri);
+}
+
+static void
+tear_down_oauth2_authorizer_interactive_data (OAuth2AuthorizerInteractiveData *data, gconstpointer user_data)
+{
+       g_free (data->authorisation_code);
+
+       /* Chain up */
+       tear_down_oauth2_authorizer_data ((OAuth2AuthorizerData*) data, user_data);
+}
+
+/* Test that synchronously authorizing an authorisation code is successful. Note
+ * that this test has to be interactive, as the user has to visit the
+ * authentication URI to retrieve an authorisation code. */
+static void
+test_oauth2_authorizer_request_authorization_sync (OAuth2AuthorizerInteractiveData *data, gconstpointer 
user_data)
+{
+       gboolean success;
+       GError *error = NULL;
+
+       /* Skip the test if the user's requested */
+       if (data->authorisation_code == NULL) {
+               return;
+       }
+
+       gdata_test_mock_server_start_trace (mock_server, "oauth2-authorizer-request-authorization-sync");
+
+       /* Check we're not authorised beforehand */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (data->parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       /* Test that authorising the token retrieved previously is successful */
+       success = gdata_oauth2_authorizer_request_authorization (data->parent.authorizer, 
data->authorisation_code, NULL,
+                                                                &error);
+       g_assert_no_error (error);
+       g_assert (success == TRUE);
+       g_clear_error (&error);
+
+       /* Are we authorised now? */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (data->parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == TRUE);
+
+       uhm_server_end_trace (mock_server);
+}
+
+/* Test that synchronously authorizing fails if an invalid authorisation code is
+ * provided. */
+static void
+test_oauth2_authorizer_request_authorization_sync_bad_credentials (OAuth2AuthorizerInteractiveData *data, 
gconstpointer user_data)
+{
+       gboolean success;
+       GError *error = NULL;
+
+       gdata_test_mock_server_start_trace (mock_server, 
"oauth2-authorizer-request-authorization-sync-bad-credentials");
+
+       /* Check we're not authorised beforehand */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (data->parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       /* Test that authorising the token retrieved above fails */
+       success = gdata_oauth2_authorizer_request_authorization (data->parent.authorizer, 
data->authorisation_code, NULL,
+                                                                &error);
+       g_assert_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_FORBIDDEN);
+       g_assert (success == FALSE);
+       g_clear_error (&error);
+
+       /* Are we authorised now? */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (data->parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       uhm_server_end_trace (mock_server);
+}
+
+/* Test that cancellation of synchronously authorizing works. Note that this
+ * test has to be interactive, as the user has to visit the authentication URI
+ * to retrieve an authorisation code. */
+static void
+test_oauth2_authorizer_request_authorization_sync_cancellation (OAuth2AuthorizerInteractiveData *data, 
gconstpointer user_data)
+{
+       gboolean success;
+       GCancellable *cancellable;
+       GError *error = NULL;
+
+       /* Skip the test if the user's requested */
+       if (data->authorisation_code == NULL) {
+               return;
+       }
+
+       gdata_test_mock_server_start_trace (mock_server, 
"oauth2-authorizer-request-authorization-sync-cancellation");
+
+       /* Check we're not authorised beforehand */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (data->parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       /* Create the cancellable */
+       cancellable = g_cancellable_new ();
+
+       /* Test that authorising the code retrieved above is unsuccessful */
+       g_cancellable_cancel (cancellable);
+       success = gdata_oauth2_authorizer_request_authorization (data->parent.authorizer, 
data->authorisation_code,
+                                                                cancellable, &error);
+       g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+       g_assert (success == FALSE);
+       g_clear_error (&error);
+
+       /* Are we authorised now? */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (data->parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       g_object_unref (cancellable);
+
+       uhm_server_end_trace (mock_server);
+}
+
+typedef struct {
+       OAuth2AuthorizerInteractiveData parent;
+       GMainLoop *main_loop;
+} OAuth2AuthorizerInteractiveAsyncData;
+
+/* NOTE: Any consumer of this data has to check for
+ * (data->authorisation_code == NULL) and skip the test in that case */
+static void
+set_up_oauth2_authorizer_interactive_async_data (OAuth2AuthorizerInteractiveAsyncData *data, gconstpointer 
user_data)
+{
+       /* Chain up */
+       set_up_oauth2_authorizer_interactive_data ((OAuth2AuthorizerInteractiveData*) data, user_data);
+
+       /* Set up the main loop */
+       data->main_loop = g_main_loop_new (NULL, FALSE);
+}
+
+static void
+set_up_oauth2_authorizer_interactive_async_data_bad_credentials (OAuth2AuthorizerInteractiveAsyncData *data, 
gconstpointer user_data)
+{
+       /* Chain up */
+       set_up_oauth2_authorizer_interactive_data_bad_credentials ((OAuth2AuthorizerInteractiveData*) data, 
user_data);
+
+       /* Set up the main loop */
+       data->main_loop = g_main_loop_new (NULL, FALSE);
+}
+
+static void
+tear_down_oauth2_authorizer_interactive_async_data (OAuth2AuthorizerInteractiveAsyncData *data, 
gconstpointer user_data)
+{
+       g_main_loop_unref (data->main_loop);
+
+       /* Chain up */
+       tear_down_oauth2_authorizer_interactive_data ((OAuth2AuthorizerInteractiveData*) data, user_data);
+}
+
+static void
+test_oauth2_authorizer_request_authorization_async_cb (GDataOAuth2Authorizer *authorizer, GAsyncResult 
*async_result,
+                                                       OAuth2AuthorizerInteractiveAsyncData *data)
+{
+       gboolean success;
+       GError *error = NULL;
+
+       success = gdata_oauth2_authorizer_request_authorization_finish (authorizer, async_result, &error);
+       g_assert_no_error (error);
+       g_assert (success == TRUE);
+       g_clear_error (&error);
+
+       /* Are we authorised now? */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == TRUE);
+
+       g_main_loop_quit (data->main_loop);
+}
+
+/* Test that asynchronously authorizing works. Note that this test has to be
+ * interactive, as the user has to visit the authentication URI to retrieve an
+ * authorisation code. */
+static void
+test_oauth2_authorizer_request_authorization_async (OAuth2AuthorizerInteractiveAsyncData *data, 
gconstpointer user_data)
+{
+       /* Skip the test if the user's requested */
+       if (data->parent.authorisation_code == NULL) {
+               return;
+       }
+
+       gdata_test_mock_server_start_trace (mock_server, "oauth2-authorizer-request-authorization-async");
+
+       /* Check we're not authorised beforehand */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER 
(data->parent.parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       /* Create a main loop and request authorization */
+       gdata_oauth2_authorizer_request_authorization_async (data->parent.parent.authorizer,
+                                                            data->parent.authorisation_code,
+                                                            NULL,
+                                                            (GAsyncReadyCallback) 
test_oauth2_authorizer_request_authorization_async_cb, data);
+
+       g_main_loop_run (data->main_loop);
+
+       uhm_server_end_trace (mock_server);
+}
+
+static void
+test_oauth2_authorizer_request_authorization_async_bad_credentials_cb (GDataOAuth2Authorizer *authorizer, 
GAsyncResult *async_result,
+                                                                       OAuth2AuthorizerInteractiveAsyncData 
*data)
+{
+       gboolean success;
+       GError *error = NULL;
+
+       success = gdata_oauth2_authorizer_request_authorization_finish (authorizer, async_result, &error);
+       g_assert_error (error, GDATA_SERVICE_ERROR, GDATA_SERVICE_ERROR_FORBIDDEN);
+       g_assert (success == FALSE);
+       g_clear_error (&error);
+
+       /* Are we authorised now? */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       g_main_loop_quit (data->main_loop);
+}
+
+/* Test that asynchronously authorizing fails if an invalid authorisation code
+ * is provided. */
+static void
+test_oauth2_authorizer_request_authorization_async_bad_credentials (OAuth2AuthorizerInteractiveAsyncData 
*data, gconstpointer user_data)
+{
+       gdata_test_mock_server_start_trace (mock_server, 
"oauth2-authorizer-request-authorization-async-bad-credentials");
+
+       /* Check we're not authorised beforehand */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER 
(data->parent.parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       /* Create a main loop and request authorization */
+       gdata_oauth2_authorizer_request_authorization_async (data->parent.parent.authorizer,
+                                                            data->parent.authorisation_code,
+                                                            NULL,
+                                                            (GAsyncReadyCallback) 
test_oauth2_authorizer_request_authorization_async_bad_credentials_cb,
+                                                            data);
+
+       g_main_loop_run (data->main_loop);
+
+       uhm_server_end_trace (mock_server);
+}
+
+static void
+test_oauth2_authorizer_request_authorization_async_cancellation_cb (GDataOAuth2Authorizer *authorizer, 
GAsyncResult *async_result,
+                                                                    OAuth2AuthorizerInteractiveAsyncData 
*data)
+{
+       gboolean success;
+       GError *error = NULL;
+
+       success = gdata_oauth2_authorizer_request_authorization_finish (authorizer, async_result, &error);
+       g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+       g_assert (success == FALSE);
+       g_clear_error (&error);
+
+       /* Are we authorised now? */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER (authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       g_main_loop_quit (data->main_loop);
+}
+
+/* Test that cancelling asynchronously authorizing works. Note that this test
+ * has to be interactive, as the user has to visit the authentication URI to
+ * retrieve an authorisation code. */
+static void
+test_oauth2_authorizer_request_authorization_async_cancellation (OAuth2AuthorizerInteractiveAsyncData *data, 
gconstpointer user_data)
+{
+       GCancellable *cancellable;
+
+       /* Skip the test if the user's requested */
+       if (data->parent.authorisation_code == NULL) {
+               return;
+       }
+
+       gdata_test_mock_server_start_trace (mock_server, 
"oauth2-authorizer-request-authorization-async-cancellation");
+
+       /* Check we're not authorised beforehand */
+       g_assert (gdata_authorizer_is_authorized_for_domain (GDATA_AUTHORIZER 
(data->parent.parent.authorizer),
+                 gdata_tasks_service_get_primary_authorization_domain ()) == FALSE);
+
+       /* Create the cancellable */
+       cancellable = g_cancellable_new ();
+
+       /* Create a main loop and request authorization */
+       gdata_oauth2_authorizer_request_authorization_async (data->parent.parent.authorizer,
+                                                            data->parent.authorisation_code,
+                                                            cancellable,
+                                                            (GAsyncReadyCallback) 
test_oauth2_authorizer_request_authorization_async_cancellation_cb,
+                                                            data);
+       g_cancellable_cancel (cancellable);
+
+       g_main_loop_run (data->main_loop);
+
+       g_object_unref (cancellable);
+
+       uhm_server_end_trace (mock_server);
+}
+
+static void
+mock_server_notify_resolver_cb (GObject *object, GParamSpec *pspec,
+                                gpointer user_data)
+{
+       UhmServer *server;
+       UhmResolver *resolver;
+
+       server = UHM_SERVER (object);
+
+       /* Set up the expected domain names here. This should technically be
+        * split up between the different unit test suites, but that’s too much
+        * effort. */
+       resolver = uhm_server_get_resolver (server);
+
+       if (resolver != NULL) {
+               const gchar *ip_address = uhm_server_get_address (server);
+
+               uhm_resolver_add_A (resolver, "www.google.com", ip_address);
+               uhm_resolver_add_A (resolver,
+                                   "accounts.google.com", ip_address);
+       }
+}
+
+int
+main (int argc, char *argv[])
+{
+       GFile *trace_directory;
+
+       gdata_test_init (argc, argv);
+
+       mock_server = gdata_test_get_mock_server ();
+       g_signal_connect (G_OBJECT (mock_server), "notify::resolver",
+                         (GCallback) mock_server_notify_resolver_cb, NULL);
+       trace_directory = g_file_new_for_path (TEST_FILE_DIR "traces/oauth2-authorizer");
+       uhm_server_set_trace_directory (mock_server, trace_directory);
+       g_object_unref (trace_directory);
+
+       main_thread = g_thread_self ();
+
+       g_test_add_func ("/oauth2-authorizer/constructor", test_oauth2_authorizer_constructor);
+       g_test_add_func ("/oauth2-authorizer/constructor/for-domains", 
test_oauth2_authorizer_constructor_for_domains);
+
+       g_test_add ("/oauth2-authorizer/properties/client-id", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_properties_client_id, tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/properties/client-secret", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_properties_client_secret, tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/properties/redirect-uri", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_properties_redirect_uri, tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/properties/locale", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_properties_locale, tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/properties/timeout", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_properties_timeout, tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/properties/proxy-resolver", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_properties_proxy_resolver, tear_down_oauth2_authorizer_data);
+
+       g_test_add ("/oauth2-authorizer/refresh-authorization/unauthenticated", OAuth2AuthorizerData, NULL,
+                   set_up_oauth2_authorizer_data, 
test_oauth2_authorizer_refresh_authorization_unauthenticated, tear_down_oauth2_authorizer_data);
+
+       g_test_add ("/oauth2-authorizer/process-request/null", OAuth2AuthorizerData, NULL,
+                   set_up_oauth2_authorizer_data, test_oauth2_authorizer_process_request_null, 
tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/process-request/unauthenticated", OAuth2AuthorizerData, NULL,
+                   set_up_oauth2_authorizer_data, test_oauth2_authorizer_process_request_unauthenticated, 
tear_down_oauth2_authorizer_data);
+
+       /* build-authentication-uri tests */
+       g_test_add ("/oauth2-authorizer/build-authentication-uri", OAuth2AuthorizerData, NULL, 
set_up_oauth2_authorizer_data,
+                   test_oauth2_authorizer_build_authentication_uri_default, 
tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/build-authentication-uri/multiple-domains", OAuth2AuthorizerData, 
NULL,
+                   set_up_oauth2_authorizer_data_multiple_domains, 
test_oauth2_authorizer_build_authentication_uri_default,
+                   tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/build-authentication-uri/locale", OAuth2AuthorizerData, NULL,
+                   set_up_oauth2_authorizer_data_locale, 
test_oauth2_authorizer_build_authentication_uri_default,
+                   tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/build-authentication-uri/hint", OAuth2AuthorizerData, NULL,
+                   set_up_oauth2_authorizer_data, test_oauth2_authorizer_build_authentication_uri_hint,
+                   tear_down_oauth2_authorizer_data);
+       g_test_add ("/oauth2-authorizer/build-authentication-uri/incremental", OAuth2AuthorizerData, NULL,
+                   set_up_oauth2_authorizer_data, 
test_oauth2_authorizer_build_authentication_uri_incremental,
+                   tear_down_oauth2_authorizer_data);
+
+       /* Sync request-authorization tests */
+       if (gdata_test_interactive () == TRUE) {
+               g_test_add ("/oauth2-authorizer/request-authorization/sync", OAuth2AuthorizerInteractiveData,
+                           "4/P-pwMETnCh47w20wexdnflDFhXum.4qZ2A1pkUGsSoiIBeO6P2m8OUKkvkQI",
+                           set_up_oauth2_authorizer_interactive_data, 
test_oauth2_authorizer_request_authorization_sync,
+                           tear_down_oauth2_authorizer_interactive_data);
+               g_test_add ("/oauth2-authorizer/request-authorization/sync/cancellation", 
OAuth2AuthorizerInteractiveData,
+                           "4/P-pwMETnCh47w20wexdnflDFhXum.4qZ2A1pkUGsSoiIBeO6P2m8OUKkvkQI",
+                           set_up_oauth2_authorizer_interactive_data, 
test_oauth2_authorizer_request_authorization_sync_cancellation,
+                           tear_down_oauth2_authorizer_interactive_data);
+       }
+
+       g_test_add ("/oauth2-authorizer/request-authorization/sync/bad-credentials", 
OAuth2AuthorizerInteractiveData, "",
+                   set_up_oauth2_authorizer_interactive_data_bad_credentials,
+                   test_oauth2_authorizer_request_authorization_sync_bad_credentials, 
tear_down_oauth2_authorizer_interactive_data);
+
+       /* Async request-authorization tests */
+       if (gdata_test_interactive () == TRUE) {
+               g_test_add ("/oauth2-authorizer/request-authorization/async", 
OAuth2AuthorizerInteractiveAsyncData,
+                           "4/Gfha9-4IeN09ibTR2Sa2MtQrG9qz.ks8v0zlKR9ceoiIBeO6P2m92f6kvkQI",
+                           set_up_oauth2_authorizer_interactive_async_data, 
test_oauth2_authorizer_request_authorization_async,
+                           tear_down_oauth2_authorizer_interactive_async_data);
+               g_test_add ("/oauth2-authorizer/request-authorization/async/cancellation", 
OAuth2AuthorizerInteractiveAsyncData,
+                           "4/Gfha9-4IeN09ibTR2Sa2MtQrG9qz.ks8v0zlKR9ceoiIBeO6P2m92f6kvkQI",
+                           set_up_oauth2_authorizer_interactive_async_data, 
test_oauth2_authorizer_request_authorization_async_cancellation,
+                           tear_down_oauth2_authorizer_interactive_async_data);
+       }
+
+       g_test_add ("/oauth2-authorizer/request-authorization/async/bad-credentials", 
OAuth2AuthorizerInteractiveAsyncData, "",
+                   set_up_oauth2_authorizer_interactive_async_data_bad_credentials,
+                   test_oauth2_authorizer_request_authorization_async_bad_credentials, 
tear_down_oauth2_authorizer_interactive_async_data);
+
+       /* Miscellaneous tests */
+       if (gdata_test_interactive () == TRUE) {
+               gboolean skip_test = FALSE;
+
+               g_test_add ("/oauth2-authorizer/refresh-authorization/authenticated", OAuth2AuthorizerData, 
&skip_test,
+                           set_up_oauth2_authorizer_data_authenticated, 
test_oauth2_authorizer_refresh_authorization_authenticated,
+                           tear_down_oauth2_authorizer_data);
+
+               g_test_add ("/oauth2-authorizer/process-request/authenticated", OAuth2AuthorizerData, 
&skip_test,
+                           set_up_oauth2_authorizer_data_authenticated, 
test_oauth2_authorizer_process_request_authenticated,
+                           tear_down_oauth2_authorizer_data);
+       }
+
+       return g_test_run ();
+}
diff --git a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-authorized 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-authorized
new file mode 100644
index 0000000..5b972c8
--- /dev/null
+++ b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-authorized
@@ -0,0 +1,34 @@
+> POST /o/oauth2/token HTTP/1.1
+> Soup-Debug-Timestamp: 1411296439
+> Soup-Debug: SoupSession 1 (0x1888240), SoupMessage 2 (0x1ace9b0), SoupSocket 2 (0x1a43330)
+> Host: accounts.google.com
+> Content-Type: application/x-www-form-urlencoded
+> Accept-Encoding: gzip, deflate
+> User-Agent: libgdata/0.16.1 - gzip
+> Connection: Keep-Alive
+> 
+> 
client_id=352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com&client_secret=-fA4pHQJxR3zJ-FyAMPQsikg&refresh_token=1%2FmIo8e1bmcRxrXd2gKfhtabh3Vvtjfc3GGJT5ODsf_t0&grant_type=refresh_token
+  
+< HTTP/1.1 200 OK
+< Soup-Debug-Timestamp: 1411296439
+< Soup-Debug: SoupMessage 2 (0x1ace9b0)
+< Content-Type: application/json; charset=utf-8
+< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+< Pragma: no-cache
+< Expires: Fri, 01 Jan 1990 00:00:00 GMT
+< Date: Sun, 21 Sep 2014 10:47:19 GMT
+< Content-Disposition: attachment; filename="json.txt"; filename*=UTF-8''json.txt
+< Content-Encoding: gzip
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< X-XSS-Protection: 1; mode=block
+< Server: GSE
+< Alternate-Protocol: 443:quic,p=0.002
+< Transfer-Encoding: chunked
+< 
+< {
+<   "access_token" : "ya29.hwDtghueiadzgPMb3otq_QAWMXEVup-cyRwyiP0GQGH3iqMMtfvQMngM",
+<   "token_type" : "Bearer",
+<   "expires_in" : 3600
+< }
+  
diff --git a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-unauthorized 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-refresh-authorization-unauthorized
new file mode 100644
index 0000000..e69de29
diff --git a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async
new file mode 100644
index 0000000..a99f707
--- /dev/null
+++ b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async
@@ -0,0 +1,35 @@
+> POST /o/oauth2/token HTTP/1.1
+> Soup-Debug-Timestamp: 1411296457
+> Soup-Debug: SoupSession 1 (0x1888110), SoupMessage 1 (0x1acee60), SoupSocket 1 (0x1a43670)
+> Host: accounts.google.com
+> Content-Type: application/x-www-form-urlencoded
+> Accept-Encoding: gzip, deflate
+> User-Agent: libgdata/0.16.1 - gzip
+> Connection: Keep-Alive
+> 
+> 
client_id=352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com&client_secret=-fA4pHQJxR3zJ-FyAMPQsikg&code=4%2FGfha9-4IeN09ibTR2Sa2MtQrG9qz.ks8v0zlKR9ceoiIBeO6P2m92f6kvkQI&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=authorization_code
+  
+< HTTP/1.1 200 OK
+< Soup-Debug-Timestamp: 1411296458
+< Soup-Debug: SoupMessage 1 (0x1acee60)
+< Content-Type: application/json; charset=utf-8
+< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+< Pragma: no-cache
+< Expires: Fri, 01 Jan 1990 00:00:00 GMT
+< Date: Sun, 21 Sep 2014 10:47:38 GMT
+< Content-Disposition: attachment; filename="json.txt"; filename*=UTF-8''json.txt
+< Content-Encoding: gzip
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< X-XSS-Protection: 1; mode=block
+< Server: GSE
+< Alternate-Protocol: 443:quic,p=0.002
+< Transfer-Encoding: chunked
+< 
+< {
+<   "access_token" : "ya29.hwBrSw9uZJ2M5qgjnwL-CsEHamHerCaKd8mizXE-Tiqp1XIpDcQoaOdw",
+<   "token_type" : "Bearer",
+<   "expires_in" : 3600,
+<   "refresh_token" : "1/B7Axq79ZCmbj81AL3hByjHm1fWF6BbVLrt7PVtLs7Ao"
+< }
+  
diff --git 
a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-bad-credentials 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-bad-credentials
new file mode 100644
index 0000000..ae11c75
--- /dev/null
+++ b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-bad-credentials
@@ -0,0 +1,31 @@
+> POST /o/oauth2/token HTTP/1.1
+> Soup-Debug-Timestamp: 1411296470
+> Soup-Debug: SoupSession 1 (0x18885d0), SoupMessage 1 (0x1acef50), SoupSocket 1 (0x1a43400)
+> Host: accounts.google.com
+> Content-Type: application/x-www-form-urlencoded
+> Accept-Encoding: gzip, deflate
+> User-Agent: libgdata/0.16.1 - gzip
+> Connection: Keep-Alive
+> 
+> 
client_id=352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com&client_secret=-fA4pHQJxR3zJ-FyAMPQsikg&code=test&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=authorization_code
+  
+< HTTP/1.1 400 Bad Request
+< Soup-Debug-Timestamp: 1411296470
+< Soup-Debug: SoupMessage 1 (0x1acef50)
+< Content-Type: application/json
+< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+< Pragma: no-cache
+< Expires: Fri, 01 Jan 1990 00:00:00 GMT
+< Date: Sun, 21 Sep 2014 10:47:50 GMT
+< Content-Encoding: gzip
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< X-XSS-Protection: 1; mode=block
+< Server: GSE
+< Alternate-Protocol: 443:quic,p=0.002
+< Transfer-Encoding: chunked
+< 
+< {
+<   "error" : "invalid_grant"
+< }
+  
diff --git a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-cancellation 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-async-cancellation
new file mode 100644
index 0000000..e69de29
diff --git a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync
new file mode 100644
index 0000000..da0480e
--- /dev/null
+++ b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync
@@ -0,0 +1,35 @@
+> POST /o/oauth2/token HTTP/1.1
+> Soup-Debug-Timestamp: 1411296451
+> Soup-Debug: SoupSession 1 (0x1888110), SoupMessage 1 (0x1aced70), SoupSocket 1 (0x1a434d0)
+> Host: accounts.google.com
+> Content-Type: application/x-www-form-urlencoded
+> Accept-Encoding: gzip, deflate
+> User-Agent: libgdata/0.16.1 - gzip
+> Connection: Keep-Alive
+> 
+> 
client_id=352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com&client_secret=-fA4pHQJxR3zJ-FyAMPQsikg&code=4%2FP-pwMETnCh47w20wexdnflDFhXum.4qZ2A1pkUGsSoiIBeO6P2m8OUKkvkQI&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=authorization_code
+  
+< HTTP/1.1 200 OK
+< Soup-Debug-Timestamp: 1411296452
+< Soup-Debug: SoupMessage 1 (0x1aced70)
+< Content-Type: application/json; charset=utf-8
+< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+< Pragma: no-cache
+< Expires: Fri, 01 Jan 1990 00:00:00 GMT
+< Date: Sun, 21 Sep 2014 10:47:32 GMT
+< Content-Disposition: attachment; filename="json.txt"; filename*=UTF-8''json.txt
+< Content-Encoding: gzip
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< X-XSS-Protection: 1; mode=block
+< Server: GSE
+< Alternate-Protocol: 443:quic,p=0.002
+< Transfer-Encoding: chunked
+< 
+< {
+<   "access_token" : "ya29.hwAp50GMEAP0LIK6U9l0GzA3lrOrvpAJvhrHxqvjsorPkgEYPoxPvXfS",
+<   "token_type" : "Bearer",
+<   "expires_in" : 3600,
+<   "refresh_token" : "1/nLWzECbPThSlbaxu6gzVK4uAUMlA_mxBPxLSTd8ebmQ"
+< }
+  
diff --git 
a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-bad-credentials 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-bad-credentials
new file mode 100644
index 0000000..882a74d
--- /dev/null
+++ b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-bad-credentials
@@ -0,0 +1,31 @@
+> POST /o/oauth2/token HTTP/1.1
+> Soup-Debug-Timestamp: 1411296464
+> Soup-Debug: SoupSession 1 (0x1888370), SoupMessage 1 (0x1aceb90), SoupSocket 1 (0x1a43670)
+> Host: accounts.google.com
+> Content-Type: application/x-www-form-urlencoded
+> Accept-Encoding: gzip, deflate
+> User-Agent: libgdata/0.16.1 - gzip
+> Connection: Keep-Alive
+> 
+> 
client_id=352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com&client_secret=-fA4pHQJxR3zJ-FyAMPQsikg&code=test&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=authorization_code
+  
+< HTTP/1.1 400 Bad Request
+< Soup-Debug-Timestamp: 1411296464
+< Soup-Debug: SoupMessage 1 (0x1aceb90)
+< Content-Type: application/json
+< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+< Pragma: no-cache
+< Expires: Fri, 01 Jan 1990 00:00:00 GMT
+< Date: Sun, 21 Sep 2014 10:47:44 GMT
+< Content-Encoding: gzip
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< X-XSS-Protection: 1; mode=block
+< Server: GSE
+< Alternate-Protocol: 443:quic,p=0.002
+< Transfer-Encoding: chunked
+< 
+< {
+<   "error" : "invalid_grant"
+< }
+  
diff --git a/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-cancellation 
b/gdata/tests/traces/oauth2-authorizer/oauth2-authorizer-request-authorization-sync-cancellation
new file mode 100644
index 0000000..e69de29
diff --git a/gdata/tests/traces/oauth2-authorizer/setup-oauth2-authorizer-data-authenticated 
b/gdata/tests/traces/oauth2-authorizer/setup-oauth2-authorizer-data-authenticated
new file mode 100644
index 0000000..b5185d6
--- /dev/null
+++ b/gdata/tests/traces/oauth2-authorizer/setup-oauth2-authorizer-data-authenticated
@@ -0,0 +1,35 @@
+> POST /o/oauth2/token HTTP/1.1
+> Soup-Debug-Timestamp: 1411296445
+> Soup-Debug: SoupSession 1 (0x18885d0), SoupMessage 1 (0x1acec80), SoupSocket 1 (0x1a435a0)
+> Host: accounts.google.com
+> Content-Type: application/x-www-form-urlencoded
+> Accept-Encoding: gzip, deflate
+> User-Agent: libgdata/0.16.1 - gzip
+> Connection: Keep-Alive
+> 
+> 
client_id=352818697630-nqu2cmt5quqd6lr17ouoqmb684u84l1f.apps.googleusercontent.com&client_secret=-fA4pHQJxR3zJ-FyAMPQsikg&code=4%2FGeYb_3HkYh4vyephp-lbvzQs1GAb.YtXAxmx-uJ0eoiIBeO6P2m9iH6kvkQI&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&grant_type=authorization_code
+  
+< HTTP/1.1 200 OK
+< Soup-Debug-Timestamp: 1411296445
+< Soup-Debug: SoupMessage 1 (0x1acec80)
+< Content-Type: application/json; charset=utf-8
+< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
+< Pragma: no-cache
+< Expires: Fri, 01 Jan 1990 00:00:00 GMT
+< Date: Sun, 21 Sep 2014 10:47:25 GMT
+< Content-Disposition: attachment; filename="json.txt"; filename*=UTF-8''json.txt
+< Content-Encoding: gzip
+< X-Content-Type-Options: nosniff
+< X-Frame-Options: SAMEORIGIN
+< X-XSS-Protection: 1; mode=block
+< Server: GSE
+< Alternate-Protocol: 443:quic,p=0.002
+< Transfer-Encoding: chunked
+< 
+< {
+<   "access_token" : "ya29.hwD5UZjOzOcQICGvnrEyGPHJ9MLwiOaooem0Bo_bDtrdWiRmFKpKvqrl",
+<   "token_type" : "Bearer",
+<   "expires_in" : 3600,
+<   "refresh_token" : "1/dOhnaCJ3B7eX23hOBDbzw0jPEptU4HdoE3LSES7B1nI"
+< }
+  


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]