Add support for OAUTHBEARER SASL mechanism
authorDaniel Gustafsson <[email protected]>
Thu, 20 Feb 2025 15:25:17 +0000 (16:25 +0100)
committerDaniel Gustafsson <[email protected]>
Thu, 20 Feb 2025 15:25:17 +0000 (16:25 +0100)
This commit implements OAUTHBEARER, RFC 7628, and OAuth 2.0 Device
Authorization Grants, RFC 8628.  In order to use this there is a
new pg_hba auth method called oauth.  When speaking to a OAuth-
enabled server, it looks a bit like this:

  $ psql 'host=example.org oauth_issuer=... oauth_client_id=...'
  Visit https://oauth.example.org/login and enter the code: FPQ2-M4BG

Device authorization is currently the only supported flow so the
OAuth issuer must support that in order for users to authenticate.
Third-party clients may however extend this and provide their own
flows.  The built-in device authorization flow is currently not
supported on Windows.

In order for validation to happen server side a new framework for
plugging in OAuth validation modules is added.  As validation is
implementation specific, with no default specified in the standard,
PostgreSQL does not ship with one built-in.  Each pg_hba entry can
specify a specific validator or be left blank for the validator
installed as default.

This adds a requirement on libcurl for the client side support,
which is optional to build, but the server side has no additional
build requirements.  In order to run the tests, Python is required
as this adds a https server written in Python.  Tests are gated
behind PG_TEST_EXTRA as they open ports.

This patch has been a multi-year project with many contributors
involved with reviews and in-depth discussions:  Michael Paquier,
Heikki Linnakangas, Zhihong Yu, Mahendrakar Srinivasarao, Andrey
Chudnovsky and Stephen Frost to name a few.  While Jacob Champion
is the main author there have been some levels of hacking by others.
Daniel Gustafsson contributed the validation module and various bits
and pieces; Thomas Munro wrote the client side support for kqueue.

Author: Jacob Champion <[email protected]>
Co-authored-by: Daniel Gustafsson <[email protected]>
Co-authored-by: Thomas Munro <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Reviewed-by: Peter Eisentraut <[email protected]>
Reviewed-by: Antonin Houska <[email protected]>
Reviewed-by: Kashif Zeeshan <[email protected]>
Discussion: https://postgr.es/m/d1b467a78e0e36ed85a09adf979d04cf124a9d4b[email protected]

60 files changed:
.cirrus.tasks.yml
config/programs.m4
configure
configure.ac
doc/src/sgml/client-auth.sgml
doc/src/sgml/config.sgml
doc/src/sgml/filelist.sgml
doc/src/sgml/installation.sgml
doc/src/sgml/libpq.sgml
doc/src/sgml/oauth-validators.sgml [new file with mode: 0644]
doc/src/sgml/postgres.sgml
doc/src/sgml/protocol.sgml
doc/src/sgml/regress.sgml
meson.build
meson_options.txt
src/Makefile.global.in
src/backend/libpq/Makefile
src/backend/libpq/auth-oauth.c [new file with mode: 0644]
src/backend/libpq/auth.c
src/backend/libpq/hba.c
src/backend/libpq/meson.build
src/backend/libpq/pg_hba.conf.sample
src/backend/utils/adt/hbafuncs.c
src/backend/utils/misc/guc_tables.c
src/backend/utils/misc/postgresql.conf.sample
src/include/common/oauth-common.h [new file with mode: 0644]
src/include/libpq/auth.h
src/include/libpq/hba.h
src/include/libpq/oauth.h [new file with mode: 0644]
src/include/pg_config.h.in
src/interfaces/libpq/Makefile
src/interfaces/libpq/exports.txt
src/interfaces/libpq/fe-auth-oauth-curl.c [new file with mode: 0644]
src/interfaces/libpq/fe-auth-oauth.c [new file with mode: 0644]
src/interfaces/libpq/fe-auth-oauth.h [new file with mode: 0644]
src/interfaces/libpq/fe-auth.c
src/interfaces/libpq/fe-auth.h
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/libpq-fe.h
src/interfaces/libpq/libpq-int.h
src/interfaces/libpq/meson.build
src/makefiles/meson.build
src/test/authentication/t/001_password.pl
src/test/modules/Makefile
src/test/modules/meson.build
src/test/modules/oauth_validator/.gitignore [new file with mode: 0644]
src/test/modules/oauth_validator/Makefile [new file with mode: 0644]
src/test/modules/oauth_validator/README [new file with mode: 0644]
src/test/modules/oauth_validator/fail_validator.c [new file with mode: 0644]
src/test/modules/oauth_validator/magic_validator.c [new file with mode: 0644]
src/test/modules/oauth_validator/meson.build [new file with mode: 0644]
src/test/modules/oauth_validator/oauth_hook_client.c [new file with mode: 0644]
src/test/modules/oauth_validator/t/001_server.pl [new file with mode: 0644]
src/test/modules/oauth_validator/t/002_client.pl [new file with mode: 0644]
src/test/modules/oauth_validator/t/OAuth/Server.pm [new file with mode: 0644]
src/test/modules/oauth_validator/t/oauth_server.py [new file with mode: 0755]
src/test/modules/oauth_validator/validator.c [new file with mode: 0644]
src/test/perl/PostgreSQL/Test/Cluster.pm
src/tools/pgindent/pgindent
src/tools/pgindent/typedefs.list

index fffa438cec1703a2eea496fb6b177aa06977c979..2f5f5ef21a85ac3195a0ebe4ad968685ca71a55a 100644 (file)
@@ -23,7 +23,7 @@ env:
   MTEST_ARGS: --print-errorlogs --no-rebuild -C build
   PGCTLTIMEOUT: 120 # avoids spurious failures during parallel tests
   TEMP_CONFIG: ${CIRRUS_WORKING_DIR}/src/tools/ci/pg_ci_base.conf
-  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance
+  PG_TEST_EXTRA: kerberos ldap ssl libpq_encryption load_balance oauth
 
 
 # What files to preserve in case tests fail
@@ -167,7 +167,7 @@ task:
     chown root:postgres /tmp/cores
     sysctl kern.corefile='/tmp/cores/%N.%P.core'
   setup_additional_packages_script: |
-    #pkg install -y ...
+    pkg install -y curl
 
   # NB: Intentionally build without -Dllvm. The freebsd image size is already
   # large enough to make VM startup slow, and even without llvm freebsd
@@ -329,6 +329,7 @@ LINUX_CONFIGURE_FEATURES: &LINUX_CONFIGURE_FEATURES >-
   --with-gssapi
   --with-icu
   --with-ldap
+  --with-libcurl
   --with-libxml
   --with-libxslt
   --with-llvm
@@ -422,8 +423,10 @@ task:
     EOF
 
   setup_additional_packages_script: |
-    #apt-get update
-    #DEBIAN_FRONTEND=noninteractive apt-get -y install ...
+    apt-get update
+    DEBIAN_FRONTEND=noninteractive apt-get -y install \
+      libcurl4-openssl-dev \
+      libcurl4-openssl-dev:i386 \
 
   matrix:
     - name: Linux - Debian Bookworm - Autoconf
@@ -799,8 +802,8 @@ task:
     folder: $CCACHE_DIR
 
   setup_additional_packages_script: |
-    #apt-get update
-    #DEBIAN_FRONTEND=noninteractive apt-get -y install ...
+    apt-get update
+    DEBIAN_FRONTEND=noninteractive apt-get -y install libcurl4-openssl-dev
 
   ###
   # Test that code can be built with gcc/clang without warnings
index 7b55c2664a6cd529b647e4f8c39597aaf0a0c842..061b13376acceb325bb1a1d3c973683132daf787 100644 (file)
@@ -274,3 +274,68 @@ AC_DEFUN([PGAC_CHECK_STRIP],
   AC_SUBST(STRIP_STATIC_LIB)
   AC_SUBST(STRIP_SHARED_LIB)
 ])# PGAC_CHECK_STRIP
+
+
+
+# PGAC_CHECK_LIBCURL
+# ------------------
+# Check for required libraries and headers, and test to see whether the current
+# installation of libcurl is thread-safe.
+
+AC_DEFUN([PGAC_CHECK_LIBCURL],
+[
+  AC_CHECK_HEADER(curl/curl.h, [],
+                 [AC_MSG_ERROR([header file <curl/curl.h> is required for --with-libcurl])])
+  AC_CHECK_LIB(curl, curl_multi_init, [],
+              [AC_MSG_ERROR([library 'curl' does not provide curl_multi_init])])
+
+  # Check to see whether the current platform supports threadsafe Curl
+  # initialization.
+  AC_CACHE_CHECK([for curl_global_init thread safety], [pgac_cv__libcurl_threadsafe_init],
+  [AC_RUN_IFELSE([AC_LANG_PROGRAM([
+#include <curl/curl.h>
+],[
+    curl_version_info_data *info;
+
+    if (curl_global_init(CURL_GLOBAL_ALL))
+        return -1;
+
+    info = curl_version_info(CURLVERSION_NOW);
+#ifdef CURL_VERSION_THREADSAFE
+    if (info->features & CURL_VERSION_THREADSAFE)
+        return 0;
+#endif
+
+    return 1;
+])],
+  [pgac_cv__libcurl_threadsafe_init=yes],
+  [pgac_cv__libcurl_threadsafe_init=no],
+  [pgac_cv__libcurl_threadsafe_init=unknown])])
+  if test x"$pgac_cv__libcurl_threadsafe_init" = xyes ; then
+    AC_DEFINE(HAVE_THREADSAFE_CURL_GLOBAL_INIT, 1,
+              [Define to 1 if curl_global_init() is guaranteed to be thread-safe.])
+  fi
+
+  # Warn if a thread-friendly DNS resolver isn't built.
+  AC_CACHE_CHECK([for curl support for asynchronous DNS], [pgac_cv__libcurl_async_dns],
+  [AC_RUN_IFELSE([AC_LANG_PROGRAM([
+#include <curl/curl.h>
+],[
+    curl_version_info_data *info;
+
+    if (curl_global_init(CURL_GLOBAL_ALL))
+        return -1;
+
+    info = curl_version_info(CURLVERSION_NOW);
+    return (info->features & CURL_VERSION_ASYNCHDNS) ? 0 : 1;
+])],
+  [pgac_cv__libcurl_async_dns=yes],
+  [pgac_cv__libcurl_async_dns=no],
+  [pgac_cv__libcurl_async_dns=unknown])])
+  if test x"$pgac_cv__libcurl_async_dns" != xyes ; then
+    AC_MSG_WARN([
+*** The installed version of libcurl does not support asynchronous DNS
+*** lookups. Connection timeouts will not be honored during DNS resolution,
+*** which may lead to hangs in client programs.])
+  fi
+])# PGAC_CHECK_LIBCURL
index 0ffcaeb436753dd6b5c381cc138e232592125fa4..93fddd699810ece1c193bab94160e0a398e5a451 100755 (executable)
--- a/configure
+++ b/configure
@@ -708,6 +708,9 @@ XML2_LIBS
 XML2_CFLAGS
 XML2_CONFIG
 with_libxml
+LIBCURL_LIBS
+LIBCURL_CFLAGS
+with_libcurl
 with_uuid
 with_readline
 with_systemd
@@ -864,6 +867,7 @@ with_readline
 with_libedit_preferred
 with_uuid
 with_ossp_uuid
+with_libcurl
 with_libxml
 with_libxslt
 with_system_tzdata
@@ -894,6 +898,8 @@ PKG_CONFIG_PATH
 PKG_CONFIG_LIBDIR
 ICU_CFLAGS
 ICU_LIBS
+LIBCURL_CFLAGS
+LIBCURL_LIBS
 XML2_CONFIG
 XML2_CFLAGS
 XML2_LIBS
@@ -1574,6 +1580,7 @@ Optional Packages:
                           prefer BSD Libedit over GNU Readline
   --with-uuid=LIB         build contrib/uuid-ossp using LIB (bsd,e2fs,ossp)
   --with-ossp-uuid        obsolete spelling of --with-uuid=ossp
+  --with-libcurl          build with libcurl support
   --with-libxml           build with XML support
   --with-libxslt          use XSLT support when building contrib/xml2
   --with-system-tzdata=DIR
@@ -1607,6 +1614,10 @@ Some influential environment variables:
               path overriding pkg-config's built-in search path
   ICU_CFLAGS  C compiler flags for ICU, overriding pkg-config
   ICU_LIBS    linker flags for ICU, overriding pkg-config
+  LIBCURL_CFLAGS
+              C compiler flags for LIBCURL, overriding pkg-config
+  LIBCURL_LIBS
+              linker flags for LIBCURL, overriding pkg-config
   XML2_CONFIG path to xml2-config utility
   XML2_CFLAGS C compiler flags for XML2, overriding pkg-config
   XML2_LIBS   linker flags for XML2, overriding pkg-config
@@ -8762,6 +8773,157 @@ fi
 
 
 
+#
+# libcurl
+#
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with libcurl support" >&5
+$as_echo_n "checking whether to build with libcurl support... " >&6; }
+
+
+
+# Check whether --with-libcurl was given.
+if test "${with_libcurl+set}" = set; then :
+  withval=$with_libcurl;
+  case $withval in
+    yes)
+
+$as_echo "#define USE_LIBCURL 1" >>confdefs.h
+
+      ;;
+    no)
+      :
+      ;;
+    *)
+      as_fn_error $? "no argument expected for --with-libcurl option" "$LINENO" 5
+      ;;
+  esac
+
+else
+  with_libcurl=no
+
+fi
+
+
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_libcurl" >&5
+$as_echo "$with_libcurl" >&6; }
+
+
+if test "$with_libcurl" = yes ; then
+  # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability
+  # to explicitly set TLS 1.3 ciphersuites).
+
+pkg_failed=no
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for libcurl >= 7.61.0" >&5
+$as_echo_n "checking for libcurl >= 7.61.0... " >&6; }
+
+if test -n "$LIBCURL_CFLAGS"; then
+    pkg_cv_LIBCURL_CFLAGS="$LIBCURL_CFLAGS"
+ elif test -n "$PKG_CONFIG"; then
+    if test -n "$PKG_CONFIG" && \
+    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl >= 7.61.0\""; } >&5
+  ($PKG_CONFIG --exists --print-errors "libcurl >= 7.61.0") 2>&5
+  ac_status=$?
+  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+  pkg_cv_LIBCURL_CFLAGS=`$PKG_CONFIG --cflags "libcurl >= 7.61.0" 2>/dev/null`
+             test "x$?" != "x0" && pkg_failed=yes
+else
+  pkg_failed=yes
+fi
+ else
+    pkg_failed=untried
+fi
+if test -n "$LIBCURL_LIBS"; then
+    pkg_cv_LIBCURL_LIBS="$LIBCURL_LIBS"
+ elif test -n "$PKG_CONFIG"; then
+    if test -n "$PKG_CONFIG" && \
+    { { $as_echo "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"libcurl >= 7.61.0\""; } >&5
+  ($PKG_CONFIG --exists --print-errors "libcurl >= 7.61.0") 2>&5
+  ac_status=$?
+  $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5
+  test $ac_status = 0; }; then
+  pkg_cv_LIBCURL_LIBS=`$PKG_CONFIG --libs "libcurl >= 7.61.0" 2>/dev/null`
+             test "x$?" != "x0" && pkg_failed=yes
+else
+  pkg_failed=yes
+fi
+ else
+    pkg_failed=untried
+fi
+
+
+
+if test $pkg_failed = yes; then
+        { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
+
+if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then
+        _pkg_short_errors_supported=yes
+else
+        _pkg_short_errors_supported=no
+fi
+        if test $_pkg_short_errors_supported = yes; then
+           LIBCURL_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libcurl >= 7.61.0" 2>&1`
+        else
+           LIBCURL_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libcurl >= 7.61.0" 2>&1`
+        fi
+   # Put the nasty error message in config.log where it belongs
+   echo "$LIBCURL_PKG_ERRORS" >&5
+
+   as_fn_error $? "Package requirements (libcurl >= 7.61.0) were not met:
+
+$LIBCURL_PKG_ERRORS
+
+Consider adjusting the PKG_CONFIG_PATH environment variable if you
+installed software in a non-standard prefix.
+
+Alternatively, you may set the environment variables LIBCURL_CFLAGS
+and LIBCURL_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details." "$LINENO" 5
+elif test $pkg_failed = untried; then
+        { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5
+$as_echo "no" >&6; }
+   { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5
+$as_echo "$as_me: error: in \`$ac_pwd':" >&2;}
+as_fn_error $? "The pkg-config script could not be found or is too old.  Make sure it
+is in your PATH or set the PKG_CONFIG environment variable to the full
+path to pkg-config.
+
+Alternatively, you may set the environment variables LIBCURL_CFLAGS
+and LIBCURL_LIBS to avoid the need to call pkg-config.
+See the pkg-config man page for more details.
+
+To get pkg-config, see <http://pkg-config.freedesktop.org/>.
+See \`config.log' for more details" "$LINENO" 5; }
+else
+   LIBCURL_CFLAGS=$pkg_cv_LIBCURL_CFLAGS
+   LIBCURL_LIBS=$pkg_cv_LIBCURL_LIBS
+        { $as_echo "$as_me:${as_lineno-$LINENO}: result: yes" >&5
+$as_echo "yes" >&6; }
+
+fi
+
+  # We only care about -I, -D, and -L switches;
+  # note that -lcurl will be added by PGAC_CHECK_LIBCURL below.
+  for pgac_option in $LIBCURL_CFLAGS; do
+    case $pgac_option in
+      -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";;
+    esac
+  done
+  for pgac_option in $LIBCURL_LIBS; do
+    case $pgac_option in
+      -L*) LDFLAGS="$LDFLAGS $pgac_option";;
+    esac
+  done
+
+  # OAuth requires python for testing
+  if test "$with_python" != yes; then
+    { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: *** OAuth support tests require --with-python to run" >&5
+$as_echo "$as_me: WARNING: *** OAuth support tests require --with-python to run" >&2;}
+  fi
+fi
+
+
 #
 # XML
 #
 
 fi
 
+# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
+# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
+# dependency on that platform?
+if test "$with_libcurl" = yes ; then
+
+  ac_fn_c_check_header_mongrel "$LINENO" "curl/curl.h" "ac_cv_header_curl_curl_h" "$ac_includes_default"
+if test "x$ac_cv_header_curl_curl_h" = xyes; then :
+
+else
+  as_fn_error $? "header file <curl/curl.h> is required for --with-libcurl" "$LINENO" 5
+fi
+
+
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_multi_init in -lcurl" >&5
+$as_echo_n "checking for curl_multi_init in -lcurl... " >&6; }
+if ${ac_cv_lib_curl_curl_multi_init+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  ac_check_lib_save_LIBS=$LIBS
+LIBS="-lcurl  $LIBS"
+cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+/* Override any GCC internal prototype to avoid an error.
+   Use char because int might match the return type of a GCC
+   builtin and then its argument prototype would still apply.  */
+#ifdef __cplusplus
+extern "C"
+#endif
+char curl_multi_init ();
+int
+main ()
+{
+return curl_multi_init ();
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_link "$LINENO"; then :
+  ac_cv_lib_curl_curl_multi_init=yes
+else
+  ac_cv_lib_curl_curl_multi_init=no
+fi
+rm -f core conftest.err conftest.$ac_objext \
+    conftest$ac_exeext conftest.$ac_ext
+LIBS=$ac_check_lib_save_LIBS
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_multi_init" >&5
+$as_echo "$ac_cv_lib_curl_curl_multi_init" >&6; }
+if test "x$ac_cv_lib_curl_curl_multi_init" = xyes; then :
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_LIBCURL 1
+_ACEOF
+
+  LIBS="-lcurl $LIBS"
+
+else
+  as_fn_error $? "library 'curl' does not provide curl_multi_init" "$LINENO" 5
+fi
+
+
+  # Check to see whether the current platform supports threadsafe Curl
+  # initialization.
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init thread safety" >&5
+$as_echo_n "checking for curl_global_init thread safety... " >&6; }
+if ${pgac_cv__libcurl_threadsafe_init+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  if test "$cross_compiling" = yes; then :
+  pgac_cv__libcurl_threadsafe_init=unknown
+else
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+#include <curl/curl.h>
+
+int
+main ()
+{
+
+    curl_version_info_data *info;
+
+    if (curl_global_init(CURL_GLOBAL_ALL))
+        return -1;
+
+    info = curl_version_info(CURLVERSION_NOW);
+#ifdef CURL_VERSION_THREADSAFE
+    if (info->features & CURL_VERSION_THREADSAFE)
+        return 0;
+#endif
+
+    return 1;
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_run "$LINENO"; then :
+  pgac_cv__libcurl_threadsafe_init=yes
+else
+  pgac_cv__libcurl_threadsafe_init=no
+fi
+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
+  conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__libcurl_threadsafe_init" >&5
+$as_echo "$pgac_cv__libcurl_threadsafe_init" >&6; }
+  if test x"$pgac_cv__libcurl_threadsafe_init" = xyes ; then
+
+$as_echo "#define HAVE_THREADSAFE_CURL_GLOBAL_INIT 1" >>confdefs.h
+
+  fi
+
+  # Warn if a thread-friendly DNS resolver isn't built.
+  { $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl support for asynchronous DNS" >&5
+$as_echo_n "checking for curl support for asynchronous DNS... " >&6; }
+if ${pgac_cv__libcurl_async_dns+:} false; then :
+  $as_echo_n "(cached) " >&6
+else
+  if test "$cross_compiling" = yes; then :
+  pgac_cv__libcurl_async_dns=unknown
+else
+  cat confdefs.h - <<_ACEOF >conftest.$ac_ext
+/* end confdefs.h.  */
+
+#include <curl/curl.h>
+
+int
+main ()
+{
+
+    curl_version_info_data *info;
+
+    if (curl_global_init(CURL_GLOBAL_ALL))
+        return -1;
+
+    info = curl_version_info(CURLVERSION_NOW);
+    return (info->features & CURL_VERSION_ASYNCHDNS) ? 0 : 1;
+
+  ;
+  return 0;
+}
+_ACEOF
+if ac_fn_c_try_run "$LINENO"; then :
+  pgac_cv__libcurl_async_dns=yes
+else
+  pgac_cv__libcurl_async_dns=no
+fi
+rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \
+  conftest.$ac_objext conftest.beam conftest.$ac_ext
+fi
+
+fi
+{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $pgac_cv__libcurl_async_dns" >&5
+$as_echo "$pgac_cv__libcurl_async_dns" >&6; }
+  if test x"$pgac_cv__libcurl_async_dns" != xyes ; then
+    { $as_echo "$as_me:${as_lineno-$LINENO}: WARNING:
+*** The installed version of libcurl does not support asynchronous DNS
+*** lookups. Connection timeouts will not be honored during DNS resolution,
+*** which may lead to hangs in client programs." >&5
+$as_echo "$as_me: WARNING:
+*** The installed version of libcurl does not support asynchronous DNS
+*** lookups. Connection timeouts will not be honored during DNS resolution,
+*** which may lead to hangs in client programs." >&2;}
+  fi
+
+fi
+
 if test "$with_gssapi" = yes ; then
   if test "$PORTNAME" != "win32"; then
     { $as_echo "$as_me:${as_lineno-$LINENO}: checking for library containing gss_store_cred_into" >&5
index f56681e0d91a7deda0238d870fcdde2335623ed7..b6d02f5ecc760469eaa22f58d571e2bc9d8400d2 100644 (file)
@@ -1007,6 +1007,40 @@ fi
 AC_SUBST(with_uuid)
 
 
+#
+# libcurl
+#
+AC_MSG_CHECKING([whether to build with libcurl support])
+PGAC_ARG_BOOL(with, libcurl, no, [build with libcurl support],
+              [AC_DEFINE([USE_LIBCURL], 1, [Define to 1 to build with libcurl support. (--with-libcurl)])])
+AC_MSG_RESULT([$with_libcurl])
+AC_SUBST(with_libcurl)
+
+if test "$with_libcurl" = yes ; then
+  # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability
+  # to explicitly set TLS 1.3 ciphersuites).
+  PKG_CHECK_MODULES(LIBCURL, [libcurl >= 7.61.0])
+
+  # We only care about -I, -D, and -L switches;
+  # note that -lcurl will be added by PGAC_CHECK_LIBCURL below.
+  for pgac_option in $LIBCURL_CFLAGS; do
+    case $pgac_option in
+      -I*|-D*) CPPFLAGS="$CPPFLAGS $pgac_option";;
+    esac
+  done
+  for pgac_option in $LIBCURL_LIBS; do
+    case $pgac_option in
+      -L*) LDFLAGS="$LDFLAGS $pgac_option";;
+    esac
+  done
+
+  # OAuth requires python for testing
+  if test "$with_python" != yes; then
+    AC_MSG_WARN([*** OAuth support tests require --with-python to run])
+  fi
+fi
+
+
 #
 # XML
 #
@@ -1294,6 +1328,13 @@ failure.  It is possible the compiler isn't looking in the proper directory.
 Use --without-zlib to disable zlib support.])])
 fi
 
+# XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
+# during gss_acquire_cred(). This is possibly related to Curl's Heimdal
+# dependency on that platform?
+if test "$with_libcurl" = yes ; then
+  PGAC_CHECK_LIBCURL
+fi
+
 if test "$with_gssapi" = yes ; then
   if test "$PORTNAME" != "win32"; then
     AC_SEARCH_LIBS(gss_store_cred_into, [gssapi_krb5 gss 'gssapi -lkrb5 -lcrypto'], [],
index 782b49c85ac5689562b6810a0ece16793ecd8da6..832b616a7bbff678ca0b978c91acb412d1cc23a6 100644 (file)
@@ -656,6 +656,16 @@ include_dir         <replaceable>directory</replaceable>
          </para>
         </listitem>
        </varlistentry>
+
+       <varlistentry>
+        <term><literal>oauth</literal></term>
+        <listitem>
+         <para>
+          Authorize and optionally authenticate using a third-party OAuth 2.0
+          identity provider. See <xref linkend="auth-oauth"/> for details.
+         </para>
+        </listitem>
+       </varlistentry>
       </variablelist>
 
       </para>
@@ -1143,6 +1153,12 @@ omicron         bryanh                  guest1
       only on OpenBSD).
      </para>
     </listitem>
+    <listitem>
+     <para>
+      <link linkend="auth-oauth">OAuth authorization/authentication</link>,
+      which relies on an external OAuth 2.0 identity provider.
+     </para>
+    </listitem>
    </itemizedlist>
   </para>
 
@@ -2329,6 +2345,242 @@ host ... radius radiusservers="server1,server2" radiussecrets="""secret one"",""
    </note>
   </sect1>
 
+  <sect1 id="auth-oauth">
+   <title>OAuth Authorization/Authentication</title>
+
+   <indexterm zone="auth-oauth">
+    <primary>OAuth Authorization/Authentication</primary>
+   </indexterm>
+
+   <para>
+    OAuth 2.0 is an industry-standard framework, defined in
+    <ulink url="https://datatracker.ietf.org/doc/html/rfc6749">RFC 6749</ulink>,
+    to enable third-party applications to obtain limited access to a protected
+    resource.
+
+    OAuth client support has to be enabled when <productname>PostgreSQL</productname>
+    is built, see <xref linkend="installation"/> for more information.
+   </para>
+
+   <para>
+    This documentation uses the following terminology when discussing the OAuth
+    ecosystem:
+
+    <variablelist>
+
+     <varlistentry>
+      <term>Resource Owner (or End User)</term>
+      <listitem>
+       <para>
+        The user or system who owns protected resources and can grant access to
+        them. This documentation also uses the term <emphasis>end user</emphasis>
+        when the resource owner is a person. When you use
+        <application>psql</application> to connect to the database using OAuth,
+        you are the resource owner/end user.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term>Client</term>
+      <listitem>
+       <para>
+        The system which accesses the protected resources using access
+        tokens. Applications using libpq, such as <application>psql</application>,
+        are the OAuth clients when connecting to a
+        <productname>PostgreSQL</productname> cluster.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term>Resource Server</term>
+      <listitem>
+       <para>
+        The system hosting the protected resources which are
+        accessed by the client. The <productname>PostgreSQL</productname>
+        cluster being connected to is the resource server.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term>Provider</term>
+      <listitem>
+       <para>
+        The organization, product vendor, or other entity which develops and/or
+        administers the OAuth authorization servers and clients for a given application.
+        Different providers typically choose different implementation details
+        for their OAuth systems; a client of one provider is not generally
+        guaranteed to have access to the servers of another.
+       </para>
+       <para>
+        This use of the term "provider" is not standard, but it seems to be in
+        wide use colloquially. (It should not be confused with OpenID's similar
+        term "Identity Provider". While the implementation of OAuth in
+        <productname>PostgreSQL</productname> is intended to be interoperable
+        and compatible with OpenID Connect/OIDC, it is not itself an OIDC client
+        and does not require its use.)
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term>Authorization Server</term>
+      <listitem>
+       <para>
+        The system which receives requests from, and issues access tokens to,
+        the client after the authenticated resource owner has given approval.
+        <productname>PostgreSQL</productname> does not provide an authorization
+        server; it is the responsibility of the OAuth provider.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term id="auth-oauth-issuer">Issuer</term>
+      <listitem>
+       <para>
+        An identifier for an authorization server, printed as an
+        <literal>https://</literal> URL, which provides a trusted "namespace"
+        for OAuth clients and applications. The issuer identifier allows a
+        single authorization server to talk to the clients of mutually
+        untrusting entities, as long as they maintain separate issuers.
+       </para>
+      </listitem>
+     </varlistentry>
+
+    </variablelist>
+
+    <note>
+     <para>
+      For small deployments, there may not be a meaningful distinction between
+      the "provider", "authorization server", and "issuer". However, for more
+      complicated setups, there may be a one-to-many (or many-to-many)
+      relationship: a provider may rent out multiple issuer identifiers to
+      separate tenants, then provide multiple authorization servers, possibly
+      with different supported feature sets, to interact with their clients.
+     </para>
+    </note>
+   </para>
+
+   <para>
+    <productname>PostgreSQL</productname> supports bearer tokens, defined in
+    <ulink url="https://datatracker.ietf.org/doc/html/rfc6750">RFC 6750</ulink>,
+    which are a type of access token used with OAuth 2.0 where the token is an
+    opaque string.  The format of the access token is implementation specific
+    and is chosen by each authorization server.
+   </para>
+
+   <para>
+    The following configuration options are supported for OAuth:
+    <variablelist>
+     <varlistentry>
+      <term><literal>issuer</literal></term>
+      <listitem>
+       <para>
+        An HTTPS URL which is either the exact
+        <link linkend="auth-oauth-issuer">issuer identifier</link> of the
+        authorization server, as defined by its discovery document, or a
+        well-known URI that points directly to that discovery document. This
+        parameter is required.
+       </para>
+       <para>
+        When an OAuth client connects to the server, a URL for the discovery
+        document will be constructed using the issuer identifier. By default,
+        this URL uses the conventions of OpenID Connect Discovery: the path
+        <literal>/.well-known/openid-configuration</literal> will be appended
+        to the end of the issuer identifier. Alternatively, if the
+        <literal>issuer</literal> contains a <literal>/.well-known/</literal>
+        path segment, that URL will be provided to the client as-is.
+       </para>
+       <warning>
+        <para>
+         The OAuth client in libpq requires the server's issuer setting to
+         exactly match the issuer identifier which is provided in the discovery
+         document, which must in turn match the client's
+         <xref linkend="libpq-connect-oauth-issuer"/> setting. No variations in
+         case or formatting are permitted.
+        </para>
+       </warning>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><literal>scope</literal></term>
+      <listitem>
+       <para>
+        A space-separated list of the OAuth scopes needed for the server to
+        both authorize the client and authenticate the user.  Appropriate values
+        are determined by the authorization server and the OAuth validation
+        module used (see <xref linkend="oauth-validators" /> for more
+        information on validators).  This parameter is required.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><literal>validator</literal></term>
+      <listitem>
+       <para>
+        The library to use for validating bearer tokens. If given, the name must
+        exactly match one of the libraries listed in
+        <xref linkend="guc-oauth-validator-libraries" />.  This parameter is
+        optional unless <literal>oauth_validator_libraries</literal> contains
+        more than one library, in which case it is required.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><literal>map</literal></term>
+      <listitem>
+       <para>
+        Allows for mapping between OAuth identity provider and database user
+        names.  See <xref linkend="auth-username-maps"/> for details.  If a
+        map is not specified, the user name associated with the token (as
+        determined by the OAuth validator) must exactly match the role name
+        being requested.  This parameter is optional.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term id="auth-oauth-delegate-ident-mapping" xreflabel="delegate_ident_mapping">
+       <literal>delegate_ident_mapping</literal>
+      </term>
+      <listitem>
+       <para>
+        An advanced option which is not intended for common use.
+       </para>
+       <para>
+        When set to <literal>1</literal>, standard user mapping with
+        <filename>pg_ident.conf</filename> is skipped, and the OAuth validator
+        takes full responsibility for mapping end user identities to database
+        roles.  If the validator authorizes the token, the server trusts that
+        the user is allowed to connect under the requested role, and the
+        connection is allowed to proceed regardless of the authentication
+        status of the user.
+       </para>
+       <para>
+        This parameter is incompatible with <literal>map</literal>.
+       </para>
+       <warning>
+        <para>
+         <literal>delegate_ident_mapping</literal> provides additional
+         flexibility in the design of the authentication system, but it also
+         requires careful implementation of the OAuth validator, which must
+         determine whether the provided token carries sufficient end-user
+         privileges in addition to the <link linkend="oauth-validators">standard
+         checks</link> required of all validators.  Use with caution.
+        </para>
+       </warning>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+  </sect1>
+
   <sect1 id="client-authentication-problems">
    <title>Authentication Problems</title>
 
index 9eedcf6f0f417db42ad303b0513d3ac40a9ed374..007746a44296c948e4aacfe533e01fc06f40b940 100644 (file)
@@ -1209,6 +1209,32 @@ include_dir 'conf.d'
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="guc-oauth-validator-libraries" xreflabel="oauth_validator_libraries">
+      <term><varname>oauth_validator_libraries</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>oauth_validator_libraries</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library/libraries to use for validating OAuth connection tokens. If
+        only one validator library is provided, it will be used by default for
+        any OAuth connections; otherwise, all
+        <link linkend="auth-oauth"><literal>oauth</literal> HBA entries</link>
+        must explicitly set a <literal>validator</literal> chosen from this
+        list. If set to an empty string (the default), OAuth connections will be
+        refused. This parameter can only be set in the
+        <filename>postgresql.conf</filename> file.
+       </para>
+       <para>
+        Validator modules must be implemented/obtained separately;
+        <productname>PostgreSQL</productname> does not ship with any default
+        implementations. For more information on implementing OAuth validators,
+        see <xref linkend="oauth-validators" />.
+       </para>
+      </listitem>
+     </varlistentry>
      </variablelist>
      </sect2>
 
index 66e6dccd4c9c1d3ec0895b57946535504c2aaa60..25fb99cee6944c83da214c43f05a8db0e3df72d9 100644 (file)
 <!ENTITY generic-wal SYSTEM "generic-wal.sgml">
 <!ENTITY custom-rmgr SYSTEM "custom-rmgr.sgml">
 <!ENTITY backup-manifest SYSTEM "backup-manifest.sgml">
+<!ENTITY oauth-validators SYSTEM "oauth-validators.sgml">
 
 <!-- contrib information -->
 <!ENTITY contrib         SYSTEM "contrib.sgml">
index 3f0a7e9c06941f0ab021e5ceb8466afa9f8c20ba..3c95c15a1e4987b8883f92a2087d6ba971c6c65c 100644 (file)
@@ -1143,6 +1143,19 @@ build-postgresql:
        </listitem>
       </varlistentry>
 
+      <varlistentry id="configure-option-with-libcurl">
+       <term><option>--with-libcurl</option></term>
+       <listitem>
+        <para>
+         Build with libcurl support for OAuth 2.0 client flows.
+         Libcurl version 7.61.0 or later is required for this feature.
+         Building with this will check for the required header files
+         and libraries to make sure that your <productname>curl</productname>
+         installation is sufficient before proceeding.
+        </para>
+       </listitem>
+      </varlistentry>
+
       <varlistentry id="configure-option-with-libxml">
        <term><option>--with-libxml</option></term>
        <listitem>
@@ -2584,6 +2597,20 @@ ninja install
       </listitem>
      </varlistentry>
 
+     <varlistentry id="configure-with-libcurl-meson">
+      <term><option>-Dlibcurl={ auto | enabled | disabled }</option></term>
+      <listitem>
+       <para>
+        Build with libcurl support for OAuth 2.0 client flows.
+        Libcurl version 7.61.0 or later is required for this feature.
+        Building with this will check for the required header files
+        and libraries to make sure that your <productname>Curl</productname>
+        installation is sufficient before proceeding. The default for this
+        option is auto.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="configure-with-libxml-meson">
       <term><option>-Dlibxml={ auto | enabled | disabled }</option></term>
       <listitem>
index c49e975b082b9537e08119aeb1ea93986a47f94d..ddb3596df83258e6cf6961628a0af47283f7a515 100644 (file)
@@ -1385,6 +1385,15 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
           </listitem>
          </varlistentry>
 
+         <varlistentry>
+          <term><literal>oauth</literal></term>
+          <listitem>
+           <para>
+            The server must request an OAuth bearer token from the client.
+           </para>
+          </listitem>
+         </varlistentry>
+
          <varlistentry>
           <term><literal>none</literal></term>
           <listitem>
@@ -2373,6 +2382,107 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
        </para>
       </listitem>
      </varlistentry>
+
+     <varlistentry id="libpq-connect-oauth-issuer" xreflabel="oauth_issuer">
+      <term><literal>oauth_issuer</literal></term>
+      <listitem>
+       <para>
+        The HTTPS URL of a trusted issuer to contact if the server requests an
+        OAuth token for the connection. This parameter is required for all OAuth
+        connections; it should exactly match the <literal>issuer</literal>
+        setting in <link linkend="auth-oauth">the server's HBA configuration</link>.
+       </para>
+       <para>
+        As part of the standard authentication handshake, <application>libpq</application>
+        will ask the server for a <emphasis>discovery document:</emphasis> a URL
+        providing a set of OAuth configuration parameters. The server must
+        provide a URL that is directly constructed from the components of the
+        <literal>oauth_issuer</literal>, and this value must exactly match the
+        issuer identifier that is declared in the discovery document itself, or
+        the connection will fail. This is required to prevent a class of
+        <ulink url="https://mailarchive.ietf.org/arch/msg/oauth/JIVxFBGsJBVtm7ljwJhPUm3Fr-w/">
+        "mix-up attacks"</ulink> on OAuth clients.
+       </para>
+       <para>
+        You may also explicitly set <literal>oauth_issuer</literal> to the
+        <literal>/.well-known/</literal> URI used for OAuth discovery. In this
+        case, if the server asks for a different URL, the connection will fail,
+        but a <link linkend="libpq-oauth-authdata-hooks">custom OAuth flow</link>
+        may be able to speed up the standard handshake by using previously
+        cached tokens. (In this case, it is recommended that
+        <xref linkend="libpq-connect-oauth-scope"/> be set as well, since the
+        client will not have a chance to ask the server for a correct scope
+        setting, and the default scopes for a token may not be sufficient to
+        connect.) <application>libpq</application> currently supports the
+        following well-known endpoints:
+        <itemizedlist spacing="compact">
+         <listitem><para><literal>/.well-known/openid-configuration</literal></para></listitem>
+         <listitem><para><literal>/.well-known/oauth-authorization-server</literal></para></listitem>
+        </itemizedlist>
+       </para>
+       <warning>
+        <para>
+         Issuers are highly privileged during the OAuth connection handshake. As
+         a rule of thumb, if you would not trust the operator of a URL to handle
+         access to your servers, or to impersonate you directly, that URL should
+         not be trusted as an <literal>oauth_issuer</literal>.
+        </para>
+       </warning>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-oauth-client-id" xreflabel="oauth_client_id">
+      <term><literal>oauth_client_id</literal></term>
+      <listitem>
+       <para>
+        An OAuth 2.0 client identifier, as issued by the authorization server.
+        If the <productname>PostgreSQL</productname> server
+        <link linkend="auth-oauth">requests an OAuth token</link> for the
+        connection (and if no <link linkend="libpq-oauth-authdata-hooks">custom
+        OAuth hook</link> is installed to provide one), then this parameter must
+        be set; otherwise, the connection will fail.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-oauth-client-secret" xreflabel="oauth_client_secret">
+      <term><literal>oauth_client_secret</literal></term>
+      <listitem>
+       <para>
+        The client password, if any, to use when contacting the OAuth
+        authorization server. Whether this parameter is required or not is
+        determined by the OAuth provider; "public" clients generally do not use
+        a secret, whereas "confidential" clients generally do.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-connect-oauth-scope" xreflabel="oauth_scope">
+      <term><literal>oauth_scope</literal></term>
+      <listitem>
+       <para>
+        The scope of the access request sent to the authorization server,
+        specified as a (possibly empty) space-separated list of OAuth scope
+        identifiers. This parameter is optional and intended for advanced usage.
+       </para>
+       <para>
+        Usually the client will obtain appropriate scope settings from the
+        <productname>PostgreSQL</productname> server. If this parameter is used,
+        the server's requested scope list will be ignored. This can prevent a
+        less-trusted server from requesting inappropriate access scopes from the
+        end user. However, if the client's scope setting does not contain the
+        server's required scopes, the server is likely to reject the issued
+        token, and the connection will fail.
+       </para>
+       <para>
+        The meaning of an empty scope list is provider-dependent. An OAuth
+        authorization server may choose to issue a token with "default scope",
+        whatever that happens to be, or it may reject the token request
+        entirely.
+       </para>
+      </listitem>
+     </varlistentry>
+
     </variablelist>
    </para>
   </sect2>
@@ -10020,6 +10130,329 @@ void PQinitSSL(int do_ssl);
 
  </sect1>
 
+ <sect1 id="libpq-oauth">
+  <title>OAuth Support</title>
+
+  <para>
+   libpq implements support for the OAuth v2 Device Authorization client flow,
+   documented in
+   <ulink url="https://datatracker.ietf.org/doc/html/rfc8628">RFC 8628</ulink>,
+   which it will attempt to use by default if the server
+   <link linkend="auth-oauth">requests a bearer token</link> during
+   authentication. This flow can be utilized even if the system running the
+   client application does not have a usable web browser, for example when
+   running a client via <application>SSH</application>. Client applications may implement their own flows
+   instead; see <xref linkend="libpq-oauth-authdata-hooks"/>.
+  </para>
+  <para>
+   The builtin flow will, by default, print a URL to visit and a user code to
+   enter there:
+<programlisting>
+$ psql 'dbname=postgres oauth_issuer=https://example.com oauth_client_id=...'
+Visit https://example.com/device and enter the code: ABCD-EFGH
+</programlisting>
+   (This prompt may be
+   <link linkend="libpq-oauth-authdata-prompt-oauth-device">customized</link>.)
+   The user will then log into their OAuth provider, which will ask whether
+   to allow libpq and the server to perform actions on their behalf. It is always
+   a good idea to carefully review the URL and permissions displayed, to ensure
+   they match expectations, before continuing. Permissions should not be given
+   to untrusted third parties.
+  </para>
+  <para>
+   For an OAuth client flow to be usable, the connection string must at minimum
+   contain <xref linkend="libpq-connect-oauth-issuer"/> and
+   <xref linkend="libpq-connect-oauth-client-id"/>. (These settings are
+   determined by your organization's OAuth provider.) The builtin flow
+   additionally requires the OAuth authorization server to publish a device
+   authorization endpoint.
+  </para>
+
+  <note>
+   <para>
+    The builtin Device Authorization flow is not currently supported on Windows.
+    Custom client flows may still be implemented.
+   </para>
+  </note>
+
+  <sect2 id="libpq-oauth-authdata-hooks">
+   <title>Authdata Hooks</title>
+
+   <para>
+    The behavior of the OAuth flow may be modified or replaced by a client using
+    the following hook API:
+
+    <variablelist>
+     <varlistentry id="libpq-PQsetAuthDataHook">
+      <term><function>PQsetAuthDataHook</function><indexterm><primary>PQsetAuthDataHook</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Sets the <symbol>PGauthDataHook</symbol>, overriding
+        <application>libpq</application>'s handling of one or more aspects of
+        its OAuth client flow.
+<synopsis>
+void PQsetAuthDataHook(PQauthDataHook_type hook);
+</synopsis>
+        If <replaceable>hook</replaceable> is <literal>NULL</literal>, the
+        default handler will be reinstalled. Otherwise, the application passes
+        a pointer to a callback function with the signature:
+<programlisting>
+int hook_fn(PGauthData type, PGconn *conn, void *data);
+</programlisting>
+        which <application>libpq</application> will call when an action is
+        required of the application. <replaceable>type</replaceable> describes
+        the request being made, <replaceable>conn</replaceable> is the
+        connection handle being authenticated, and <replaceable>data</replaceable>
+        points to request-specific metadata. The contents of this pointer are
+        determined by <replaceable>type</replaceable>; see
+        <xref linkend="libpq-oauth-authdata-hooks-types"/> for the supported
+        list.
+       </para>
+       <para>
+        Hooks can be chained together to allow cooperative and/or fallback
+        behavior. In general, a hook implementation should examine the incoming
+        <replaceable>type</replaceable> (and, potentially, the request metadata
+        and/or the settings for the particular <replaceable>conn</replaceable>
+        in use) to decide whether or not to handle a specific piece of authdata.
+        If not, it should delegate to the previous hook in the chain
+        (retrievable via <function>PQgetAuthDataHook</function>).
+       </para>
+       <para>
+        Success is indicated by returning an integer greater than zero.
+        Returning a negative integer signals an error condition and abandons the
+        connection attempt. (A zero value is reserved for the default
+        implementation.)
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="libpq-PQgetAuthDataHook">
+      <term><function>PQgetAuthDataHook</function><indexterm><primary>PQgetAuthDataHook</primary></indexterm></term>
+
+      <listitem>
+       <para>
+        Retrieves the current value of <symbol>PGauthDataHook</symbol>.
+<synopsis>
+PQauthDataHook_type PQgetAuthDataHook(void);
+</synopsis>
+        At initialization time (before the first call to
+        <function>PQsetAuthDataHook</function>), this function will return
+        <symbol>PQdefaultAuthDataHook</symbol>.
+       </para>
+      </listitem>
+     </varlistentry>
+    </variablelist>
+   </para>
+
+   <sect3 id="libpq-oauth-authdata-hooks-types">
+    <title>Hook Types</title>
+    <para>
+     The following <symbol>PGauthData</symbol> types and their corresponding
+     <replaceable>data</replaceable> structures are defined:
+
+     <variablelist>
+      <varlistentry id="libpq-oauth-authdata-prompt-oauth-device">
+       <term>
+        <symbol>PQAUTHDATA_PROMPT_OAUTH_DEVICE</symbol>
+        <indexterm><primary>PQAUTHDATA_PROMPT_OAUTH_DEVICE</primary></indexterm>
+       </term>
+       <listitem>
+        <para>
+         Replaces the default user prompt during the builtin device
+         authorization client flow. <replaceable>data</replaceable> points to
+         an instance of <symbol>PGpromptOAuthDevice</symbol>:
+<synopsis>
+typedef struct _PGpromptOAuthDevice
+{
+    const char *verification_uri;   /* verification URI to visit */
+    const char *user_code;          /* user code to enter */
+    const char *verification_uri_complete;  /* optional combination of URI and
+                                             * code, or NULL */
+    int         expires_in;         /* seconds until user code expires */
+} PGpromptOAuthDevice;
+</synopsis>
+        </para>
+        <para>
+         The OAuth Device Authorization flow included in <application>libpq</application>
+         requires the end user to visit a URL with a browser, then enter a code
+         which permits <application>libpq</application> to connect to the server
+         on their behalf. The default prompt simply prints the
+         <literal>verification_uri</literal> and <literal>user_code</literal>
+         on standard error. Replacement implementations may display this
+         information using any preferred method, for example with a GUI.
+        </para>
+        <para>
+         This callback is only invoked during the builtin device
+         authorization flow. If the application installs a
+         <link linkend="libpq-oauth-authdata-oauth-bearer-token">custom OAuth
+         flow</link>, this authdata type will not be used.
+        </para>
+        <para>
+         If a non-NULL <structfield>verification_uri_complete</structfield> is
+         provided, it may optionally be used for non-textual verification (for
+         example, by displaying a QR code). The URL and user code should still
+         be displayed to the end user in this case, because the code will be
+         manually confirmed by the provider, and the URL lets users continue
+         even if they can't use the non-textual method. For more information,
+         see section 3.3.1 in
+         <ulink url="https://datatracker.ietf.org/doc/html/rfc8628#section-3.3.1">RFC 8628</ulink>.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry id="libpq-oauth-authdata-oauth-bearer-token">
+       <term>
+        <symbol>PQAUTHDATA_OAUTH_BEARER_TOKEN</symbol>
+        <indexterm><primary>PQAUTHDATA_OAUTH_BEARER_TOKEN</primary></indexterm>
+       </term>
+       <listitem>
+        <para>
+         Replaces the entire OAuth flow with a custom implementation. The hook
+         should either directly return a Bearer token for the current
+         user/issuer/scope combination, if one is available without blocking, or
+         else set up an asynchronous callback to retrieve one.
+        </para>
+        <para>
+         <replaceable>data</replaceable> points to an instance
+         of <symbol>PGoauthBearerRequest</symbol>, which should be filled in
+         by the implementation:
+<synopsis>
+typedef struct _PGoauthBearerRequest
+{
+    /* Hook inputs (constant across all calls) */
+    const char *const openid_configuration; /* OIDC discovery URL */
+    const char *const scope;                /* required scope(s), or NULL */
+
+    /* Hook outputs */
+
+    /* Callback implementing a custom asynchronous OAuth flow. */
+    PostgresPollingStatusType (*async) (PGconn *conn,
+                                        struct _PGoauthBearerRequest *request,
+                                        SOCKTYPE *altsock);
+
+    /* Callback to clean up custom allocations. */
+    void        (*cleanup) (PGconn *conn, struct _PGoauthBearerRequest *request);
+
+    char       *token;   /* acquired Bearer token */
+    void       *user;    /* hook-defined allocated data */
+} PGoauthBearerRequest;
+</synopsis>
+        </para>
+        <para>
+         Two pieces of information are provided to the hook by
+         <application>libpq</application>:
+         <replaceable>openid_configuration</replaceable> contains the URL of an
+         OAuth discovery document describing the authorization server's
+         supported flows, and <replaceable>scope</replaceable> contains a
+         (possibly empty) space-separated list of OAuth scopes which are
+         required to access the server. Either or both may be
+         <literal>NULL</literal> to indicate that the information was not
+         discoverable. (In this case, implementations may be able to establish
+         the requirements using some other preconfigured knowledge, or they may
+         choose to fail.)
+        </para>
+        <para>
+         The final output of the hook is <replaceable>token</replaceable>, which
+         must point to a valid Bearer token for use on the connection. (This
+         token should be issued by the
+         <xref linkend="libpq-connect-oauth-issuer"/> and hold the requested
+         scopes, or the connection will be rejected by the server's validator
+         module.) The allocated token string must remain valid until
+         <application>libpq</application> is finished connecting; the hook
+         should set a <replaceable>cleanup</replaceable> callback which will be
+         called when <application>libpq</application> no longer requires it.
+        </para>
+        <para>
+         If an implementation cannot immediately produce a
+         <replaceable>token</replaceable> during the initial call to the hook,
+         it should set the <replaceable>async</replaceable> callback to handle
+         nonblocking communication with the authorization server.
+         <footnote>
+          <para>
+           Performing blocking operations during the
+           <symbol>PQAUTHDATA_OAUTH_BEARER_TOKEN</symbol> hook callback will
+           interfere with nonblocking connection APIs such as
+           <function>PQconnectPoll</function> and prevent concurrent connections
+           from making progress. Applications which only ever use the
+           synchronous connection primitives, such as
+           <function>PQconnectdb</function>, may synchronously retrieve a token
+           during the hook instead of implementing the
+           <replaceable>async</replaceable> callback, but they will necessarily
+           be limited to one connection at a time.
+          </para>
+         </footnote>
+         This will be called to begin the flow immediately upon return from the
+         hook. When the callback cannot make further progress without blocking,
+         it should return either <symbol>PGRES_POLLING_READING</symbol> or
+         <symbol>PGRES_POLLING_WRITING</symbol> after setting
+         <literal>*pgsocket</literal> to the file descriptor that will be marked
+         ready to read/write when progress can be made again. (This descriptor
+         is then provided to the top-level polling loop via
+         <function>PQsocket()</function>.) Return <symbol>PGRES_POLLING_OK</symbol>
+         after setting <replaceable>token</replaceable> when the flow is
+         complete, or <symbol>PGRES_POLLING_FAILED</symbol> to indicate failure.
+        </para>
+        <para>
+         Implementations may wish to store additional data for bookkeeping
+         across calls to the <replaceable>async</replaceable> and
+         <replaceable>cleanup</replaceable> callbacks. The
+         <replaceable>user</replaceable> pointer is provided for this purpose;
+         <application>libpq</application> will not touch its contents and the
+         application may use it at its convenience. (Remember to free any
+         allocations during token cleanup.)
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </para>
+   </sect3>
+  </sect2>
+
+  <sect2 id="libpq-oauth-debugging">
+   <title>Debugging and Developer Settings</title>
+
+   <para>
+    A "dangerous debugging mode" may be enabled by setting the environment
+    variable <envar>PGOAUTHDEBUG=UNSAFE</envar>. This functionality is provided
+    for ease of local development and testing only. It does several things that
+    you will not want a production system to do:
+
+    <itemizedlist spacing="compact">
+     <listitem>
+      <para>
+       permits the use of unencrypted HTTP during the OAuth provider exchange
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       allows the system's trusted CA list to be completely replaced using the
+       <envar>PGOAUTHCAFILE</envar> environment variable
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       prints HTTP traffic (containing several critical secrets) to standard
+       error during the OAuth flow
+      </para>
+     </listitem>
+     <listitem>
+      <para>
+       permits the use of zero-second retry intervals, which can cause the
+       client to busy-loop and pointlessly consume CPU
+      </para>
+     </listitem>
+    </itemizedlist>
+   </para>
+   <warning>
+    <para>
+     Do not share the output of the OAuth flow traffic with third parties. It
+     contains secrets that can be used to attack your clients and servers.
+    </para>
+   </warning>
+  </sect2>
+ </sect1>
+
 
  <sect1 id="libpq-threading">
   <title>Behavior in Threaded Programs</title>
@@ -10092,6 +10525,18 @@ int PQisthreadsafe();
    <application>libpq</application> source code for a way to do cooperative
    locking between <application>libpq</application> and your application.
   </para>
+
+  <para>
+   Similarly, if you are using <productname>Curl</productname> inside your application,
+   <emphasis>and</emphasis> you do not already
+   <ulink url="https://curl.se/libcurl/c/curl_global_init.html">initialize
+   libcurl globally</ulink> before starting new threads, you will need to
+   cooperatively lock (again via <function>PQregisterThreadLock</function>)
+   around any code that may initialize libcurl. This restriction is lifted for
+   more recent versions of <productname>Curl</productname> that are built to support thread-safe
+   initialization; those builds can be identified by the advertisement of a
+   <literal>threadsafe</literal> feature in their version metadata.
+  </para>
  </sect1>
 
 
diff --git a/doc/src/sgml/oauth-validators.sgml b/doc/src/sgml/oauth-validators.sgml
new file mode 100644 (file)
index 0000000..356f11d
--- /dev/null
@@ -0,0 +1,414 @@
+<!-- doc/src/sgml/oauth-validators.sgml -->
+
+<chapter id="oauth-validators">
+ <title>OAuth Validator Modules</title>
+ <indexterm zone="oauth-validators">
+  <primary>OAuth Validators</primary>
+ </indexterm>
+ <para>
+  <productname>PostgreSQL</productname> provides infrastructure for creating
+  custom modules to perform server-side validation of OAuth bearer tokens.
+  Because OAuth implementations vary so wildly, and bearer token validation is
+  heavily dependent on the issuing party, the server cannot check the token
+  itself; validator modules provide the integration layer between the server
+  and the OAuth provider in use.
+ </para>
+ <para>
+  OAuth validator modules must at least consist of an initialization function
+  (see <xref linkend="oauth-validator-init"/>) and the required callback for
+  performing validation (see <xref linkend="oauth-validator-callback-validate"/>).
+ </para>
+ <warning>
+  <para>
+   Since a misbehaving validator might let unauthorized users into the database,
+   correct implementation is crucial for server safety. See
+   <xref linkend="oauth-validator-design"/> for design considerations.
+  </para>
+ </warning>
+
+ <sect1 id="oauth-validator-design">
+  <title>Safely Designing a Validator Module</title>
+  <warning>
+   <para>
+    Read and understand the entirety of this section before implementing a
+    validator module. A malfunctioning validator is potentially worse than no
+    authentication at all, both because of the false sense of security it
+    provides, and because it may contribute to attacks against other pieces of
+    an OAuth ecosystem.
+   </para>
+  </warning>
+
+  <sect2 id="oauth-validator-design-responsibilities">
+   <title>Validator Responsibilities</title>
+   <para>
+    Although different modules may take very different approaches to token
+    validation, implementations generally need to perform three separate
+    actions:
+   </para>
+   <variablelist>
+    <varlistentry>
+     <term>Validate the Token</term>
+     <listitem>
+      <para>
+       The validator must first ensure that the presented token is in fact a
+       valid Bearer token for use in client authentication. The correct way to
+       do this depends on the provider, but it generally involves either
+       cryptographic operations to prove that the token was created by a trusted
+       party (offline validation), or the presentation of the token to that
+       trusted party so that it can perform validation for you (online
+       validation).
+      </para>
+      <para>
+       Online validation, usually implemented via
+       <ulink url="https://datatracker.ietf.org/doc/html/rfc7662">OAuth Token
+       Introspection</ulink>, requires fewer steps of a validator module and
+       allows central revocation of a token in the event that it is stolen
+       or misissued. However, it does require the module to make at least one
+       network call per authentication attempt (all of which must complete
+       within the configured <xref linkend="guc-authentication-timeout"/>).
+       Additionally, your provider may not provide introspection endpoints for
+       use by external resource servers.
+      </para>
+      <para>
+       Offline validation is much more involved, typically requiring a validator
+       to maintain a list of trusted signing keys for a provider and then
+       check the token's cryptographic signature along with its contents.
+       Implementations must follow the provider's instructions to the letter,
+       including any verification of issuer ("where is this token from?"),
+       audience ("who is this token for?"), and validity period ("when can this
+       token be used?"). Since there is no communication between the module and
+       the provider, tokens cannot be centrally revoked using this method;
+       offline validator implementations may wish to place restrictions on the
+       maximum length of a token's validity period.
+      </para>
+      <para>
+       If the token cannot be validated, the module should immediately fail.
+       Further authentication/authorization is pointless if the bearer token
+       wasn't issued by a trusted party.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>Authorize the Client</term>
+     <listitem>
+      <para>
+       Next the validator must ensure that the end user has given the client
+       permission to access the server on their behalf. This generally involves
+       checking the scopes that have been assigned to the token, to make sure
+       that they cover database access for the current HBA parameters.
+      </para>
+      <para>
+       The purpose of this step is to prevent an OAuth client from obtaining a
+       token under false pretenses. If the validator requires all tokens to
+       carry scopes that cover database access, the provider should then loudly
+       prompt the user to grant that access during the flow. This gives them the
+       opportunity to reject the request if the client isn't supposed to be
+       using their credentials to connect to databases.
+      </para>
+      <para>
+       While it is possible to establish client authorization without explicit
+       scopes by using out-of-band knowledge of the deployed architecture, doing
+       so removes the user from the loop, which prevents them from catching
+       deployment mistakes and allows any such mistakes to be exploited
+       silently. Access to the database must be tightly restricted to only
+       trusted clients
+       <footnote>
+        <para>
+         That is, "trusted" in the sense that the OAuth client and the
+         <productname>PostgreSQL</productname> server are controlled by the same
+         entity. Notably, the Device Authorization client flow supported by
+         libpq does not usually meet this bar, since it's designed for use by
+         public/untrusted clients.
+        </para>
+       </footnote>
+       if users are not prompted for additional scopes.
+      </para>
+      <para>
+       Even if authorization fails, a module may choose to continue to pull
+       authentication information from the token for use in auditing and
+       debugging.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>Authenticate the End User</term>
+     <listitem>
+      <para>
+       Finally, the validator should determine a user identifier for the token,
+       either by asking the provider for this information or by extracting it
+       from the token itself, and return that identifier to the server (which
+       will then make a final authorization decision using the HBA
+       configuration). This identifier will be available within the session via
+       <link linkend="functions-info-session-table"><function>system_user</function></link>
+       and recorded in the server logs if <xref linkend="guc-log-connections"/>
+       is enabled.
+      </para>
+      <para>
+       Different providers may record a variety of different authentication
+       information for an end user, typically referred to as
+       <emphasis>claims</emphasis>. Providers usually document which of these
+       claims are trustworthy enough to use for authorization decisions and
+       which are not. (For instance, it would probably not be wise to use an
+       end user's full name as the identifier for authentication, since many
+       providers allow users to change their display names arbitrarily.)
+       Ultimately, the choice of which claim (or combination of claims) to use
+       comes down to the provider implementation and application requirements.
+      </para>
+      <para>
+       Note that anonymous/pseudonymous login is possible as well, by enabling
+       usermap delegation; see
+       <xref linkend="oauth-validator-design-usermap-delegation"/>.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+  </sect2>
+
+  <sect2 id="oauth-validator-design-guidelines">
+   <title>General Coding Guidelines</title>
+   <para>
+    Developers should keep the following in mind when implementing token
+    validation:
+   </para>
+   <variablelist>
+    <varlistentry>
+     <term>Token Confidentiality</term>
+     <listitem>
+      <para>
+       Modules should not write tokens, or pieces of tokens, into the server
+       log. This is true even if the module considers the token invalid; an
+       attacker who confuses a client into communicating with the wrong provider
+       should not be able to retrieve that (otherwise valid) token from the
+       disk.
+      </para>
+      <para>
+       Implementations that send tokens over the network (for example, to
+       perform online token validation with a provider) must authenticate the
+       peer and ensure that strong transport security is in use.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>Logging</term>
+     <listitem>
+      <para>
+       Modules may use the same <link linkend="error-message-reporting">logging
+       facilities</link> as standard extensions; however, the rules for emitting
+       log entries to the client are subtly different during the authentication
+       phase of the connection. Generally speaking, modules should log
+       verification problems at the <symbol>COMMERROR</symbol> level and return
+       normally, instead of using <symbol>ERROR</symbol>/<symbol>FATAL</symbol>
+       to unwind the stack, to avoid leaking information to unauthenticated
+       clients.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>Interruptibility</term>
+     <listitem>
+      <para>
+       Modules must remain interruptible by signals so that the server can
+       correctly handle authentication timeouts and shutdown signals from
+       <application>pg_ctl</application>. For example, a module receiving
+       <symbol>EINTR</symbol>/<symbol>EAGAIN</symbol> from a blocking call
+       should call <function>CHECK_FOR_INTERRUPTS()</function> before retrying.
+       The same should be done during any long-running loops. Failure to follow
+       this guidance may result in unresponsive backend sessions.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>Testing</term>
+     <listitem>
+      <para>
+       The breadth of testing an OAuth system is well beyond the scope of this
+       documentation, but at minimum, negative testing should be considered
+       mandatory. It's trivial to design a module that lets authorized users in;
+       the whole point of the system is to keep unauthorized users out.
+      </para>
+     </listitem>
+    </varlistentry>
+    <varlistentry>
+     <term>Documentation</term>
+     <listitem>
+      <para>
+       Validator implementations should document the contents and format of the
+       authenticated ID that is reported to the server for each end user, since
+       DBAs may need to use this information to construct pg_ident maps. (For
+       instance, is it an email address? an organizational ID number? a UUID?)
+       They should also document whether or not it is safe to use the module in
+       <symbol>delegate_ident_mapping=1</symbol> mode, and what additional
+       configuration is required in order to do so.
+      </para>
+     </listitem>
+    </varlistentry>
+   </variablelist>
+  </sect2>
+
+  <sect2 id="oauth-validator-design-usermap-delegation">
+   <title>Authorizing Users (Usermap Delegation)</title>
+   <para>
+    The standard deliverable of a validation module is the user identifier,
+    which the server will then compare to any configured
+    <link linkend="auth-username-maps"><filename>pg_ident.conf</filename>
+    mappings</link> and determine whether the end user is authorized to connect.
+    However, OAuth is itself an authorization framework, and tokens may carry
+    information about user privileges. For example, a token may be associated
+    with the organizational groups that a user belongs to, or list the roles
+    that a user may assume, and duplicating that knowledge into local usermaps
+    for every server may not be desirable.
+   </para>
+   <para>
+    To bypass username mapping entirely, and have the validator module assume
+    the additional responsibility of authorizing user connections, the HBA may
+    be configured with <xref linkend="auth-oauth-delegate-ident-mapping"/>.
+    The module may then use token scopes or an equivalent method to decide
+    whether the user is allowed to connect under their desired role. The user
+    identifier will still be recorded by the server, but it plays no part in
+    determining whether to continue the connection.
+   </para>
+   <para>
+    Using this scheme, authentication itself is optional. As long as the module
+    reports that the connection is authorized, login will continue even if there
+    is no recorded user identifier at all. This makes it possible to implement
+    anonymous or pseudonymous access to the database, where the third-party
+    provider performs all necessary authentication but does not provide any
+    user-identifying information to the server. (Some providers may create an
+    anonymized ID number that can be recorded instead, for later auditing.)
+   </para>
+   <para>
+    Usermap delegation provides the most architectural flexibility, but it turns
+    the validator module into a single point of failure for connection
+    authorization. Use with caution.
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="oauth-validator-init">
+  <title>Initialization Functions</title>
+  <indexterm zone="oauth-validator-init">
+   <primary>_PG_oauth_validator_module_init</primary>
+  </indexterm>
+  <para>
+   OAuth validator modules are dynamically loaded from the shared
+   libraries listed in <xref linkend="guc-oauth-validator-libraries"/>.
+   Modules are loaded on demand when requested from a login in progress.
+   The normal library search path is used to locate the library. To
+   provide the validator callbacks and to indicate that the library is an OAuth
+   validator module a function named
+   <function>_PG_oauth_validator_module_init</function> must be provided. The
+   return value of the function must be a pointer to a struct of type
+   <structname>OAuthValidatorCallbacks</structname>, which contains a magic
+   number and pointers to the module's token validation functions. The returned
+   pointer must be of server lifetime, which is typically achieved by defining
+   it as a <literal>static const</literal> variable in global scope.
+<programlisting>
+typedef struct OAuthValidatorCallbacks
+{
+    uint32        magic;            /* must be set to PG_OAUTH_VALIDATOR_MAGIC */
+
+    ValidatorStartupCB startup_cb;
+    ValidatorShutdownCB shutdown_cb;
+    ValidatorValidateCB validate_cb;
+} OAuthValidatorCallbacks;
+
+typedef const OAuthValidatorCallbacks *(*OAuthValidatorModuleInit) (void);
+</programlisting>
+
+   Only the <function>validate_cb</function> callback is required, the others
+   are optional.
+  </para>
+ </sect1>
+
+ <sect1 id="oauth-validator-callbacks">
+  <title>OAuth Validator Callbacks</title>
+  <para>
+   OAuth validator modules implement their functionality by defining a set of
+   callbacks. The server will call them as required to process the
+   authentication request from the user.
+  </para>
+
+  <sect2 id="oauth-validator-callback-startup">
+   <title>Startup Callback</title>
+   <para>
+    The <function>startup_cb</function> callback is executed directly after
+    loading the module. This callback can be used to set up local state and
+    perform additional initialization if required. If the validator module
+    has state it can use <structfield>state->private_data</structfield> to
+    store it.
+
+<programlisting>
+typedef void (*ValidatorStartupCB) (ValidatorModuleState *state);
+</programlisting>
+   </para>
+  </sect2>
+
+  <sect2 id="oauth-validator-callback-validate">
+   <title>Validate Callback</title>
+   <para>
+    The <function>validate_cb</function> callback is executed during the OAuth
+    exchange when a user attempts to authenticate using OAuth.  Any state set in
+    previous calls will be available in <structfield>state->private_data</structfield>.
+
+<programlisting>
+typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
+                                     const char *token, const char *role,
+                                     ValidatorModuleResult *result);
+</programlisting>
+
+    <replaceable>token</replaceable> will contain the bearer token to validate.
+    <application>PostgreSQL</application> has ensured that the token is well-formed syntactically, but no
+    other validation has been performed.  <replaceable>role</replaceable> will
+    contain the role the user has requested to log in as.  The callback must
+    set output parameters in the <literal>result</literal> struct, which is
+    defined as below:
+
+<programlisting>
+typedef struct ValidatorModuleResult
+{
+    bool        authorized;
+    char       *authn_id;
+} ValidatorModuleResult;
+</programlisting>
+
+    The connection will only proceed if the module sets
+    <structfield>result->authorized</structfield> to <literal>true</literal>.  To
+    authenticate the user, the authenticated user name (as determined using the
+    token) shall be palloc'd and returned in the <structfield>result->authn_id</structfield>
+    field.  Alternatively, <structfield>result->authn_id</structfield> may be set to
+    NULL if the token is valid but the associated user identity cannot be
+    determined.
+   </para>
+   <para>
+    A validator may return <literal>false</literal> to signal an internal error,
+    in which case any result parameters are ignored and the connection fails.
+    Otherwise the validator should return <literal>true</literal> to indicate
+    that it has processed the token and made an authorization decision.
+   </para>
+   <para>
+    The behavior after <function>validate_cb</function> returns depends on the
+    specific HBA setup.  Normally, the <structfield>result->authn_id</structfield> user
+    name must exactly match the role that the user is logging in as.  (This
+    behavior may be modified with a usermap.)  But when authenticating against
+    an HBA rule with <literal>delegate_ident_mapping</literal> turned on,
+    <productname>PostgreSQL</productname> will not perform any checks on the value of
+    <structfield>result->authn_id</structfield> at all; in this case it is up to the
+    validator to ensure that the token carries enough privileges for the user to
+    log in under the indicated <replaceable>role</replaceable>.
+   </para>
+  </sect2>
+
+  <sect2 id="oauth-validator-callback-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is executed when the backend
+    process associated with the connection exits. If the validator module has
+    any allocated state, this callback should free it to avoid resource leaks.
+<programlisting>
+typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
+</programlisting>
+   </para>
+  </sect2>
+
+ </sect1>
+</chapter>
index 7be25c58507fb61a37ea70416859ec596e09644f..af476c82fcc1e2f83ff3d12498506b99a21183e9 100644 (file)
@@ -229,6 +229,7 @@ break is not needed in a wider output rendering.
   &logicaldecoding;
   &replication-origins;
   &archive-modules;
+  &oauth-validators;
 
  </part>
 
index fb5dec1172e169548a13898f642802cbaa60d803..3bd9e68e6ce41a25fb88ae9ac8b48f7b5f297aa0 100644 (file)
@@ -1688,11 +1688,11 @@ SELCT 1/0;<!-- this typo is intentional -->
 
   <para>
    <firstterm>SASL</firstterm> is a framework for authentication in connection-oriented
-   protocols. At the moment, <productname>PostgreSQL</productname> implements two SASL
-   authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS. More
-   might be added in the future. The below steps illustrate how SASL
-   authentication is performed in general, while the next subsection gives
-   more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
+   protocols. At the moment, <productname>PostgreSQL</productname> implements three
+   SASL authentication mechanisms: SCRAM-SHA-256, SCRAM-SHA-256-PLUS, and
+   OAUTHBEARER. More might be added in the future. The below steps illustrate how SASL
+   authentication is performed in general, while the next subsections give
+   more details on particular mechanisms.
   </para>
 
   <procedure>
@@ -1727,7 +1727,7 @@ SELCT 1/0;<!-- this typo is intentional -->
    <step id="sasl-auth-end">
     <para>
      Finally, when the authentication exchange is completed successfully, the
-     server sends an AuthenticationSASLFinal message, followed
+     server sends an optional AuthenticationSASLFinal message, followed
      immediately by an AuthenticationOk message. The AuthenticationSASLFinal
      contains additional server-to-client data, whose content is particular to the
      selected authentication mechanism. If the authentication mechanism doesn't
@@ -1746,9 +1746,9 @@ SELCT 1/0;<!-- this typo is intentional -->
    <title>SCRAM-SHA-256 Authentication</title>
 
    <para>
-    The implemented SASL mechanisms at the moment
-    are <literal>SCRAM-SHA-256</literal> and its variant with channel
-    binding <literal>SCRAM-SHA-256-PLUS</literal>. They are described in
+    <literal>SCRAM-SHA-256</literal>, and its variant with channel
+    binding <literal>SCRAM-SHA-256-PLUS</literal>, are password-based
+    authentication mechanisms. They are described in
     detail in <ulink url="https://datatracker.ietf.org/doc/html/rfc7677">RFC 7677</ulink>
     and <ulink url="https://datatracker.ietf.org/doc/html/rfc5802">RFC 5802</ulink>.
    </para>
@@ -1850,6 +1850,121 @@ SELCT 1/0;<!-- this typo is intentional -->
     </step>
    </procedure>
   </sect2>
+
+  <sect2 id="sasl-oauthbearer">
+   <title>OAUTHBEARER Authentication</title>
+
+   <para>
+    <literal>OAUTHBEARER</literal> is a token-based mechanism for federated
+    authentication. It is described in detail in
+    <ulink url="https://datatracker.ietf.org/doc/html/rfc7628">RFC 7628</ulink>.
+   </para>
+
+   <para>
+    A typical exchange differs depending on whether or not the client already
+    has a bearer token cached for the current user. If it does not, the exchange
+    will take place over two connections: the first "discovery" connection to
+    obtain OAuth metadata from the server, and the second connection to send
+    the token after the client has obtained it. (libpq does not currently
+    implement a caching method as part of its builtin flow, so it uses the
+    two-connection exchange.)
+   </para>
+
+   <para>
+    This mechanism is client-initiated, like SCRAM. The client initial response
+    consists of the standard "GS2" header used by SCRAM, followed by a list of
+    <literal>key=value</literal> pairs. The only key currently supported by
+    the server is <literal>auth</literal>, which contains the bearer token.
+    <literal>OAUTHBEARER</literal> additionally specifies three optional
+    components of the client initial response (the <literal>authzid</literal> of
+    the GS2 header, and the <structfield>host</structfield> and
+    <structfield>port</structfield> keys) which are currently ignored by the
+    server.
+   </para>
+
+   <para>
+    <literal>OAUTHBEARER</literal> does not support channel binding, and there
+    is no "OAUTHBEARER-PLUS" mechanism. This mechanism does not make use of
+    server data during a successful authentication, so the
+    AuthenticationSASLFinal message is not used in the exchange.
+   </para>
+
+   <procedure>
+    <title>Example</title>
+    <step>
+     <para>
+      During the first exchange, the server sends an AuthenticationSASL message
+      with the <literal>OAUTHBEARER</literal> mechanism advertised.
+     </para>
+    </step>
+
+    <step>
+     <para>
+      The client responds by sending a SASLInitialResponse message which
+      indicates the <literal>OAUTHBEARER</literal> mechanism. Assuming the
+      client does not already have a valid bearer token for the current user,
+      the <structfield>auth</structfield> field is empty, indicating a discovery
+      connection.
+     </para>
+    </step>
+
+    <step>
+     <para>
+      Server sends an AuthenticationSASLContinue message containing an error
+      <literal>status</literal> alongside a well-known URI and scopes that the
+      client should use to conduct an OAuth flow.
+     </para>
+    </step>
+
+    <step>
+     <para>
+      Client sends a SASLResponse message containing the empty set (a single
+      <literal>0x01</literal> byte) to finish its half of the discovery
+      exchange.
+     </para>
+    </step>
+
+    <step>
+     <para>
+      Server sends an ErrorMessage to fail the first exchange.
+     </para>
+     <para>
+      At this point, the client conducts one of many possible OAuth flows to
+      obtain a bearer token, using any metadata that it has been configured with
+      in addition to that provided by the server. (This description is left
+      deliberately vague; <literal>OAUTHBEARER</literal> does not specify or
+      mandate any particular method for obtaining a token.)
+     </para>
+     <para>
+      Once it has a token, the client reconnects to the server for the final
+      exchange:
+     </para>
+    </step>
+
+    <step>
+     <para>
+      The server once again sends an AuthenticationSASL message with the
+      <literal>OAUTHBEARER</literal> mechanism advertised.
+     </para>
+    </step>
+
+    <step>
+     <para>
+      The client responds by sending a SASLInitialResponse message, but this
+      time the <structfield>auth</structfield> field in the message contains the
+      bearer token that was obtained during the client flow.
+     </para>
+    </step>
+
+    <step>
+     <para>
+      The server validates the token according to the instructions of the
+      token provider. If the client is authorized to connect, it sends an
+      AuthenticationOk message to end the SASL exchange.
+     </para>
+    </step>
+   </procedure>
+  </sect2>
  </sect1>
 
  <sect1 id="protocol-replication">
index 7c474559bdfa46fc4ad03d9b81b2758ae0bbb072..0e5e8e8f3090cc0e59615c3a7256da046b6d0730 100644 (file)
@@ -347,6 +347,16 @@ make check-world PG_TEST_EXTRA='kerberos ldap ssl load_balance libpq_encryption'
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><literal>oauth</literal></term>
+     <listitem>
+      <para>
+       Runs the test suite under <filename>src/test/modules/oauth_validator</filename>.
+       This opens TCP/IP listen sockets for a test-server running HTTPS.
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
 
    Tests for features that are not supported by the current build
index 7dd7110318dba7834d0a56737e74c90057570997..574f992ed499503dd161ca73e8e657150f8f96ee 100644 (file)
@@ -855,6 +855,101 @@ endif
 
 
 
+###############################################################
+# Library: libcurl
+###############################################################
+
+libcurlopt = get_option('libcurl')
+if not libcurlopt.disabled()
+  # Check for libcurl 7.61.0 or higher (corresponding to RHEL8 and the ability
+  # to explicitly set TLS 1.3 ciphersuites).
+  libcurl = dependency('libcurl', version: '>= 7.61.0', required: libcurlopt)
+  if libcurl.found()
+    cdata.set('USE_LIBCURL', 1)
+
+    # Check to see whether the current platform supports thread-safe Curl
+    # initialization.
+    libcurl_threadsafe_init = false
+
+    if not meson.is_cross_build()
+      r = cc.run('''
+        #include <curl/curl.h>
+
+        int main(void)
+        {
+            curl_version_info_data *info;
+
+            if (curl_global_init(CURL_GLOBAL_ALL))
+                return -1;
+
+            info = curl_version_info(CURLVERSION_NOW);
+        #ifdef CURL_VERSION_THREADSAFE
+            if (info->features & CURL_VERSION_THREADSAFE)
+                return 0;
+        #endif
+
+            return 1;
+        }''',
+        name: 'test for curl_global_init thread safety',
+        dependencies: libcurl,
+      )
+
+      assert(r.compiled())
+      if r.returncode() == 0
+        libcurl_threadsafe_init = true
+        message('curl_global_init is thread-safe')
+      elif r.returncode() == 1
+        message('curl_global_init is not thread-safe')
+      else
+        message('curl_global_init failed; assuming not thread-safe')
+      endif
+    endif
+
+    if libcurl_threadsafe_init
+      cdata.set('HAVE_THREADSAFE_CURL_GLOBAL_INIT', 1)
+    endif
+
+    # Warn if a thread-friendly DNS resolver isn't built.
+    libcurl_async_dns = false
+
+    if not meson.is_cross_build()
+      r = cc.run('''
+        #include <curl/curl.h>
+
+        int main(void)
+        {
+            curl_version_info_data *info;
+
+            if (curl_global_init(CURL_GLOBAL_ALL))
+                return -1;
+
+            info = curl_version_info(CURLVERSION_NOW);
+            return (info->features & CURL_VERSION_ASYNCHDNS) ? 0 : 1;
+        }''',
+        name: 'test for curl support for asynchronous DNS',
+        dependencies: libcurl,
+      )
+
+      assert(r.compiled())
+      if r.returncode() == 0
+        libcurl_async_dns = true
+      endif
+    endif
+
+    if not libcurl_async_dns
+      warning('''
+*** The installed version of libcurl does not support asynchronous DNS
+*** lookups. Connection timeouts will not be honored during DNS resolution,
+*** which may lead to hangs in client programs.''')
+    endif
+  endif
+
+else
+  libcurl = not_found_dep
+endif
+
+
+
 ###############################################################
 # Library: libxml
 ###############################################################
@@ -3045,6 +3140,10 @@ libpq_deps += [
 
   gssapi,
   ldap_r,
+  # XXX libcurl must link after libgssapi_krb5 on FreeBSD to avoid segfaults
+  # during gss_acquire_cred(). This is possibly related to Curl's Heimdal
+  # dependency on that platform?
+  libcurl,
   libintl,
   ssl,
 ]
@@ -3721,6 +3820,7 @@ if meson.version().version_compare('>=0.57')
       'gss': gssapi,
       'icu': icu,
       'ldap': ldap,
+      'libcurl': libcurl,
       'libxml': libxml,
       'libxslt': libxslt,
       'llvm': llvm,
index d9c7ddccbc440a9f2312420a117f0f75b578228b..702c4517145df85bf3dee2e9e91e2b7ae7a7f4e0 100644 (file)
@@ -100,6 +100,9 @@ option('icu', type: 'feature', value: 'auto',
 option('ldap', type: 'feature', value: 'auto',
   description: 'LDAP support')
 
+option('libcurl', type : 'feature', value: 'auto',
+  description: 'libcurl support')
+
 option('libedit_preferred', type: 'boolean', value: false,
   description: 'Prefer BSD Libedit over GNU Readline')
 
index bbe11e75bf0d02c9ebaa2066a6dacb75a80c3ccf..3b620bac5acd4c01b3d19e2b355d036800dc8335 100644 (file)
@@ -190,6 +190,7 @@ with_systemd    = @with_systemd@
 with_gssapi    = @with_gssapi@
 with_krb_srvnam    = @with_krb_srvnam@
 with_ldap  = @with_ldap@
+with_libcurl   = @with_libcurl@
 with_libxml    = @with_libxml@
 with_libxslt   = @with_libxslt@
 with_llvm  = @with_llvm@
index 6d385fd6a450d120935d486c5e852ecf3acffaf5..98eb2a8242d55f2de60641acc60969aa9814d16c 100644 (file)
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 # be-fsstubs is here for historical reasons, probably belongs elsewhere
 
 OBJS = \
+   auth-oauth.o \
    auth-sasl.o \
    auth-scram.o \
    auth.o \
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
new file mode 100644 (file)
index 0000000..27f7af7
--- /dev/null
@@ -0,0 +1,894 @@
+/*-------------------------------------------------------------------------
+ *
+ * auth-oauth.c
+ *   Server-side implementation of the SASL OAUTHBEARER mechanism.
+ *
+ * See the following RFC for more details:
+ * - RFC 7628: https://datatracker.ietf.org/doc/html/rfc7628
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/backend/libpq/auth-oauth.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <unistd.h>
+#include <fcntl.h>
+
+#include "common/oauth-common.h"
+#include "fmgr.h"
+#include "lib/stringinfo.h"
+#include "libpq/auth.h"
+#include "libpq/hba.h"
+#include "libpq/oauth.h"
+#include "libpq/sasl.h"
+#include "storage/fd.h"
+#include "storage/ipc.h"
+#include "utils/json.h"
+#include "utils/varlena.h"
+
+/* GUC */
+char      *oauth_validator_libraries_string = NULL;
+
+static void oauth_get_mechanisms(Port *port, StringInfo buf);
+static void *oauth_init(Port *port, const char *selected_mech, const char *shadow_pass);
+static int oauth_exchange(void *opaq, const char *input, int inputlen,
+                          char **output, int *outputlen, const char **logdetail);
+
+static void load_validator_library(const char *libname);
+static void shutdown_validator_library(void *arg);
+
+static ValidatorModuleState *validator_module_state;
+static const OAuthValidatorCallbacks *ValidatorCallbacks;
+
+/* Mechanism declaration */
+const pg_be_sasl_mech pg_be_oauth_mech = {
+   .get_mechanisms = oauth_get_mechanisms,
+   .init = oauth_init,
+   .exchange = oauth_exchange,
+
+   .max_message_length = PG_MAX_AUTH_TOKEN_LENGTH,
+};
+
+/* Valid states for the oauth_exchange() machine. */
+enum oauth_state
+{
+   OAUTH_STATE_INIT = 0,
+   OAUTH_STATE_ERROR,
+   OAUTH_STATE_FINISHED,
+};
+
+/* Mechanism callback state. */
+struct oauth_ctx
+{
+   enum oauth_state state;
+   Port       *port;
+   const char *issuer;
+   const char *scope;
+};
+
+static char *sanitize_char(char c);
+static char *parse_kvpairs_for_auth(char **input);
+static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen);
+static bool validate(Port *port, const char *auth);
+
+/* Constants seen in an OAUTHBEARER client initial response. */
+#define KVSEP 0x01             /* separator byte for key/value pairs */
+#define AUTH_KEY "auth"            /* key containing the Authorization header */
+#define BEARER_SCHEME "Bearer " /* required header scheme (case-insensitive!) */
+
+/*
+ * Retrieves the OAUTHBEARER mechanism list (currently a single item).
+ *
+ * For a full description of the API, see libpq/sasl.h.
+ */
+static void
+oauth_get_mechanisms(Port *port, StringInfo buf)
+{
+   /* Only OAUTHBEARER is supported. */
+   appendStringInfoString(buf, OAUTHBEARER_NAME);
+   appendStringInfoChar(buf, '\0');
+}
+
+/*
+ * Initializes mechanism state and loads the configured validator module.
+ *
+ * For a full description of the API, see libpq/sasl.h.
+ */
+static void *
+oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
+{
+   struct oauth_ctx *ctx;
+
+   if (strcmp(selected_mech, OAUTHBEARER_NAME) != 0)
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("client selected an invalid SASL authentication mechanism"));
+
+   ctx = palloc0(sizeof(*ctx));
+
+   ctx->state = OAUTH_STATE_INIT;
+   ctx->port = port;
+
+   Assert(port->hba);
+   ctx->issuer = port->hba->oauth_issuer;
+   ctx->scope = port->hba->oauth_scope;
+
+   load_validator_library(port->hba->oauth_validator);
+
+   return ctx;
+}
+
+/*
+ * Implements the OAUTHBEARER SASL exchange (RFC 7628, Sec. 3.2). This pulls
+ * apart the client initial response and validates the Bearer token. It also
+ * handles the dummy error response for a failed handshake, as described in
+ * Sec. 3.2.3.
+ *
+ * For a full description of the API, see libpq/sasl.h.
+ */
+static int
+oauth_exchange(void *opaq, const char *input, int inputlen,
+              char **output, int *outputlen, const char **logdetail)
+{
+   char       *input_copy;
+   char       *p;
+   char        cbind_flag;
+   char       *auth;
+   int         status;
+
+   struct oauth_ctx *ctx = opaq;
+
+   *output = NULL;
+   *outputlen = -1;
+
+   /*
+    * If the client didn't include an "Initial Client Response" in the
+    * SASLInitialResponse message, send an empty challenge, to which the
+    * client will respond with the same data that usually comes in the
+    * Initial Client Response.
+    */
+   if (input == NULL)
+   {
+       Assert(ctx->state == OAUTH_STATE_INIT);
+
+       *output = pstrdup("");
+       *outputlen = 0;
+       return PG_SASL_EXCHANGE_CONTINUE;
+   }
+
+   /*
+    * Check that the input length agrees with the string length of the input.
+    */
+   if (inputlen == 0)
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("The message is empty."));
+   if (inputlen != strlen(input))
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Message length does not match input length."));
+
+   switch (ctx->state)
+   {
+       case OAUTH_STATE_INIT:
+           /* Handle this case below. */
+           break;
+
+       case OAUTH_STATE_ERROR:
+
+           /*
+            * Only one response is valid for the client during authentication
+            * failure: a single kvsep.
+            */
+           if (inputlen != 1 || *input != KVSEP)
+               ereport(ERROR,
+                       errcode(ERRCODE_PROTOCOL_VIOLATION),
+                       errmsg("malformed OAUTHBEARER message"),
+                       errdetail("Client did not send a kvsep response."));
+
+           /* The (failed) handshake is now complete. */
+           ctx->state = OAUTH_STATE_FINISHED;
+           return PG_SASL_EXCHANGE_FAILURE;
+
+       default:
+           elog(ERROR, "invalid OAUTHBEARER exchange state");
+           return PG_SASL_EXCHANGE_FAILURE;
+   }
+
+   /* Handle the client's initial message. */
+   p = input_copy = pstrdup(input);
+
+   /*
+    * OAUTHBEARER does not currently define a channel binding (so there is no
+    * OAUTHBEARER-PLUS, and we do not accept a 'p' specifier). We accept a
+    * 'y' specifier purely for the remote chance that a future specification
+    * could define one; then future clients can still interoperate with this
+    * server implementation. 'n' is the expected case.
+    */
+   cbind_flag = *p;
+   switch (cbind_flag)
+   {
+       case 'p':
+           ereport(ERROR,
+                   errcode(ERRCODE_PROTOCOL_VIOLATION),
+                   errmsg("malformed OAUTHBEARER message"),
+                   errdetail("The server does not support channel binding for OAuth, but the client message includes channel binding data."));
+           break;
+
+       case 'y':               /* fall through */
+       case 'n':
+           p++;
+           if (*p != ',')
+               ereport(ERROR,
+                       errcode(ERRCODE_PROTOCOL_VIOLATION),
+                       errmsg("malformed OAUTHBEARER message"),
+                       errdetail("Comma expected, but found character \"%s\".",
+                                 sanitize_char(*p)));
+           p++;
+           break;
+
+       default:
+           ereport(ERROR,
+                   errcode(ERRCODE_PROTOCOL_VIOLATION),
+                   errmsg("malformed OAUTHBEARER message"),
+                   errdetail("Unexpected channel-binding flag \"%s\".",
+                             sanitize_char(cbind_flag)));
+   }
+
+   /*
+    * Forbid optional authzid (authorization identity).  We don't support it.
+    */
+   if (*p == 'a')
+       ereport(ERROR,
+               errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+               errmsg("client uses authorization identity, but it is not supported"));
+   if (*p != ',')
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Unexpected attribute \"%s\" in client-first-message.",
+                         sanitize_char(*p)));
+   p++;
+
+   /* All remaining fields are separated by the RFC's kvsep (\x01). */
+   if (*p != KVSEP)
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Key-value separator expected, but found character \"%s\".",
+                         sanitize_char(*p)));
+   p++;
+
+   auth = parse_kvpairs_for_auth(&p);
+   if (!auth)
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Message does not contain an auth value."));
+
+   /* We should be at the end of our message. */
+   if (*p)
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Message contains additional data after the final terminator."));
+
+   if (!validate(ctx->port, auth))
+   {
+       generate_error_response(ctx, output, outputlen);
+
+       ctx->state = OAUTH_STATE_ERROR;
+       status = PG_SASL_EXCHANGE_CONTINUE;
+   }
+   else
+   {
+       ctx->state = OAUTH_STATE_FINISHED;
+       status = PG_SASL_EXCHANGE_SUCCESS;
+   }
+
+   /* Don't let extra copies of the bearer token hang around. */
+   explicit_bzero(input_copy, inputlen);
+
+   return status;
+}
+
+/*
+ * Convert an arbitrary byte to printable form.  For error messages.
+ *
+ * If it's a printable ASCII character, print it as a single character.
+ * otherwise, print it in hex.
+ *
+ * The returned pointer points to a static buffer.
+ */
+static char *
+sanitize_char(char c)
+{
+   static char buf[5];
+
+   if (c >= 0x21 && c <= 0x7E)
+       snprintf(buf, sizeof(buf), "'%c'", c);
+   else
+       snprintf(buf, sizeof(buf), "0x%02x", (unsigned char) c);
+   return buf;
+}
+
+/*
+ * Performs syntactic validation of a key and value from the initial client
+ * response. (Semantic validation of interesting values must be performed
+ * later.)
+ */
+static void
+validate_kvpair(const char *key, const char *val)
+{
+   /*-----
+    * From Sec 3.1:
+    *     key            = 1*(ALPHA)
+    */
+   static const char *key_allowed_set =
+       "abcdefghijklmnopqrstuvwxyz"
+       "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+   size_t      span;
+
+   if (!key[0])
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Message contains an empty key name."));
+
+   span = strspn(key, key_allowed_set);
+   if (key[span] != '\0')
+       ereport(ERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAUTHBEARER message"),
+               errdetail("Message contains an invalid key name."));
+
+   /*-----
+    * From Sec 3.1:
+    *     value          = *(VCHAR / SP / HTAB / CR / LF )
+    *
+    * The VCHAR (visible character) class is large; a loop is more
+    * straightforward than strspn().
+    */
+   for (; *val; ++val)
+   {
+       if (0x21 <= *val && *val <= 0x7E)
+           continue;           /* VCHAR */
+
+       switch (*val)
+       {
+           case ' ':
+           case '\t':
+           case '\r':
+           case '\n':
+               continue;       /* SP, HTAB, CR, LF */
+
+           default:
+               ereport(ERROR,
+                       errcode(ERRCODE_PROTOCOL_VIOLATION),
+                       errmsg("malformed OAUTHBEARER message"),
+                       errdetail("Message contains an invalid value."));
+       }
+   }
+}
+
+/*
+ * Consumes all kvpairs in an OAUTHBEARER exchange message. If the "auth" key is
+ * found, its value is returned.
+ */
+static char *
+parse_kvpairs_for_auth(char **input)
+{
+   char       *pos = *input;
+   char       *auth = NULL;
+
+   /*----
+    * The relevant ABNF, from Sec. 3.1:
+    *
+    *     kvsep          = %x01
+    *     key            = 1*(ALPHA)
+    *     value          = *(VCHAR / SP / HTAB / CR / LF )
+    *     kvpair         = key "=" value kvsep
+    *   ;;gs2-header     = See RFC 5801
+    *     client-resp    = (gs2-header kvsep *kvpair kvsep) / kvsep
+    *
+    * By the time we reach this code, the gs2-header and initial kvsep have
+    * already been validated. We start at the beginning of the first kvpair.
+    */
+
+   while (*pos)
+   {
+       char       *end;
+       char       *sep;
+       char       *key;
+       char       *value;
+
+       /*
+        * Find the end of this kvpair. Note that input is null-terminated by
+        * the SASL code, so the strchr() is bounded.
+        */
+       end = strchr(pos, KVSEP);
+       if (!end)
+           ereport(ERROR,
+                   errcode(ERRCODE_PROTOCOL_VIOLATION),
+                   errmsg("malformed OAUTHBEARER message"),
+                   errdetail("Message contains an unterminated key/value pair."));
+       *end = '\0';
+
+       if (pos == end)
+       {
+           /* Empty kvpair, signifying the end of the list. */
+           *input = pos + 1;
+           return auth;
+       }
+
+       /*
+        * Find the end of the key name.
+        */
+       sep = strchr(pos, '=');
+       if (!sep)
+           ereport(ERROR,
+                   errcode(ERRCODE_PROTOCOL_VIOLATION),
+                   errmsg("malformed OAUTHBEARER message"),
+                   errdetail("Message contains a key without a value."));
+       *sep = '\0';
+
+       /* Both key and value are now safely terminated. */
+       key = pos;
+       value = sep + 1;
+       validate_kvpair(key, value);
+
+       if (strcmp(key, AUTH_KEY) == 0)
+       {
+           if (auth)
+               ereport(ERROR,
+                       errcode(ERRCODE_PROTOCOL_VIOLATION),
+                       errmsg("malformed OAUTHBEARER message"),
+                       errdetail("Message contains multiple auth values."));
+
+           auth = value;
+       }
+       else
+       {
+           /*
+            * The RFC also defines the host and port keys, but they are not
+            * required for OAUTHBEARER and we do not use them. Also, per Sec.
+            * 3.1, any key/value pairs we don't recognize must be ignored.
+            */
+       }
+
+       /* Move to the next pair. */
+       pos = end + 1;
+   }
+
+   ereport(ERROR,
+           errcode(ERRCODE_PROTOCOL_VIOLATION),
+           errmsg("malformed OAUTHBEARER message"),
+           errdetail("Message did not contain a final terminator."));
+
+   pg_unreachable();
+   return NULL;
+}
+
+/*
+ * Builds the JSON response for failed authentication (RFC 7628, Sec. 3.2.2).
+ * This contains the required scopes for entry and a pointer to the OAuth/OpenID
+ * discovery document, which the client may use to conduct its OAuth flow.
+ */
+static void
+generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
+{
+   StringInfoData buf;
+   StringInfoData issuer;
+
+   /*
+    * The admin needs to set an issuer and scope for OAuth to work. There's
+    * not really a way to hide this from the user, either, because we can't
+    * choose a "default" issuer, so be honest in the failure message. (In
+    * practice such configurations are rejected during HBA parsing.)
+    */
+   if (!ctx->issuer || !ctx->scope)
+       ereport(FATAL,
+               errcode(ERRCODE_INTERNAL_ERROR),
+               errmsg("OAuth is not properly configured for this user"),
+               errdetail_log("The issuer and scope parameters must be set in pg_hba.conf."));
+
+   /*
+    * Build a default .well-known URI based on our issuer, unless the HBA has
+    * already provided one.
+    */
+   initStringInfo(&issuer);
+   appendStringInfoString(&issuer, ctx->issuer);
+   if (strstr(ctx->issuer, "/.well-known/") == NULL)
+       appendStringInfoString(&issuer, "/.well-known/openid-configuration");
+
+   initStringInfo(&buf);
+
+   /*
+    * Escaping the string here is belt-and-suspenders defensive programming
+    * since escapable characters aren't valid in either the issuer URI or the
+    * scope list, but the HBA doesn't enforce that yet.
+    */
+   appendStringInfoString(&buf, "{ \"status\": \"invalid_token\", ");
+
+   appendStringInfoString(&buf, "\"openid-configuration\": ");
+   escape_json(&buf, issuer.data);
+   pfree(issuer.data);
+
+   appendStringInfoString(&buf, ", \"scope\": ");
+   escape_json(&buf, ctx->scope);
+
+   appendStringInfoString(&buf, " }");
+
+   *output = buf.data;
+   *outputlen = buf.len;
+}
+
+/*-----
+ * Validates the provided Authorization header and returns the token from
+ * within it. NULL is returned on validation failure.
+ *
+ * Only Bearer tokens are accepted. The ABNF is defined in RFC 6750, Sec.
+ * 2.1:
+ *
+ *      b64token    = 1*( ALPHA / DIGIT /
+ *                        "-" / "." / "_" / "~" / "+" / "/" ) *"="
+ *      credentials = "Bearer" 1*SP b64token
+ *
+ * The "credentials" construction is what we receive in our auth value.
+ *
+ * Since that spec is subordinate to HTTP (i.e. the HTTP Authorization
+ * header format; RFC 9110 Sec. 11), the "Bearer" scheme string must be
+ * compared case-insensitively. (This is not mentioned in RFC 6750, but the
+ * OAUTHBEARER spec points it out: RFC 7628 Sec. 4.)
+ *
+ * Invalid formats are technically a protocol violation, but we shouldn't
+ * reflect any information about the sensitive Bearer token back to the
+ * client; log at COMMERROR instead.
+ */
+static const char *
+validate_token_format(const char *header)
+{
+   size_t      span;
+   const char *token;
+   static const char *const b64token_allowed_set =
+       "abcdefghijklmnopqrstuvwxyz"
+       "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+       "0123456789-._~+/";
+
+   /* Missing auth headers should be handled by the caller. */
+   Assert(header);
+
+   if (header[0] == '\0')
+   {
+       /*
+        * A completely empty auth header represents a query for
+        * authentication parameters. The client expects it to fail; there's
+        * no need to make any extra noise in the logs.
+        *
+        * TODO: should we find a way to return STATUS_EOF at the top level,
+        * to suppress the authentication error entirely?
+        */
+       return NULL;
+   }
+
+   if (pg_strncasecmp(header, BEARER_SCHEME, strlen(BEARER_SCHEME)))
+   {
+       ereport(COMMERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAuth bearer token"),
+               errdetail_log("Client response indicated a non-Bearer authentication scheme."));
+       return NULL;
+   }
+
+   /* Pull the bearer token out of the auth value. */
+   token = header + strlen(BEARER_SCHEME);
+
+   /* Swallow any additional spaces. */
+   while (*token == ' ')
+       token++;
+
+   /* Tokens must not be empty. */
+   if (!*token)
+   {
+       ereport(COMMERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAuth bearer token"),
+               errdetail_log("Bearer token is empty."));
+       return NULL;
+   }
+
+   /*
+    * Make sure the token contains only allowed characters. Tokens may end
+    * with any number of '=' characters.
+    */
+   span = strspn(token, b64token_allowed_set);
+   while (token[span] == '=')
+       span++;
+
+   if (token[span] != '\0')
+   {
+       /*
+        * This error message could be more helpful by printing the
+        * problematic character(s), but that'd be a bit like printing a piece
+        * of someone's password into the logs.
+        */
+       ereport(COMMERROR,
+               errcode(ERRCODE_PROTOCOL_VIOLATION),
+               errmsg("malformed OAuth bearer token"),
+               errdetail_log("Bearer token is not in the correct format."));
+       return NULL;
+   }
+
+   return token;
+}
+
+/*
+ * Checks that the "auth" kvpair in the client response contains a syntactically
+ * valid Bearer token, then passes it along to the loaded validator module for
+ * authorization. Returns true if validation succeeds.
+ */
+static bool
+validate(Port *port, const char *auth)
+{
+   int         map_status;
+   ValidatorModuleResult *ret;
+   const char *token;
+   bool        status;
+
+   /* Ensure that we have a correct token to validate */
+   if (!(token = validate_token_format(auth)))
+       return false;
+
+   /*
+    * Ensure that we have a validation library loaded, this should always be
+    * the case and an error here is indicative of a bug.
+    */
+   if (!ValidatorCallbacks || !ValidatorCallbacks->validate_cb)
+       ereport(FATAL,
+               errcode(ERRCODE_INTERNAL_ERROR),
+               errmsg("validation of OAuth token requested without a validator loaded"));
+
+   /* Call the validation function from the validator module */
+   ret = palloc0(sizeof(ValidatorModuleResult));
+   if (!ValidatorCallbacks->validate_cb(validator_module_state, token,
+                                        port->user_name, ret))
+   {
+       ereport(WARNING,
+               errcode(ERRCODE_INTERNAL_ERROR),
+               errmsg("internal error in OAuth validator module"));
+       return false;
+   }
+
+   /*
+    * Log any authentication results even if the token isn't authorized; it
+    * might be useful for auditing or troubleshooting.
+    */
+   if (ret->authn_id)
+       set_authn_id(port, ret->authn_id);
+
+   if (!ret->authorized)
+   {
+       ereport(LOG,
+               errmsg("OAuth bearer authentication failed for user \"%s\"",
+                      port->user_name),
+               errdetail_log("Validator failed to authorize the provided token."));
+
+       status = false;
+       goto cleanup;
+   }
+
+   if (port->hba->oauth_skip_usermap)
+   {
+       /*
+        * If the validator is our authorization authority, we're done.
+        * Authentication may or may not have been performed depending on the
+        * validator implementation; all that matters is that the validator
+        * says the user can log in with the target role.
+        */
+       status = true;
+       goto cleanup;
+   }
+
+   /* Make sure the validator authenticated the user. */
+   if (ret->authn_id == NULL || ret->authn_id[0] == '\0')
+   {
+       ereport(LOG,
+               errmsg("OAuth bearer authentication failed for user \"%s\"",
+                      port->user_name),
+               errdetail_log("Validator provided no identity."));
+
+       status = false;
+       goto cleanup;
+   }
+
+   /* Finally, check the user map. */
+   map_status = check_usermap(port->hba->usermap, port->user_name,
+                              MyClientConnectionInfo.authn_id, false);
+   status = (map_status == STATUS_OK);
+
+cleanup:
+
+   /*
+    * Clear and free the validation result from the validator module once
+    * we're done with it.
+    */
+   if (ret->authn_id != NULL)
+       pfree(ret->authn_id);
+   pfree(ret);
+
+   return status;
+}
+
+/*
+ * load_validator_library
+ *
+ * Load the configured validator library in order to perform token validation.
+ * There is no built-in fallback since validation is implementation specific. If
+ * no validator library is configured, or if it fails to load, then error out
+ * since token validation won't be possible.
+ */
+static void
+load_validator_library(const char *libname)
+{
+   OAuthValidatorModuleInit validator_init;
+   MemoryContextCallback *mcb;
+
+   /*
+    * The presence, and validity, of libname has already been established by
+    * check_oauth_validator so we don't need to perform more than Assert
+    * level checking here.
+    */
+   Assert(libname && *libname);
+
+   validator_init = (OAuthValidatorModuleInit)
+       load_external_function(libname, "_PG_oauth_validator_module_init",
+                              false, NULL);
+
+   /*
+    * The validator init function is required since it will set the callbacks
+    * for the validator library.
+    */
+   if (validator_init == NULL)
+       ereport(ERROR,
+               errmsg("%s module \"%s\" must define the symbol %s",
+                      "OAuth validator", libname, "_PG_oauth_validator_module_init"));
+
+   ValidatorCallbacks = (*validator_init) ();
+   Assert(ValidatorCallbacks);
+
+   /*
+    * Check the magic number, to protect against break-glass scenarios where
+    * the ABI must change within a major version. load_external_function()
+    * already checks for compatibility across major versions.
+    */
+   if (ValidatorCallbacks->magic != PG_OAUTH_VALIDATOR_MAGIC)
+       ereport(ERROR,
+               errmsg("%s module \"%s\": magic number mismatch",
+                      "OAuth validator", libname),
+               errdetail("Server has magic number 0x%08X, module has 0x%08X.",
+                         PG_OAUTH_VALIDATOR_MAGIC, ValidatorCallbacks->magic));
+
+   /*
+    * Make sure all required callbacks are present in the ValidatorCallbacks
+    * structure. Right now only the validation callback is required.
+    */
+   if (ValidatorCallbacks->validate_cb == NULL)
+       ereport(ERROR,
+               errmsg("%s module \"%s\" must provide a %s callback",
+                      "OAuth validator", libname, "validate_cb"));
+
+   /* Allocate memory for validator library private state data */
+   validator_module_state = (ValidatorModuleState *) palloc0(sizeof(ValidatorModuleState));
+   validator_module_state->sversion = PG_VERSION_NUM;
+
+   if (ValidatorCallbacks->startup_cb != NULL)
+       ValidatorCallbacks->startup_cb(validator_module_state);
+
+   /* Shut down the library before cleaning up its state. */
+   mcb = palloc0(sizeof(*mcb));
+   mcb->func = shutdown_validator_library;
+
+   MemoryContextRegisterResetCallback(CurrentMemoryContext, mcb);
+}
+
+/*
+ * Call the validator module's shutdown callback, if one is provided. This is
+ * invoked during memory context reset.
+ */
+static void
+shutdown_validator_library(void *arg)
+{
+   if (ValidatorCallbacks->shutdown_cb != NULL)
+       ValidatorCallbacks->shutdown_cb(validator_module_state);
+}
+
+/*
+ * Ensure an OAuth validator named in the HBA is permitted by the configuration.
+ *
+ * If the validator is currently unset and exactly one library is declared in
+ * oauth_validator_libraries, then that library will be used as the validator.
+ * Otherwise the name must be present in the list of oauth_validator_libraries.
+ */
+bool
+check_oauth_validator(HbaLine *hbaline, int elevel, char **err_msg)
+{
+   int         line_num = hbaline->linenumber;
+   const char *file_name = hbaline->sourcefile;
+   char       *rawstring;
+   List       *elemlist = NIL;
+
+   *err_msg = NULL;
+
+   if (oauth_validator_libraries_string[0] == '\0')
+   {
+       ereport(elevel,
+               errcode(ERRCODE_CONFIG_FILE_ERROR),
+               errmsg("oauth_validator_libraries must be set for authentication method %s",
+                      "oauth"),
+               errcontext("line %d of configuration file \"%s\"",
+                          line_num, file_name));
+       *err_msg = psprintf("oauth_validator_libraries must be set for authentication method %s",
+                           "oauth");
+       return false;
+   }
+
+   /* SplitDirectoriesString needs a modifiable copy */
+   rawstring = pstrdup(oauth_validator_libraries_string);
+
+   if (!SplitDirectoriesString(rawstring, ',', &elemlist))
+   {
+       /* syntax error in list */
+       ereport(elevel,
+               errcode(ERRCODE_CONFIG_FILE_ERROR),
+               errmsg("invalid list syntax in parameter \"%s\"",
+                      "oauth_validator_libraries"));
+       *err_msg = psprintf("invalid list syntax in parameter \"%s\"",
+                           "oauth_validator_libraries");
+       goto done;
+   }
+
+   if (!hbaline->oauth_validator)
+   {
+       if (elemlist->length == 1)
+       {
+           hbaline->oauth_validator = pstrdup(linitial(elemlist));
+           goto done;
+       }
+
+       ereport(elevel,
+               errcode(ERRCODE_CONFIG_FILE_ERROR),
+               errmsg("authentication method \"oauth\" requires argument \"validator\" to be set when oauth_validator_libraries contains multiple options"),
+               errcontext("line %d of configuration file \"%s\"",
+                          line_num, file_name));
+       *err_msg = "authentication method \"oauth\" requires argument \"validator\" to be set when oauth_validator_libraries contains multiple options";
+       goto done;
+   }
+
+   foreach_ptr(char, allowed, elemlist)
+   {
+       if (strcmp(allowed, hbaline->oauth_validator) == 0)
+           goto done;
+   }
+
+   ereport(elevel,
+           errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+           errmsg("validator \"%s\" is not permitted by %s",
+                  hbaline->oauth_validator, "oauth_validator_libraries"),
+           errcontext("line %d of configuration file \"%s\"",
+                      line_num, file_name));
+   *err_msg = psprintf("validator \"%s\" is not permitted by %s",
+                       hbaline->oauth_validator, "oauth_validator_libraries");
+
+done:
+   list_free_deep(elemlist);
+   pfree(rawstring);
+
+   return (*err_msg == NULL);
+}
index d6ef32cc823eef5e2323051607bbb973b409cd60..0f65014e64fd126706b406762ca961c2af4634b4 100644 (file)
@@ -29,6 +29,7 @@
 #include "libpq/auth.h"
 #include "libpq/crypt.h"
 #include "libpq/libpq.h"
+#include "libpq/oauth.h"
 #include "libpq/pqformat.h"
 #include "libpq/sasl.h"
 #include "libpq/scram.h"
@@ -45,7 +46,6 @@
  */
 static void auth_failed(Port *port, int status, const char *logdetail);
 static char *recv_password_packet(Port *port);
-static void set_authn_id(Port *port, const char *id);
 
 
 /*----------------------------------------------------------------
@@ -289,6 +289,9 @@ auth_failed(Port *port, int status, const char *logdetail)
        case uaRADIUS:
            errstr = gettext_noop("RADIUS authentication failed for user \"%s\"");
            break;
+       case uaOAuth:
+           errstr = gettext_noop("OAuth bearer authentication failed for user \"%s\"");
+           break;
        default:
            errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method");
            break;
@@ -324,7 +327,7 @@ auth_failed(Port *port, int status, const char *logdetail)
  * lifetime of MyClientConnectionInfo, so it is safe to pass a string that is
  * managed by an external library.
  */
-static void
+void
 set_authn_id(Port *port, const char *id)
 {
    Assert(id);
@@ -611,6 +614,9 @@ ClientAuthentication(Port *port)
        case uaTrust:
            status = STATUS_OK;
            break;
+       case uaOAuth:
+           status = CheckSASLAuth(&pg_be_oauth_mech, port, NULL, NULL);
+           break;
    }
 
    if ((status == STATUS_OK && port->hba->clientcert == clientCertFull)
index 510c9ffc6d79a27aaeb617ac914d81d8b7b548a5..332fad278351c62b0dcbb3ea4e7fff246a87a742 100644 (file)
@@ -32,6 +32,7 @@
 #include "libpq/hba.h"
 #include "libpq/ifaddr.h"
 #include "libpq/libpq-be.h"
+#include "libpq/oauth.h"
 #include "postmaster/postmaster.h"
 #include "regex/regex.h"
 #include "replication/walsender.h"
@@ -114,7 +115,8 @@ static const char *const UserAuthName[] =
    "ldap",
    "cert",
    "radius",
-   "peer"
+   "peer",
+   "oauth",
 };
 
 /*
@@ -1747,6 +1749,8 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 #endif
    else if (strcmp(token->string, "radius") == 0)
        parsedline->auth_method = uaRADIUS;
+   else if (strcmp(token->string, "oauth") == 0)
+       parsedline->auth_method = uaOAuth;
    else
    {
        ereport(elevel,
@@ -2039,6 +2043,36 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
        parsedline->clientcert = clientCertFull;
    }
 
+   /*
+    * Enforce proper configuration of OAuth authentication.
+    */
+   if (parsedline->auth_method == uaOAuth)
+   {
+       MANDATORY_AUTH_ARG(parsedline->oauth_scope, "scope", "oauth");
+       MANDATORY_AUTH_ARG(parsedline->oauth_issuer, "issuer", "oauth");
+
+       /* Ensure a validator library is set and permitted by the config. */
+       if (!check_oauth_validator(parsedline, elevel, err_msg))
+           return NULL;
+
+       /*
+        * Supplying a usermap combined with the option to skip usermapping is
+        * nonsensical and indicates a configuration error.
+        */
+       if (parsedline->oauth_skip_usermap && parsedline->usermap != NULL)
+       {
+           ereport(elevel,
+                   errcode(ERRCODE_CONFIG_FILE_ERROR),
+           /* translator: strings are replaced with hba options */
+                   errmsg("%s cannot be used in combination with %s",
+                          "map", "delegate_ident_mapping"),
+                   errcontext("line %d of configuration file \"%s\"",
+                              line_num, file_name));
+           *err_msg = "map cannot be used in combination with delegate_ident_mapping";
+           return NULL;
+       }
+   }
+
    return parsedline;
 }
 
@@ -2066,8 +2100,9 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
            hbaline->auth_method != uaPeer &&
            hbaline->auth_method != uaGSS &&
            hbaline->auth_method != uaSSPI &&
-           hbaline->auth_method != uaCert)
-           INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
+           hbaline->auth_method != uaCert &&
+           hbaline->auth_method != uaOAuth)
+           INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, cert, and oauth"));
        hbaline->usermap = pstrdup(val);
    }
    else if (strcmp(name, "clientcert") == 0)
@@ -2450,6 +2485,29 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
        hbaline->radiusidentifiers = parsed_identifiers;
        hbaline->radiusidentifiers_s = pstrdup(val);
    }
+   else if (strcmp(name, "issuer") == 0)
+   {
+       REQUIRE_AUTH_OPTION(uaOAuth, "issuer", "oauth");
+       hbaline->oauth_issuer = pstrdup(val);
+   }
+   else if (strcmp(name, "scope") == 0)
+   {
+       REQUIRE_AUTH_OPTION(uaOAuth, "scope", "oauth");
+       hbaline->oauth_scope = pstrdup(val);
+   }
+   else if (strcmp(name, "validator") == 0)
+   {
+       REQUIRE_AUTH_OPTION(uaOAuth, "validator", "oauth");
+       hbaline->oauth_validator = pstrdup(val);
+   }
+   else if (strcmp(name, "delegate_ident_mapping") == 0)
+   {
+       REQUIRE_AUTH_OPTION(uaOAuth, "delegate_ident_mapping", "oauth");
+       if (strcmp(val, "1") == 0)
+           hbaline->oauth_skip_usermap = true;
+       else
+           hbaline->oauth_skip_usermap = false;
+   }
    else
    {
        ereport(elevel,
index 0f0421037e4815b6c578e032d9b2bf367a79ff21..31aa2faae1eccf98fd06aa58e231fe1e695dd702 100644 (file)
@@ -1,6 +1,7 @@
 # Copyright (c) 2022-2025, PostgreSQL Global Development Group
 
 backend_sources += files(
+  'auth-oauth.c',
   'auth-sasl.c',
   'auth-scram.c',
   'auth.c',
index bad13497a3465d7b6aa7bad8f278aefd1a9d912b..b64c8dea97c3138822cd9bc4dbc35b71718f1138 100644 (file)
@@ -53,8 +53,8 @@
 # directly connected to.
 #
 # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256",
-# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert".
-# Note that "password" sends passwords in clear text; "md5" or
+# "gss", "sspi", "ident", "peer", "pam", "oauth", "ldap", "radius" or
+# "cert".  Note that "password" sends passwords in clear text; "md5" or
 # "scram-sha-256" are preferred since they send encrypted passwords.
 #
 # OPTIONS are a set of options for the authentication in the format
index 03c38e8c451c756978ddceb96266e600b0347904..b62c3d944cf1a92a2c528e9852e5710211dca226 100644 (file)
@@ -152,6 +152,25 @@ get_hba_options(HbaLine *hba)
                CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
    }
 
+   if (hba->auth_method == uaOAuth)
+   {
+       if (hba->oauth_issuer)
+           options[noptions++] =
+               CStringGetTextDatum(psprintf("issuer=%s", hba->oauth_issuer));
+
+       if (hba->oauth_scope)
+           options[noptions++] =
+               CStringGetTextDatum(psprintf("scope=%s", hba->oauth_scope));
+
+       if (hba->oauth_validator)
+           options[noptions++] =
+               CStringGetTextDatum(psprintf("validator=%s", hba->oauth_validator));
+
+       if (hba->oauth_skip_usermap)
+           options[noptions++] =
+               CStringGetTextDatum(psprintf("delegate_ident_mapping=true"));
+   }
+
    /* If you add more options, consider increasing MAX_HBA_OPTIONS. */
    Assert(noptions <= MAX_HBA_OPTIONS);
 
index 3cde94a175959078a895522fa705939ac72e8076..03a6dd491540f83cfb442084965d00906ef1de15 100644 (file)
@@ -49,6 +49,7 @@
 #include "jit/jit.h"
 #include "libpq/auth.h"
 #include "libpq/libpq.h"
+#include "libpq/oauth.h"
 #include "libpq/scram.h"
 #include "nodes/queryjumble.h"
 #include "optimizer/cost.h"
@@ -4873,6 +4874,17 @@ struct config_string ConfigureNamesString[] =
        check_restrict_nonsystem_relation_kind, assign_restrict_nonsystem_relation_kind, NULL
    },
 
+   {
+       {"oauth_validator_libraries", PGC_SIGHUP, CONN_AUTH_AUTH,
+           gettext_noop("Lists libraries that may be called to validate OAuth v2 bearer tokens."),
+           NULL,
+           GUC_LIST_INPUT | GUC_LIST_QUOTE | GUC_SUPERUSER_ONLY
+       },
+       &oauth_validator_libraries_string,
+       "",
+       NULL, NULL, NULL
+   },
+
    /* End-of-list marker */
    {
        {NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
index 415f253096c13cf6bb51f08116a95a2ff43a9b99..5362ff805195f9a059c412dbbe6027824088f446 100644 (file)
 #ssl_passphrase_command = ''
 #ssl_passphrase_command_supports_reload = off
 
+# OAuth
+#oauth_validator_libraries = ''    # comma-separated list of trusted validator modules
+
 
 #------------------------------------------------------------------------------
 # RESOURCE USAGE (except WAL)
diff --git a/src/include/common/oauth-common.h b/src/include/common/oauth-common.h
new file mode 100644 (file)
index 0000000..5fb559d
--- /dev/null
@@ -0,0 +1,19 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth-common.h
+ *     Declarations for helper functions used for OAuth/OIDC authentication
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/oauth-common.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef OAUTH_COMMON_H
+#define OAUTH_COMMON_H
+
+/* Name of SASL mechanism per IANA */
+#define OAUTHBEARER_NAME "OAUTHBEARER"
+
+#endif                         /* OAUTH_COMMON_H */
index 902c5f6de326bc406791cc6c10e4123bd35222ee..25b5742068f1c24188793888728ea4c4b70eb971 100644 (file)
@@ -39,6 +39,7 @@ extern PGDLLIMPORT bool pg_gss_accept_delegation;
 extern void ClientAuthentication(Port *port);
 extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
                            int extralen);
+extern void set_authn_id(Port *port, const char *id);
 
 /* Hook for plugins to get control in ClientAuthentication() */
 typedef void (*ClientAuthentication_hook_type) (Port *, int);
index b20d0051f7d6a3e5f4fbef4b35de610662cf8f2e..3657f182db3e3ee3a4ca52bfe53738a79a84082a 100644 (file)
@@ -39,7 +39,8 @@ typedef enum UserAuth
    uaCert,
    uaRADIUS,
    uaPeer,
-#define USER_AUTH_LAST uaPeer  /* Must be last value of this enum */
+   uaOAuth,
+#define USER_AUTH_LAST uaOAuth /* Must be last value of this enum */
 } UserAuth;
 
 /*
@@ -135,6 +136,10 @@ typedef struct HbaLine
    char       *radiusidentifiers_s;
    List       *radiusports;
    char       *radiusports_s;
+   char       *oauth_issuer;
+   char       *oauth_scope;
+   char       *oauth_validator;
+   bool        oauth_skip_usermap;
 } HbaLine;
 
 typedef struct IdentLine
diff --git a/src/include/libpq/oauth.h b/src/include/libpq/oauth.h
new file mode 100644 (file)
index 0000000..2c6892f
--- /dev/null
@@ -0,0 +1,101 @@
+/*-------------------------------------------------------------------------
+ *
+ * oauth.h
+ *   Interface to libpq/auth-oauth.c
+ *
+ * Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/libpq/oauth.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_OAUTH_H
+#define PG_OAUTH_H
+
+#include "libpq/libpq-be.h"
+#include "libpq/sasl.h"
+
+extern PGDLLIMPORT char *oauth_validator_libraries_string;
+
+typedef struct ValidatorModuleState
+{
+   /* Holds the server's PG_VERSION_NUM. Reserved for future extensibility. */
+   int         sversion;
+
+   /*
+    * Private data pointer for use by a validator module. This can be used to
+    * store state for the module that will be passed to each of its
+    * callbacks.
+    */
+   void       *private_data;
+} ValidatorModuleState;
+
+typedef struct ValidatorModuleResult
+{
+   /*
+    * Should be set to true if the token carries sufficient permissions for
+    * the bearer to connect.
+    */
+   bool        authorized;
+
+   /*
+    * If the token authenticates the user, this should be set to a palloc'd
+    * string containing the SYSTEM_USER to use for HBA mapping. Consider
+    * setting this even if result->authorized is false so that DBAs may use
+    * the logs to match end users to token failures.
+    *
+    * This is required if the module is not configured for ident mapping
+    * delegation. See the validator module documentation for details.
+    */
+   char       *authn_id;
+} ValidatorModuleResult;
+
+/*
+ * Validator module callbacks
+ *
+ * These callback functions should be defined by validator modules and returned
+ * via _PG_oauth_validator_module_init().  ValidatorValidateCB is the only
+ * required callback. For more information about the purpose of each callback,
+ * refer to the OAuth validator modules documentation.
+ */
+typedef void (*ValidatorStartupCB) (ValidatorModuleState *state);
+typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
+typedef&