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
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
--with-gssapi
--with-icu
--with-ldap
+ --with-libcurl
--with-libxml
--with-libxslt
--with-llvm
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
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
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
XML2_CFLAGS
XML2_CONFIG
with_libxml
+LIBCURL_LIBS
+LIBCURL_CFLAGS
+with_libcurl
with_uuid
with_readline
with_systemd
with_libedit_preferred
with_uuid
with_ossp_uuid
+with_libcurl
with_libxml
with_libxslt
with_system_tzdata
PKG_CONFIG_LIBDIR
ICU_CFLAGS
ICU_LIBS
+LIBCURL_CFLAGS
+LIBCURL_LIBS
XML2_CONFIG
XML2_CFLAGS
XML2_LIBS
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
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
+#
+# 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
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
#
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'], [],
</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>
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>
</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>
</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>
<!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">
</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>
</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>
</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>
</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>
</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>
<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>
--- /dev/null
+<!-- 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>
&logicaldecoding;
&replication-origins;
&archive-modules;
+ &oauth-validators;
</part>
<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>
<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
<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>
</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">
</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
+###############################################################
+# 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
###############################################################
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,
]
'gss': gssapi,
'icu': icu,
'ldap': ldap,
+ 'libcurl': libcurl,
'libxml': libxml,
'libxslt': libxslt,
'llvm': llvm,
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')
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@
# be-fsstubs is here for historical reasons, probably belongs elsewhere
OBJS = \
+ auth-oauth.o \
auth-sasl.o \
auth-scram.o \
auth.o \
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * 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);
+}
#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"
*/
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);
/*----------------------------------------------------------------
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;
* 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);
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)
#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"
"ldap",
"cert",
"radius",
- "peer"
+ "peer",
+ "oauth",
};
/*
#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,
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;
}
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)
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,
# Copyright (c) 2022-2025, PostgreSQL Global Development Group
backend_sources += files(
+ 'auth-oauth.c',
'auth-sasl.c',
'auth-scram.c',
'auth.c',
# 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
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);
#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"
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
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+# OAuth
+#oauth_validator_libraries = '' # comma-separated list of trusted validator modules
+
#------------------------------------------------------------------------------
# RESOURCE USAGE (except WAL)
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * 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 */
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);
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;
/*
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
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * 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&