[librest: 1/2] OAuth2 Pkce Workflow




commit 6d65d8862ad7d39e55be0adb868c05f194250e74
Author: Günther Wagner <info gunibert de>
Date:   Wed Jan 12 18:26:38 2022 +0000

    OAuth2 Pkce Workflow

 .gitlab-ci.yml                                     |   3 +-
 AUTHORS                                            |   1 +
 ChangeLog                                          |   0
 INSTALL                                            | 237 ------
 Makefile.am                                        |  36 -
 NEWS                                               |   5 +
 autogen.sh                                         |  24 -
 build/Makefile.am                                  |   1 -
 build/introspection.m4                             |  94 ---
 configure.ac                                       | 161 -----
 docs/Makefile.am                                   |   1 -
 docs/librest.toml.in                               |   6 +-
 docs/meson.build                                   |  17 +-
 docs/reference/Makefile.am                         |   1 -
 docs/reference/rest/Makefile.am                    |  92 ---
 docs/reference/rest/meson.build                    |  26 -
 docs/reference/rest/rest-docs.xml                  |  51 --
 docs/reference/rest/rest-overrides.txt             |   0
 docs/reference/rest/rest-sections.txt              | 303 --------
 docs/reference/rest/rest.types                     |  12 -
 examples/Makefile.am                               |  13 -
 examples/gitlab-oauth2-example.c                   | 148 ++++
 examples/meson.build                               |   1 +
 meson.build                                        |  28 +-
 meson_options.txt                                  |   7 +-
 rest-extras/Makefile.am                            |  79 --
 rest-extras/flickr-proxy.c                         |   2 +-
 rest/Makefile.am                                   | 139 ----
 rest/meson.build                                   |  28 +-
 rest/oauth2-proxy-call.c                           |  68 --
 rest/oauth2-proxy-call.h                           |  49 --
 rest/oauth2-proxy.c                                | 397 ----------
 rest/oauth2-proxy.h                                |  95 ---
 rest/rest-oauth2-proxy-call.c                      |  63 ++
 ...h2-proxy-private.h => rest-oauth2-proxy-call.h} |  29 +-
 rest/rest-oauth2-proxy.c                           | 802 +++++++++++++++++++++
 rest/rest-oauth2-proxy.h                           | 101 +++
 rest/rest-pkce-code-challenge.c                    | 121 ++++
 rest/rest-pkce-code-challenge.h                    |  44 ++
 rest/rest-proxy-call.c                             |  18 +-
 rest/rest-proxy.c                                  |   4 +-
 rest/rest-utils.c                                  |  44 ++
 rest/rest-utils.h                                  |  27 +
 rest/rest.h                                        |  35 +
 tests/Makefile.am                                  |  18 -
 tests/flickr.c                                     |  13 +
 tests/helper/test-server.c                         | 121 ++++
 tests/helper/test-server.h                         |  32 +
 tests/meson.build                                  |   4 +-
 tests/oauth2.c                                     | 333 ++++++++-
 tests/proxy.c                                      | 381 ++++------
 51 files changed, 2118 insertions(+), 2197 deletions(-)
---
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e897e44..4ec1589 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,7 +13,7 @@ build-librest:
     - dnf update -y --nogpgcheck
     - dnf -y install --nogpgcheck redhat-rpm-config
       glib2-devel gobject-introspection-devel libxml2-devel meson ninja-build
-      libsoup-devel vala
+      libsoup-devel vala json-glib-devel git python3-jinja2 python3-toml python3-typogrify python3-pygments
   script:
     - meson _build
     - ninja -C _build
@@ -64,6 +64,7 @@ reference:
       libxslt
       libsoup-devel
       gtk-doc
+      json-glib-devel
     MESON_VERSION: "0.55.3"
     MESON_EXTRA_FLAGS: "-Dintrospection=true"
     DOCS_FLAGS: -Dgtk_doc=true
diff --git a/AUTHORS b/AUTHORS
index 1a3ad8a..c309f7e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,2 +1,3 @@
 Rob Bradford <rob linux intel com>
 Ross Burton <ross linux intel com>
+Günther Wagner <info gunibert de>
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..0c6127f
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,5 @@
+Overview of changes for 0.9
+===========================
+* new oauth2 proxy to accomplish pkce workflow with api endpoints
+* introduced meson as buildsystem
+* introduced the possibility to build librest with soup-2.4 or soup-3.0
diff --git a/docs/librest.toml.in b/docs/librest.toml.in
index 4f4c59d..1f95b0b 100644
--- a/docs/librest.toml.in
+++ b/docs/librest.toml.in
@@ -6,7 +6,7 @@ repository_url = "https://gitlab.gnome.org/GNOME/librest.git";
 authors = "Günther Wagner"
 license = "LGPL-2.1-or-later"
 description = "REST client library"
-dependencies = [ "GObject-2.0", "Gio-2.0", "Soup-2.4" ]
+dependencies = [ "GObject-2.0", "Gio-2.0", "Soup-@SOUP_VERSION@" ]
 devhelp = true
 search_index = true
 
@@ -20,10 +20,10 @@ search_index = true
   description = "GObject interfaces and objects"
   docs_url = "https://developer.gnome.org/gio/stable";
 
-  [dependencies."Soup-2.4"]
+  [dependencies."Soup-@SOUP_VERSION@"]
   name = "Soup"
   description = "HTTP library"
-  docs_url = "https://developer.gnome.org/libsoup/stable";
+  docs_url = "https://libsoup.org/libsoup-@SOUP_VERSION@/index.html";
 
 [theme]
 name = "basic"
diff --git a/docs/meson.build b/docs/meson.build
index 2eeb084..f2aeb76 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -1,5 +1,3 @@
-subdir('reference/rest')
-
 if get_option('gtk_doc') and get_option('introspection')
   dependency('gi-docgen', version: '>= 2021.6',
     fallback: ['gi-docgen', 'dummy_dep'],
@@ -11,6 +9,7 @@ if get_option('gtk_doc') and get_option('introspection')
 
   toml_conf = configuration_data()
   toml_conf.set('REST_VERSION', meson.project_version())
+  toml_conf.set('SOUP_VERSION', libsoup_api_version)
 
   rest_toml = configure_file(
     input: 'librest.toml.in',
@@ -39,5 +38,19 @@ if get_option('gtk_doc') and get_option('introspection')
     install_dir: rest_docdir,
   )
 
+  # if get_option('tests')
+  #   test('doc-check',
+  #     gidocgen,
+  #     args: [
+  #       'check',
+  #       '--config', rest_toml,
+  #       '--add-include-path=@0@'.format(meson.current_build_dir() / '../rest'),
+  #       librest_gir[0],
+  #     ],
+  #     depends: librest_gir[0],
+  #     suite: ['docs'],
+  #   )
+  # endif
+
 endif
 
diff --git a/examples/gitlab-oauth2-example.c b/examples/gitlab-oauth2-example.c
new file mode 100644
index 0000000..f18cbab
--- /dev/null
+++ b/examples/gitlab-oauth2-example.c
@@ -0,0 +1,148 @@
+/* gitlab-oauth2-example.c
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+/**
+ * This example shows the recommended PKCE authorization flow for Gitlab.
+ */
+
+#include <glib.h>
+#include <rest/rest.h>
+#include <stdio.h>
+#include <libsoup/soup.h>
+
+GMainLoop *loop;
+
+static void
+load_projects (RestProxy *proxy)
+{
+  g_autoptr(GError) error = NULL;
+  RestProxyCall *call;
+
+  call = rest_proxy_new_call (proxy);
+  rest_proxy_call_set_method (call, "GET");
+  rest_proxy_call_set_function (call, "projects/426/issues");
+  rest_proxy_call_sync (call, &error);
+
+  g_print ("%s\n", rest_proxy_call_get_payload (call));
+  g_main_loop_quit (loop);
+}
+
+static void
+gitlab_oauth2_example_refreshed_access_token (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  RestOAuth2Proxy *oauth2_proxy = (RestOAuth2Proxy *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  rest_oauth2_proxy_refresh_access_token_finish (oauth2_proxy, result, &error);
+  if (error)
+    g_error ("%s", error->message);
+
+  g_print ("Access Token: %s\n", rest_oauth2_proxy_get_access_token (oauth2_proxy));
+  g_print ("Refresh Token: %s\n", rest_oauth2_proxy_get_refresh_token (oauth2_proxy));
+  GDateTime *expiration_date = rest_oauth2_proxy_get_expiration_date (oauth2_proxy);
+  if (expiration_date)
+    g_print ("Expires in: %s\n", g_date_time_format (expiration_date, "%X %x"));
+
+  load_projects (REST_PROXY (oauth2_proxy));
+}
+
+static void
+gitlab_oauth2_example_fetched_access_token (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  RestOAuth2Proxy *oauth2_proxy = (RestOAuth2Proxy *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  rest_oauth2_proxy_fetch_access_token_finish (oauth2_proxy, result, &error);
+  if (error)
+    g_error ("%s", error->message);
+
+  g_print ("Access Token: %s\n", rest_oauth2_proxy_get_access_token (oauth2_proxy));
+  g_print ("Refresh Token: %s\n", rest_oauth2_proxy_get_refresh_token (oauth2_proxy));
+  GDateTime *expiration_date = rest_oauth2_proxy_get_expiration_date (oauth2_proxy);
+  if (expiration_date)
+    g_print ("Expires in: %s\n", g_date_time_format (expiration_date, "%X %x"));
+
+  /* refresh token */
+  rest_oauth2_proxy_refresh_access_token_async (oauth2_proxy, NULL, 
gitlab_oauth2_example_refreshed_access_token, user_data);
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  g_autofree gchar *line = NULL;
+  size_t len = 0;
+
+  gchar **environment = g_get_environ ();
+  gchar *authurl = "https://gitlab.gnome.org/oauth/authorize";;
+  gchar *tokenurl = "https://gitlab.gnome.org/oauth/token";;
+  gchar *redirecturl = "http://example.com";;
+  gchar *baseurl = "https://gitlab.gnome.org/api/v4/";;
+  const gchar *clientid = g_environ_getenv (environment, "REST_OAUTH2_CLIENT_ID");
+  if (!clientid)
+    {
+      g_print ("You have to define your Gitlab Client ID as REST_OAUTH2_CLIENT_ID environment variable\n");
+      return EXIT_SUCCESS;
+    }
+
+  const gchar *clientsecret = g_environ_getenv (environment, "REST_OAUTH2_CLIENT_SECRET");
+  if (!clientsecret)
+    {
+      g_print ("You have to define your Gitlab Client Secret as REST_OAUTH2_CLIENT_SECRET environment 
variable\n");
+      return EXIT_SUCCESS;
+    }
+  RestPkceCodeChallenge *pkce = rest_pkce_code_challenge_new_random ();
+  gchar *state = NULL;
+
+#ifdef WITH_SOUP_2
+  SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS, -1);
+#else
+  SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS);
+#endif
+
+  RestOAuth2Proxy *oauth2_proxy = rest_oauth2_proxy_new (authurl, tokenurl, redirecturl, clientid, 
clientsecret, baseurl);
+  rest_proxy_add_soup_feature (REST_PROXY (oauth2_proxy), SOUP_SESSION_FEATURE (logger));
+  const gchar *authorize_url = rest_oauth2_proxy_build_authorization_url (oauth2_proxy, 
rest_pkce_code_challenge_get_challenge (pkce), NULL, &state);
+
+  g_print ("URL to authorize: %s\n", authorize_url);
+
+  ssize_t chars = getline (&line, &len, stdin);
+  if (line[chars - 1] == '\n') {
+    line[chars - 1] = '\0';
+  }
+
+  g_print ("Got Authorization Grant: %s\n", line);
+
+  /* fetch access token */
+  rest_oauth2_proxy_fetch_access_token_async (oauth2_proxy, line, rest_pkce_code_challenge_get_verifier 
(pkce), NULL, gitlab_oauth2_example_fetched_access_token, NULL);
+
+  loop = g_main_loop_new (NULL, FALSE);
+  g_main_loop_run (loop);
+
+  return 0;
+}
diff --git a/examples/meson.build b/examples/meson.build
index 7d16dc7..375a72a 100644
--- a/examples/meson.build
+++ b/examples/meson.build
@@ -7,6 +7,7 @@ example_names = [
   'get-flickr-favorites',
   'lastfm-shout',
   'continuous-twitter',
+  'gitlab-oauth2-example',
 ]
 
 example_deps = [
diff --git a/meson.build b/meson.build
index 81c5ed1..4fd3195 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,7 @@
 project('rest', 'c',
   version: '0.9.0',
   license: 'LGPL2.1+',
-  meson_version: '>= 0.49',
+  meson_version: '>= 0.53',
 )
 
 # Versioning
@@ -57,6 +57,7 @@ endif
 glib_dep = dependency('glib-2.0', version: '>= 2.44')
 gobject_dep = dependency('gobject-2.0', version: '>= 2.44')
 libsoup_dep = dependency(libsoup_name, version: libsoup_req_version)
+libjson_glib_dep = dependency('json-glib-1.0')
 libxml_dep = dependency('libxml-2.0')
 
 # config.h
@@ -73,7 +74,9 @@ config_h_inc = include_directories('.')
 # Subdirectories
 subdir('rest')
 subdir('rest-extras')
-subdir('tests')
+if get_option('tests')
+  subdir('tests')
+endif
 if get_option('examples')
   subdir('examples')
 endif
@@ -87,7 +90,7 @@ pkgconfig.generate(librest_lib,
   filebase: librest_pkg_string,
   description: 'RESTful web api query library',
   subdirs: librest_pkg_string,
-  requires: [ glib_dep, libsoup_dep, libxml_dep, ],
+  requires: [ glib_dep, libsoup_dep, libxml_dep, libjson_glib_dep ],
   variables: [
     'apiversion=@0@'.format(librest_api_version),
   ],
@@ -98,8 +101,25 @@ pkgconfig.generate(librest_extras_lib,
   filebase: librest_extras_pkg_string,
   description: 'RESTful web api query library',
   subdirs: librest_pkg_string,
-  requires: [ glib_dep, libsoup_dep, libxml_dep, ],
+  requires: [ glib_dep, libsoup_dep, libxml_dep],
   variables: [
     'apiversion=@0@'.format(librest_api_version),
   ],
 )
+
+summary({
+    'prefix': get_option('prefix'),
+    'libdir': get_option('prefix') / get_option('libdir'),
+  },
+  section: 'Directories',
+)
+
+summary({
+    'Introspection': get_option('introspection'),
+    'Documentation': get_option('gtk_doc'),
+    'Tests': get_option('tests'),
+    'Soup 2': get_option('soup2'),
+  },
+  section: 'Build',
+  bool_yn: true,
+)
diff --git a/meson_options.txt b/meson_options.txt
index 49f7a2d..83e5d2f 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -20,7 +20,7 @@ option('examples',
 )
 option('gtk_doc',
   type: 'boolean',
-  value: false,
+  value: true,
   description: 'Build the gtk-doc reference docs',
 )
 option('soup2',
@@ -28,3 +28,8 @@ option('soup2',
   value: true,
   description: 'Whether to build with libsoup2',
 )
+option('tests',
+  type: 'boolean',
+  value: true,
+  description: 'Whether to build the tests',
+)
diff --git a/rest-extras/flickr-proxy.c b/rest-extras/flickr-proxy.c
index 7726359..91403d9 100644
--- a/rest-extras/flickr-proxy.c
+++ b/rest-extras/flickr-proxy.c
@@ -328,7 +328,7 @@ flickr_proxy_build_login_url (FlickrProxy *proxy,
                      NULL,
                      "flickr.com",
                      -1,
-                     "services/auth/",
+                     "/services/auth/",
                      query,
                      NULL);
 
diff --git a/rest/meson.build b/rest/meson.build
index 396ec83..a22742e 100644
--- a/rest/meson.build
+++ b/rest/meson.build
@@ -19,10 +19,13 @@ librest_sources = [
   'rest-main.c',
   'oauth-proxy.c',
   'oauth-proxy-call.c',
-  'oauth2-proxy.c',
-  'oauth2-proxy-call.c',
   'sha1.c',
 
+  'rest-oauth2-proxy.c',
+  'rest-oauth2-proxy-call.c',
+  'rest-pkce-code-challenge.c',
+  'rest-utils.c',
+
   librest_enums,
   librest_marshal,
 ]
@@ -30,8 +33,6 @@ librest_sources = [
 librest_headers = [
   'oauth-proxy-call.h',
   'oauth-proxy.h',
-  'oauth2-proxy-call.h',
-  'oauth2-proxy.h',
   'rest-param.h',
   'rest-params.h',
   'rest-proxy-auth.h',
@@ -39,11 +40,18 @@ librest_headers = [
   'rest-proxy.h',
   'rest-xml-node.h',
   'rest-xml-parser.h',
+
+  'rest-oauth2-proxy.h',
+  'rest-oauth2-proxy-call.h',
+  'rest-pkce-code-challenge.h',
+  'rest-utils.h',
+  'rest.h',
 ]
 
 librest_deps = [
   glib_dep,
   libsoup_dep,
+  libjson_glib_dep,
   libxml_dep,
 ]
 
@@ -65,6 +73,8 @@ install_headers(librest_headers,
   subdir: librest_pkg_string / 'rest',
 )
 
+rest_dep_sources = []
+
 # GObject Introspection
 if get_option('introspection')
   librest_gir_extra_args = [
@@ -77,17 +87,25 @@ if get_option('introspection')
   endforeach
 
   librest_gir = gnome.generate_gir(librest_lib,
-    sources: [ librest_headers, librest_enums[1] ],
+    sources: [ librest_headers, librest_sources, librest_enums[1] ],
+    dependencies: librest_deps,
     namespace: 'Rest',
+    identifier_prefix: 'Rest',
+    symbol_prefix: 'rest',
     nsversion: librest_api_version,
     includes: [ 'GObject-2.0', 'Gio-2.0', 'Soup-@0@'.format(libsoup_api_version) ],
     extra_args: librest_gir_extra_args,
     install: true,
   )
+
+  rest_dep_sources += librest_gir
 endif
 
 librest_dep = declare_dependency(
+  sources: rest_dep_sources,
   link_with: librest_lib,
+  include_directories: include_directories('..'),
+  dependencies: librest_deps,
 )
 
 # Test suite
diff --git a/rest/rest-oauth2-proxy-call.c b/rest/rest-oauth2-proxy-call.c
new file mode 100644
index 0000000..cbea6f5
--- /dev/null
+++ b/rest/rest-oauth2-proxy-call.c
@@ -0,0 +1,63 @@
+/* rest-oauth2-proxy-call.c
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "rest-oauth2-proxy-call.h"
+
+G_DEFINE_TYPE (RestOAuth2ProxyCall, rest_oauth2_proxy_call, REST_TYPE_PROXY_CALL)
+
+static gboolean
+rest_oauth2_proxy_call_prepare (RestProxyCall  *call,
+                                GError        **error)
+{
+  RestOAuth2ProxyCall *self = (RestOAuth2ProxyCall *)call;
+  RestOAuth2Proxy *proxy = NULL;
+  g_autoptr(GDateTime) now = NULL;
+  GDateTime *expiration_date = NULL;
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY_CALL (call), FALSE);
+
+  g_object_get (call, "proxy", &proxy, NULL);
+
+  now = g_date_time_new_now_local ();
+  expiration_date = rest_oauth2_proxy_get_expiration_date (proxy);
+
+  // access token expired -> refresh
+  if (g_date_time_compare (now, expiration_date) > 0)
+    {
+      g_set_error (error,
+                   REST_OAUTH2_ERROR,
+                   REST_OAUTH2_ERROR_ACCESS_TOKEN_EXPIRED,
+                   "Access token is expired");
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+rest_oauth2_proxy_call_class_init (RestOAuth2ProxyCallClass *klass)
+{
+  RestProxyCallClass *call_class = REST_PROXY_CALL_CLASS (klass);
+
+  call_class->prepare = rest_oauth2_proxy_call_prepare;
+}
+
+static void
+rest_oauth2_proxy_call_init (RestOAuth2ProxyCall *self)
+{
+}
diff --git a/rest/oauth2-proxy-private.h b/rest/rest-oauth2-proxy-call.h
similarity index 59%
rename from rest/oauth2-proxy-private.h
rename to rest/rest-oauth2-proxy-call.h
index 565c1c3..fafc61f 100644
--- a/rest/oauth2-proxy-private.h
+++ b/rest/rest-oauth2-proxy-call.h
@@ -1,10 +1,6 @@
-/*
- * librest - RESTful web services access
- * Copyright (c) 2008, 2009, 2010 Intel Corporation.
+/* rest-oauth2-proxy-call.h
  *
- * Authors: Rob Bradford <rob linux intel com>
- *          Ross Burton <ross linux intel com>
- *          Jonathon Jongsma <jonathon jongsma collabora co uk>
+ * Copyright 2021 Günther Wagner <info gunibert de>
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms and conditions of the GNU Lesser General Public License,
@@ -18,17 +14,20 @@
  * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
- *
  */
-#ifndef _OAUTH2_PROXY_PRIVATE
-#define _OAUTH2_PROXY_PRIVATE
 
-#include "oauth2-proxy.h"
+#pragma once
+
+#include <rest.h>
+
+G_BEGIN_DECLS
+
+#define REST_TYPE_OAUTH2_PROXY_CALL (rest_oauth2_proxy_call_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (RestOAuth2ProxyCall, rest_oauth2_proxy_call, REST, OAUTH2_PROXY_CALL, 
RestProxyCall)
 
-struct _OAuth2ProxyPrivate {
-  char *client_id;
-  char *auth_endpoint;
-  char *access_token;
+struct _RestOAuth2ProxyCallClass {
+  RestProxyCallClass parent_class;
 };
 
-#endif /* _OAUTH2_PROXY_PRIVATE */
+G_END_DECLS
diff --git a/rest/rest-oauth2-proxy.c b/rest/rest-oauth2-proxy.c
new file mode 100644
index 0000000..f15b589
--- /dev/null
+++ b/rest/rest-oauth2-proxy.c
@@ -0,0 +1,802 @@
+/* rest-oauth2-proxy.c
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "rest-oauth2-proxy.h"
+#include "rest-oauth2-proxy-call.h"
+#include "rest-utils.h"
+#include "rest-private.h"
+#include <json-glib/json-glib.h>
+
+typedef struct
+{
+  gchar *authurl;
+  gchar *tokenurl;
+  gchar *redirect_uri;
+  gchar *client_id;
+  gchar *client_secret;
+
+  gchar *access_token;
+  gchar *refresh_token;
+
+  GDateTime *expiration_date;
+} RestOAuth2ProxyPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (RestOAuth2Proxy, rest_oauth2_proxy, REST_TYPE_PROXY)
+
+G_DEFINE_QUARK (rest-oauth2-error-quark, rest_oauth2_error)
+
+enum {
+  PROP_0,
+  PROP_AUTH_URL,
+  PROP_TOKEN_URL,
+  PROP_REDIRECT_URI,
+  PROP_CLIENT_ID,
+  PROP_CLIENT_SECRET,
+  PROP_ACCESS_TOKEN,
+  PROP_REFRESH_TOKEN,
+  PROP_EXPIRATION_DATE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+rest_oauth2_proxy_parse_access_token (RestOAuth2Proxy *self,
+                                      GBytes          *payload,
+                                      GTask           *task)
+{
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GError) error = NULL;
+  JsonNode *root;
+  JsonObject *root_object;
+  const gchar *data;
+  gsize size;
+  gint expires_in;
+  gint created_at;
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  data = g_bytes_get_data (payload, &size);
+
+  parser = json_parser_new ();
+  json_parser_load_from_data (parser, data, size, &error);
+  if (error != NULL)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  root = json_parser_get_root (parser);
+  root_object = json_node_get_object (root);
+
+  if (json_object_has_member (root_object, "access_token"))
+    rest_oauth2_proxy_set_access_token (self, json_object_get_string_member (root_object, "access_token"));
+  if (json_object_has_member (root_object, "refresh_token"))
+    rest_oauth2_proxy_set_refresh_token (self, json_object_get_string_member (root_object, "refresh_token"));
+
+  if (json_object_has_member (root_object, "expires_in") && json_object_has_member (root_object, 
"created_at"))
+    {
+      expires_in = json_object_get_int_member (root_object, "expires_in");
+      created_at = json_object_get_int_member (root_object, "created_at");
+
+      rest_oauth2_proxy_set_expiration_date (self, g_date_time_new_from_unix_local (created_at+expires_in));
+    }
+  else if (json_object_has_member (root_object, "expires_in"))
+    {
+      g_autoptr(GDateTime) now = g_date_time_new_now_utc ();
+      expires_in = json_object_get_int_member (root_object, "expires_in");
+      rest_oauth2_proxy_set_expiration_date (self, g_date_time_add_seconds (now, expires_in));
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+RestProxyCall *
+rest_oauth2_proxy_new_call (RestProxy *proxy)
+{
+  RestOAuth2Proxy *self = (RestOAuth2Proxy *)proxy;
+  RestProxyCall *call;
+  g_autofree gchar *auth;
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  auth = g_strdup_printf ("Bearer %s", rest_oauth2_proxy_get_access_token (self));
+
+  call = g_object_new (REST_TYPE_OAUTH2_PROXY_CALL, "proxy", proxy, NULL);
+  rest_proxy_call_add_header (call, "Authorization", auth);
+
+  return call;
+}
+
+/**
+ * rest_oauth2_proxy_new:
+ *
+ * Create a new #RestOAuth2Proxy.
+ *
+ * Returns: (transfer full): a newly created #RestOAuth2Proxy
+ */
+RestOAuth2Proxy *
+rest_oauth2_proxy_new (const gchar *authurl,
+                       const gchar *tokenurl,
+                       const gchar *redirecturl,
+                       const gchar *client_id,
+                       const gchar *client_secret,
+                       const gchar *baseurl)
+{
+  return g_object_new (REST_TYPE_OAUTH2_PROXY,
+                       "url-format", baseurl,
+                       "auth-url", authurl,
+                       "token-url", tokenurl,
+                       "redirect-uri", redirecturl,
+                       "client-id", client_id,
+                       "client-secret", client_secret,
+                       NULL);
+}
+
+static void
+rest_oauth2_proxy_finalize (GObject *object)
+{
+  RestOAuth2Proxy *self = (RestOAuth2Proxy *)object;
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_clear_pointer (&priv->authurl, g_free);
+  g_clear_pointer (&priv->tokenurl, g_free);
+  g_clear_pointer (&priv->redirect_uri, g_free);
+  g_clear_pointer (&priv->client_id, g_free);
+  g_clear_pointer (&priv->client_secret, g_free);
+  g_clear_pointer (&priv->access_token, g_free);
+  g_clear_pointer (&priv->refresh_token, g_free);
+  g_clear_pointer (&priv->expiration_date, g_date_time_unref);
+
+  G_OBJECT_CLASS (rest_oauth2_proxy_parent_class)->finalize (object);
+}
+
+static void
+rest_oauth2_proxy_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  RestOAuth2Proxy *self = REST_OAUTH2_PROXY (object);
+
+  switch (prop_id)
+    {
+    case PROP_AUTH_URL:
+      g_value_set_string (value, rest_oauth2_proxy_get_auth_url (self));
+      break;
+    case PROP_TOKEN_URL:
+      g_value_set_string (value, rest_oauth2_proxy_get_token_url (self));
+      break;
+    case PROP_REDIRECT_URI:
+      g_value_set_string (value, rest_oauth2_proxy_get_redirect_uri (self));
+      break;
+    case PROP_CLIENT_ID:
+      g_value_set_string (value, rest_oauth2_proxy_get_client_id (self));
+      break;
+    case PROP_CLIENT_SECRET:
+      g_value_set_string (value, rest_oauth2_proxy_get_client_secret (self));
+      break;
+    case PROP_ACCESS_TOKEN:
+      g_value_set_string (value, rest_oauth2_proxy_get_access_token (self));
+      break;
+    case PROP_REFRESH_TOKEN:
+      g_value_set_string (value, rest_oauth2_proxy_get_refresh_token (self));
+      break;
+    case PROP_EXPIRATION_DATE:
+      g_value_set_boxed (value, rest_oauth2_proxy_get_expiration_date (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rest_oauth2_proxy_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  RestOAuth2Proxy *self = REST_OAUTH2_PROXY (object);
+
+  switch (prop_id)
+    {
+    case PROP_AUTH_URL:
+      rest_oauth2_proxy_set_auth_url (self, g_value_get_string (value));
+      break;
+    case PROP_TOKEN_URL:
+      rest_oauth2_proxy_set_token_url (self, g_value_get_string (value));
+      break;
+    case PROP_REDIRECT_URI:
+      rest_oauth2_proxy_set_redirect_uri (self, g_value_get_string (value));
+      break;
+    case PROP_CLIENT_ID:
+      rest_oauth2_proxy_set_client_id (self, g_value_get_string (value));
+      break;
+    case PROP_CLIENT_SECRET:
+      rest_oauth2_proxy_set_client_secret (self, g_value_get_string (value));
+      break;
+    case PROP_ACCESS_TOKEN:
+      rest_oauth2_proxy_set_access_token (self, g_value_get_string (value));
+      break;
+    case PROP_REFRESH_TOKEN:
+      rest_oauth2_proxy_set_refresh_token (self, g_value_get_string (value));
+      break;
+    case PROP_EXPIRATION_DATE:
+      rest_oauth2_proxy_set_expiration_date (self, g_value_get_boxed (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+rest_oauth2_proxy_class_init (RestOAuth2ProxyClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  RestOAuth2ProxyClass *oauth2_class = REST_OAUTH2_PROXY_CLASS (klass);
+  RestProxyClass *proxy_class = REST_PROXY_CLASS (klass);
+
+  object_class->finalize = rest_oauth2_proxy_finalize;
+  object_class->get_property = rest_oauth2_proxy_get_property;
+  object_class->set_property = rest_oauth2_proxy_set_property;
+  oauth2_class->parse_access_token = rest_oauth2_proxy_parse_access_token;
+  proxy_class->new_call = rest_oauth2_proxy_new_call;
+
+  properties [PROP_AUTH_URL] =
+    g_param_spec_string ("auth-url",
+                         "AuthUrl",
+                         "AuthUrl",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TOKEN_URL] =
+    g_param_spec_string ("token-url",
+                         "TokenUrl",
+                         "TokenUrl",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_REDIRECT_URI] =
+    g_param_spec_string ("redirect-uri",
+                         "RedirectUri",
+                         "RedirectUri",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CLIENT_ID] =
+    g_param_spec_string ("client-id",
+                         "ClientId",
+                         "ClientId",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CLIENT_SECRET] =
+    g_param_spec_string ("client-secret",
+                         "ClientSecret",
+                         "ClientSecret",
+                         "",
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ACCESS_TOKEN] =
+    g_param_spec_string ("access-token",
+                         "AccessToken",
+                         "AccessToken",
+                         NULL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_REFRESH_TOKEN] =
+    g_param_spec_string ("refresh-token",
+                         "RefreshToken",
+                         "RefreshToken",
+                         NULL,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_EXPIRATION_DATE] =
+    g_param_spec_boxed ("expiration-date",
+                        "ExpirationDate",
+                        "ExpirationDate",
+                        G_TYPE_DATE_TIME,
+                        (G_PARAM_READWRITE |
+                         G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+rest_oauth2_proxy_init (RestOAuth2Proxy *self)
+{
+}
+
+/**
+ * rest_oauth2_proxy_build_authorization_url:
+ * @self: a #RestOAuth2Proxy
+ * @code_challenge: the code challenge (see #RestPkceCodeChallenge)
+ * @scope: (nullable): the requesting scope of the resource
+ * @state: (out): a CRSF token which should be verified from the redirect_uri
+ *
+ *
+ * Returns: (transfer full): the authorization url which should be shown in a WebView in order to 
accept/decline the request
+ * to authorize the application
+ *
+ * Since: 0.8
+ */
+gchar *
+rest_oauth2_proxy_build_authorization_url (RestOAuth2Proxy  *self,
+                                           const gchar      *code_challenge,
+                                           const gchar      *scope,
+                                           gchar           **state)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+  g_autoptr(GHashTable) params = NULL;
+  g_autoptr(GUri) auth = NULL;
+  g_autoptr(GUri) authorization_url = NULL;
+  g_autofree gchar *params_string;
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  if (state != NULL)
+    *state = random_string (10);
+  params = g_hash_table_new (g_str_hash, g_str_equal);
+
+  g_hash_table_insert (params, "response_type", "code");
+  g_hash_table_insert (params, "client_id", priv->client_id);
+  g_hash_table_insert (params, "redirect_uri", priv->redirect_uri);
+  if (state != NULL)
+    g_hash_table_insert (params, "state", *state);
+  g_hash_table_insert (params, "code_challenge", (gchar *)code_challenge);
+  g_hash_table_insert (params, "code_challenge_method", "S256");
+  if (scope)
+    g_hash_table_insert (params, "scope", (gchar *)scope);
+
+  params_string = soup_form_encode_hash (params);
+  auth = g_uri_parse (priv->authurl, G_URI_FLAGS_NONE, NULL);
+  authorization_url = g_uri_build (G_URI_FLAGS_ENCODED,
+                                   g_uri_get_scheme (auth),
+                                   NULL,
+                                   g_uri_get_host (auth),
+                                   g_uri_get_port (auth),
+                                   g_uri_get_path (auth),
+                                   params_string,
+                                   NULL);
+  return g_uri_to_string (authorization_url);
+}
+
+static void
+rest_oauth2_proxy_fetch_access_token_cb (SoupMessage *msg,
+                                         GBytes      *body,
+                                         GError      *error,
+                                         gpointer     user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  RestOAuth2Proxy *self;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  REST_OAUTH2_PROXY_GET_CLASS (self)->parse_access_token (self, body, g_steal_pointer (&task));
+}
+
+void
+rest_oauth2_proxy_fetch_access_token_async (RestOAuth2Proxy     *self,
+                                            const gchar         *authorization_code,
+                                            const gchar         *code_verifier,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+  g_autoptr(SoupMessage) msg = NULL;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GHashTable) params = NULL;
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+  g_return_if_fail (authorization_code != NULL);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  params = g_hash_table_new (g_str_hash, g_str_equal);
+
+  g_hash_table_insert (params, "client_id", priv->client_id);
+  g_hash_table_insert (params, "grant_type", "authorization_code");
+  g_hash_table_insert (params, "code", (gchar *)authorization_code);
+  g_hash_table_insert (params, "redirect_uri", priv->redirect_uri);
+  g_hash_table_insert (params, "code_verifier", (gchar *)code_verifier);
+
+#if WITH_SOUP_2
+  msg = soup_form_request_new_from_hash (SOUP_METHOD_POST, priv->tokenurl, params);
+#else
+  msg = soup_message_new_from_encoded_form (SOUP_METHOD_POST, priv->tokenurl, soup_form_encode_hash 
(params));
+#endif
+
+  _rest_proxy_queue_message (REST_PROXY (self),
+#if WITH_SOUP_2
+                             g_steal_pointer (&msg),
+#else
+                             msg,
+#endif
+                             cancellable, rest_oauth2_proxy_fetch_access_token_cb, g_steal_pointer (&task));
+
+}
+
+/**
+ * rest_oauth2_proxy_fetch_access_token_finish:
+ * @self: an #RestOauth2Proxy
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns:
+ */
+gboolean
+rest_oauth2_proxy_fetch_access_token_finish (RestOAuth2Proxy  *self,
+                                             GAsyncResult     *result,
+                                             GError          **error)
+{
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), FALSE);
+  g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+rest_oauth2_proxy_refresh_access_token (RestOAuth2Proxy *self,
+                                        GError         **error)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+  g_autoptr(SoupMessage) msg = NULL;
+  g_autoptr(GHashTable) params = NULL;
+  g_autoptr(GTask) task = NULL;
+  GBytes *payload;
+
+  task = g_task_new (self, NULL, NULL, NULL);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), FALSE);
+
+  if (priv->refresh_token == NULL)
+    {
+      *error = g_error_new (REST_OAUTH2_ERROR,
+                            REST_OAUTH2_ERROR_NO_REFRESH_TOKEN,
+                            "No refresh token available");
+      return FALSE;
+    }
+
+  params = g_hash_table_new (g_str_hash, g_str_equal);
+
+  g_hash_table_insert (params, "client_id", priv->client_id);
+  g_hash_table_insert (params, "refresh_token", priv->refresh_token);
+  g_hash_table_insert (params, "redirect_uri", priv->redirect_uri);
+  g_hash_table_insert (params, "grant_type", "refresh_token");
+
+#if WITH_SOUP_2
+  msg = soup_form_request_new_from_hash (SOUP_METHOD_POST, priv->tokenurl, params);
+#else
+  msg = soup_message_new_from_encoded_form (SOUP_METHOD_POST, priv->tokenurl, soup_form_encode_hash 
(params));
+#endif
+  payload = _rest_proxy_send_message (REST_PROXY (self), msg, NULL, error);
+  if (error && *error)
+    {
+      return FALSE;
+    }
+
+  REST_OAUTH2_PROXY_GET_CLASS (self)->parse_access_token (self, payload, g_steal_pointer (&task));
+  return TRUE;
+}
+
+static void
+rest_oauth2_proxy_refresh_access_token_cb (SoupMessage *msg,
+                                           GBytes      *payload,
+                                           GError      *error,
+                                           gpointer     user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  RestOAuth2Proxy *self;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  REST_OAUTH2_PROXY_GET_CLASS (self)->parse_access_token (self, payload, g_steal_pointer (&task));
+}
+
+void
+rest_oauth2_proxy_refresh_access_token_async (RestOAuth2Proxy     *self,
+                                              GCancellable        *cancellable,
+                                              GAsyncReadyCallback  callback,
+                                              gpointer             user_data)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+  g_autoptr(SoupMessage) msg = NULL;
+  g_autoptr(GHashTable) params = NULL;
+  g_autoptr(GTask) task = NULL;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (priv->refresh_token == NULL)
+    {
+      g_task_return_new_error (task,
+                               REST_OAUTH2_ERROR,
+                               REST_OAUTH2_ERROR_NO_REFRESH_TOKEN,
+                               "No refresh token available");
+      return;
+    }
+
+  params = g_hash_table_new (g_str_hash, g_str_equal);
+
+  g_hash_table_insert (params, "client_id", priv->client_id);
+  g_hash_table_insert (params, "refresh_token", priv->refresh_token);
+  g_hash_table_insert (params, "redirect_uri", priv->redirect_uri);
+  g_hash_table_insert (params, "grant_type", "refresh_token");
+
+#if WITH_SOUP_2
+  msg = soup_form_request_new_from_hash (SOUP_METHOD_POST, priv->tokenurl, params);
+#else
+  msg = soup_message_new_from_encoded_form (SOUP_METHOD_POST, priv->tokenurl, soup_form_encode_hash 
(params));
+#endif
+  _rest_proxy_queue_message (REST_PROXY (self),
+#if WITH_SOUP_2
+                             g_steal_pointer (&msg),
+#else
+                             msg,
+#endif
+                             cancellable,
+                             rest_oauth2_proxy_refresh_access_token_cb,
+                             g_steal_pointer (&task));
+}
+
+/**
+ * rest_oauth2_proxy_refresh_access_token_finish:
+ * @self: an #RestOauth2Proxy
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns:
+ */
+gboolean
+rest_oauth2_proxy_refresh_access_token_finish (RestOAuth2Proxy  *self,
+                                               GAsyncResult     *result,
+                                               GError          **error)
+{
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), FALSE);
+  g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+const gchar *
+rest_oauth2_proxy_get_auth_url (RestOAuth2Proxy *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->authurl;
+}
+
+void
+rest_oauth2_proxy_set_auth_url (RestOAuth2Proxy *self,
+                                const gchar     *authurl)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->authurl, authurl) != 0)
+    {
+      g_clear_pointer (&priv->authurl, g_free);
+      priv->authurl = g_strdup (authurl);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_AUTH_URL]);
+    }
+}
+
+const gchar *
+rest_oauth2_proxy_get_token_url (RestOAuth2Proxy *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->tokenurl;
+}
+
+void
+rest_oauth2_proxy_set_token_url (RestOAuth2Proxy *self,
+                                 const gchar     *tokenurl)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->tokenurl, tokenurl) != 0)
+    {
+      g_clear_pointer (&priv->tokenurl, g_free);
+      priv->tokenurl = g_strdup (tokenurl);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TOKEN_URL]);
+    }
+}
+
+const gchar *
+rest_oauth2_proxy_get_redirect_uri (RestOAuth2Proxy *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->redirect_uri;
+}
+
+void
+rest_oauth2_proxy_set_redirect_uri (RestOAuth2Proxy *self,
+                                    const gchar     *redirect_uri)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->redirect_uri, redirect_uri) != 0)
+    {
+      g_clear_pointer (&priv->redirect_uri, g_free);
+      priv->redirect_uri = g_strdup (redirect_uri);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_REDIRECT_URI]);
+    }
+}
+
+const gchar *
+rest_oauth2_proxy_get_client_id (RestOAuth2Proxy *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->client_id;
+}
+
+void
+rest_oauth2_proxy_set_client_id (RestOAuth2Proxy *self,
+                                 const gchar     *client_id)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->client_id, client_id) != 0)
+    {
+      g_clear_pointer (&priv->client_id, g_free);
+      priv->client_id = g_strdup (client_id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CLIENT_ID]);
+    }
+}
+
+const gchar *
+rest_oauth2_proxy_get_client_secret (RestOAuth2Proxy *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->client_secret;
+}
+
+void
+rest_oauth2_proxy_set_client_secret (RestOAuth2Proxy *self,
+                                     const gchar     *client_secret)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->client_secret, client_secret) != 0)
+    {
+      g_clear_pointer (&priv->client_secret, g_free);
+      priv->client_secret = g_strdup (client_secret);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CLIENT_SECRET]);
+    }
+}
+
+const gchar *
+rest_oauth2_proxy_get_access_token (RestOAuth2Proxy *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->access_token;
+}
+
+void
+rest_oauth2_proxy_set_access_token (RestOAuth2Proxy *self,
+                                    const gchar     *access_token)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->access_token, access_token) != 0)
+    {
+      g_clear_pointer (&priv->access_token, g_free);
+      priv->access_token = g_strdup (access_token);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ACCESS_TOKEN]);
+    }
+}
+
+const gchar *
+rest_oauth2_proxy_get_refresh_token (RestOAuth2Proxy  *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->refresh_token;
+}
+
+void
+rest_oauth2_proxy_set_refresh_token (RestOAuth2Proxy *self,
+                                     const gchar     *refresh_token)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  if (g_strcmp0 (priv->refresh_token, refresh_token) != 0)
+    {
+      g_clear_pointer (&priv->refresh_token, g_free);
+      priv->refresh_token = g_strdup (refresh_token);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_REFRESH_TOKEN]);
+    }
+}
+
+GDateTime *
+rest_oauth2_proxy_get_expiration_date (RestOAuth2Proxy  *self)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+  g_return_val_if_fail (REST_IS_OAUTH2_PROXY (self), NULL);
+
+  return priv->expiration_date;
+}
+
+void
+rest_oauth2_proxy_set_expiration_date (RestOAuth2Proxy *self,
+                                       GDateTime       *expiration_date)
+{
+  RestOAuth2ProxyPrivate *priv = rest_oauth2_proxy_get_instance_private (self);
+
+  g_return_if_fail (REST_IS_OAUTH2_PROXY (self));
+
+  g_clear_pointer (&priv->expiration_date, g_date_time_unref);
+  priv->expiration_date = g_date_time_ref (expiration_date);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_EXPIRATION_DATE]);
+}
diff --git a/rest/rest-oauth2-proxy.h b/rest/rest-oauth2-proxy.h
new file mode 100644
index 0000000..dd4148e
--- /dev/null
+++ b/rest/rest-oauth2-proxy.h
@@ -0,0 +1,101 @@
+/* rest-oauth2-proxy.h
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <rest/rest-proxy.h>
+
+G_BEGIN_DECLS
+
+#define REST_TYPE_OAUTH2_PROXY (rest_oauth2_proxy_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (RestOAuth2Proxy, rest_oauth2_proxy, REST, OAUTH2_PROXY, RestProxy)
+
+struct _RestOAuth2ProxyClass
+{
+  RestProxyClass parent_class;
+
+  void (*parse_access_token) (RestOAuth2Proxy *self,
+                              GBytes          *payload,
+                              GTask           *task);
+
+  gpointer padding[8];
+};
+
+enum {
+  REST_OAUTH2_ERROR_NO_REFRESH_TOKEN,
+  REST_OAUTH2_ERROR_ACCESS_TOKEN_EXPIRED,
+};
+
+#define REST_OAUTH2_ERROR rest_oauth2_error_quark ()
+GQuark rest_oauth2_error_quark ();
+
+RestOAuth2Proxy *rest_oauth2_proxy_new                         (const gchar          *authurl,
+                                                                const gchar          *tokenurl,
+                                                                const gchar          *redirecturl,
+                                                                const gchar          *client_id,
+                                                                const gchar          *client_secret,
+                                                                const gchar          *baseurl);
+gchar           *rest_oauth2_proxy_build_authorization_url     (RestOAuth2Proxy      *self,
+                                                                const gchar          *code_challenge,
+                                                                const gchar          *scope,
+                                                                gchar               **state);
+void             rest_oauth2_proxy_fetch_access_token_async    (RestOAuth2Proxy      *self,
+                                                                const gchar          *authorization_code,
+                                                                const gchar          *code_verifier,
+                                                                GCancellable         *cancellable,
+                                                                GAsyncReadyCallback   callback,
+                                                                gpointer              user_data);
+gboolean         rest_oauth2_proxy_fetch_access_token_finish   (RestOAuth2Proxy      *self,
+                                                                GAsyncResult         *result,
+                                                                GError              **error);
+gboolean         rest_oauth2_proxy_refresh_access_token        (RestOAuth2Proxy      *self,
+                                                                GError              **error);
+void             rest_oauth2_proxy_refresh_access_token_async  (RestOAuth2Proxy      *self,
+                                                                GCancellable         *cancellable,
+                                                                GAsyncReadyCallback   callback,
+                                                                gpointer              user_data);
+gboolean         rest_oauth2_proxy_refresh_access_token_finish (RestOAuth2Proxy      *self,
+                                                                GAsyncResult         *result,
+                                                                GError              **error);
+const gchar     *rest_oauth2_proxy_get_auth_url                (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_auth_url                (RestOAuth2Proxy      *self,
+                                                                const gchar          *tokenurl);
+const gchar     *rest_oauth2_proxy_get_token_url               (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_token_url               (RestOAuth2Proxy      *self,
+                                                                const gchar          *tokenurl);
+const gchar     *rest_oauth2_proxy_get_redirect_uri            (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_redirect_uri            (RestOAuth2Proxy      *self,
+                                                                const gchar          *redirect_uri);
+const gchar     *rest_oauth2_proxy_get_client_id               (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_client_id               (RestOAuth2Proxy      *self,
+                                                                const gchar          *client_id);
+const gchar     *rest_oauth2_proxy_get_client_secret           (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_client_secret           (RestOAuth2Proxy      *self,
+                                                                const gchar          *client_secret);
+const gchar     *rest_oauth2_proxy_get_access_token            (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_access_token            (RestOAuth2Proxy      *self,
+                                                                const gchar          *access_token);
+const gchar     *rest_oauth2_proxy_get_refresh_token           (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_refresh_token           (RestOAuth2Proxy      *self,
+                                                                const gchar          *refresh_token);
+GDateTime       *rest_oauth2_proxy_get_expiration_date         (RestOAuth2Proxy      *self);
+void             rest_oauth2_proxy_set_expiration_date         (RestOAuth2Proxy      *self,
+                                                                GDateTime            *expiration_date);
+
+G_END_DECLS
diff --git a/rest/rest-pkce-code-challenge.c b/rest/rest-pkce-code-challenge.c
new file mode 100644
index 0000000..0d936f8
--- /dev/null
+++ b/rest/rest-pkce-code-challenge.c
@@ -0,0 +1,121 @@
+/* rest-pkce-code-challenge.c
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "rest-pkce-code-challenge.h"
+#include "rest-utils.h"
+
+G_DEFINE_BOXED_TYPE (RestPkceCodeChallenge, rest_pkce_code_challenge, rest_pkce_code_challenge_copy, 
rest_pkce_code_challenge_free)
+
+struct _RestPkceCodeChallenge
+{
+  gchar *code_verifier;
+  gchar *code_challenge;
+};
+
+/**
+ * rest_pkce_code_challenge_new_random:
+ *
+ * Creates a new #RestPkceCodeChallenge.
+ *
+ * Returns: (transfer full): A newly created #RestPkceCodeChallenge
+ */
+RestPkceCodeChallenge *
+rest_pkce_code_challenge_new_random (void)
+{
+  RestPkceCodeChallenge *self;
+  gint length = g_random_int_range (43, 128);
+  gsize digest_len = 200;
+  guchar code_verifier_sha256[200];
+  GChecksum *sha256 = g_checksum_new (G_CHECKSUM_SHA256);
+
+  self = g_slice_new0 (RestPkceCodeChallenge);
+  self->code_verifier = random_string (length);
+  g_checksum_update (sha256, (guchar *)self->code_verifier, -1);
+  g_checksum_get_digest (sha256, (guchar *)&code_verifier_sha256, &digest_len);
+
+  self->code_challenge = g_base64_encode (code_verifier_sha256, digest_len);
+  g_strdelimit (self->code_challenge, "=", '\0');
+  g_strdelimit (self->code_challenge, "+", '-');
+  g_strdelimit (self->code_challenge, "/", '_');
+
+  return self;
+}
+
+/**
+ * rest_pkce_code_challenge_copy:
+ * @self: a #RestPkceCodeChallenge
+ *
+ * Makes a deep copy of a #RestPkceCodeChallenge.
+ *
+ * Returns: (transfer full): A newly created #RestPkceCodeChallenge with the same
+ *   contents as @self
+ */
+RestPkceCodeChallenge *
+rest_pkce_code_challenge_copy (RestPkceCodeChallenge *self)
+{
+  RestPkceCodeChallenge *copy;
+
+  g_return_val_if_fail (self, NULL);
+
+  copy = g_slice_new0 (RestPkceCodeChallenge);
+  copy->code_verifier = self->code_verifier;
+  copy->code_challenge = self->code_challenge;
+
+  return copy;
+}
+
+/**
+ * rest_pkce_code_challenge_free:
+ * @self: a #RestPkceCodeChallenge
+ *
+ * Frees a #RestPkceCodeChallenge allocated using rest_pkce_code_challenge_new()
+ * or rest_pkce_code_challenge_copy().
+ */
+void
+rest_pkce_code_challenge_free (RestPkceCodeChallenge *self)
+{
+  g_return_if_fail (self);
+
+  g_slice_free (RestPkceCodeChallenge, self);
+}
+
+/**
+ * rest_pkce_code_challenge_get_challenge:
+ *
+ * Returns the Code Challenge for the Pkce verification.
+ *
+ * Returns: the code_challenge
+ */
+const gchar *
+rest_pkce_code_challenge_get_challenge (RestPkceCodeChallenge *self)
+{
+  return self->code_challenge;
+}
+
+/**
+ * rest_pkce_code_challenge_get_verifier:
+ *
+ * Returns the Code Verifier for the Pkce verification.
+ *
+ * Returns: the code_verifier
+ */
+const gchar *
+rest_pkce_code_challenge_get_verifier (RestPkceCodeChallenge *self)
+{
+  return self->code_verifier;
+}
diff --git a/rest/rest-pkce-code-challenge.h b/rest/rest-pkce-code-challenge.h
new file mode 100644
index 0000000..9ce3907
--- /dev/null
+++ b/rest/rest-pkce-code-challenge.h
@@ -0,0 +1,44 @@
+/* rest-pkce-code-challenge.h
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define REST_TYPE_PKCE_CODE_CHALLENGE (rest_pkce_code_challenge_get_type ())
+
+/**
+ * RestPkceCodeChallenge:
+ *
+ * In order to play a Pkce Code Verification during a OAuth2 authorization
+ * you need this structure which handles the algorithmic part.
+ */
+typedef struct _RestPkceCodeChallenge RestPkceCodeChallenge;
+
+GType                  rest_pkce_code_challenge_get_type      (void) G_GNUC_CONST;
+RestPkceCodeChallenge *rest_pkce_code_challenge_new_random    (void);
+RestPkceCodeChallenge *rest_pkce_code_challenge_copy          (RestPkceCodeChallenge *self);
+void                   rest_pkce_code_challenge_free          (RestPkceCodeChallenge *self);
+const gchar           *rest_pkce_code_challenge_get_challenge (RestPkceCodeChallenge *self);
+const gchar           *rest_pkce_code_challenge_get_verifier  (RestPkceCodeChallenge *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (RestPkceCodeChallenge, rest_pkce_code_challenge_free)
+
+G_END_DECLS
diff --git a/rest/rest-proxy-call.c b/rest/rest-proxy-call.c
index 62b00da..24d952d 100644
--- a/rest/rest-proxy-call.c
+++ b/rest/rest-proxy-call.c
@@ -238,6 +238,8 @@ rest_proxy_call_set_method (RestProxyCall *call,
  * @call: The #RestProxyCall
  *
  * Get the HTTP method to use when making the call, for example GET or POST.
+ *
+ * Returns: (transfer none): the HTTP method
  */
 const char *
 rest_proxy_call_get_method (RestProxyCall *call)
@@ -1005,19 +1007,23 @@ _call_message_call_completed_cb (SoupMessage *message,
                                  GError      *error,
                                  gpointer     user_data)
 {
-  GTask *task = user_data;
+  g_autoptr(GTask) task = user_data;
   RestProxyCall *call;
 
   call = REST_PROXY_CALL (g_task_get_source_object (task));
 
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
   finish_call (call, message, payload, &error);
 
   if (error != NULL)
     g_task_return_error (task, error);
   else
     g_task_return_boolean (task, TRUE);
-
-  g_object_unref (task);
 }
 
 /**
@@ -1158,6 +1164,8 @@ _continuous_call_message_sent_cb (GObject      *source,
  *
  * You may unref the call after calling this function since there is an
  * internal reference, or you may unref in the callback.
+ *
+ * Returns: %TRUE on success
  */
 gboolean
 rest_proxy_call_continuous (RestProxyCall                    *call,
@@ -1491,11 +1499,7 @@ rest_proxy_call_get_payload_length (RestProxyCall *call)
   g_return_val_if_fail (REST_IS_PROXY_CALL (call), 0);
 
   payload = GET_PRIVATE (call)->payload;
-#ifdef WITH_SOUP_2
-  return payload ? g_bytes_get_size (payload) - 1 : 0;
-#else
   return payload ? g_bytes_get_size (payload) : 0;
-#endif
 }
 
 /**
diff --git a/rest/rest-proxy.c b/rest/rest-proxy.c
index 171f6cb..8231b6f 100644
--- a/rest/rest-proxy.c
+++ b/rest/rest-proxy.c
@@ -763,7 +763,7 @@ message_finished_cb (SoupSession *session,
   GError *error = NULL;
 
   body = g_bytes_new (message->response_body->data,
-                      message->response_body->length + 1);
+                      message->response_body->length);
   data->callback (message, body, error, data->user_data);
   g_free (data);
 }
@@ -894,7 +894,7 @@ _rest_proxy_send_message (RestProxy    *proxy,
 #ifdef WITH_SOUP_2
   soup_session_send_message (priv->session, message);
   body = g_bytes_new (message->response_body->data,
-                      message->response_body->length + 1);
+                      message->response_body->length);
 #else
   body = soup_session_send_and_read (priv->session,
                                      message,
diff --git a/rest/rest-utils.c b/rest/rest-utils.c
new file mode 100644
index 0000000..939d49e
--- /dev/null
+++ b/rest/rest-utils.c
@@ -0,0 +1,44 @@
+/* rest-utils.c
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "rest-utils.h"
+
+/**
+ * random_string:
+ * @length: the length of the random string
+ *
+ * Creates a random string from a given alphabeth with length @length
+ *
+ * Returns: (transfer full): a random string
+ */
+gchar *
+random_string (guint length)
+{
+  g_autoptr(GRand) rand = g_rand_new ();
+  gchar *buffer = g_slice_alloc0 (sizeof (gchar) * length + 1);
+  gchar alphabeth[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
+
+  for (guint i = 0; i < length; i++)
+    {
+      buffer[i] = alphabeth[g_rand_int (rand) % (sizeof (alphabeth) - 1)];
+    }
+  buffer[length] = '\0';
+
+  return buffer;
+}
+
diff --git a/rest/rest-utils.h b/rest/rest-utils.h
new file mode 100644
index 0000000..cd61145
--- /dev/null
+++ b/rest/rest-utils.h
@@ -0,0 +1,27 @@
+/* rest-utils.h
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+gchar *random_string (guint length);
+
+G_END_DECLS
diff --git a/rest/rest.h b/rest/rest.h
new file mode 100644
index 0000000..9bc964c
--- /dev/null
+++ b/rest/rest.h
@@ -0,0 +1,35 @@
+/* rest.h
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This file 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 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define REST_INSIDE
+# include "rest-proxy.h"
+# include "rest-proxy-call.h"
+# include "rest-oauth2-proxy.h"
+# include "rest-utils.h"
+# include "rest-pkce-code-challenge.h"
+#undef REST_INSIDE
+
+G_END_DECLS
diff --git a/tests/flickr.c b/tests/flickr.c
index 11301fa..2d1ee99 100644
--- a/tests/flickr.c
+++ b/tests/flickr.c
@@ -108,6 +108,18 @@ test_flickr ()
 
 }
 
+static void
+test_build_login_url (void)
+{
+  RestProxy *p = flickr_proxy_new ("api", "secret");
+  g_autofree gchar *login_url = flickr_proxy_build_login_url (FLICKR_PROXY (p), NULL, "read");
+
+  g_assert_cmpstr (login_url, ==, 
"http://flickr.com/services/auth/?api_key=api&perms=read&api_sig=55e7647bc1a6e512172b8fda472a64a8";);
+
+  login_url = flickr_proxy_build_login_url (FLICKR_PROXY (p), "746563215463214621", "read");
+
+  g_assert_cmpstr (login_url, ==, 
"http://flickr.com/services/auth/?frob=746563215463214621&api_key=api&perms=read&api_sig=bcabfd22f3beb489aeb3605b8c9e0441";);
+}
 
 int
 main (int argc, char **argv)
@@ -115,6 +127,7 @@ main (int argc, char **argv)
   g_test_init (&argc, &argv, NULL);
 
   g_test_add_func ("/flickr/flickr", test_flickr);
+  g_test_add_func ("/flickr/test_build_login_url", test_build_login_url);
 
   return g_test_run ();
 }
diff --git a/tests/helper/test-server.c b/tests/helper/test-server.c
new file mode 100644
index 0000000..1ddcb67
--- /dev/null
+++ b/tests/helper/test-server.c
@@ -0,0 +1,121 @@
+/* test-server.c
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "test-server.h"
+
+static GMutex server_start_mutex;
+static GCond server_start_cond;
+
+SoupServer *
+test_server_new ()
+{
+  SoupServer *server = soup_server_new ("tls-certificate", NULL, NULL);
+  return server;
+}
+
+gchar *
+test_server_get_uri (SoupServer *server,
+                     const char *scheme,
+                     const char *host)
+{
+       GSList *uris, *u;
+#ifdef WITH_SOUP_2
+  SoupURI *uri, *ret_uri = NULL;
+#else
+       GUri *uri, *ret_uri = NULL;
+#endif
+
+       uris = soup_server_get_uris (server);
+       for (u = uris; u; u = u->next) {
+               uri = u->data;
+
+#ifdef WITH_SOUP_2
+               if (scheme && strcmp (soup_uri_get_scheme (uri), scheme) != 0)
+                       continue;
+               if (host && strcmp (soup_uri_get_host (uri), host) != 0)
+                       continue;
+
+               ret_uri = soup_uri_copy (uri);
+#else
+               if (scheme && strcmp (g_uri_get_scheme (uri), scheme) != 0)
+                       continue;
+               if (host && strcmp (g_uri_get_host (uri), host) != 0)
+                       continue;
+
+               ret_uri = g_uri_ref (uri);
+#endif
+               break;
+       }
+
+#ifdef WITH_SOUP_2
+       g_slist_free_full (uris, (GDestroyNotify)soup_uri_free);
+  return soup_uri_to_string (ret_uri, FALSE);
+#else
+       g_slist_free_full (uris, (GDestroyNotify)g_uri_unref);
+  return g_uri_to_string (ret_uri);
+#endif
+}
+
+static gpointer
+run_server_thread (gpointer user_data)
+{
+       SoupServer *server = user_data;
+       GMainContext *context;
+       GMainLoop *loop;
+  GError *error = NULL;
+
+       context = g_main_context_new ();
+       g_main_context_push_thread_default (context);
+       loop = g_main_loop_new (context, FALSE);
+       g_object_set_data (G_OBJECT (server), "GMainLoop", loop);
+
+       // TODO: error handling
+       soup_server_listen_local (server, 0, 0, &error);
+  if (error != NULL)
+    g_error ("%s", error->message);
+
+       g_mutex_lock (&server_start_mutex);
+       g_cond_signal (&server_start_cond);
+       g_mutex_unlock (&server_start_mutex);
+
+       g_main_loop_run (loop);
+       g_main_loop_unref (loop);
+
+  g_print("%s\n", "Shutting down server...");
+
+       soup_server_disconnect (server);
+
+       g_main_context_pop_thread_default (context);
+       g_main_context_unref (context);
+
+       return NULL;
+}
+
+void
+test_server_run_in_thread (SoupServer *server)
+{
+       GThread *thread;
+
+       g_mutex_lock (&server_start_mutex);
+
+       thread = g_thread_new ("server_thread", run_server_thread, server);
+       g_cond_wait (&server_start_cond, &server_start_mutex);
+       g_mutex_unlock (&server_start_mutex);
+
+       g_object_set_data (G_OBJECT (server), "thread", thread);
+}
diff --git a/tests/helper/test-server.h b/tests/helper/test-server.h
new file mode 100644
index 0000000..ee25600
--- /dev/null
+++ b/tests/helper/test-server.h
@@ -0,0 +1,32 @@
+/* test-server.h
+ *
+ * Copyright 2021 Günther Wagner <info gunibert de>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms and conditions of the GNU Lesser General Public License,
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <libsoup/soup.h>
+
+G_BEGIN_DECLS
+
+SoupServer *test_server_new           (void);
+void        test_server_run_in_thread (SoupServer *server);
+gchar      *test_server_get_uri       (SoupServer *server,
+                                       const char *scheme,
+                                       const char *host);
+
+G_END_DECLS
diff --git a/tests/meson.build b/tests/meson.build
index 0fe80e8..c7c9170 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -3,9 +3,9 @@ test_suites = {
     'proxy',
     'proxy-continuous',
     'threaded',
-    'oauth2',
     'xml',
     'custom-serialize',
+    'oauth2',
   ],
   'rest-extras': [
     'flickr',
@@ -23,7 +23,7 @@ test_deps = [
 foreach suite, test_names : test_suites
   foreach name : test_names
     test_bin = executable(name,
-      '@0@.c'.format(name),
+      ['@0@.c'.format(name), 'helper/test-server.c'],
       dependencies: test_deps,
       include_directories: config_h_inc,
     )
diff --git a/tests/oauth2.c b/tests/oauth2.c
index 12721bc..a9b6e9d 100644
--- a/tests/oauth2.c
+++ b/tests/oauth2.c
@@ -1,8 +1,6 @@
-/*
- * librest - RESTful web services access
- * Copyright (c) 2010 Intel Corporation.
+/* oauth2.c
  *
- * Authors: Jonathon Jongsma <jonathon jongsma collabora co uk>
+ * Copyright 2021 Günther Wagner <info gunibert de>
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms and conditions of the GNU Lesser General Public License,
@@ -16,54 +14,333 @@
  * You should have received a copy of the GNU Lesser General Public License
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
- *
  */
 
-#include <rest/oauth2-proxy.h>
-#include <string.h>
+#include <glib.h>
+#include "rest/rest.h"
+#include "helper/test-server.h"
 
+#ifdef WITH_SOUP_2
+static void
+server_callback (SoupServer        *server,
+                 SoupMessage       *msg,
+                 const gchar       *path,
+                 GHashTable        *query,
+                 SoupClientContext *client,
+                 gpointer           user_data)
+#else
 static void
-test_url_no_fragment ()
+server_callback (SoupServer        *server,
+                 SoupServerMessage *msg,
+                 const gchar       *path,
+                 GHashTable        *query,
+                 gpointer           user_data)
+#endif
 {
-  char *token = oauth2_proxy_extract_access_token ("http://example.com";);
+  if (g_strcmp0 (path, "/token") == 0)
+    {
+      gchar *json = "{"
+           "\"access_token\":\"2YotnFZFEjr1zCsicMWpAA\","
+           "\"token_type\":\"example\","
+           "\"expires_in\":3600,"
+           "\"refresh_token\":\"tGzv3JOkF0XG5Qx2TlKWIA\","
+           "\"example_parameter\":\"example_value\""
+         "}";
+#ifdef WITH_SOUP_2
+      soup_message_set_status (msg, SOUP_STATUS_OK);
+      soup_message_set_response (msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
+#else
+      soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
+      soup_server_message_set_response (msg, "application/json", SOUP_MEMORY_COPY, json, strlen(json));
+#endif
 
-  g_assert_null (token);
+      return;
+    }
+  else if (g_strcmp0 (path, "/api/bearer") == 0)
+    {
+#ifdef WITH_SOUP_2
+      const gchar *authorization = soup_message_headers_get_one (msg->request_headers, "Authorization");
+      soup_message_set_status (msg, SOUP_STATUS_OK);
+      soup_message_set_response (msg, "text/plain", SOUP_MEMORY_COPY, authorization, strlen(authorization));
+#else
+      SoupMessageHeaders *headers = soup_server_message_get_request_headers (msg);
+      const gchar *authorization = soup_message_headers_get_one (headers, "Authorization");
+      soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
+      soup_server_message_set_response (msg, "text/plain", SOUP_MEMORY_COPY, authorization, 
strlen(authorization));
+#endif
+      return;
+    }
+  else if (g_strcmp0 (path, "/api/invalid") == 0)
+    {
+      gchar *resp = "{"
+           "\"error\":\"invalid_grant\""
+         "}";
+#ifdef WITH_SOUP_2
+      soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
+      soup_message_set_response (msg, "application/json", SOUP_MEMORY_COPY, resp, strlen(resp));
+#else
+      soup_server_message_set_status (msg, SOUP_STATUS_BAD_REQUEST, NULL);
+      soup_server_message_set_response (msg, "application/json", SOUP_MEMORY_COPY, resp, strlen(resp));
+#endif
+      return;
+    }
 }
 
 static void
-test_url_fragment_no_access_token ()
+test_authorization_url (gconstpointer url)
 {
-  char *token = oauth2_proxy_extract_access_token ("http://example.com/foo?foo=1#bar";);
+  g_autoptr(RestProxy) proxy = REST_PROXY (rest_oauth2_proxy_new ("http://www.example.com/auth";,
+                                                                  "http://www.example.com/token";,
+                                                                  "http://www.example.com";,
+                                                                  "client-id",
+                                                                  "client-secret",
+                                                                  "http://www.example.com/api";));
+  g_autoptr(RestPkceCodeChallenge) pkce = rest_pkce_code_challenge_new_random ();
 
-  g_assert_null (token);
-  g_free (token);
+  gchar *authorization_url = rest_oauth2_proxy_build_authorization_url (REST_OAUTH2_PROXY (proxy),
+                                             rest_pkce_code_challenge_get_challenge (pkce),
+                                             NULL,
+                                             NULL);
+  g_autofree gchar *expected = g_strdup_printf 
("http://www.example.com/auth?code_challenge_method=S256&redirect_uri=http%%3A%%2F%%2Fwww.example.com&client_id=client-id&code_challenge=%s&response_type=code";,
 rest_pkce_code_challenge_get_challenge (pkce));
+  g_assert_cmpstr (authorization_url, ==, expected);
 }
 
 static void
-test_access_token_simple ()
+test_fetch_access_token_finished (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
 {
-  char *token = oauth2_proxy_extract_access_token 
("http://example.com/foo?foo=1#access_token=1234567890_12.34561abcdefg&bar";);
+  g_autoptr(GError) error = NULL;
+  gboolean *finished = user_data;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
 
-  g_assert_cmpstr (token, ==, "1234567890_12.34561abcdefg");
-  g_free (token);
+  rest_oauth2_proxy_fetch_access_token_finish (REST_OAUTH2_PROXY (object), result, &error);
+  g_assert_no_error (error);
+
+  *finished = TRUE;
 }
 
-static void test_url_encoding_access_token ()
+static void
+test_fetch_access_token (gconstpointer url)
 {
-  char *token = oauth2_proxy_extract_access_token 
("http://example.com/foo?foo=1#access_token=1234567890%5F12%2E34561abcdefg&bar";);
+  GMainContext *async_context = g_main_context_ref_thread_default ();
+  g_autoptr(GError) error = NULL;
+
+  g_autofree gchar *tokenurl = g_strdup_printf ("%stoken", (gchar *)url);
+  g_autofree gchar *baseurl = g_strdup_printf ("%sapi", (gchar *)url);
+
+  gboolean finished = FALSE;
+  g_autoptr(RestProxy) proxy = REST_PROXY (rest_oauth2_proxy_new ("http://www.example.com/auth";,
+                                                                  tokenurl,
+                                                                  "http://www.example.com";,
+                                                                  "client-id",
+                                                                  "client-secret",
+                                                                  baseurl));
+  rest_oauth2_proxy_fetch_access_token_async (REST_OAUTH2_PROXY (proxy),
+                                              "1234567890",
+                                              "code_verifier",
+                                              NULL,
+                                              test_fetch_access_token_finished,
+                                              &finished);
+  while (!finished) {
+    g_main_context_iteration (async_context, TRUE);
+  }
+
+  g_assert_cmpstr ("2YotnFZFEjr1zCsicMWpAA", ==, rest_oauth2_proxy_get_access_token (REST_OAUTH2_PROXY 
(proxy)));
+  g_assert_cmpstr ("tGzv3JOkF0XG5Qx2TlKWIA", ==, rest_oauth2_proxy_get_refresh_token (REST_OAUTH2_PROXY 
(proxy)));
+
+  g_autoptr(RestProxyCall) call = rest_proxy_new_call (proxy);
+  rest_proxy_call_set_method (call, "GET");
+  rest_proxy_call_set_function (call, "bearer");
+  rest_proxy_call_sync (call, &error);
+  g_assert_no_error (error);
+  g_autofree gchar *payload = g_strndup (rest_proxy_call_get_payload (call), 
rest_proxy_call_get_payload_length (call));
+  g_assert_cmpstr ("Bearer 2YotnFZFEjr1zCsicMWpAA", ==, payload);
 
-  g_assert_cmpstr (token, ==, "1234567890_12.34561abcdefg");
-  g_free (token);
+  g_main_context_unref (async_context);
 }
 
-int
-main (int argc, char **argv)
+static void
+test_refresh_access_token_finished_error (GObject      *object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+  gboolean *finished = user_data;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  rest_oauth2_proxy_refresh_access_token_finish (REST_OAUTH2_PROXY (object), result, &error);
+  g_assert_error (error, REST_OAUTH2_ERROR, REST_OAUTH2_ERROR_NO_REFRESH_TOKEN);
+
+  *finished = TRUE;
+}
+
+static void
+test_refresh_access_token_finished (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
 {
+  g_autoptr(GError) error = NULL;
+  gboolean *finished = user_data;
+
+  g_assert (G_IS_OBJECT (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  rest_oauth2_proxy_refresh_access_token_finish (REST_OAUTH2_PROXY (object), result, &error);
+  g_assert_no_error (error);
+
+  *finished = TRUE;
+}
+
+static void
+test_refresh_access_token (gconstpointer url)
+{
+  GMainContext *async_context = g_main_context_ref_thread_default ();
+
+  g_autofree gchar *tokenurl = g_strdup_printf ("%stoken", (gchar *)url);
+  g_autofree gchar *baseurl = g_strdup_printf ("%sapi", (gchar *)url);
+
+  gboolean finished = FALSE;
+  g_autoptr(RestProxy) proxy = REST_PROXY (rest_oauth2_proxy_new ("http://www.example.com/auth";,
+                                                                  tokenurl,
+                                                                  "http://www.example.com";,
+                                                                  "client-id",
+                                                                  "client-secret",
+                                                                  baseurl));
+
+  // Test error if no refresh token is set (note assertion happens in callback)
+  rest_oauth2_proxy_refresh_access_token_async (REST_OAUTH2_PROXY (proxy), NULL, 
test_refresh_access_token_finished_error, &finished);
+
+  while (!finished) {
+    g_main_context_iteration (async_context, TRUE);
+  }
+
+  // Test if refresh token gets refreshed
+  rest_oauth2_proxy_set_refresh_token (REST_OAUTH2_PROXY (proxy), "refresh_token");
+  rest_oauth2_proxy_refresh_access_token_async (REST_OAUTH2_PROXY (proxy), NULL, 
test_refresh_access_token_finished, &finished);
+
+  finished = FALSE;
+
+  while (!finished) {
+    g_main_context_iteration (async_context, TRUE);
+  }
+
+  g_assert_cmpstr ("2YotnFZFEjr1zCsicMWpAA", ==, rest_oauth2_proxy_get_access_token (REST_OAUTH2_PROXY 
(proxy)));
+  g_assert_cmpstr ("tGzv3JOkF0XG5Qx2TlKWIA", ==, rest_oauth2_proxy_get_refresh_token (REST_OAUTH2_PROXY 
(proxy)));
+
+  g_main_context_unref (async_context);
+}
+
+static void
+test_refresh_access_token_sync (gconstpointer url)
+{
+  g_autoptr(GError) error = NULL;
+
+  g_autofree gchar *tokenurl = g_strdup_printf ("%stoken", (gchar *)url);
+  g_autofree gchar *baseurl = g_strdup_printf ("%sapi", (gchar *)url);
+
+  g_autoptr(RestProxy) proxy = REST_PROXY (rest_oauth2_proxy_new ("http://www.example.com/auth";,
+                                                                  tokenurl,
+                                                                  "http://www.example.com";,
+                                                                  "client-id",
+                                                                  "client-secret",
+                                                                  baseurl));
+
+  rest_oauth2_proxy_refresh_access_token (REST_OAUTH2_PROXY (proxy), &error);
+  g_assert_error (error, REST_OAUTH2_ERROR, REST_OAUTH2_ERROR_NO_REFRESH_TOKEN);
+  error = NULL;
+
+  rest_oauth2_proxy_set_refresh_token (REST_OAUTH2_PROXY (proxy), "refresh_token");
+  rest_oauth2_proxy_refresh_access_token (REST_OAUTH2_PROXY (proxy), &error);
+
+  g_assert_cmpstr ("2YotnFZFEjr1zCsicMWpAA", ==, rest_oauth2_proxy_get_access_token (REST_OAUTH2_PROXY 
(proxy)));
+  g_assert_cmpstr ("tGzv3JOkF0XG5Qx2TlKWIA", ==, rest_oauth2_proxy_get_refresh_token (REST_OAUTH2_PROXY 
(proxy)));
+
+  rest_oauth2_proxy_set_refresh_token (REST_OAUTH2_PROXY (proxy), "refresh_token");
+  rest_oauth2_proxy_refresh_access_token (REST_OAUTH2_PROXY (proxy), NULL);
+
+  g_assert_cmpstr ("2YotnFZFEjr1zCsicMWpAA", ==, rest_oauth2_proxy_get_access_token (REST_OAUTH2_PROXY 
(proxy)));
+  g_assert_cmpstr ("tGzv3JOkF0XG5Qx2TlKWIA", ==, rest_oauth2_proxy_get_refresh_token (REST_OAUTH2_PROXY 
(proxy)));
+}
+
+static void
+test_access_token_expired (gconstpointer url)
+{
+  g_autofree gchar *tokenurl = g_strdup_printf ("%stoken", (gchar *)url);
+  g_autofree gchar *baseurl = g_strdup_printf ("%sapi", (gchar *)url);
+  g_autoptr(GError) error = NULL;
+
+  g_autoptr(RestProxy) proxy = REST_PROXY (rest_oauth2_proxy_new ("http://www.example.com/auth";,
+                                                                  tokenurl,
+                                                                  "http://www.example.com";,
+                                                                  "client-id",
+                                                                  "client-secret",
+                                                                  baseurl));
+  GDateTime *now = g_date_time_new_now_local ();
+  rest_oauth2_proxy_set_expiration_date (REST_OAUTH2_PROXY (proxy), now);
+
+  g_autoptr(RestProxyCall) call = rest_proxy_new_call (proxy);
+  rest_proxy_call_set_method (call, "GET");
+  rest_proxy_call_set_function (call, "/expired");
+  rest_proxy_call_sync (call, &error);
+  g_assert_error (error, REST_OAUTH2_ERROR, REST_OAUTH2_ERROR_ACCESS_TOKEN_EXPIRED);
+}
+
+static void
+test_access_token_invalid (gconstpointer url)
+{
+  GMainContext *async_context = g_main_context_ref_thread_default ();
+  g_autofree gchar *tokenurl = g_strdup_printf ("%stoken", (gchar *)url);
+  g_autofree gchar *baseurl = g_strdup_printf ("%sapi", (gchar *)url);
+  g_autoptr(GError) error = NULL;
+  gboolean finished = FALSE;
+
+  g_autoptr(RestProxy) proxy = REST_PROXY (rest_oauth2_proxy_new ("http://www.example.com/auth";,
+                                                                  tokenurl,
+                                                                  "http://www.example.com";,
+                                                                  "client-id",
+                                                                  "client-secret",
+                                                                  baseurl));
+
+  rest_oauth2_proxy_fetch_access_token_async (REST_OAUTH2_PROXY (proxy), "1234567890", "code_verifier", 
NULL, test_fetch_access_token_finished, &finished);
+  while (!finished) {
+    g_main_context_iteration (async_context, TRUE);
+  }
+
+  g_autoptr(RestProxyCall) call = rest_proxy_new_call (proxy);
+  rest_proxy_call_set_method (call, "GET");
+  rest_proxy_call_set_function (call, "/invalid");
+  rest_proxy_call_sync (call, &error);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, SOUP_STATUS_BAD_REQUEST);
+
+
+  g_main_context_unref (async_context);
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  SoupServer *server;
+  g_autofree gchar *url;
+
   g_test_init (&argc, &argv, NULL);
-  g_test_add_func ("/oauth2/url-no-fragment", test_url_no_fragment);
-  g_test_add_func ("/oauth2/url-fragment-no-access-token", test_url_fragment_no_access_token);
-  g_test_add_func ("/oauth2/access-token-simple", test_access_token_simple);
-  g_test_add_func ("/oauth2/access-token-url-encoding", test_url_encoding_access_token);
+
+  server = test_server_new ();
+  soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
+  test_server_run_in_thread (server);
+  url = test_server_get_uri (server, "http", NULL);
+
+  g_test_add_data_func ("/oauth2/authorization_url", url, test_authorization_url);
+  g_test_add_data_func ("/oauth2/fetch_access_token", url, test_fetch_access_token);
+  g_test_add_data_func ("/oauth2/refresh_access_token", url, test_refresh_access_token);
+  g_test_add_data_func ("/oauth2/refresh_access_token_sync", url, test_refresh_access_token_sync);
+  g_test_add_data_func ("/oauth2/access_token_expired", url, test_access_token_expired);
+  g_test_add_data_func ("/oauth2/access_token_invalid", url, test_access_token_invalid);
 
   return g_test_run ();
 }
diff --git a/tests/proxy.c b/tests/proxy.c
index d6c05a5..fc77869 100644
--- a/tests/proxy.c
+++ b/tests/proxy.c
@@ -35,49 +35,98 @@
 #include <stdlib.h>
 #include <libsoup/soup.h>
 #include <rest/rest-proxy.h>
+#include "helper/test-server.h"
 
 #if SOUP_CHECK_VERSION (2, 28, 0)
 /* Avoid deprecation warning with newer libsoup */
 #define soup_message_headers_get soup_message_headers_get_one
 #endif
 
-#define PORT 8080
-
-static int errors = 0;
-
-SoupServer *server;
-GMainLoop *server_loop;
-
-static void
 #ifdef WITH_SOUP_2
-server_callback (SoupServer *server, SoupMessage *msg,
-                 const char *path, GHashTable *query,
-                 SoupClientContext *client, gpointer user_data)
-#else
-server_callback (SoupServer *server, SoupServerMessage *msg,
-                 const char *path, GHashTable *query, gpointer user_data)
-#endif
+static void
+server_callback (SoupServer        *server,
+                 SoupMessage       *msg,
+                 const gchar       *path,
+                 GHashTable        *query,
+                 SoupClientContext *client,
+                 gpointer           user_data)
 {
-  if (g_str_equal (path, "/ping")) {
-#ifdef WITH_SOUP_2
+  if (g_str_equal (path, "/ping") && g_strcmp0 ("GET", msg->method) == 0) {
     soup_message_set_status (msg, SOUP_STATUS_OK);
-#else
-    soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
-#endif
+  }
+  else if (g_str_equal (path, "/ping") && g_strcmp0 ("POST", msg->method) == 0) {
+    soup_message_set_status (msg, SOUP_STATUS_NOT_FOUND);
   }
   else if (g_str_equal (path, "/echo")) {
     const char *value;
 
     value = g_hash_table_lookup (query, "value");
-#ifdef WITH_SOUP_2
     soup_message_set_response (msg, "text/plain", SOUP_MEMORY_COPY,
                                value, strlen (value));
     soup_message_set_status (msg, SOUP_STATUS_OK);
+  }
+  else if (g_str_equal (path, "/reverse")) {
+    char *value;
+
+    value = g_strdup (g_hash_table_lookup (query, "value"));
+    g_strreverse (value);
+    soup_message_set_response (msg, "text/plain", SOUP_MEMORY_TAKE,
+                               value, strlen (value));
+    soup_message_set_status (msg, SOUP_STATUS_OK);
+  }
+  else if (g_str_equal (path, "/status")) {
+    const char *value;
+    int status;
+
+    value = g_hash_table_lookup (query, "status");
+    if (value) {
+      status = atoi (value);
+      soup_message_set_status (msg, status ?: SOUP_STATUS_INTERNAL_SERVER_ERROR);
+    } else {
+      soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
+    }
+  }
+  else if (g_str_equal (path, "/useragent/none")) {
+    SoupMessageHeaders *request_headers = msg->request_headers;
+
+    if (soup_message_headers_get (request_headers, "User-Agent") == NULL) {
+      soup_message_set_status (msg, SOUP_STATUS_OK);
+    } else {
+      soup_message_set_status (msg, SOUP_STATUS_EXPECTATION_FAILED);
+    }
+  }
+  else if (g_str_equal (path, "/useragent/testsuite")) {
+    SoupMessageHeaders *request_headers = msg->request_headers;
+    const char *value;
+    value = soup_message_headers_get (request_headers, "User-Agent");
+    if (g_strcmp0 (value, "TestSuite-1.0") == 0) {
+      soup_message_set_status (msg, SOUP_STATUS_OK);
+    } else {
+      soup_message_set_status (msg, SOUP_STATUS_EXPECTATION_FAILED);
+    }
+  }
+}
 #else
+static void
+server_callback (SoupServer        *server,
+                 SoupServerMessage *msg,
+                 const char        *path,
+                 GHashTable        *query,
+                 gpointer           user_data)
+{
+  if (g_str_equal (path, "/ping") && g_strcmp0 ("GET", soup_server_message_get_method (msg)) == 0) {
+    soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
+  }
+  else if (g_str_equal (path, "/ping") && g_strcmp0 ("POST", soup_server_message_get_method (msg)) == 0) {
+    soup_server_message_set_status (msg, SOUP_STATUS_NOT_FOUND, NULL);
+  }
+  else if (g_str_equal (path, "/echo")) {
+    const char *value;
+
+    value = g_hash_table_lookup (query, "value");
     soup_server_message_set_response (msg, "text/plain", SOUP_MEMORY_COPY,
                                       value, strlen (value));
     soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
-#endif
   }
   else if (g_str_equal (path, "/reverse")) {
     char *value;
@@ -85,15 +134,9 @@ server_callback (SoupServer *server, SoupServerMessage *msg,
     value = g_strdup (g_hash_table_lookup (query, "value"));
     g_strreverse (value);
 
-#ifdef WITH_SOUP_2
-    soup_message_set_response (msg, "text/plain", SOUP_MEMORY_TAKE,
-                               value, strlen (value));
-    soup_message_set_status (msg, SOUP_STATUS_OK);
-#else
     soup_server_message_set_response (msg, "text/plain", SOUP_MEMORY_TAKE,
                                        value, strlen (value));
     soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
-#endif
   }
   else if (g_str_equal (path, "/status")) {
     const char *value;
@@ -102,304 +145,184 @@ server_callback (SoupServer *server, SoupServerMessage *msg,
     value = g_hash_table_lookup (query, "status");
     if (value) {
       status = atoi (value);
-#ifdef WITH_SOUP_2
-      soup_message_set_status (msg, status ?: SOUP_STATUS_INTERNAL_SERVER_ERROR);
-#else
       soup_server_message_set_status (msg, status ?: SOUP_STATUS_INTERNAL_SERVER_ERROR, NULL);
-#endif
     } else {
-#ifdef WITH_SOUP_2
-      soup_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR);
-#else
       soup_server_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR, NULL);
-#endif
     }
   }
   else if (g_str_equal (path, "/useragent/none")) {
-#ifdef WITH_SOUP_2
-    SoupMessageHeaders *request_headers = msg->request_headers;
-#else
     SoupMessageHeaders *request_headers = soup_server_message_get_request_headers (msg);
-#endif
 
     if (soup_message_headers_get (request_headers, "User-Agent") == NULL) {
-#ifdef WITH_SOUP_2
-      soup_message_set_status (msg, SOUP_STATUS_OK);
-#else
       soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
-#endif
     } else {
-#ifdef WITH_SOUP_2
-      soup_message_set_status (msg, SOUP_STATUS_EXPECTATION_FAILED);
-#else
       soup_server_message_set_status (msg, SOUP_STATUS_EXPECTATION_FAILED, NULL);
-#endif
     }
   }
   else if (g_str_equal (path, "/useragent/testsuite")) {
-#ifdef WITH_SOUP_2
-    SoupMessageHeaders *request_headers = msg->request_headers;
-#else
     SoupMessageHeaders *request_headers = soup_server_message_get_request_headers (msg);
-#endif
     const char *value;
     value = soup_message_headers_get (request_headers, "User-Agent");
     if (g_strcmp0 (value, "TestSuite-1.0") == 0) {
-#ifdef WITH_SOUP_2
-      soup_message_set_status (msg, SOUP_STATUS_OK);
-#else
       soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
-#endif
     } else {
-#ifdef WITH_SOUP_2
-      soup_message_set_status (msg, SOUP_STATUS_EXPECTATION_FAILED);
-#else
       soup_server_message_set_status (msg, SOUP_STATUS_EXPECTATION_FAILED, NULL);
-#endif
-
     }
   }
 }
+#endif
+
 
 static void
 ping_test (RestProxy *proxy)
 {
-  RestProxyCall *call;
+  g_autoptr(RestProxyCall) call;
   GError *error = NULL;
 
   call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_function (call, "ping");
-
-  if (!rest_proxy_call_sync (call, &error)) {
-    g_printerr ("2: Call failed: %s\n", error->message);
-    g_error_free (error);
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_assert(error == NULL);
-
-  if (rest_proxy_call_get_status_code (call) != SOUP_STATUS_OK) {
-    g_printerr ("wrong response code\n");
-    errors++;
-    return;
-  }
-
-  if (rest_proxy_call_get_payload_length (call) != 0) {
-    g_printerr ("wrong length returned\n");
-    errors++;
-    return;
-  }
+  rest_proxy_call_set_method (call, "GET");
+  rest_proxy_call_sync (call, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, SOUP_STATUS_OK);
+  g_assert_cmpint (rest_proxy_call_get_payload_length (call), ==, 0);
 
   g_object_unref (call);
+  call = rest_proxy_new_call (proxy);
+  rest_proxy_call_set_function (call, "ping");
+  rest_proxy_call_set_method (call, "POST");
+  rest_proxy_call_sync (call, &error);
+  g_assert_error (error, REST_PROXY_ERROR, 404);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, SOUP_STATUS_NOT_FOUND);
 }
 
 static void
 echo_test (RestProxy *proxy)
 {
-  RestProxyCall *call;
+  g_autoptr(RestProxyCall) call;
   GError *error = NULL;
 
   call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_function (call, "echo");
   rest_proxy_call_add_param (call, "value", "echome");
-
-  if (!rest_proxy_call_sync (call, &error)) {
-    g_printerr ("3: Call failed: %s\n", error->message);
-    g_error_free (error);
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_assert(error == NULL);
-
-  if (rest_proxy_call_get_status_code (call) != SOUP_STATUS_OK) {
-    g_printerr ("wrong response code\n");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  if (rest_proxy_call_get_payload_length (call) != 6) {
-    g_printerr ("wrong length returned\n");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  if (g_strcmp0 ("echome", rest_proxy_call_get_payload (call)) != 0) {
-    g_printerr ("wrong string returned\n");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_object_unref (call);
+  rest_proxy_call_sync (call, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, SOUP_STATUS_OK);
+  g_assert_cmpint (rest_proxy_call_get_payload_length (call), ==, 6);
+  g_assert_cmpstr (rest_proxy_call_get_payload (call), ==, "echome");
 }
 
 static void
 reverse_test (RestProxy *proxy)
 {
-  RestProxyCall *call;
+  g_autoptr(RestProxyCall) call;
   GError *error = NULL;
 
   call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_function (call, "reverse");
   rest_proxy_call_add_param (call, "value", "reverseme");
-
-  if (!rest_proxy_call_sync (call, &error)) {
-    g_printerr ("4: Call failed: %s\n", error->message);
-    g_error_free (error);
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_assert(error == NULL);
-
-  if (rest_proxy_call_get_status_code (call) != SOUP_STATUS_OK) {
-    g_printerr ("wrong response code\n");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  if (rest_proxy_call_get_payload_length (call) != 9) {
-    g_printerr ("wrong length returned\n");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  if (g_strcmp0 ("emesrever", rest_proxy_call_get_payload (call)) != 0) {
-    g_printerr ("wrong string returned\n");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_object_unref (call);
+  rest_proxy_call_sync (call, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, SOUP_STATUS_OK);
+  g_assert_cmpint (rest_proxy_call_get_payload_length (call), ==, 9);
+  g_assert_cmpstr (rest_proxy_call_get_payload (call), ==, "emesrever");
 }
 
 static void
 status_ok_test (RestProxy *proxy, guint status)
 {
-  RestProxyCall *call;
+  g_autoptr(RestProxyCall) call;
+  g_autofree gchar *status_str;
   GError *error = NULL;
-  char *status_str;
 
   call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_function (call, "status");
   status_str = g_strdup_printf ("%d", status);
   rest_proxy_call_add_param (call, "status", status_str);
-  g_free (status_str);
-
-  if (!rest_proxy_call_sync (call, &error)) {
-    g_printerr ("1: Call failed: %s\n", error->message);
-    g_error_free (error);
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_assert(error == NULL);
-
-  if (rest_proxy_call_get_status_code (call) != status) {
-    g_printerr ("wrong response code, got %d\n", rest_proxy_call_get_status_code (call));
-    errors++;
-    return;
-  }
+  rest_proxy_call_sync (call, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, status);
+}
 
-  g_object_unref (call);
+static void
+status_test (RestProxy *proxy)
+{
+  status_ok_test (proxy, SOUP_STATUS_OK);
+  status_ok_test (proxy, SOUP_STATUS_NO_CONTENT);
 }
 
 static void
 status_error_test (RestProxy *proxy, guint status)
 {
-  RestProxyCall *call;
-  GError *error = NULL;
-  char *status_str;
+  g_autoptr(RestProxyCall) call;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *status_str;
 
   call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_function (call, "status");
   status_str = g_strdup_printf ("%d", status);
   rest_proxy_call_add_param (call, "status", status_str);
-  g_free (status_str);
-
-  if (rest_proxy_call_sync (call, &error)) {
-    g_printerr ("Call succeeded should have failed");
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_error_free (error);
-
-  if (rest_proxy_call_get_status_code (call) != status) {
-    g_printerr ("wrong response code, got %d\n", rest_proxy_call_get_status_code (call));
-    errors++;
-    return;
-  }
+  rest_proxy_call_sync (call, &error);
+  g_assert_error (error, REST_PROXY_ERROR, status);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, status);
+}
 
-  g_object_unref (call);
+static void
+status_test_error (RestProxy *proxy)
+{
+  status_error_test (proxy, SOUP_STATUS_BAD_REQUEST);
+  status_error_test (proxy, SOUP_STATUS_NOT_IMPLEMENTED);
 }
 
 static void
 test_status_ok (RestProxy *proxy, const char *function)
 {
-  RestProxyCall *call;
-  GError *error = NULL;
+  g_autoptr(RestProxyCall) call;
+  g_autoptr(GError) error = NULL;
 
   call = rest_proxy_new_call (proxy);
   rest_proxy_call_set_function (call, function);
-
-  if (!rest_proxy_call_sync (call, &error)) {
-    g_printerr ("%s call failed: %s\n", function, error->message);
-    g_error_free (error);
-    errors++;
-    g_object_unref (call);
-    return;
-  }
-  g_assert(error == NULL);
-
-  if (rest_proxy_call_get_status_code (call) != SOUP_STATUS_OK) {
-    g_printerr ("wrong response code, got %d\n", rest_proxy_call_get_status_code (call));
-    errors++;
-    return;
-  }
-
-  g_object_unref (call);
+  rest_proxy_call_sync (call, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (rest_proxy_call_get_status_code (call), ==, SOUP_STATUS_OK);
 }
 
-static void *
-server_thread_func (gpointer data)
+static void
+test_user_agent (RestProxy *proxy)
 {
-  server_loop = g_main_loop_new (NULL, TRUE);
-  /*SoupServer *server = soup_server_new (NULL);*/
-  soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
-
-  soup_server_listen_local (server, PORT, 0, NULL);
-  g_main_loop_run (server_loop);
-
-  return NULL;
+  test_status_ok (proxy, "useragent/none");
+  rest_proxy_set_user_agent (proxy, "TestSuite-1.0");
+  test_status_ok (proxy, "useragent/testsuite");
 }
 
 int
-main (int argc, char **argv)
+main (int     argc,
+      gchar **argv)
 {
-  char *url;
   RestProxy *proxy;
+  SoupServer *server;
+  gint ret;
+  gchar *uri;
 
-  server = soup_server_new (NULL);
-  g_thread_new ("Server Thread", server_thread_func, NULL);
+  g_test_init (&argc, &argv, NULL);
 
-  url = g_strdup_printf ("http://127.0.0.1:%d/";, PORT);
-  proxy = rest_proxy_new (url, FALSE);
-  g_free (url);
+  server = test_server_new ();
+  soup_server_add_handler (server, NULL, server_callback, NULL, NULL);
+  test_server_run_in_thread (server);
+  uri = test_server_get_uri (server, "http", NULL);
 
-  ping_test (proxy);
-  echo_test (proxy);
-  reverse_test (proxy);
-  status_ok_test (proxy, SOUP_STATUS_OK);
-  status_ok_test (proxy, SOUP_STATUS_NO_CONTENT);
-  status_error_test (proxy, SOUP_STATUS_BAD_REQUEST);
-  status_error_test (proxy, SOUP_STATUS_NOT_IMPLEMENTED);
+  proxy = rest_proxy_new (uri, FALSE);
 
-  test_status_ok (proxy, "useragent/none");
-  rest_proxy_set_user_agent (proxy, "TestSuite-1.0");
-  test_status_ok (proxy, "useragent/testsuite");
+  g_test_add_data_func ("/proxy/ping", proxy, ping_test);
+  g_test_add_data_func ("/proxy/echo", proxy, echo_test);
+  g_test_add_data_func ("/proxy/reverse", proxy, reverse_test);
+  g_test_add_data_func ("/proxy/status_ok_test", proxy, status_test);
+  g_test_add_data_func ("/proxy/status_error_test", proxy, status_test_error);
+  g_test_add_data_func ("/proxy/user_agent", proxy, test_user_agent);
+
+  ret = g_test_run ();
+
+       g_main_context_unref (g_main_context_default ());
 
-  g_main_loop_quit (server_loop);
-  return errors != 0;
+  return ret;
 }


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