Blob Blame History Raw
/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
/*
 * GData Client
 * Copyright (C) Philip Withnall 2011, 2014, 2015 <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/>.
 */

/**
 * SECTION:gdata-oauth2-authorizer
 * @short_description: GData OAuth 2.0 authorization interface
 * @stability: Stable
 * @include: gdata/gdata-oauth2-authorizer.h
 *
 * #GDataOAuth2Authorizer provides an implementation of the #GDataAuthorizer
 * interface for authentication and authorization using the
 * <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.
 *
 * The refresh token may also be accessed as
 * #GDataOAuth2Authorizer:refresh-token and saved by the application. It may
 * later be set on a new instance of #GDataOAuth2Authorizer, and
 * gdata_authorizer_refresh_authorization_async() called to establish a new
 * access token without requiring the user to re-authenticate unless they have
 * explicitly revoked the refresh token.
 *
 * 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: 0.17.0
 */

#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. refresh_token may be
	 * non-NULL if access_token is NULL and refresh_authorization() has not
	 * yet been called on this authorizer. They may be both NULL. */
	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,
	PROP_REFRESH_TOKEN,
};

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: 0.17.0
	 */
	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: 0.17.0
	 */
	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: 0.17.0
	 */
	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: 0.17.0
	 */
	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: 0.17.0
	 */
	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: 0.17.0
	 */
	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));

	/**
	 * GDataOAuth2Authorizer:refresh-token:
	 *
	 * The server provided refresh token, which can be stored and passed in
	 * to new #GDataOAuth2Authorizer instances before calling
	 * gdata_authorizer_refresh_authorization_async() to create a new
	 * short-lived access token.
	 *
	 * The refresh token is opaque data and must not be parsed.
	 *
	 * Since: 0.17.2
	 */
	g_object_class_install_property (gobject_class, PROP_REFRESH_TOKEN,
	                                 g_param_spec_string ("refresh-token",
	                                                      "Refresh Token",
	                                                      "The server provided refresh token.",
	                                                      NULL,
	                                                      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;
	case PROP_REFRESH_TOKEN:
		g_mutex_lock (&priv->mutex);
		g_value_set_string (value, priv->refresh_token);
		g_mutex_unlock (&priv->mutex);
		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;
	case PROP_REFRESH_TOKEN:
		gdata_oauth2_authorizer_set_refresh_token (self,
		                                           g_value_get_string (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: 0.17.0
 */
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: 0.17.0
 */
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: 0.17.0
 */
gchar *
gdata_oauth2_authorizer_build_authentication_uri (GDataOAuth2Authorizer *self,
                                                  const gchar *login_hint,
                                                  gboolean include_granted_scopes)
{
	GDataOAuth2AuthorizerPrivate *priv;
	GString *uri = NULL;  /* owned */
	GDataAuthorizationDomain *domain;  /* unowned */
	GHashTableIter iter;
	gboolean is_first = TRUE;

	g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL);

	priv = self->priv;

	g_mutex_lock (&priv->mutex);

	/* Build and memoise the URI.
	 *
	 * Reference: https://developers.google.com/accounts/docs/OAuth2InstalledApp#formingtheurl
	 */
	g_assert (g_hash_table_size (priv->authentication_domains) > 0);

	uri = g_string_new ("https://accounts.google.com/o/oauth2/auth"
	                    "?response_type=code"
	                    "&client_id=");
	g_string_append_uri_escaped (uri, priv->client_id, NULL, TRUE);
	g_string_append (uri, "&redirect_uri=");
	g_string_append_uri_escaped (uri, priv->redirect_uri, NULL, TRUE);
	g_string_append (uri, "&scope=");

	/* Add the scopes of all our domains */
	g_hash_table_iter_init (&iter, priv->authentication_domains);

	while (g_hash_table_iter_next (&iter, (gpointer *) &domain, NULL)) {
		const gchar *scope;

		if (!is_first) {
			/* Delimiter */
			g_string_append (uri, "%20");
		}

		scope = gdata_authorization_domain_get_scope (domain);
		g_string_append_uri_escaped (uri, scope, NULL, TRUE);

		is_first = FALSE;
	}

	if (login_hint != NULL && *login_hint != '\0') {
		g_string_append (uri, "&login_hint=");
		g_string_append_uri_escaped (uri, login_hint, NULL, TRUE);
	}

	if (priv->locale != NULL) {
		g_string_append (uri, "&hl=");
		g_string_append_uri_escaped (uri, priv->locale, NULL, TRUE);
	}

	if (include_granted_scopes) {
		g_string_append (uri, "&include_granted_scopes=true");
	} else {
		g_string_append (uri, "&include_granted_scopes=false");
	}

	g_mutex_unlock (&priv->mutex);

	return g_string_free (uri, FALSE);
}

/* NOTE: This has to be thread safe, as it can be called from
 * refresh_authorization() at any time.
 *
 * Reference: https://developers.google.com/accounts/docs/OAuth2InstalledApp#handlingtheresponse
 */
static void
parse_grant_response (GDataOAuth2Authorizer *self, guint status,
                      const gchar *reason_phrase, const gchar *response_body,
                      gssize length, GError **error)
{
	GDataOAuth2AuthorizerPrivate *priv;
	JsonParser *parser = NULL;  /* owned */
	JsonNode *root_node;  /* unowned */
	JsonObject *root_object;  /* unowned */
	const gchar *access_token = NULL, *refresh_token = NULL;
	GError *child_error = NULL;

	priv = self->priv;

	/* Parse the successful response */
	parser = json_parser_new ();

	json_parser_load_from_data (parser, response_body, length,
	                            &child_error);

	if (child_error != NULL) {
		g_clear_error (&child_error);
		g_set_error_literal (&child_error, GDATA_SERVICE_ERROR,
		                     GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
		                     _("The server returned a malformed response."));

		goto done;
	}

	/* Extract the access token, TTL and refresh token */
	root_node = json_parser_get_root (parser);

	if (JSON_NODE_HOLDS_OBJECT (root_node) == FALSE) {
		g_set_error_literal (&child_error, GDATA_SERVICE_ERROR,
		                     GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
		                     _("The server returned a malformed response."));
		goto done;
	}

	root_object = json_node_get_object (root_node);

	if (json_object_has_member (root_object, "access_token")) {
		access_token = json_object_get_string_member (root_object,
		                                              "access_token");
	}
	if (json_object_has_member (root_object, "refresh_token")) {
		refresh_token = json_object_get_string_member (root_object,
		                                               "refresh_token");
	}

	/* Always require an access token. */
	if (access_token == NULL || *access_token == '\0') {
		g_set_error_literal (&child_error, GDATA_SERVICE_ERROR,
		                     GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
		                     _("The server returned a malformed response."));

		access_token = NULL;
		refresh_token = NULL;

		goto done;
	}

	/* Only require a refresh token if this is the first authentication.
	 * See the documentation for refreshing authentication:
	 * https://developers.google.com/accounts/docs/OAuth2InstalledApp#refresh
	 */
	if ((refresh_token == NULL || *refresh_token == '\0') &&
	    priv->refresh_token == NULL) {
		g_set_error_literal (&child_error, GDATA_SERVICE_ERROR,
		                     GDATA_SERVICE_ERROR_PROTOCOL_ERROR,
		                     _("The server returned a malformed response."));

		access_token = NULL;
		refresh_token = NULL;

		goto done;
	}

done:
	/* Postconditions. */
	g_assert ((refresh_token == NULL) || (access_token != NULL));
	g_assert ((child_error != NULL) == (access_token == NULL));

	/* Update state. */
	g_mutex_lock (&priv->mutex);

	g_free (priv->access_token);
	priv->access_token = g_strdup (access_token);

	if (refresh_token != NULL) {
		g_free (priv->refresh_token);
		priv->refresh_token = g_strdup (refresh_token);
	}

	g_mutex_unlock (&priv->mutex);

	if (child_error != NULL) {
		g_propagate_error (error, child_error);
	}

	g_object_unref (parser);
}

/* NOTE: This has to be thread safe, as it can be called from
 * refresh_authorization() at any time.
 *
 * There is no reference for this, because Google apparently don’t deem it
 * necessary to document.
 *
 * Example response:
 *     HTTP/1.1 400 Bad Request
 *     Content-Type: application/json
 *
 *     {
 *       "error" : "invalid_grant"
 *     }
 */
static void
parse_grant_error (GDataOAuth2Authorizer *self, guint status,
                   const gchar *reason_phrase, const gchar *response_body,
                   gssize length, GError **error)
{
	JsonParser *parser = NULL;  /* owned */
	JsonNode *root_node;  /* unowned */
	JsonObject *root_object;  /* unowned */
	const gchar *error_code = NULL;
	GError *child_error = NULL;

	/* Parse the error response */
	parser = json_parser_new ();

	if (response_body == 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;
	}

	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: 0.17.0
 */
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 (GTask        *task,
                              gpointer      source_object,
                              gpointer      task_data,
                              GCancellable *cancellable)
{
	GDataOAuth2Authorizer *authorizer = GDATA_OAUTH2_AUTHORIZER (source_object);
	g_autoptr(GError) error = NULL;
	const gchar *authorization_code = task_data;

	if (!gdata_oauth2_authorizer_request_authorization (authorizer,
	                                                    authorization_code,
	                                                    cancellable,
	                                                    &error))
		g_task_return_error (task, g_steal_pointer (&error));
	else
		g_task_return_boolean (task, TRUE);
}

/**
 * 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: 0.17.0
 */
void
gdata_oauth2_authorizer_request_authorization_async (GDataOAuth2Authorizer *self,
                                                     const gchar *authorization_code,
                                                     GCancellable *cancellable,
                                                     GAsyncReadyCallback callback,
                                                     gpointer user_data)
{
	g_autoptr(GTask) task = NULL;

	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));

	task = g_task_new (self, cancellable, callback, user_data);
	g_task_set_source_tag (task, gdata_oauth2_authorizer_request_authorization_async);
	g_task_set_task_data (task, g_strdup (authorization_code),
	                      (GDestroyNotify) g_free);
	g_task_run_in_thread (task, request_authorization_thread);
}

/**
 * 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: 0.17.0
 */
gboolean
gdata_oauth2_authorizer_request_authorization_finish (GDataOAuth2Authorizer *self,
                                                      GAsyncResult *async_result,
                                                      GError **error)
{
	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);
	g_return_val_if_fail (g_task_is_valid (async_result, self), FALSE);
	g_return_val_if_fail (g_async_result_is_tagged (async_result, gdata_oauth2_authorizer_request_authorization_async), FALSE);

	return g_task_propagate_boolean (G_TASK (async_result), error);
}

/**
 * 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: 0.17.0
 */
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: 0.17.0
 */
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: 0.17.0
 */
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_dup_refresh_token:
 * @self: a #GDataOAuth2Authorizer
 *
 * Returns the authorizer's refresh token, #GDataOAuth2Authorizer:refresh-token,
 * as set by client code previously on the #GDataOAuth2Authorizer, or as
 * returned by the most recent authentication operation.
 *
 * Return value: (transfer full): the authorizer's refresh token
 *
 * Since: 0.17.2
 */
gchar *
gdata_oauth2_authorizer_dup_refresh_token (GDataOAuth2Authorizer *self)
{
	GDataOAuth2AuthorizerPrivate *priv;
	gchar *refresh_token;  /* owned */

	g_return_val_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self), NULL);

	priv = self->priv;

	g_mutex_lock (&priv->mutex);
	refresh_token = g_strdup (priv->refresh_token);
	g_mutex_unlock (&priv->mutex);

	return refresh_token;
}

/**
 * gdata_oauth2_authorizer_set_refresh_token:
 * @self: a #GDataOAuth2Authorizer
 * @refresh_token: (nullable): the new refresh token, or %NULL to clear
 *   authorization
 *
 * Sets the authorizer's refresh token, #GDataOAuth2Authorizer:refresh-token.
 * This is used to periodically refresh the access token. Set it to %NULL to
 * clear the current authentication from the authorizer.
 *
 * Since: 0.17.2
 */
void
gdata_oauth2_authorizer_set_refresh_token (GDataOAuth2Authorizer *self,
                                           const gchar *refresh_token)
{
	GDataOAuth2AuthorizerPrivate *priv;

	g_return_if_fail (GDATA_IS_OAUTH2_AUTHORIZER (self));

	priv = self->priv;

	g_mutex_lock (&priv->mutex);

	if (g_strcmp0 (priv->refresh_token, refresh_token) == 0) {
		g_mutex_unlock (&priv->mutex);
		return;
	}

	/* Clear the access token; if the refresh token has changed, it can
	 * no longer be valid, and we must avoid the situation:
	 *    (access_token != NULL) && (refresh_token == NULL) */
	g_free (priv->access_token);
	priv->access_token = NULL;

	/* Update the refresh token. */
	g_free (priv->refresh_token);
	priv->refresh_token = g_strdup (refresh_token);

	g_mutex_unlock (&priv->mutex);

	g_object_notify (G_OBJECT (self), "refresh-token");
}

/**
 * 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: 0.17.0
 */
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: 0.17.0
 */
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: 0.17.0
 */
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: 0.17.0
 */
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: 0.17.0
 */
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: 0.17.0
 */
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");
}