From 229f602d5c801cd602fe61b23c87f4f769497f64 Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Tue, 13 Apr 2021 10:27:27 -0700 Subject: [PATCH v18 4/9] libpq: add OAUTHBEARER SASL mechanism DO NOT USE THIS PROOF OF CONCEPT IN PRODUCTION. Implement OAUTHBEARER (RFC 7628) and OAuth 2.0 Device Authorization Grants (RFC 8628) on the client side. When speaking to a OAuth-enabled server, it looks a bit like this: $ psql 'host=example.org oauth_client_id=f02c6361-0635-...' Visit https://oauth.example.org/login and enter the code: FPQ2-M4BG The OAuth issuer must support device authorization. No other OAuth flows are currently implemented (but clients may provide their own flows; see below). The client implementation requires either libcurl or libiddawc and their development headers. Pass `curl` or `iddawc` to --with-oauth/-Doauth during configuration. Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks! = PQauthDataHook = Clients may override two pieces of OAuth handling using the new PQsetAuthDataHook(): - PQAUTHDATA_PROMPT_OAUTH_DEVICE: replaces the default user prompt to standard error when using the builtin device authorization flow - PQAUTHDATA_OAUTH_BEARER_TOKEN: replaces the entire OAuth flow with a custom asynchronous implementation In general, a hook implementation should examine the incoming `type` 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 PQgetAuthDataHook()). Otherwise, it should return an integer > 0 and follow the authdata-specific instructions. Returning an integer < 0 signals an error condition and abandons the connection attempt. == PQAUTHDATA_PROMPT_OAUTH_DEVICE == The hook should display the device prompt (URL + code) using whatever method it prefers. == PQAUTHDATA_OAUTH_BEARER_TOKEN == 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. See the documentation for PQoauthBearerRequest. Several TODOs: - don't retry forever if the server won't accept our token - perform several sanity checks on the OAuth issuer's responses - handle cases where the client has been set up with an issuer and scope, but the Postgres server wants to use something different - improve error debuggability during the OAuth handshake - fix libcurl initialization thread-safety - harden the libcurl flow implementation - figure out pgsocket/int difference on Windows - fix intermittent failure in the cleanup callback tests (race condition?) - support require_auth - ...and more. Co-authored-by: Daniel Gustafsson --- configure | 186 ++ configure.ac | 37 + meson.build | 45 + meson_options.txt | 4 + src/Makefile.global.in | 1 + src/include/common/oauth-common.h | 19 + src/include/pg_config.h.in | 18 + src/interfaces/libpq/Makefile | 12 +- src/interfaces/libpq/exports.txt | 3 + src/interfaces/libpq/fe-auth-oauth-curl.c | 1982 +++++++++++++++++++ src/interfaces/libpq/fe-auth-oauth-iddawc.c | 319 +++ src/interfaces/libpq/fe-auth-oauth.c | 659 ++++++ src/interfaces/libpq/fe-auth-oauth.h | 42 + src/interfaces/libpq/fe-auth-sasl.h | 10 +- src/interfaces/libpq/fe-auth-scram.c | 6 +- src/interfaces/libpq/fe-auth.c | 105 +- src/interfaces/libpq/fe-auth.h | 9 +- src/interfaces/libpq/fe-connect.c | 85 +- src/interfaces/libpq/fe-misc.c | 7 +- src/interfaces/libpq/libpq-fe.h | 77 +- src/interfaces/libpq/libpq-int.h | 14 + src/interfaces/libpq/meson.build | 9 + src/makefiles/meson.build | 1 + src/tools/pgindent/typedefs.list | 10 + 24 files changed, 3633 insertions(+), 27 deletions(-) create mode 100644 src/include/common/oauth-common.h create mode 100644 src/interfaces/libpq/fe-auth-oauth-curl.c create mode 100644 src/interfaces/libpq/fe-auth-oauth-iddawc.c create mode 100644 src/interfaces/libpq/fe-auth-oauth.c create mode 100644 src/interfaces/libpq/fe-auth-oauth.h diff --git a/configure b/configure index 46859a4244..142d49d7b6 100755 --- a/configure +++ b/configure @@ -712,6 +712,7 @@ with_uuid with_readline with_systemd with_selinux +with_oauth with_ldap with_krb_srvnam krb_srvtab @@ -858,6 +859,7 @@ with_krb_srvnam with_pam with_bsd_auth with_ldap +with_oauth with_bonjour with_selinux with_systemd @@ -1568,6 +1570,7 @@ Optional Packages: --with-pam build with PAM support --with-bsd-auth build with BSD Authentication support --with-ldap build with LDAP support + --with-oauth=LIB use LIB for OAuth 2.0 support (curl, iddawc) --with-bonjour build with Bonjour support --with-selinux build with SELinux support --with-systemd build with systemd support @@ -8485,6 +8488,59 @@ $as_echo "$with_ldap" >&6; } +# +# OAuth 2.0 +# +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether to build with OAuth support" >&5 +$as_echo_n "checking whether to build with OAuth support... " >&6; } + + + +# Check whether --with-oauth was given. +if test "${with_oauth+set}" = set; then : + withval=$with_oauth; + case $withval in + yes) + as_fn_error $? "argument required for --with-oauth option" "$LINENO" 5 + ;; + no) + as_fn_error $? "argument required for --with-oauth option" "$LINENO" 5 + ;; + *) + + ;; + esac + +fi + + +if test x"$with_oauth" = x"" ; then + with_oauth=no +fi + +if test x"$with_oauth" = x"curl"; then + +$as_echo "#define USE_OAUTH 1" >>confdefs.h + + +$as_echo "#define USE_OAUTH_CURL 1" >>confdefs.h + +elif test x"$with_oauth" = x"iddawc"; then + +$as_echo "#define USE_OAUTH 1" >>confdefs.h + + +$as_echo "#define USE_OAUTH_IDDAWC 1" >>confdefs.h + +elif test x"$with_oauth" != x"no"; then + as_fn_error $? "--with-oauth must specify curl or iddawc" "$LINENO" 5 +fi + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_oauth" >&5 +$as_echo "$with_oauth" >&6; } + + + # # Bonjour # @@ -13037,6 +13093,116 @@ fi +if test "$with_oauth" = curl ; then + { $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' is required for --with-oauth=curl" "$LINENO" 5 +fi + +elif test "$with_oauth" = iddawc ; then + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for i_init_session in -liddawc" >&5 +$as_echo_n "checking for i_init_session in -liddawc... " >&6; } +if ${ac_cv_lib_iddawc_i_init_session+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-liddawc $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 i_init_session (); +int +main () +{ +return i_init_session (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_iddawc_i_init_session=yes +else + ac_cv_lib_iddawc_i_init_session=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_iddawc_i_init_session" >&5 +$as_echo "$ac_cv_lib_iddawc_i_init_session" >&6; } +if test "x$ac_cv_lib_iddawc_i_init_session" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBIDDAWC 1 +_ACEOF + + LIBS="-liddawc $LIBS" + +else + as_fn_error $? "library 'iddawc' is required for --with-oauth=iddawc" "$LINENO" 5 +fi + + # Check for an older spelling of i_get_openid_config + for ac_func in i_load_openid_config +do : + ac_fn_c_check_func "$LINENO" "i_load_openid_config" "ac_cv_func_i_load_openid_config" +if test "x$ac_cv_func_i_load_openid_config" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_I_LOAD_OPENID_CONFIG 1 +_ACEOF + +fi +done + +fi + # for contrib/sepgsql if test "$with_selinux" = yes; then { $as_echo "$as_me:${as_lineno-$LINENO}: checking for security_compute_create_name in -lselinux" >&5 @@ -14062,6 +14228,26 @@ fi done +fi + +if test "$with_oauth" = curl; 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 is required for OAuth" "$LINENO" 5 +fi + + +elif test "$with_oauth" = iddawc; then + ac_fn_c_check_header_mongrel "$LINENO" "iddawc.h" "ac_cv_header_iddawc_h" "$ac_includes_default" +if test "x$ac_cv_header_iddawc_h" = xyes; then : + +else + as_fn_error $? "header file is required for OAuth" "$LINENO" 5 +fi + + fi if test "$PORTNAME" = "win32" ; then diff --git a/configure.ac b/configure.ac index 88b75a7696..a4c2e558f9 100644 --- a/configure.ac +++ b/configure.ac @@ -927,6 +927,29 @@ AC_MSG_RESULT([$with_ldap]) AC_SUBST(with_ldap) +# +# OAuth 2.0 +# +AC_MSG_CHECKING([whether to build with OAuth support]) +PGAC_ARG_REQ(with, oauth, [LIB], [use LIB for OAuth 2.0 support (curl, iddawc)]) +if test x"$with_oauth" = x"" ; then + with_oauth=no +fi + +if test x"$with_oauth" = x"curl"; then + AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)]) + AC_DEFINE([USE_OAUTH_CURL], 1, [Define to 1 to use libcurl for OAuth support.]) +elif test x"$with_oauth" = x"iddawc"; then + AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)]) + AC_DEFINE([USE_OAUTH_IDDAWC], 1, [Define to 1 to use libiddawc for OAuth support.]) +elif test x"$with_oauth" != x"no"; then + AC_MSG_ERROR([--with-oauth must specify curl or iddawc]) +fi + +AC_MSG_RESULT([$with_oauth]) +AC_SUBST(with_oauth) + + # # Bonjour # @@ -1423,6 +1446,14 @@ fi AC_SUBST(LDAP_LIBS_FE) AC_SUBST(LDAP_LIBS_BE) +if test "$with_oauth" = curl ; then + AC_CHECK_LIB(curl, curl_multi_init, [], [AC_MSG_ERROR([library 'curl' is required for --with-oauth=curl])]) +elif test "$with_oauth" = iddawc ; then + AC_CHECK_LIB(iddawc, i_init_session, [], [AC_MSG_ERROR([library 'iddawc' is required for --with-oauth=iddawc])]) + # Check for an older spelling of i_get_openid_config + AC_CHECK_FUNCS([i_load_openid_config]) +fi + # for contrib/sepgsql if test "$with_selinux" = yes; then AC_CHECK_LIB(selinux, security_compute_create_name, [], @@ -1614,6 +1645,12 @@ elif test "$with_uuid" = ossp ; then [AC_MSG_ERROR([header file or is required for OSSP UUID])])]) fi +if test "$with_oauth" = curl; then + AC_CHECK_HEADER(curl/curl.h, [], [AC_MSG_ERROR([header file is required for OAuth])]) +elif test "$with_oauth" = iddawc; then + AC_CHECK_HEADER(iddawc.h, [], [AC_MSG_ERROR([header file is required for OAuth])]) +fi + if test "$PORTNAME" = "win32" ; then AC_CHECK_HEADERS(crtdefs.h) fi diff --git a/meson.build b/meson.build index a198eca25d..43066a017f 100644 --- a/meson.build +++ b/meson.build @@ -830,6 +830,49 @@ endif +############################################################### +# Library: oauth +############################################################### + +oauth = not_found_dep +oauth_library = 'none' +oauthopt = get_option('oauth') + +if oauthopt == 'auto' and auto_features.disabled() + oauthopt = 'none' +endif + +if oauthopt in ['auto', 'curl'] + oauth = dependency('libcurl', required: (oauthopt == 'curl')) + + if oauth.found() + oauth_library = 'curl' + cdata.set('USE_OAUTH', 1) + cdata.set('USE_OAUTH_CURL', 1) + endif +endif + +if not oauth.found() and oauthopt in ['auto', 'iddawc'] + oauth = dependency('libiddawc', required: (oauthopt == 'iddawc')) + + if oauth.found() + oauth_library = 'iddawc' + cdata.set('USE_OAUTH', 1) + cdata.set('USE_OAUTH_IDDAWC', 1) + + # Check for an older spelling of i_get_openid_config + if cc.has_function('i_load_openid_config', + dependencies: oauth, args: test_c_args) + cdata.set('HAVE_I_LOAD_OPENID_CONFIG', 1) + endif + endif +endif + +if oauthopt == 'auto' and auto_features.enabled() and not oauth.found() + error('no OAuth implementation library found') +endif + + ############################################################### # Library: Tcl (for pltcl) # @@ -2834,6 +2877,7 @@ libpq_deps += [ gssapi, ldap_r, libintl, + oauth, ssl, ] @@ -3435,6 +3479,7 @@ if meson.version().version_compare('>=0.57') 'llvm': llvm, 'lz4': lz4, 'nls': libintl, + 'oauth': oauth, 'openssl': ssl, 'pam': pam, 'plperl': perl_dep, diff --git a/meson_options.txt b/meson_options.txt index 249ecc5ffd..f54f7fd717 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -121,6 +121,10 @@ option('lz4', type: 'feature', value: 'auto', option('nls', type: 'feature', value: 'auto', description: 'Native language support') +option('oauth', type : 'combo', choices : ['auto', 'none', 'curl', 'iddawc'], + value: 'auto', + description: 'use LIB for OAuth 2.0 support (curl, iddawc)') + option('pam', type: 'feature', value: 'auto', description: 'PAM support') diff --git a/src/Makefile.global.in b/src/Makefile.global.in index 8b3f8c24e0..79b3647834 100644 --- a/src/Makefile.global.in +++ b/src/Makefile.global.in @@ -193,6 +193,7 @@ with_ldap = @with_ldap@ with_libxml = @with_libxml@ with_libxslt = @with_libxslt@ with_llvm = @with_llvm@ +with_oauth = @with_oauth@ with_system_tzdata = @with_system_tzdata@ with_uuid = @with_uuid@ with_zlib = @with_zlib@ diff --git a/src/include/common/oauth-common.h b/src/include/common/oauth-common.h new file mode 100644 index 0000000000..5ff3488bfb --- /dev/null +++ b/src/include/common/oauth-common.h @@ -0,0 +1,19 @@ +/*------------------------------------------------------------------------- + * + * oauth-common.h + * Declarations for helper functions used for OAuth/OIDC authentication + * + * Portions Copyright (c) 1996-2021, 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 */ diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 07e73567dc..f470c77669 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -231,6 +231,9 @@ /* Define to 1 if __builtin_constant_p(x) implies "i"(x) acceptance. */ #undef HAVE_I_CONSTRAINT__BUILTIN_CONSTANT_P +/* Define to 1 if you have the `i_load_openid_config' function. */ +#undef HAVE_I_LOAD_OPENID_CONFIG + /* Define to 1 if you have the `kqueue' function. */ #undef HAVE_KQUEUE @@ -243,6 +246,12 @@ /* Define to 1 if you have the `crypto' library (-lcrypto). */ #undef HAVE_LIBCRYPTO +/* Define to 1 if you have the `curl' library (-lcurl). */ +#undef HAVE_LIBCURL + +/* Define to 1 if you have the `iddawc' library (-liddawc). */ +#undef HAVE_LIBIDDAWC + /* Define to 1 if you have the `ldap' library (-lldap). */ #undef HAVE_LIBLDAP @@ -711,6 +720,15 @@ /* Define to select named POSIX semaphores. */ #undef USE_NAMED_POSIX_SEMAPHORES +/* Define to 1 to build with OAuth 2.0 support. (--with-oauth) */ +#undef USE_OAUTH + +/* Define to 1 to use libcurl for OAuth support. */ +#undef USE_OAUTH_CURL + +/* Define to 1 to use libiddawc for OAuth support. */ +#undef USE_OAUTH_IDDAWC + /* Define to 1 to build with OpenSSL support. (--with-ssl=openssl) */ #undef USE_OPENSSL diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index fe2af575c5..86aada810f 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -61,6 +61,16 @@ OBJS += \ fe-secure-gssapi.o endif +ifneq ($(with_oauth),no) +OBJS += fe-auth-oauth.o + +ifeq ($(with_oauth),iddawc) +OBJS += fe-auth-oauth-iddawc.o +else +OBJS += fe-auth-oauth-curl.o +endif +endif + ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif @@ -79,7 +89,7 @@ endif # that are built correctly for use in a shlib. SHLIB_LINK_INTERNAL = -lpgcommon_shlib -lpgport_shlib ifneq ($(PORTNAME), win32) -SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) +SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi_krb5 -lgss -lgssapi -lssl -lcurl -liddawc -lsocket -lnsl -lresolv -lintl -lm, $(LIBS)) $(LDAP_LIBS_FE) $(PTHREAD_LIBS) else SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE) endif diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index 088592deb1..0f8f5e3125 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -193,3 +193,6 @@ PQsendClosePrepared 190 PQsendClosePortal 191 PQchangePassword 192 PQsendPipelineSync 193 +PQsetAuthDataHook 194 +PQgetAuthDataHook 195 +PQdefaultAuthDataHook 196 diff --git a/src/interfaces/libpq/fe-auth-oauth-curl.c b/src/interfaces/libpq/fe-auth-oauth-curl.c new file mode 100644 index 0000000000..0504f96e4e --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth-curl.c @@ -0,0 +1,1982 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth-curl.c + * The libcurl implementation of OAuth/OIDC authentication. + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-oauth-curl.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include +#include +#ifdef HAVE_SYS_EPOLL_H +#include +#include +#endif +#ifdef HAVE_SYS_EVENT_H +#include +#endif +#include + +#include "common/jsonapi.h" +#include "fe-auth.h" +#include "fe-auth-oauth.h" +#include "libpq-int.h" +#include "mb/pg_wchar.h" + +/* + * Parsed JSON Representations + * + * As a general rule, we parse and cache only the fields we're currently using. + * When adding new fields, ensure the corresponding free_*() function is updated + * too. + */ + +/* + * The OpenID Provider configuration (alternatively named "authorization server + * metadata") jointly described by OpenID Connect Discovery 1.0 and RFC 8414: + * + * https://openid.net/specs/openid-connect-discovery-1_0.html + * https://www.rfc-editor.org/rfc/rfc8414#section-3.2 + */ +struct provider +{ + char *issuer; + char *token_endpoint; + char *device_authorization_endpoint; + struct curl_slist *grant_types_supported; +}; + +static void +free_provider(struct provider *provider) +{ + free(provider->issuer); + free(provider->token_endpoint); + free(provider->device_authorization_endpoint); + curl_slist_free_all(provider->grant_types_supported); +} + +/* + * The Device Authorization response, described by RFC 8628: + * + * https://www.rfc-editor.org/rfc/rfc8628#section-3.2 + */ +struct device_authz +{ + char *device_code; + char *user_code; + char *verification_uri; + char *interval_str; + + /* Fields below are parsed from the corresponding string above. */ + int interval; +}; + +static void +free_device_authz(struct device_authz *authz) +{ + free(authz->device_code); + free(authz->user_code); + free(authz->verification_uri); + free(authz->interval_str); +} + +/* + * The Token Endpoint error response, as described by RFC 6749: + * + * https://www.rfc-editor.org/rfc/rfc6749#section-5.2 + * + * Note that this response type can also be returned from the Device + * Authorization Endpoint. + */ +struct token_error +{ + char *error; + char *error_description; +}; + +static void +free_token_error(struct token_error *err) +{ + free(err->error); + free(err->error_description); +} + +/* + * The Access Token response, as described by RFC 6749: + * + * https://www.rfc-editor.org/rfc/rfc6749#section-4.1.4 + * + * During the Device Authorization flow, several temporary errors are expected + * as part of normal operation. To make it easy to handle these in the happy + * path, this contains an embedded token_error that is filled in if needed. + */ +struct token +{ + /* for successful responses */ + char *access_token; + char *token_type; + + /* for error responses */ + struct token_error err; +}; + +static void +free_token(struct token *tok) +{ + free(tok->access_token); + free(tok->token_type); + free_token_error(&tok->err); +} + +/* + * Asynchronous State + */ + +/* States for the overall async machine. */ +typedef enum +{ + OAUTH_STEP_INIT, + OAUTH_STEP_DISCOVERY, + OAUTH_STEP_DEVICE_AUTHORIZATION, + OAUTH_STEP_TOKEN_REQUEST, + OAUTH_STEP_WAIT_INTERVAL, +} OAuthStep; + +/* + * The async_ctx holds onto state that needs to persist across multiple calls to + * pg_fe_run_oauth_flow(). Almost everything interacts with this in some way. + */ +struct async_ctx +{ + OAuthStep step; /* where are we in the flow? */ + +#ifdef HAVE_SYS_EPOLL_H + int timerfd; /* a timerfd for signaling async timeouts */ +#endif + pgsocket mux; /* the multiplexer socket containing all + * descriptors tracked by cURL, plus the + * timerfd */ + CURLM *curlm; /* top-level multi handle for cURL operations */ + CURL *curl; /* the (single) easy handle for serial + * requests */ + + struct curl_slist *headers; /* common headers for all requests */ + PQExpBufferData work_data; /* scratch buffer for general use (remember to + * clear out prior contents first!) */ + + /*------ + * Since a single logical operation may stretch across multiple calls to + * our entry point, errors have three parts: + * + * - errctx: an optional static string, describing the global operation + * currently in progress. It'll be translated for you. + * + * - errbuf: contains the actual error message. Generally speaking, use + * actx_error[_str] to manipulate this. This must be filled + * with something useful on an error. + * + * - curl_err: an optional static error buffer used by cURL to put + * detailed information about failures. Unfortunately + * untranslatable. + * + * These pieces will be combined into a single error message looking + * something like the following, with errctx and/or curl_err omitted when + * absent: + * + * connection to server ... failed: errctx: errbuf (curl_err) + */ + const char *errctx; /* not freed; must point to static allocation */ + PQExpBufferData errbuf; + char curl_err[CURL_ERROR_SIZE]; + + /* + * These documents need to survive over multiple calls, and are therefore + * cached directly in the async_ctx. + */ + struct provider provider; + struct device_authz authz; + + bool user_prompted; /* have we already sent the authz prompt? */ +}; + +/* + * Frees the async_ctx, which is stored directly on the PGconn. This is called + * during pqDropConnection() so that we don't leak resources even if + * PQconnectPoll() never calls us back. + * + * TODO: we should probably call this at the end of a successful authentication, + * too, to proactively free up resources. + */ +static void +free_curl_async_ctx(PGconn *conn, void *ctx) +{ + struct async_ctx *actx = ctx; + + Assert(actx); /* oauth_free() shouldn't call us otherwise */ + + /* + * TODO: in general, none of the error cases below should ever happen if + * we have no bugs above. But if we do hit them, surfacing those errors + * somehow might be the only way to have a chance to debug them. What's + * the best way to do that? Assertions? Spraying messages on stderr? + * Bubbling an error code to the top? Appending to the connection's error + * message only helps if the bug caused a connection failure; otherwise + * it'll be buried... + */ + + if (actx->curlm && actx->curl) + { + CURLMcode err = curl_multi_remove_handle(actx->curlm, actx->curl); + + if (err) + libpq_append_conn_error(conn, + "cURL easy handle removal failed: %s", + curl_multi_strerror(err)); + } + + if (actx->curl) + { + /* + * curl_multi_cleanup() doesn't free any associated easy handles; we + * need to do that separately. We only ever have one easy handle per + * multi handle. + */ + curl_easy_cleanup(actx->curl); + } + + if (actx->curlm) + { + CURLMcode err = curl_multi_cleanup(actx->curlm); + + if (err) + libpq_append_conn_error(conn, + "cURL multi handle cleanup failed: %s", + curl_multi_strerror(err)); + } + + free_provider(&actx->provider); + free_device_authz(&actx->authz); + + curl_slist_free_all(actx->headers); + termPQExpBuffer(&actx->work_data); + termPQExpBuffer(&actx->errbuf); + + if (actx->mux != PGINVALID_SOCKET) + close(actx->mux); +#ifdef HAVE_SYS_EPOLL_H + if (actx->timerfd >= 0) + close(actx->timerfd); +#endif + + free(actx); +} + +/* + * Macros for manipulating actx->errbuf. actx_error() translates and formats a + * string for you; actx_error_str() appends a string directly without + * translation. + */ + +#define actx_error(ACTX, FMT, ...) \ + appendPQExpBuffer(&(ACTX)->errbuf, libpq_gettext(FMT), ##__VA_ARGS__) + +#define actx_error_str(ACTX, S) \ + appendPQExpBufferStr(&(ACTX)->errbuf, S) + +/* + * Macros for getting and setting state for the connection's two cURL handles, + * so you don't have to write out the error handling every time. + */ + +#define CHECK_MSETOPT(ACTX, OPT, VAL, FAILACTION) \ + do { \ + struct async_ctx *_actx = (ACTX); \ + CURLMcode _setopterr = curl_multi_setopt(_actx->curlm, OPT, VAL); \ + if (_setopterr) { \ + actx_error(_actx, "failed to set %s on OAuth connection: %s",\ + #OPT, curl_multi_strerror(_setopterr)); \ + FAILACTION; \ + } \ + } while (0) + +#define CHECK_SETOPT(ACTX, OPT, VAL, FAILACTION) \ + do { \ + struct async_ctx *_actx = (ACTX); \ + CURLcode _setopterr = curl_easy_setopt(_actx->curl, OPT, VAL); \ + if (_setopterr) { \ + actx_error(_actx, "failed to set %s on OAuth connection: %s",\ + #OPT, curl_easy_strerror(_setopterr)); \ + FAILACTION; \ + } \ + } while (0) + +#define CHECK_GETINFO(ACTX, INFO, OUT, FAILACTION) \ + do { \ + struct async_ctx *_actx = (ACTX); \ + CURLcode _getinfoerr = curl_easy_getinfo(_actx->curl, INFO, OUT); \ + if (_getinfoerr) { \ + actx_error(_actx, "failed to get %s from OAuth response: %s",\ + #INFO, curl_easy_strerror(_getinfoerr)); \ + FAILACTION; \ + } \ + } while (0) + +/* + * General JSON Parsing for OAuth Responses + */ + +/* + * Represents a single name/value pair in a JSON object. This is the primary + * interface to parse_oauth_json(). + * + * All fields are stored internally as strings or lists of strings, so clients + * have to explicitly parse other scalar types (though they will have gone + * through basic lexical validation). Storing nested objects is not currently + * supported, nor is parsing arrays of anything other than strings. + */ +struct json_field +{ + const char *name; /* name (key) of the member */ + + JsonTokenType type; /* currently supports JSON_TOKEN_STRING, + * JSON_TOKEN_NUMBER, and + * JSON_TOKEN_ARRAY_START */ + union + { + char **scalar; /* for all scalar types */ + struct curl_slist **array; /* for type == JSON_TOKEN_ARRAY_START */ + }; + + bool required; /* REQUIRED field, or just OPTIONAL? */ +}; + +/* Documentation macros for json_field.required. */ +#define REQUIRED true +#define OPTIONAL false + +/* Parse state for parse_oauth_json(). */ +struct oauth_parse +{ + PQExpBuffer errbuf; /* detail message for JSON_SEM_ACTION_FAILED */ + int nested; /* nesting level (zero is the top) */ + + const struct json_field *fields; /* field definition array */ + const struct json_field *active; /* points inside the fields array */ +}; + +#define oauth_parse_set_error(ctx, fmt, ...) \ + appendPQExpBuffer((ctx)->errbuf, libpq_gettext(fmt), ##__VA_ARGS__) + +static void +report_type_mismatch(struct oauth_parse *ctx) +{ + char *msgfmt; + + Assert(ctx->active); + + /* + * At the moment, the only fields we're interested in are strings, + * numbers, and arrays of strings. + */ + switch (ctx->active->type) + { + case JSON_TOKEN_STRING: + msgfmt = "field \"%s\" must be a string"; + break; + + case JSON_TOKEN_NUMBER: + msgfmt = "field \"%s\" must be a number"; + break; + + case JSON_TOKEN_ARRAY_START: + msgfmt = "field \"%s\" must be an array of strings"; + break; + + default: + Assert(false); + msgfmt = "field \"%s\" has unexpected type"; + } + + oauth_parse_set_error(ctx, msgfmt, ctx->active->name); +} + +static JsonParseErrorType +oauth_json_object_start(void *state) +{ + struct oauth_parse *ctx = state; + + if (ctx->active) + { + /* + * Currently, none of the fields we're interested in can be or contain + * objects, so we can reject this case outright. + */ + report_type_mismatch(ctx); + return JSON_SEM_ACTION_FAILED; + } + + ++ctx->nested; + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_object_field_start(void *state, char *name, bool isnull) +{ + struct oauth_parse *ctx = state; + + /* We care only about the top-level fields. */ + if (ctx->nested == 1) + { + const struct json_field *field = ctx->fields; + + /* + * We should never start parsing a new field while a previous one is + * still active. + * + * TODO: this code relies on assertions too much. We need to exit + * sanely on internal logic errors, to avoid turning bugs into + * vulnerabilities. + */ + Assert(!ctx->active); + + while (field->name) + { + if (strcmp(name, field->name) == 0) + { + ctx->active = field; + break; + } + + ++field; + } + } + + free(name); + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_object_end(void *state) +{ + struct oauth_parse *ctx = state; + + --ctx->nested; + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_array_start(void *state) +{ + struct oauth_parse *ctx = state; + + if (!ctx->nested) + { + oauth_parse_set_error(ctx, "top-level element must be an object"); + return JSON_SEM_ACTION_FAILED; + } + + if (ctx->active) + { + if (ctx->active->type != JSON_TOKEN_ARRAY_START + /* The arrays we care about must not have arrays as values. */ + || ctx->nested > 1) + { + report_type_mismatch(ctx); + return JSON_SEM_ACTION_FAILED; + } + } + + ++ctx->nested; + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_array_end(void *state) +{ + struct oauth_parse *ctx = state; + + if (ctx->active) + { + /* + * This assumes that no target arrays can contain other arrays, which + * we check in the array_start callback. + */ + Assert(ctx->nested == 2); + Assert(ctx->active->type == JSON_TOKEN_ARRAY_START); + + ctx->active = NULL; + } + + --ctx->nested; + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_scalar(void *state, char *token, JsonTokenType type) +{ + struct oauth_parse *ctx = state; + JsonParseErrorType result = JSON_SUCCESS; + + if (!ctx->nested) + { + oauth_parse_set_error(ctx, "top-level element must be an object"); + result = JSON_SEM_ACTION_FAILED; + goto cleanup; + } + + if (ctx->active) + { + JsonTokenType expected; + + /* + * Make sure this matches what the active field expects. Arrays must + * contain only strings with the current implementation. + */ + if (ctx->active->type == JSON_TOKEN_ARRAY_START) + expected = JSON_TOKEN_STRING; + else + expected = ctx->active->type; + + if (type != expected) + { + report_type_mismatch(ctx); + result = JSON_SEM_ACTION_FAILED; + goto cleanup; + } + + /* + * FIXME if the JSON field is duplicated, we'll leak the prior value. + * Error out in that case instead. + */ + if (ctx->active->type != JSON_TOKEN_ARRAY_START) + { + Assert(ctx->nested == 1); + + *ctx->active->scalar = token; + ctx->active = NULL; + + return JSON_SUCCESS; /* don't free the token */ + } + else /* ctx->target_array */ + { + struct curl_slist *temp; + + Assert(ctx->nested == 2); + + temp = curl_slist_append(*ctx->active->array, token); + if (!temp) + { + oauth_parse_set_error(ctx, "out of memory"); + result = JSON_SEM_ACTION_FAILED; + goto cleanup; + } + + *ctx->active->array = temp; + + /* + * Note that curl_slist_append() makes a copy of the token, so we + * can free it below. + */ + } + } + else + { + /* otherwise we just ignore it */ + } + +cleanup: + free(token); + return result; +} + +/* + * A helper function for general JSON parsing. fields is the array of field + * definitions with their backing pointers. The response will be parsed from + * actx->curl and actx->work_data (as set up by start_request()), and any + * parsing errors will be placed into actx->errbuf. + */ +static bool +parse_oauth_json(struct async_ctx *actx, const struct json_field *fields) +{ + PQExpBuffer resp = &actx->work_data; + char *content_type; + JsonLexContext lex = {0}; + JsonSemAction sem = {0}; + JsonParseErrorType err; + struct oauth_parse ctx = {0}; + bool success = false; + + /* Make sure the server thinks it's given us JSON. */ + CHECK_GETINFO(actx, CURLINFO_CONTENT_TYPE, &content_type, return false); + + if (!content_type) + { + actx_error(actx, "no content type was provided"); + goto cleanup; + } + else if (strcasecmp(content_type, "application/json") != 0) + { + actx_error(actx, "unexpected content type \"%s\"", content_type); + goto cleanup; + } + + if (strlen(resp->data) != resp->len) + { + actx_error(actx, "response contains embedded NULLs"); + goto cleanup; + } + + makeJsonLexContextCstringLen(&lex, resp->data, resp->len, PG_UTF8, true); + + ctx.errbuf = &actx->errbuf; + ctx.fields = fields; + sem.semstate = &ctx; + + sem.object_start = oauth_json_object_start; + sem.object_field_start = oauth_json_object_field_start; + sem.object_end = oauth_json_object_end; + sem.array_start = oauth_json_array_start; + sem.array_end = oauth_json_array_end; + sem.scalar = oauth_json_scalar; + + err = pg_parse_json(&lex, &sem); + + if (err != JSON_SUCCESS) + { + /* + * For JSON_SEM_ACTION_FAILED, we've already written the error + * message. Other errors come directly from pg_parse_json(), already + * translated. + */ + if (err != JSON_SEM_ACTION_FAILED) + actx_error_str(actx, json_errdetail(err, &lex)); + + goto cleanup; + } + + /* Check all required fields. */ + while (fields->name) + { + if (fields->required && !*fields->scalar && !*fields->array) + { + actx_error(actx, "field \"%s\" is missing", fields->name); + goto cleanup; + } + + fields++; + } + + success = true; + +cleanup: + freeJsonLexContext(&lex); + return success; +} + +/* + * JSON Parser Definitions + */ + +static bool +parse_provider(struct async_ctx *actx, struct provider *provider) +{ + struct json_field fields[] = { + {"issuer", JSON_TOKEN_STRING, {&provider->issuer}, REQUIRED}, + {"token_endpoint", JSON_TOKEN_STRING, {&provider->token_endpoint}, REQUIRED}, + + /*---- + * The following fields are technically REQUIRED, but we don't use + * them anywhere yet: + * + * - jwks_uri + * - response_types_supported + * - subject_types_supported + * - id_token_signing_alg_values_supported + */ + + {"device_authorization_endpoint", JSON_TOKEN_STRING, {&provider->device_authorization_endpoint}, OPTIONAL}, + {"grant_types_supported", JSON_TOKEN_ARRAY_START, {.array = &provider->grant_types_supported}, OPTIONAL}, + + {0}, + }; + + return parse_oauth_json(actx, fields); +} + +/* + * Parses the "interval" JSON number, corresponding to the number of seconds to + * wait between token endpoint requests. + * + * RFC 8628 is pretty silent on sanity checks for the interval. As a matter of + * practicality, round any fractional intervals up to the next second, and clamp + * the result at a minimum of one. (Zero-second intervals would result in an + * expensive network polling loop.) + * + * TODO: maybe clamp the upper bound too, based on the libpq timeout and/or the + * code expiration time? + */ +static int +parse_interval(const char *interval_str) +{ + double parsed; + int cnt; + + /* + * The JSON lexer has already validated the number, which is stricter than + * the %f format, so we should be good to use sscanf(). + */ + cnt = sscanf(interval_str, "%lf", &parsed); + + if (cnt != 1) + { + /* + * Either the lexer screwed up or our assumption above isn't true, and + * either way a developer needs to take a look. + */ + Assert(cnt == 1); + return 1; /* don't fall through in release builds */ + } + + parsed = ceil(parsed); + + if (parsed < 1) + return 1; /* TODO this slows down the tests + * considerably... */ + else if (INT_MAX <= parsed) + return INT_MAX; + + return parsed; +} + +static bool +parse_device_authz(struct async_ctx *actx, struct device_authz *authz) +{ + struct json_field fields[] = { + {"device_code", JSON_TOKEN_STRING, {&authz->device_code}, REQUIRED}, + {"user_code", JSON_TOKEN_STRING, {&authz->user_code}, REQUIRED}, + {"verification_uri", JSON_TOKEN_STRING, {&authz->verification_uri}, REQUIRED}, + + /* + * The following fields are technically REQUIRED, but we don't use + * them anywhere yet: + * + * - expires_in + */ + + {"interval", JSON_TOKEN_NUMBER, {&authz->interval_str}, OPTIONAL}, + + {0}, + }; + + if (!parse_oauth_json(actx, fields)) + return false; + + /* + * Parse our numeric fields. Lexing has already completed by this time, so + * we at least know they're valid JSON numbers. + */ + if (authz->interval_str) + authz->interval = parse_interval(authz->interval_str); + else + { + /* TODO: handle default interval of 5 seconds */ + } + + return true; +} + +static bool +parse_token_error(struct async_ctx *actx, struct token_error *err) +{ + bool result; + struct json_field fields[] = { + {"error", JSON_TOKEN_STRING, {&err->error}, REQUIRED}, + + {"error_description", JSON_TOKEN_STRING, {&err->error_description}, OPTIONAL}, + + {0}, + }; + + result = parse_oauth_json(actx, fields); + + /* + * Since token errors are parsed during other active error paths, only + * override the errctx if parsing explicitly fails. + */ + if (!result) + actx->errctx = "failed to parse token error response"; + + return result; +} + +static bool +parse_access_token(struct async_ctx *actx, struct token *tok) +{ + struct json_field fields[] = { + {"access_token", JSON_TOKEN_STRING, {&tok->access_token}, REQUIRED}, + {"token_type", JSON_TOKEN_STRING, {&tok->token_type}, REQUIRED}, + + /* + * The following fields are technically REQUIRED, but we don't use + * them anywhere yet: + * + * - scope (only required if different than requested -- TODO check) + */ + + {0}, + }; + + return parse_oauth_json(actx, fields); +} + +/* + * cURL Multi Setup/Callbacks + */ + +/* + * Sets up the actx->mux, which is the altsock that PQconnectPoll clients will + * select() on instead of the Postgres socket during OAuth negotiation. + * + * This is just an epoll set or kqueue abstracting multiple other descriptors. + * A timerfd is always part of the set when using epoll; it's just disabled + * when we're not using it. + */ +static bool +setup_multiplexer(struct async_ctx *actx) +{ +#ifdef HAVE_SYS_EPOLL_H + struct epoll_event ev = {.events = EPOLLIN}; + + actx->mux = epoll_create1(EPOLL_CLOEXEC); + if (actx->mux < 0) + { + actx_error(actx, "failed to create epoll set: %m"); + return false; + } + + actx->timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); + if (actx->timerfd < 0) + { + actx_error(actx, "failed to create timerfd: %m"); + return false; + } + + if (epoll_ctl(actx->mux, EPOLL_CTL_ADD, actx->timerfd, &ev) < 0) + { + actx_error(actx, "failed to add timerfd to epoll set: %m"); + return false; + } + + return true; +#endif +#ifdef HAVE_SYS_EVENT_H + actx->mux = kqueue(); + if (actx->mux < 0) + { + actx_error(actx, "failed to create kqueue: %m"); + return false; + } + + return true; +#endif + + actx_error(actx, "here's a nickel kid, get yourself a better computer"); + return false; +} + +/* + * Adds and removes sockets from the multiplexer set, as directed by the + * cURL multi handle. + */ +static int +register_socket(CURL *curl, curl_socket_t socket, int what, void *ctx, + void *socketp) +{ +#ifdef HAVE_SYS_EPOLL_H + struct async_ctx *actx = ctx; + struct epoll_event ev = {0}; + int res; + int op = EPOLL_CTL_ADD; + + switch (what) + { + case CURL_POLL_IN: + ev.events = EPOLLIN; + break; + + case CURL_POLL_OUT: + ev.events = EPOLLOUT; + break; + + case CURL_POLL_INOUT: + ev.events = EPOLLIN | EPOLLOUT; + break; + + case CURL_POLL_REMOVE: + op = EPOLL_CTL_DEL; + break; + + default: + actx_error(actx, "unknown cURL socket operation (%d)", what); + return -1; + } + + res = epoll_ctl(actx->mux, op, socket, &ev); + if (res < 0 && errno == EEXIST) + { + /* We already had this socket in the pollset. */ + op = EPOLL_CTL_MOD; + res = epoll_ctl(actx->mux, op, socket, &ev); + } + + if (res < 0) + { + switch (op) + { + case EPOLL_CTL_ADD: + actx_error(actx, "could not add to epoll set: %m"); + break; + + case EPOLL_CTL_DEL: + actx_error(actx, "could not delete from epoll set: %m"); + break; + + default: + actx_error(actx, "could not update epoll set: %m"); + } + + return -1; + } +#endif +#ifdef HAVE_SYS_EVENT_H + struct async_ctx *actx = ctx; + struct kevent ev[2] = {{0}}; + struct kevent ev_out[2]; + struct timespec timeout = {0}; + int nev = 0; + int res; + + switch (what) + { + case CURL_POLL_IN: + EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD | EV_RECEIPT, 0, 0, 0); + nev++; + break; + + case CURL_POLL_OUT: + EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD | EV_RECEIPT, 0, 0, 0); + nev++; + break; + + case CURL_POLL_INOUT: + EV_SET(&ev[nev], socket, EVFILT_READ, EV_ADD | EV_RECEIPT, 0, 0, 0); + nev++; + EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_ADD | EV_RECEIPT, 0, 0, 0); + nev++; + break; + + case CURL_POLL_REMOVE: + + /* + * We don't know which of these is currently registered, perhaps + * both, so we try to remove both. This means we need to tolerate + * ENOENT below. + */ + EV_SET(&ev[nev], socket, EVFILT_READ, EV_DELETE | EV_RECEIPT, 0, 0, 0); + nev++; + EV_SET(&ev[nev], socket, EVFILT_WRITE, EV_DELETE | EV_RECEIPT, 0, 0, 0); + nev++; + break; + + default: + actx_error(actx, "unknown cURL socket operation (%d)", what); + return -1; + } + + res = kevent(actx->mux, ev, nev, ev_out, lengthof(ev_out), &timeout); + if (res < 0) + { + actx_error(actx, "could not modify kqueue: %m"); + return -1; + } + + /* + * We can't use the simple errno version of kevent, because we need to + * skip over ENOENT while still allowing a second change to be processed. + * So we need a longer-form error checking loop. + */ + for (int i = 0; i < res; ++i) + { + /* + * EV_RECEIPT should guarantee one EV_ERROR result for every change, + * whether successful or not. Failed entries contain a non-zero errno + * in the `data` field. + */ + Assert(ev_out[i].flags & EV_ERROR); + + errno = ev_out[i].data; + if (errno && errno != ENOENT) + { + switch (what) + { + case CURL_POLL_REMOVE: + actx_error(actx, "could not delete from kqueue: %m"); + break; + default: + actx_error(actx, "could not add to kqueue: %m"); + } + return -1; + } + } +#endif + + return 0; +} + +/* + * Adds or removes timeouts from the multiplexer set, as directed by the + * cURL multi handle. Rather than continually adding and removing the timer, + * we keep it in the set at all times and just disarm it when it's not + * needed. + */ +static int +register_timer(CURLM *curlm, long timeout, void *ctx) +{ +#if HAVE_SYS_EPOLL_H + struct async_ctx *actx = ctx; + struct itimerspec spec = {0}; + + if (timeout < 0) + { + /* the zero itimerspec will disarm the timer below */ + } + else if (timeout == 0) + { + /* + * A zero timeout means cURL wants us to call back immediately. That's + * not technically an option for timerfd, but we can make the timeout + * ridiculously short. + * + * TODO: maybe just signal drive_request() to immediately call back in + * this case? + */ + spec.it_value.tv_nsec = 1; + } + else + { + spec.it_value.tv_sec = timeout / 1000; + spec.it_value.tv_nsec = (timeout % 1000) * 1000000; + } + + if (timerfd_settime(actx->timerfd, 0 /* no flags */ , &spec, NULL) < 0) + { + actx_error(actx, "setting timerfd to %ld: %m", timeout); + return -1; + } +#endif +#ifdef HAVE_SYS_EVENT_H + struct async_ctx *actx = ctx; + struct kevent ev; + + EV_SET(&ev, 1, EVFILT_TIMER, timeout < 0 ? EV_DELETE : EV_ADD, + 0, timeout, 0); + if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0 && errno != ENOENT) + { + actx_error(actx, "setting kqueue timer to %ld: %m", timeout); + return -1; + } +#endif + + return 0; +} + +/* + * Initializes the two cURL handles in the async_ctx. The multi handle, + * actx->curlm, is what drives the asynchronous engine and tells us what to do + * next. The easy handle, actx->curl, encapsulates the state for a single + * request/response. It's added to the multi handle as needed, during + * start_request(). + */ +static bool +setup_curl_handles(struct async_ctx *actx) +{ + curl_version_info_data *curl_info; + + /* + * Create our multi handle. This encapsulates the entire conversation with + * cURL for this connection. + */ + actx->curlm = curl_multi_init(); + if (!actx->curlm) + { + /* We don't get a lot of feedback on the failure reason. */ + actx_error(actx, "failed to create cURL multi handle"); + return false; + } + + /* + * Extract information about the libcurl we are linked against. + */ + curl_info = curl_version_info(CURLVERSION_NOW); + + /* + * The multi handle tells us what to wait on using two callbacks. These + * will manipulate actx->mux as needed. + */ + CHECK_MSETOPT(actx, CURLMOPT_SOCKETFUNCTION, register_socket, return false); + CHECK_MSETOPT(actx, CURLMOPT_SOCKETDATA, actx, return false); + CHECK_MSETOPT(actx, CURLMOPT_TIMERFUNCTION, register_timer, return false); + CHECK_MSETOPT(actx, CURLMOPT_TIMERDATA, actx, return false); + + /* + * Set up an easy handle. All of our requests are made serially, so we + * only ever need to keep track of one. + */ + actx->curl = curl_easy_init(); + if (!actx->curl) + { + actx_error(actx, "failed to create cURL handle"); + return false; + } + + /* + * Multi-threaded applications must set CURLOPT_NOSIGNAL. This requires us + * to handle the possibility of SIGPIPE ourselves. + * + * TODO: handle SIGPIPE via pq_block_sigpipe(), or via a + * CURLOPT_SOCKOPTFUNCTION maybe... + */ + CHECK_SETOPT(actx, CURLOPT_NOSIGNAL, 1L, return false); + if (!curl_info->ares_num) + { + /* No alternative resolver, TODO: warn about timeouts */ + } + + /* TODO investigate using conn->Pfdebug and CURLOPT_DEBUGFUNCTION here */ + CHECK_SETOPT(actx, CURLOPT_VERBOSE, 1L, return false); + CHECK_SETOPT(actx, CURLOPT_ERRORBUFFER, actx->curl_err, return false); + + /* + * Only HTTP[S] is allowed. TODO: disallow HTTP without user opt-in + */ + CHECK_SETOPT(actx, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS, return false); + + /* + * Suppress the Accept header to make our request as minimal as possible. + * (Ideally we would set it to "application/json" instead, but OpenID is + * pretty strict when it comes to provider behavior, so we have to check + * what comes back anyway.) + */ + actx->headers = curl_slist_append(actx->headers, "Accept:"); /* TODO: check result */ + CHECK_SETOPT(actx, CURLOPT_HTTPHEADER, actx->headers, return false); + + return true; +} + +/* + * Generic HTTP Request Handlers + */ + +/* + * Response callback from cURL; appends the response body into actx->work_data. + * See start_request(). + */ +static size_t +append_data(char *buf, size_t size, size_t nmemb, void *userdata) +{ + PQExpBuffer resp = userdata; + size_t len = size * nmemb; + + /* TODO: cap the maximum size */ + appendBinaryPQExpBuffer(resp, buf, len); + /* TODO: check for broken buffer */ + + return len; +} + +/* + * Begins an HTTP request on the multi handle. The caller should have set up all + * request-specific options on actx->curl first. The server's response body will + * be accumulated in actx->work_data (which will be reset, so don't store + * anything important there across this call). + * + * Once a request is queued, it can be driven to completion via drive_request(). + */ +static bool +start_request(struct async_ctx *actx) +{ + CURLMcode err; + int running; + + resetPQExpBuffer(&actx->work_data); + CHECK_SETOPT(actx, CURLOPT_WRITEFUNCTION, append_data, return false); + CHECK_SETOPT(actx, CURLOPT_WRITEDATA, &actx->work_data, return false); + + err = curl_multi_add_handle(actx->curlm, actx->curl); + if (err) + { + actx_error(actx, "failed to queue HTTP request: %s", + curl_multi_strerror(err)); + return false; + } + + err = curl_multi_socket_action(actx->curlm, CURL_SOCKET_TIMEOUT, 0, &running); + if (err) + { + actx_error(actx, "asynchronous HTTP request failed: %s", + curl_multi_strerror(err)); + return false; + } + + /* + * Sanity check. + * + * TODO: even though this is nominally an asynchronous process, there are + * apparently operations that can synchronously fail by this point, such + * as connections to closed local ports. Maybe we need to let this case + * fall through to drive_request instead, or else perform a + * curl_multi_info_read immediately. + */ + if (running != 1) + { + actx_error(actx, "failed to queue HTTP request"); + return false; + } + + return true; +} + +/* + * Drives the multi handle towards completion. The caller should have already + * set up an asynchronous request via start_request(). + */ +static PostgresPollingStatusType +drive_request(struct async_ctx *actx) +{ + CURLMcode err; + int running; + CURLMsg *msg; + int msgs_left; + bool done; + + err = curl_multi_socket_all(actx->curlm, &running); + if (err) + { + actx_error(actx, "asynchronous HTTP request failed: %s", + curl_multi_strerror(err)); + return PGRES_POLLING_FAILED; + } + + if (running) + { + /* We'll come back again. */ + return PGRES_POLLING_READING; + } + + done = false; + while ((msg = curl_multi_info_read(actx->curlm, &msgs_left)) != NULL) + { + if (msg->msg != CURLMSG_DONE) + { + /* + * Future cURL versions may define new message types; we don't + * know how to handle them, so we'll ignore them. + */ + continue; + } + + /* First check the status of the request itself. */ + if (msg->data.result != CURLE_OK) + { + actx_error_str(actx, curl_easy_strerror(msg->data.result)); + return PGRES_POLLING_FAILED; + } + + /* Now remove the finished handle; we'll add it back later if needed. */ + err = curl_multi_remove_handle(actx->curlm, msg->easy_handle); + if (err) + { + actx_error(actx, "cURL easy handle removal failed: %s", + curl_multi_strerror(err)); + return PGRES_POLLING_FAILED; + } + + done = true; + } + + /* Sanity check. */ + if (!done) + { + actx_error(actx, "no result was retrieved for the finished handle"); + return PGRES_POLLING_FAILED; + } + + return PGRES_POLLING_OK; +} + +/* + * Specific HTTP Request Handlers + * + * This is finally the beginning of the actual application logic. Generally + * speaking, a single request consists of a start_* and a finish_* step, with + * drive_request() pumping the machine in between. + */ + +/* + * Queue an OpenID Provider Configuration Request: + * + * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest + * https://www.rfc-editor.org/rfc/rfc8414#section-3.1 + * + * This is done first to get the endpoint URIs we need to contact and to make + * sure the provider provides a device authorization flow. finish_discovery() + * will fill in actx->provider. + */ +static bool +start_discovery(struct async_ctx *actx, const char *discovery_uri) +{ + CHECK_SETOPT(actx, CURLOPT_HTTPGET, 1L, return false); + CHECK_SETOPT(actx, CURLOPT_URL, discovery_uri, return false); + + return start_request(actx); +} + +static bool +finish_discovery(struct async_ctx *actx) +{ + long response_code; + + /*---- + * Now check the response. OIDC Discovery 1.0 is pretty strict: + * + * A successful response MUST use the 200 OK HTTP status code and + * return a JSON object using the application/json content type that + * contains a set of Claims as its members that are a subset of the + * Metadata values defined in Section 3. + * + * Compared to standard HTTP semantics, this makes life easy -- we don't + * need to worry about redirections (which would call the Issuer host + * validation into question), or non-authoritative responses, or any other + * complications. + */ + CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, return false); + + if (response_code != 200) + { + actx_error(actx, "unexpected response code %ld", response_code); + return false; + } + + /* + * Pull the fields we care about from the document. + */ + actx->errctx = "failed to parse OpenID discovery document"; + if (!parse_provider(actx, &actx->provider)) + return false; /* error message already set */ + + /* + * Fill in any defaults for OPTIONAL/RECOMMENDED fields we care about. + */ + if (!actx->provider.grant_types_supported) + { + /* + * Per Section 3, the default is ["authorization_code", "implicit"]. + */ + struct curl_slist *temp = actx->provider.grant_types_supported; + + temp = curl_slist_append(temp, "authorization_code"); + if (temp) + { + temp = curl_slist_append(temp, "implicit"); + } + + if (!temp) + { + actx_error(actx, "out of memory"); + return false; + } + + actx->provider.grant_types_supported = temp; + } + + return true; +} + +#define OAUTH_GRANT_TYPE_DEVICE_CODE "urn:ietf:params:oauth:grant-type:device_code" + +/* + * Ensure that the provider supports the Device Authorization flow (i.e. it + * accepts the device_code grant type and provides an authorization endpoint). + */ +static bool +check_for_device_flow(struct async_ctx *actx) +{ + const struct provider *provider = &actx->provider; + const struct curl_slist *grant; + bool device_grant_found = false; + + Assert(provider->issuer); /* ensured by get_discovery_document() */ + + /*------ + * First, sanity checks for discovery contents that are OPTIONAL in the + * spec but required for our flow: + * - the issuer must support the device_code grant + * - the issuer must have actually given us a + * device_authorization_endpoint + */ + + grant = provider->grant_types_supported; + while (grant) + { + if (strcmp(grant->data, OAUTH_GRANT_TYPE_DEVICE_CODE) == 0) + { + device_grant_found = true; + break; + } + + grant = grant->next; + } + + if (!device_grant_found) + { + actx_error(actx, "issuer \"%s\" does not support device code grants", + provider->issuer); + return false; + } + + if (!provider->device_authorization_endpoint) + { + actx_error(actx, + "issuer \"%s\" does not provide a device authorization endpoint", + provider->issuer); + return false; + } + + /* TODO: check that the endpoint uses HTTPS */ + + return true; +} + +/* + * Queue a Device Authorization Request: + * + * https://www.rfc-editor.org/rfc/rfc8628#section-3.1 + * + * This is the second step. We ask the provider to verify the end user out of + * band and authorize us to act on their behalf; it will give us the required + * nonces for us to later poll the request status, which we'll grab in + * finish_device_authz(). + */ +static bool +start_device_authz(struct async_ctx *actx, PGconn *conn) +{ + const char *device_authz_uri = actx->provider.device_authorization_endpoint; + PQExpBuffer work_buffer = &actx->work_data; + + Assert(conn->oauth_client_id); /* ensured by get_auth_token() */ + Assert(device_authz_uri); /* ensured by check_for_device_flow() */ + + /* Construct our request body. TODO: url-encode */ + resetPQExpBuffer(work_buffer); + appendPQExpBuffer(work_buffer, "client_id=%s", conn->oauth_client_id); + if (conn->oauth_scope) + appendPQExpBuffer(work_buffer, "&scope=%s", conn->oauth_scope); + /* TODO check for broken buffer */ + + /* Make our request. */ + CHECK_SETOPT(actx, CURLOPT_URL, device_authz_uri, return false); + CHECK_SETOPT(actx, CURLOPT_COPYPOSTFIELDS, work_buffer->data, return false); + + if (conn->oauth_client_secret) + { + /*---- + * Use HTTP Basic auth to send the password. Per RFC 6749, Sec. 2.3.1, + * + * Including the client credentials in the request-body using the + * two parameters is NOT RECOMMENDED and SHOULD be limited to + * clients unable to directly utilize the HTTP Basic authentication + * scheme (or other password-based HTTP authentication schemes). + * + * TODO: should we omit client_id from the body in this case? + */ + CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_BASIC, return false); + CHECK_SETOPT(actx, CURLOPT_USERNAME, conn->oauth_client_id, return false); + CHECK_SETOPT(actx, CURLOPT_PASSWORD, conn->oauth_client_secret, return false); + } + else + CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_NONE, return false); + + return start_request(actx); +} + +static bool +finish_device_authz(struct async_ctx *actx) +{ + long response_code; + + CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, return false); + + /* + * The device authorization endpoint uses the same error response as the + * token endpoint, so we have to handle 400/401 here too. + */ + if (response_code != 200 + && response_code != 400 + /* && response_code != 401 TODO */ ) + { + actx_error(actx, "unexpected response code %ld", response_code); + return false; + } + + if (response_code != 200) + { + struct token_error err = {0}; + + if (!parse_token_error(actx, &err)) + { + free_token_error(&err); + return false; + } + + if (err.error_description) + appendPQExpBuffer(&actx->errbuf, "%s ", err.error_description); + + appendPQExpBuffer(&actx->errbuf, "(%s)", err.error); + + free_token_error(&err); + return false; + } + + /* + * Pull the fields we care about from the document. + */ + actx->errctx = "failed to parse device authorization"; + if (!parse_device_authz(actx, &actx->authz)) + return false; /* error message already set */ + + return true; +} + +/* + * Queue an Access Token Request: + * + * https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3 + * + * This is the final step. We continually poll the token endpoint to see if the + * user has authorized us yet. finish_token_request() will pull either the token + * or a (ideally temporary) error status from the provider. + */ +static bool +start_token_request(struct async_ctx *actx, PGconn *conn) +{ + const char *token_uri = actx->provider.token_endpoint; + const char *device_code = actx->authz.device_code; + PQExpBuffer work_buffer = &actx->work_data; + + Assert(conn->oauth_client_id); /* ensured by get_auth_token() */ + Assert(token_uri); /* ensured by get_discovery_document() */ + Assert(device_code); /* ensured by run_device_authz() */ + + /* Construct our request body. TODO: url-encode */ + resetPQExpBuffer(work_buffer); + appendPQExpBuffer(work_buffer, "client_id=%s", conn->oauth_client_id); + appendPQExpBuffer(work_buffer, "&device_code=%s", device_code); + appendPQExpBuffer(work_buffer, "&grant_type=%s", + OAUTH_GRANT_TYPE_DEVICE_CODE); + /* TODO check for broken buffer */ + + /* Make our request. */ + CHECK_SETOPT(actx, CURLOPT_URL, token_uri, return false); + CHECK_SETOPT(actx, CURLOPT_COPYPOSTFIELDS, work_buffer->data, return false); + + if (conn->oauth_client_secret) + { + /*---- + * Use HTTP Basic auth to send the password. Per RFC 6749, Sec. 2.3.1, + * + * Including the client credentials in the request-body using the + * two parameters is NOT RECOMMENDED and SHOULD be limited to + * clients unable to directly utilize the HTTP Basic authentication + * scheme (or other password-based HTTP authentication schemes). + * + * TODO: should we omit client_id from the body in this case? + */ + CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_BASIC, return false); + CHECK_SETOPT(actx, CURLOPT_USERNAME, conn->oauth_client_id, return false); + CHECK_SETOPT(actx, CURLOPT_PASSWORD, conn->oauth_client_secret, return false); + } + else + CHECK_SETOPT(actx, CURLOPT_HTTPAUTH, CURLAUTH_NONE, return false); + + resetPQExpBuffer(work_buffer); + CHECK_SETOPT(actx, CURLOPT_WRITEFUNCTION, append_data, return false); + CHECK_SETOPT(actx, CURLOPT_WRITEDATA, work_buffer, return false); + + return start_request(actx); +} + +static bool +finish_token_request(struct async_ctx *actx, struct token *tok) +{ + long response_code; + + CHECK_GETINFO(actx, CURLINFO_RESPONSE_CODE, &response_code, return false); + + /* + * Per RFC 6749, Section 5, a successful response uses 200 OK. An error + * response uses either 400 Bad Request or 401 Unauthorized. + * + * TODO: there are references online to 403 appearing in the wild... + */ + if (response_code != 200 + && response_code != 400 + /* && response_code != 401 TODO */ ) + { + actx_error(actx, "unexpected response code %ld", response_code); + return false; + } + + /* + * Pull the fields we care about from the document. + */ + if (response_code == 200) + { + actx->errctx = "failed to parse access token response"; + if (!parse_access_token(actx, tok)) + return false; /* error message already set */ + } + else if (!parse_token_error(actx, &tok->err)) + return false; + + return true; +} + +/* + * The top-level, nonblocking entry point for the cURL implementation. This will + * be called several times to pump the async engine. + * + * The architecture is based on PQconnectPoll(). The first half drives the + * connection state forward as necessary, returning if we're not ready to + * proceed to the next step yet. The second half performs the actual transition + * between states. + * + * You can trace the overall OAuth flow through the second half. It's linear + * until we get to the end, where we flip back and forth between + * OAUTH_STEP_TOKEN_REQUEST and OAUTH_STEP_WAIT_INTERVAL to regularly ping the + * provider. + */ +PostgresPollingStatusType +pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock) +{ + fe_oauth_state *state = conn->sasl_state; + struct async_ctx *actx; + + struct token tok = {0}; + + /* + * XXX This is not safe. cURL has stringent requirements for the thread + * context in which you call curl_global_init(), because it's going to try + * initializing a bunch of other libraries (OpenSSL, Winsock...). And we + * probably need to consider both the TLS backend libcurl is compiled + * against and what the user has asked us to do via PQinit[Open]SSL. + * + * Recent versions of libcurl have improved the thread-safety situation, + * but you apparently can't check at compile time whether the + * implementation is thread-safe, and there's a chicken-and-egg problem + * where you can't check the thread safety until you've initialized cURL, + * which you can't do before you've made sure it's thread-safe... + * + * We know we've already initialized Winsock by this point, so we should + * be able to safely skip that bit. But we have to tell cURL to initialize + * everything else, because other pieces of our client executable may + * already be using cURL for their own purposes. If we initialize libcurl + * first, with only a subset of its features, we could break those other + * clients nondeterministically, and that would probably be a nightmare to + * debug. + */ + curl_global_init(CURL_GLOBAL_ALL + & ~CURL_GLOBAL_WIN32); /* we already initialized Winsock */ + + if (!state->async_ctx) + { + /* + * Create our asynchronous state, and hook it into the upper-level + * OAuth state immediately, so any failures below won't leak the + * context allocation. + */ + actx = calloc(1, sizeof(*actx)); + if (!actx) + { + libpq_append_conn_error(conn, "out of memory"); + return PGRES_POLLING_FAILED; + } + + actx->mux = PGINVALID_SOCKET; +#ifdef HAVE_SYS_EPOLL_H + actx->timerfd = -1; +#endif + + state->async_ctx = actx; + state->free_async_ctx = free_curl_async_ctx; + + initPQExpBuffer(&actx->work_data); + initPQExpBuffer(&actx->errbuf); + + if (!setup_multiplexer(actx)) + goto error_return; + + if (!setup_curl_handles(actx)) + goto error_return; + } + + actx = state->async_ctx; + + /* By default, the multiplexer is the altsock. Reassign as desired. */ + *altsock = actx->mux; + + switch (actx->step) + { + case OAUTH_STEP_INIT: + break; + + case OAUTH_STEP_DISCOVERY: + case OAUTH_STEP_DEVICE_AUTHORIZATION: + case OAUTH_STEP_TOKEN_REQUEST: + { + PostgresPollingStatusType status; + + status = drive_request(actx); + + if (status == PGRES_POLLING_FAILED) + goto error_return; + else if (status != PGRES_POLLING_OK) + { + /* not done yet */ + free_token(&tok); + return status; + } + } + + case OAUTH_STEP_WAIT_INTERVAL: + /* TODO check that the timer has expired */ + break; + } + + switch (actx->step) + { + case OAUTH_STEP_INIT: + actx->errctx = "failed to fetch OpenID discovery document"; + if (!start_discovery(actx, conn->oauth_discovery_uri)) + goto error_return; + + actx->step = OAUTH_STEP_DISCOVERY; + break; + + case OAUTH_STEP_DISCOVERY: + if (!finish_discovery(actx)) + goto error_return; + + /* TODO: check issuer */ + + actx->errctx = "cannot run OAuth device authorization"; + if (!check_for_device_flow(actx)) + goto error_return; + + actx->errctx = "failed to obtain device authorization"; + if (!start_device_authz(actx, conn)) + goto error_return; + + actx->step = OAUTH_STEP_DEVICE_AUTHORIZATION; + break; + + case OAUTH_STEP_DEVICE_AUTHORIZATION: + if (!finish_device_authz(actx)) + goto error_return; + + actx->errctx = "failed to obtain access token"; + if (!start_token_request(actx, conn)) + goto error_return; + + actx->step = OAUTH_STEP_TOKEN_REQUEST; + break; + + case OAUTH_STEP_TOKEN_REQUEST: + { + const struct token_error *err; +#ifdef HAVE_SYS_EPOLL_H + struct itimerspec spec = {0}; +#endif +#ifdef HAVE_SYS_EVENT_H + struct kevent ev = {0}; +#endif + + if (!finish_token_request(actx, &tok)) + goto error_return; + + if (!actx->user_prompted) + { + int res; + PQpromptOAuthDevice prompt = { + .verification_uri = actx->authz.verification_uri, + .user_code = actx->authz.user_code, + /* TODO: optional fields */ + }; + + /* + * Now that we know the token endpoint isn't broken, give + * the user the login instructions. + */ + res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, + &prompt); + + if (!res) + { + fprintf(stderr, "Visit %s and enter the code: %s", + prompt.verification_uri, prompt.user_code); + } + else if (res < 0) + { + actx_error(actx, "device prompt failed"); + goto error_return; + } + + actx->user_prompted = true; + } + + if (tok.access_token) + { + /* Construct our Bearer token. */ + resetPQExpBuffer(&actx->work_data); + appendPQExpBuffer(&actx->work_data, "Bearer %s", + tok.access_token); + + if (PQExpBufferDataBroken(actx->work_data)) + { + actx_error(actx, "out of memory"); + goto error_return; + } + + state->token = strdup(actx->work_data.data); + break; + } + + /* + * authorization_pending and slow_down are the only acceptable + * errors; anything else and we bail. + */ + err = &tok.err; + if (!err->error || (strcmp(err->error, "authorization_pending") + && strcmp(err->error, "slow_down"))) + { + /* TODO handle !err->error */ + if (err->error_description) + appendPQExpBuffer(&actx->errbuf, "%s ", + err->error_description); + + appendPQExpBuffer(&actx->errbuf, "(%s)", err->error); + + goto error_return; + } + + /* + * A slow_down error requires us to permanently increase our + * retry interval by five seconds. RFC 8628, Sec. 3.5. + */ + if (strcmp(err->error, "slow_down") == 0) + { + actx->authz.interval += 5; /* TODO check for overflow? */ + } + + /* + * Wait for the required interval before issuing the next + * request. + */ + Assert(actx->authz.interval > 0); +#ifdef HAVE_SYS_EPOLL_H + spec.it_value.tv_sec = actx->authz.interval; + + if (timerfd_settime(actx->timerfd, 0 /* no flags */ , &spec, NULL) < 0) + { + actx_error(actx, "failed to set timerfd: %m"); + goto error_return; + } + + *altsock = actx->timerfd; +#endif +#ifdef HAVE_SYS_EVENT_H + /* XXX: I guess this wants to be hidden in a routine */ + EV_SET(&ev, 1, EVFILT_TIMER, EV_ADD, 0, + actx->authz.interval * 1000, 0); + if (kevent(actx->mux, &ev, 1, NULL, 0, NULL) < 0) + { + actx_error(actx, "failed to set kqueue timer: %m"); + goto error_return; + } + /* XXX: why did we change the altsock in the epoll version? */ +#endif + actx->step = OAUTH_STEP_WAIT_INTERVAL; + break; + } + + case OAUTH_STEP_WAIT_INTERVAL: + actx->errctx = "failed to obtain access token"; + if (!start_token_request(actx, conn)) + goto error_return; + + actx->step = OAUTH_STEP_TOKEN_REQUEST; + break; + } + + free_token(&tok); + + /* If we've stored a token, we're done. Otherwise come back later. */ + return state->token ? PGRES_POLLING_OK : PGRES_POLLING_READING; + +error_return: + + /* + * Assemble the three parts of our error: context, body, and detail. See + * also the documentation for struct async_ctx. + */ + if (actx->errctx) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext(actx->errctx)); + appendPQExpBufferStr(&conn->errorMessage, ": "); + } + + if (PQExpBufferDataBroken(actx->errbuf)) + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("out of memory")); + else + appendPQExpBufferStr(&conn->errorMessage, actx->errbuf.data); + + if (actx->curl_err[0]) + { + size_t len; + + appendPQExpBuffer(&conn->errorMessage, " (%s)", actx->curl_err); + + /* Sometimes libcurl adds a newline to the error buffer. :( */ + len = conn->errorMessage.len; + if (len >= 2 && conn->errorMessage.data[len - 2] == '\n') + { + conn->errorMessage.data[len - 2] = ')'; + conn->errorMessage.data[len - 1] = '\0'; + conn->errorMessage.len--; + } + } + + appendPQExpBufferStr(&conn->errorMessage, "\n"); + + free_token(&tok); + return PGRES_POLLING_FAILED; +} diff --git a/src/interfaces/libpq/fe-auth-oauth-iddawc.c b/src/interfaces/libpq/fe-auth-oauth-iddawc.c new file mode 100644 index 0000000000..e78d4304d3 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth-iddawc.c @@ -0,0 +1,319 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth-iddawc.c + * The libiddawc implementation of OAuth/OIDC authentication. + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-oauth-iddawc.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include + +#include "fe-auth.h" +#include "fe-auth-oauth.h" +#include "libpq-int.h" + +#ifdef HAVE_I_LOAD_OPENID_CONFIG +/* Older versions of iddawc used 'load' instead of 'get' for some APIs. */ +#define i_get_openid_config i_load_openid_config +#endif + +static const char * +iddawc_error_string(int errcode) +{ + switch (errcode) + { + case I_OK: + return "I_OK"; + + case I_ERROR: + return "I_ERROR"; + + case I_ERROR_PARAM: + return "I_ERROR_PARAM"; + + case I_ERROR_MEMORY: + return "I_ERROR_MEMORY"; + + case I_ERROR_UNAUTHORIZED: + return "I_ERROR_UNAUTHORIZED"; + + case I_ERROR_SERVER: + return "I_ERROR_SERVER"; + } + + return ""; +} + +static void +iddawc_error(PGconn *conn, int errcode, const char *msg) +{ + appendPQExpBufferStr(&conn->errorMessage, libpq_gettext(msg)); + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext(" (iddawc error %s)\n"), + iddawc_error_string(errcode)); +} + +static void +iddawc_request_error(PGconn *conn, struct _i_session *i, int err, const char *msg) +{ + const char *error_code; + const char *desc; + + appendPQExpBuffer(&conn->errorMessage, "%s: ", libpq_gettext(msg)); + + error_code = i_get_str_parameter(i, I_OPT_ERROR); + if (!error_code) + { + /* + * The server didn't give us any useful information, so just print the + * error code. + */ + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("(iddawc error %s)\n"), + iddawc_error_string(err)); + return; + } + + /* If the server gave a string description, print that too. */ + desc = i_get_str_parameter(i, I_OPT_ERROR_DESCRIPTION); + if (desc) + appendPQExpBuffer(&conn->errorMessage, "%s ", desc); + + appendPQExpBuffer(&conn->errorMessage, "(%s)\n", error_code); +} + +/* + * Runs the device authorization flow using libiddawc. If successful, a malloc'd + * token string in "Bearer xxxx..." format, suitable for sending to an + * OAUTHBEARER server, is returned. NULL is returned on error. + */ +static char * +run_iddawc_auth_flow(PGconn *conn, const char *discovery_uri) +{ + struct _i_session session; + PQExpBuffer token_buf = NULL; + int err; + int auth_method; + bool user_prompted = false; + const char *verification_uri; + const char *user_code; + const char *access_token; + const char *token_type; + char *token = NULL; + + i_init_session(&session); + + token_buf = createPQExpBuffer(); + if (!token_buf) + goto cleanup; + + err = i_set_str_parameter(&session, I_OPT_OPENID_CONFIG_ENDPOINT, discovery_uri); + if (err) + { + iddawc_error(conn, err, "failed to set OpenID config endpoint"); + goto cleanup; + } + + err = i_get_openid_config(&session); + if (err) + { + iddawc_error(conn, err, "failed to fetch OpenID discovery document"); + goto cleanup; + } + + if (!i_get_str_parameter(&session, I_OPT_TOKEN_ENDPOINT)) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("issuer has no token endpoint")); + goto cleanup; + } + + if (!i_get_str_parameter(&session, I_OPT_DEVICE_AUTHORIZATION_ENDPOINT)) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("issuer does not support device authorization")); + goto cleanup; + } + + err = i_set_response_type(&session, I_RESPONSE_TYPE_DEVICE_CODE); + if (err) + { + iddawc_error(conn, err, "failed to set device code response type"); + goto cleanup; + } + + auth_method = I_TOKEN_AUTH_METHOD_NONE; + if (conn->oauth_client_secret && *conn->oauth_client_secret) + auth_method = I_TOKEN_AUTH_METHOD_SECRET_BASIC; + + err = i_set_parameter_list(&session, + I_OPT_CLIENT_ID, conn->oauth_client_id, + I_OPT_CLIENT_SECRET, conn->oauth_client_secret, + I_OPT_TOKEN_METHOD, auth_method, + I_OPT_SCOPE, conn->oauth_scope, + I_OPT_NONE + ); + if (err) + { + iddawc_error(conn, err, "failed to set client identifier"); + goto cleanup; + } + + err = i_run_device_auth_request(&session); + if (err) + { + iddawc_request_error(conn, &session, err, + "failed to obtain device authorization"); + goto cleanup; + } + + verification_uri = i_get_str_parameter(&session, I_OPT_DEVICE_AUTH_VERIFICATION_URI); + if (!verification_uri) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("issuer did not provide a verification URI")); + goto cleanup; + } + + user_code = i_get_str_parameter(&session, I_OPT_DEVICE_AUTH_USER_CODE); + if (!user_code) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("issuer did not provide a user code")); + goto cleanup; + } + + /* + * Poll the token endpoint until either the user logs in and authorizes + * the use of a token, or a hard failure occurs. We perform one ping + * _before_ prompting the user, so that we don't make them do the work of + * logging in only to find that the token endpoint is completely + * unreachable. + */ + err = i_run_token_request(&session); + while (err) + { + const char *error_code; + uint interval; + + error_code = i_get_str_parameter(&session, I_OPT_ERROR); + + /* + * authorization_pending and slow_down are the only acceptable errors; + * anything else and we bail. + */ + if (!error_code || (strcmp(error_code, "authorization_pending") + && strcmp(error_code, "slow_down"))) + { + iddawc_request_error(conn, &session, err, + "failed to obtain access token"); + goto cleanup; + } + + if (!user_prompted) + { + int res; + PQpromptOAuthDevice prompt = { + .verification_uri = verification_uri, + .user_code = user_code, + /* TODO: optional fields */ + }; + + /* + * Now that we know the token endpoint isn't broken, give the user + * the login instructions. + */ + res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn, + &prompt); + + if (!res) + { + fprintf(stderr, "Visit %s and enter the code: %s", + prompt.verification_uri, prompt.user_code); + } + else if (res < 0) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("device prompt failed\n")); + goto cleanup; + } + + user_prompted = true; + } + + /*--- + * We are required to wait between polls; the server tells us how + * long. + * TODO: if interval's not set, we need to default to five seconds + * TODO: sanity check the interval + */ + interval = i_get_int_parameter(&session, I_OPT_DEVICE_AUTH_INTERVAL); + + /* + * A slow_down error requires us to permanently increase our retry + * interval by five seconds. RFC 8628, Sec. 3.5. + */ + if (!strcmp(error_code, "slow_down")) + { + interval += 5; + i_set_int_parameter(&session, I_OPT_DEVICE_AUTH_INTERVAL, interval); + } + + sleep(interval); + + /* + * XXX Reset the error code before every call, because iddawc won't do + * that for us. This matters if the server first sends a "pending" + * error code, then later hard-fails without sending an error code to + * overwrite the first one. + * + * That we have to do this at all seems like a bug in iddawc. + */ + i_set_str_parameter(&session, I_OPT_ERROR, NULL); + + err = i_run_token_request(&session); + } + + access_token = i_get_str_parameter(&session, I_OPT_ACCESS_TOKEN); + token_type = i_get_str_parameter(&session, I_OPT_TOKEN_TYPE); + + if (!access_token || !token_type || strcasecmp(token_type, "Bearer")) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("issuer did not provide a bearer token")); + goto cleanup; + } + + appendPQExpBufferStr(token_buf, "Bearer "); + appendPQExpBufferStr(token_buf, access_token); + + if (PQExpBufferBroken(token_buf)) + goto cleanup; + + token = strdup(token_buf->data); + +cleanup: + if (token_buf) + destroyPQExpBuffer(token_buf); + i_clean_session(&session); + + return token; +} + +PostgresPollingStatusType +pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock) +{ + fe_oauth_state *state = conn->sasl_state; + + /* TODO: actually make this asynchronous */ + state->token = run_iddawc_auth_flow(conn, conn->oauth_discovery_uri); + return state->token ? PGRES_POLLING_OK : PGRES_POLLING_FAILED; +} diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c new file mode 100644 index 0000000000..66ee8ff076 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -0,0 +1,659 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth.c + * The front-end (client) implementation of OAuth/OIDC authentication. + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-oauth.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres_fe.h" + +#include "common/base64.h" +#include "common/hmac.h" +#include "common/jsonapi.h" +#include "common/oauth-common.h" +#include "fe-auth.h" +#include "fe-auth-oauth.h" +#include "mb/pg_wchar.h" + +/* The exported OAuth callback mechanism. */ +static void *oauth_init(PGconn *conn, const char *password, + const char *sasl_mechanism); +static SASLStatus oauth_exchange(void *opaq, bool final, + char *input, int inputlen, + char **output, int *outputlen); +static bool oauth_channel_bound(void *opaq); +static void oauth_free(void *opaq); + +const pg_fe_sasl_mech pg_oauth_mech = { + oauth_init, + oauth_exchange, + oauth_channel_bound, + oauth_free, +}; + +static void * +oauth_init(PGconn *conn, const char *password, + const char *sasl_mechanism) +{ + fe_oauth_state *state; + + /* + * We only support one SASL mechanism here; anything else is programmer + * error. + */ + Assert(sasl_mechanism != NULL); + Assert(strcmp(sasl_mechanism, OAUTHBEARER_NAME) == 0); + + state = calloc(1, sizeof(*state)); + if (!state) + return NULL; + + state->state = FE_OAUTH_INIT; + state->conn = conn; + + return state; +} + +#define kvsep "\x01" + +static char * +client_initial_response(PGconn *conn, const char *token) +{ + static const char *const resp_format = "n,," kvsep "auth=%s" kvsep kvsep; + + PQExpBufferData buf; + char *response = NULL; + + if (!token) + { + /* + * Either programmer error, or something went badly wrong during the + * asynchronous fetch. + * + * TODO: users shouldn't see this; what action should they take if + * they do? + */ + libpq_append_conn_error(conn, "no OAuth token was set for the connection"); + return NULL; + } + + initPQExpBuffer(&buf); + appendPQExpBuffer(&buf, resp_format, token); + + if (!PQExpBufferDataBroken(buf)) + response = strdup(buf.data); + + termPQExpBuffer(&buf); + return response; +} + +#define ERROR_STATUS_FIELD "status" +#define ERROR_SCOPE_FIELD "scope" +#define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration" + +struct json_ctx +{ + char *errmsg; /* any non-NULL value stops all processing */ + PQExpBufferData errbuf; /* backing memory for errmsg */ + int nested; /* nesting level (zero is the top) */ + + const char *target_field_name; /* points to a static allocation */ + char **target_field; /* see below */ + + /* target_field, if set, points to one of the following: */ + char *status; + char *scope; + char *discovery_uri; +}; + +#define oauth_json_has_error(ctx) \ + (PQExpBufferDataBroken((ctx)->errbuf) || (ctx)->errmsg) + +#define oauth_json_set_error(ctx, ...) \ + do { \ + appendPQExpBuffer(&(ctx)->errbuf, __VA_ARGS__); \ + (ctx)->errmsg = (ctx)->errbuf.data; \ + } while (0) + +static JsonParseErrorType +oauth_json_object_start(void *state) +{ + struct json_ctx *ctx = state; + + if (ctx->target_field) + { + Assert(ctx->nested == 1); + + oauth_json_set_error(ctx, + libpq_gettext("field \"%s\" must be a string"), + ctx->target_field_name); + } + + ++ctx->nested; + return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_object_end(void *state) +{ + struct json_ctx *ctx = state; + + --ctx->nested; + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_object_field_start(void *state, char *name, bool isnull) +{ + struct json_ctx *ctx = state; + + if (ctx->nested == 1) + { + if (strcmp(name, ERROR_STATUS_FIELD) == 0) + { + ctx->target_field_name = ERROR_STATUS_FIELD; + ctx->target_field = &ctx->status; + } + else if (strcmp(name, ERROR_SCOPE_FIELD) == 0) + { + ctx->target_field_name = ERROR_SCOPE_FIELD; + ctx->target_field = &ctx->scope; + } + else if (strcmp(name, ERROR_OPENID_CONFIGURATION_FIELD) == 0) + { + ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD; + ctx->target_field = &ctx->discovery_uri; + } + } + + free(name); + return JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_array_start(void *state) +{ + struct json_ctx *ctx = state; + + if (!ctx->nested) + { + ctx->errmsg = libpq_gettext("top-level element must be an object"); + } + else if (ctx->target_field) + { + Assert(ctx->nested == 1); + + oauth_json_set_error(ctx, + libpq_gettext("field \"%s\" must be a string"), + ctx->target_field_name); + } + + return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS; +} + +static JsonParseErrorType +oauth_json_scalar(void *state, char *token, JsonTokenType type) +{ + struct json_ctx *ctx = state; + + if (!ctx->nested) + { + ctx->errmsg = libpq_gettext("top-level element must be an object"); + } + else if (ctx->target_field) + { + Assert(ctx->nested == 1); + + if (type == JSON_TOKEN_STRING) + { + *ctx->target_field = token; + + ctx->target_field = NULL; + ctx->target_field_name = NULL; + + return JSON_SUCCESS; /* don't free the token we're using */ + } + + oauth_json_set_error(ctx, + libpq_gettext("field \"%s\" must be a string"), + ctx->target_field_name); + } + + free(token); + return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS; +} + +static bool +handle_oauth_sasl_error(PGconn *conn, char *msg, int msglen) +{ + JsonLexContext lex = {0}; + JsonSemAction sem = {0}; + JsonParseErrorType err; + struct json_ctx ctx = {0}; + char *errmsg = NULL; + + /* Sanity check. */ + if (strlen(msg) != msglen) + { + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("server's error message contained an embedded NULL, and was discarded")); + return false; + } + + makeJsonLexContextCstringLen(&lex, msg, msglen, PG_UTF8, true); + + initPQExpBuffer(&ctx.errbuf); + sem.semstate = &ctx; + + sem.object_start = oauth_json_object_start; + sem.object_end = oauth_json_object_end; + sem.object_field_start = oauth_json_object_field_start; + sem.array_start = oauth_json_array_start; + sem.scalar = oauth_json_scalar; + + err = pg_parse_json(&lex, &sem); + + if (err == JSON_SEM_ACTION_FAILED) + { + if (PQExpBufferDataBroken(ctx.errbuf)) + errmsg = libpq_gettext("out of memory"); + else if (ctx.errmsg) + errmsg = ctx.errmsg; + else + { + /* + * Developer error: one of the action callbacks didn't call + * oauth_json_set_error() before erroring out. + */ + Assert(oauth_json_has_error(&ctx)); + errmsg = ""; + } + } + else if (err != JSON_SUCCESS) + errmsg = json_errdetail(err, &lex); + + if (errmsg) + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("failed to parse server's error response: %s"), + errmsg); + + /* Don't need the error buffer or the JSON lexer anymore. */ + termPQExpBuffer(&ctx.errbuf); + freeJsonLexContext(&lex); + + if (errmsg) + return false; + + /* TODO: what if these override what the user already specified? */ + if (ctx.discovery_uri) + { + if (conn->oauth_discovery_uri) + free(conn->oauth_discovery_uri); + + conn->oauth_discovery_uri = ctx.discovery_uri; + } + + if (ctx.scope) + { + if (conn->oauth_scope) + free(conn->oauth_scope); + + conn->oauth_scope = ctx.scope; + } + /* TODO: missing error scope should clear any existing connection scope */ + + if (!ctx.status) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("server sent error response without a status")); + return false; + } + + if (strcmp(ctx.status, "invalid_token") == 0) + { + /* + * invalid_token is the only error code we'll automatically retry for, + * but only if we have enough information to do so. + */ + if (conn->oauth_discovery_uri) + conn->oauth_want_retry = true; + } + /* TODO: include status in hard failure message */ + + return true; +} + +static void +free_request(PGconn *conn, void *vreq) +{ + PQoauthBearerRequest *request = vreq; + + if (request->cleanup) + request->cleanup(conn, request); + + free(request); +} + +static PostgresPollingStatusType +run_user_oauth_flow(PGconn *conn, pgsocket *altsock) +{ + fe_oauth_state *state = conn->sasl_state; + PQoauthBearerRequest *request = state->async_ctx; + PostgresPollingStatusType status; + + if (!request->async) + { + libpq_append_conn_error(conn, "user-defined OAuth flow provided neither a token nor an async callback"); + return PGRES_POLLING_FAILED; + } + + status = request->async(conn, request, altsock); + if (status == PGRES_POLLING_FAILED) + { + libpq_append_conn_error(conn, "user-defined OAuth flow failed"); + return status; + } + else if (status == PGRES_POLLING_OK) + { + /* + * We already have a token, so copy it into the state. (We can't hold + * onto the original string, since it may not be safe for us to free() + * it.) + */ + PQExpBufferData token; + + if (!request->token) + { + libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a token"); + return PGRES_POLLING_FAILED; + } + + initPQExpBuffer(&token); + appendPQExpBuffer(&token, "Bearer %s", request->token); + + if (PQExpBufferDataBroken(token)) + { + libpq_append_conn_error(conn, "out of memory"); + return PGRES_POLLING_FAILED; + } + + state->token = token.data; + return PGRES_POLLING_OK; + } + + /* TODO: what if no altsock was set? */ + return status; +} + +static bool +setup_token_request(PGconn *conn, fe_oauth_state *state) +{ + int res; + PQoauthBearerRequest request = { + .openid_configuration = conn->oauth_discovery_uri, + .scope = conn->oauth_scope, + }; + + Assert(request.openid_configuration); + + /* The client may have overridden the OAuth flow. */ + res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request); + if (res > 0) + { + PQoauthBearerRequest *request_copy; + + if (request.token) + { + /* + * We already have a token, so copy it into the state. (We can't + * hold onto the original string, since it may not be safe for us + * to free() it.) + */ + PQExpBufferData token; + + initPQExpBuffer(&token); + appendPQExpBuffer(&token, "Bearer %s", request.token); + + if (PQExpBufferDataBroken(token)) + { + libpq_append_conn_error(conn, "out of memory"); + goto fail; + } + + state->token = token.data; + + /* short-circuit */ + if (request.cleanup) + request.cleanup(conn, &request); + return true; + } + + request_copy = malloc(sizeof(*request_copy)); + if (!request_copy) + { + libpq_append_conn_error(conn, "out of memory"); + goto fail; + } + + memcpy(request_copy, &request, sizeof(request)); + + conn->async_auth = run_user_oauth_flow; + state->async_ctx = request_copy; + state->free_async_ctx = free_request; + } + else if (res < 0) + { + libpq_append_conn_error(conn, "user-defined OAuth flow failed"); + goto fail; + } + else + { + /* Use our built-in OAuth flow. */ + conn->async_auth = pg_fe_run_oauth_flow; + } + + return true; + +fail: + if (request.cleanup) + request.cleanup(conn, &request); + return false; +} + +static bool +derive_discovery_uri(PGconn *conn) +{ + PQExpBufferData discovery_buf; + + if (conn->oauth_discovery_uri || !conn->oauth_issuer) + { + /* + * Either we already have one, or we aren't able to derive one + * ourselves. The latter case is not an error condition; we'll just + * ask the server to provide one for us. + */ + return true; + } + + initPQExpBuffer(&discovery_buf); + + Assert(!conn->oauth_discovery_uri); + Assert(conn->oauth_issuer); + + /* + * If we don't yet have a discovery URI, but the user gave us an explicit + * issuer, use the .well-known discovery URI for that issuer. + */ + appendPQExpBufferStr(&discovery_buf, conn->oauth_issuer); + appendPQExpBufferStr(&discovery_buf, "/.well-known/openid-configuration"); + + if (PQExpBufferDataBroken(discovery_buf)) + goto cleanup; + + conn->oauth_discovery_uri = strdup(discovery_buf.data); + +cleanup: + termPQExpBuffer(&discovery_buf); + return (conn->oauth_discovery_uri != NULL); +} + +static SASLStatus +oauth_exchange(void *opaq, bool final, + char *input, int inputlen, + char **output, int *outputlen) +{ + fe_oauth_state *state = opaq; + PGconn *conn = state->conn; + + *output = NULL; + *outputlen = 0; + + switch (state->state) + { + case FE_OAUTH_INIT: + Assert(inputlen == -1); + + if (!derive_discovery_uri(conn)) + return SASL_FAILED; + + if (conn->oauth_discovery_uri) + { + if (!conn->oauth_client_id) + { + /* We can't talk to a server without a client identifier. */ + libpq_append_conn_error(conn, "no oauth_client_id is set for the connection"); + return SASL_FAILED; + } + + /* + * Decide whether we're using a user-provided OAuth flow, or + * the one we have built in. + */ + if (!setup_token_request(conn, state)) + return SASL_FAILED; + + if (state->token) + { + /* + * A really smart user implementation may have already + * given us the token (e.g. if there was an unexpired copy + * already cached). In that case, we can just fall + * through. + */ + } + else + { + /* + * Otherwise, we have to hand the connection over to our + * OAuth implementation. This involves a number of HTTP + * connections and timed waits, so we escape the + * synchronous auth processing and tell PQconnectPoll to + * transfer control to our async implementation. + */ + Assert(conn->async_auth); /* should have been set + * already */ + state->state = FE_OAUTH_REQUESTING_TOKEN; + return SASL_ASYNC; + } + } + else + { + /* + * If we don't have a discovery URI to be able to request a + * token, we ask the server for one explicitly with an empty + * token. This doesn't require any asynchronous work. + */ + state->token = strdup(""); + if (!state->token) + { + libpq_append_conn_error(conn, "out of memory"); + return SASL_FAILED; + } + } + + /* fall through */ + + case FE_OAUTH_REQUESTING_TOKEN: + /* We should still be in the initial response phase. */ + Assert(inputlen == -1); + + *output = client_initial_response(conn, state->token); + if (!*output) + return SASL_FAILED; + + *outputlen = strlen(*output); + state->state = FE_OAUTH_BEARER_SENT; + + return SASL_CONTINUE; + + case FE_OAUTH_BEARER_SENT: + if (final) + { + /* TODO: ensure there is no message content here. */ + return SASL_COMPLETE; + } + + /* + * Error message sent by the server. + */ + if (!handle_oauth_sasl_error(conn, input, inputlen)) + return SASL_FAILED; + + /* + * Respond with the required dummy message (RFC 7628, sec. 3.2.3). + */ + *output = strdup(kvsep); + *outputlen = strlen(*output); /* == 1 */ + + state->state = FE_OAUTH_SERVER_ERROR; + return SASL_CONTINUE; + + case FE_OAUTH_SERVER_ERROR: + + /* + * After an error, the server should send an error response to + * fail the SASL handshake, which is handled in higher layers. + * + * If we get here, the server either sent *another* challenge + * which isn't defined in the RFC, or completed the handshake + * successfully after telling us it was going to fail. Neither is + * acceptable. + */ + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("server sent additional OAuth data after error\n")); + return SASL_FAILED; + + default: + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("invalid OAuth exchange state\n")); + break; + } + + return SASL_FAILED; +} + +static bool +oauth_channel_bound(void *opaq) +{ + /* This mechanism does not support channel binding. */ + return false; +} + +static void +oauth_free(void *opaq) +{ + fe_oauth_state *state = opaq; + + free(state->token); + if (state->async_ctx) + state->free_async_ctx(state->conn, state->async_ctx); + + free(state); +} diff --git a/src/interfaces/libpq/fe-auth-oauth.h b/src/interfaces/libpq/fe-auth-oauth.h new file mode 100644 index 0000000000..8d4ea45aa8 --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth.h @@ -0,0 +1,42 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth.h + * + * Definitions for OAuth authentication implementations + * + * Portions Copyright (c) 2023, PostgreSQL Global Development Group + * + * src/interfaces/libpq/fe-auth-oauth.h + * + *------------------------------------------------------------------------- + */ + +#ifndef FE_AUTH_OAUTH_H +#define FE_AUTH_OAUTH_H + +#include "libpq-fe.h" +#include "libpq-int.h" + + +typedef enum +{ + FE_OAUTH_INIT, + FE_OAUTH_REQUESTING_TOKEN, + FE_OAUTH_BEARER_SENT, + FE_OAUTH_SERVER_ERROR, +} fe_oauth_state_enum; + +typedef struct +{ + fe_oauth_state_enum state; + + PGconn *conn; + char *token; + + void *async_ctx; + void (*free_async_ctx) (PGconn *conn, void *ctx); +} fe_oauth_state; + +extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock); + +#endif /* FE_AUTH_OAUTH_H */ diff --git a/src/interfaces/libpq/fe-auth-sasl.h b/src/interfaces/libpq/fe-auth-sasl.h index 4eecf53a15..83bdab1f40 100644 --- a/src/interfaces/libpq/fe-auth-sasl.h +++ b/src/interfaces/libpq/fe-auth-sasl.h @@ -30,6 +30,7 @@ typedef enum SASL_COMPLETE = 0, SASL_FAILED, SASL_CONTINUE, + SASL_ASYNC, } SASLStatus; /* @@ -77,6 +78,8 @@ typedef struct pg_fe_sasl_mech * * state: The opaque mechanism state returned by init() * + * final: true if the server has sent a final exchange outcome + * * input: The challenge data sent by the server, or NULL when * generating a client-first initial response (that is, when * the server expects the client to send a message to start @@ -101,12 +104,17 @@ typedef struct pg_fe_sasl_mech * * SASL_CONTINUE: The output buffer is filled with a client response. * Additional server challenge is expected + * SASL_ASYNC: Some asynchronous processing external to the + * connection needs to be done before a response can be + * generated. The mechanism is responsible for setting up + * conn->async_auth appropriately before returning. * SASL_COMPLETE: The SASL exchange has completed successfully. * SASL_FAILED: The exchange has failed and the connection should be * dropped. *-------- */ - SASLStatus (*exchange) (void *state, char *input, int inputlen, + SASLStatus (*exchange) (void *state, bool final, + char *input, int inputlen, char **output, int *outputlen); /*-------- diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index 0bb820e0d9..da168eb2f5 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -24,7 +24,8 @@ /* The exported SCRAM callback mechanism. */ static void *scram_init(PGconn *conn, const char *password, const char *sasl_mechanism); -static SASLStatus scram_exchange(void *opaq, char *input, int inputlen, +static SASLStatus scram_exchange(void *opaq, bool final, + char *input, int inputlen, char **output, int *outputlen); static bool scram_channel_bound(void *opaq); static void scram_free(void *opaq); @@ -202,7 +203,8 @@ scram_free(void *opaq) * Exchange a SCRAM message with backend. */ static SASLStatus -scram_exchange(void *opaq, char *input, int inputlen, +scram_exchange(void *opaq, bool final, + char *input, int inputlen, char **output, int *outputlen) { fe_scram_state *state = (fe_scram_state *) opaq; diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 81ec08485d..9cd5c8cfb1 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -39,6 +39,7 @@ #endif #include "common/md5.h" +#include "common/oauth-common.h" #include "common/scram-common.h" #include "fe-auth.h" #include "fe-auth-sasl.h" @@ -419,7 +420,7 @@ pg_SSPI_startup(PGconn *conn, int use_negotiate, int payloadlen) * Initialize SASL authentication exchange. */ static int -pg_SASL_init(PGconn *conn, int payloadlen) +pg_SASL_init(PGconn *conn, int payloadlen, bool *async) { char *initialresponse = NULL; int initialresponselen; @@ -437,7 +438,7 @@ pg_SASL_init(PGconn *conn, int payloadlen) goto error; } - if (conn->sasl_state) + if (conn->sasl_state && !conn->async_auth) { libpq_append_conn_error(conn, "duplicate SASL authentication request"); goto error; @@ -524,6 +525,15 @@ pg_SASL_init(PGconn *conn, int payloadlen) conn->sasl = &pg_scram_mech; conn->password_needed = true; } +#ifdef USE_OAUTH + else if (strcmp(mechanism_buf.data, OAUTHBEARER_NAME) == 0 && + !selected_mechanism) + { + selected_mechanism = OAUTHBEARER_NAME; + conn->sasl = &pg_oauth_mech; + conn->password_needed = false; + } +#endif } if (!selected_mechanism) @@ -563,26 +573,48 @@ pg_SASL_init(PGconn *conn, int payloadlen) Assert(conn->sasl); - /* - * Initialize the SASL state information with all the information gathered - * during the initial exchange. - * - * Note: Only tls-unique is supported for the moment. - */ - conn->sasl_state = conn->sasl->init(conn, - password, - selected_mechanism); if (!conn->sasl_state) - goto oom_error; + { + /* + * Initialize the SASL state information with all the information + * gathered during the initial exchange. + * + * Note: Only tls-unique is supported for the moment. + */ + conn->sasl_state = conn->sasl->init(conn, + password, + selected_mechanism); + if (!conn->sasl_state) + goto oom_error; + } + else + { + /* + * This is only possible if we're returning from an async loop. + * Disconnect it now. + */ + Assert(conn->async_auth); + conn->async_auth = NULL; + } /* Get the mechanism-specific Initial Client Response, if any */ - status = conn->sasl->exchange(conn->sasl_state, + status = conn->sasl->exchange(conn->sasl_state, false, NULL, -1, &initialresponse, &initialresponselen); if (status == SASL_FAILED) goto error; + if (status == SASL_ASYNC) + { + /* + * The mechanism should have set up the necessary callbacks; all we + * need to do is signal the caller. + */ + *async = true; + return STATUS_OK; + } + /* * Build a SASLInitialResponse message, and send it. */ @@ -625,7 +657,7 @@ oom_error: * the protocol. */ static int -pg_SASL_continue(PGconn *conn, int payloadlen, bool final) +pg_SASL_continue(PGconn *conn, int payloadlen, bool final, bool *async) { char *output; int outputlen; @@ -650,11 +682,21 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final) /* For safety and convenience, ensure the buffer is NULL-terminated. */ challenge[payloadlen] = '\0'; - status = conn->sasl->exchange(conn->sasl_state, + status = conn->sasl->exchange(conn->sasl_state, final, challenge, payloadlen, &output, &outputlen); free(challenge); /* don't need the input anymore */ + if (status == SASL_ASYNC) + { + /* + * The mechanism should have set up the necessary callbacks; all we + * need to do is signal the caller. + */ + *async = true; + return STATUS_OK; + } + if (final && status == SASL_CONTINUE) { if (outputlen != 0) @@ -955,12 +997,18 @@ check_expected_areq(AuthRequest areq, PGconn *conn) * it. We are responsible for reading any remaining extra data, specific * to the authentication method. 'payloadlen' is the remaining length in * the message. + * + * If *async is set to true on return, the client doesn't yet have enough + * information to respond, and the caller must temporarily switch to + * conn->async_auth() to continue driving the exchange. */ int -pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn) +pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn, bool *async) { int oldmsglen; + *async = false; + if (!check_expected_areq(areq, conn)) return STATUS_ERROR; @@ -1118,7 +1166,7 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn) * The request contains the name (as assigned by IANA) of the * authentication mechanism. */ - if (pg_SASL_init(conn, payloadlen) != STATUS_OK) + if (pg_SASL_init(conn, payloadlen, async) != STATUS_OK) { /* pg_SASL_init already set the error message */ return STATUS_ERROR; @@ -1135,7 +1183,8 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn) } oldmsglen = conn->errorMessage.len; if (pg_SASL_continue(conn, payloadlen, - (areq == AUTH_REQ_SASL_FIN)) != STATUS_OK) + (areq == AUTH_REQ_SASL_FIN), + async) != STATUS_OK) { /* Use this message if pg_SASL_continue didn't supply one */ if (conn->errorMessage.len == oldmsglen) @@ -1451,3 +1500,23 @@ PQchangePassword(PGconn *conn, const char *user, const char *passwd) } } } + +PQauthDataHook_type PQauthDataHook = PQdefaultAuthDataHook; + +PQauthDataHook_type +PQgetAuthDataHook(void) +{ + return PQauthDataHook; +} + +void +PQsetAuthDataHook(PQauthDataHook_type hook) +{ + PQauthDataHook = hook ? hook : PQdefaultAuthDataHook; +} + +int +PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data) +{ + return 0; /* handle nothing */ +} diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index a18c508688..3e2bc1333f 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -18,8 +18,12 @@ #include "libpq-int.h" +extern PQauthDataHook_type PQauthDataHook; + + /* Prototypes for functions in fe-auth.c */ -extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn); +extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn, + bool *async); extern char *pg_fe_getusername(uid_t user_id, PQExpBuffer errorMessage); extern char *pg_fe_getauthname(PQExpBuffer errorMessage); @@ -29,4 +33,7 @@ extern char *pg_fe_scram_build_secret(const char *password, int iterations, const char **errstr); +/* Mechanisms in fe-auth-oauth.c */ +extern const pg_fe_sasl_mech pg_oauth_mech; + #endif /* FE_AUTH_H */ diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index d4e10a0c4f..15ceb73d01 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -359,6 +359,23 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Load-Balance-Hosts", "", 8, /* sizeof("disable") = 8 */ offsetof(struct pg_conn, load_balance_hosts)}, + /* OAuth v2 */ + {"oauth_issuer", NULL, NULL, NULL, + "OAuth-Issuer", "", 40, + offsetof(struct pg_conn, oauth_issuer)}, + + {"oauth_client_id", NULL, NULL, NULL, + "OAuth-Client-ID", "", 40, + offsetof(struct pg_conn, oauth_client_id)}, + + {"oauth_client_secret", NULL, NULL, NULL, + "OAuth-Client-Secret", "", 40, + offsetof(struct pg_conn, oauth_client_secret)}, + + {"oauth_scope", NULL, NULL, NULL, + "OAuth-Scope", "", 15, + offsetof(struct pg_conn, oauth_scope)}, + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} @@ -618,6 +635,7 @@ pqDropServerData(PGconn *conn) conn->write_err_msg = NULL; conn->be_pid = 0; conn->be_key = 0; + /* conn->oauth_want_retry = false; TODO */ } @@ -2536,6 +2554,7 @@ PQconnectPoll(PGconn *conn) case CONNECTION_NEEDED: case CONNECTION_GSS_STARTUP: case CONNECTION_CHECK_TARGET: + case CONNECTION_AUTHENTICATING: break; default: @@ -3517,6 +3536,7 @@ keep_going: /* We will come back to here until there is int avail; AuthRequest areq; int res; + bool async; /* * Scan the message from current point (note that if we find @@ -3672,6 +3692,16 @@ keep_going: /* We will come back to here until there is /* Check to see if we should mention pgpassfile */ pgpassfileWarning(conn); +#ifdef USE_OAUTH + if (conn->sasl == &pg_oauth_mech + && conn->oauth_want_retry) + { + /* TODO: only allow retry once */ + need_new_connection = true; + goto keep_going; + } +#endif + #ifdef ENABLE_GSS /* @@ -3753,7 +3783,17 @@ keep_going: /* We will come back to here until there is * Note that conn->pghost must be non-NULL if we are going to * avoid the Kerberos code doing a hostname look-up. */ - res = pg_fe_sendauth(areq, msgLength, conn); + res = pg_fe_sendauth(areq, msgLength, conn, &async); + + if (async && (res == STATUS_OK)) + { + /* + * We'll come back later once we're ready to respond. + * Don't consume the request yet. + */ + conn->status = CONNECTION_AUTHENTICATING; + goto keep_going; + } /* OK, we have processed the message; mark data consumed */ conn->inStart = conn->inCursor; @@ -3786,6 +3826,41 @@ keep_going: /* We will come back to here until there is goto keep_going; } + case CONNECTION_AUTHENTICATING: + { + PostgresPollingStatusType status; + pgsocket altsock = PGINVALID_SOCKET; + + if (!conn->async_auth) + { + /* programmer error; should not happen */ + libpq_append_conn_error(conn, "async authentication has no handler"); + goto error_return; + } + + status = conn->async_auth(conn, &altsock); + + if (status == PGRES_POLLING_FAILED) + goto error_return; + + if (status == PGRES_POLLING_OK) + { + /* + * Reenter the authentication exchange with the server. We + * didn't consume the message that started external + * authentication, so it'll be reprocessed as if we just + * received it. + */ + conn->status = CONNECTION_AWAITING_RESPONSE; + conn->altsock = PGINVALID_SOCKET; /* TODO: what frees + * this? */ + goto keep_going; + } + + conn->altsock = altsock; + return status; + } + case CONNECTION_AUTH_OK: { /* @@ -4285,6 +4360,7 @@ pqMakeEmptyPGconn(void) conn->verbosity = PQERRORS_DEFAULT; conn->show_context = PQSHOW_CONTEXT_ERRORS; conn->sock = PGINVALID_SOCKET; + conn->altsock = PGINVALID_SOCKET; conn->Pfdebug = NULL; /* @@ -4400,6 +4476,11 @@ freePGconn(PGconn *conn) free(conn->rowBuf); free(conn->target_session_attrs); free(conn->load_balance_hosts); + free(conn->oauth_issuer); + free(conn->oauth_discovery_uri); + free(conn->oauth_client_id); + free(conn->oauth_client_secret); + free(conn->oauth_scope); termPQExpBuffer(&conn->errorMessage); termPQExpBuffer(&conn->workBuffer); @@ -6868,6 +6949,8 @@ PQsocket(const PGconn *conn) { if (!conn) return -1; + if (conn->altsock != PGINVALID_SOCKET) + return conn->altsock; return (conn->sock != PGINVALID_SOCKET) ? conn->sock : -1; } diff --git a/src/interfaces/libpq/fe-misc.c b/src/interfaces/libpq/fe-misc.c index f2fc78a481..663b1c1acf 100644 --- a/src/interfaces/libpq/fe-misc.c +++ b/src/interfaces/libpq/fe-misc.c @@ -1039,10 +1039,13 @@ static int pqSocketCheck(PGconn *conn, int forRead, int forWrite, time_t end_time) { int result; + pgsocket sock; if (!conn) return -1; - if (conn->sock == PGINVALID_SOCKET) + + sock = (conn->altsock != PGINVALID_SOCKET) ? conn->altsock : conn->sock; + if (sock == PGINVALID_SOCKET) { libpq_append_conn_error(conn, "invalid socket"); return -1; @@ -1059,7 +1062,7 @@ pqSocketCheck(PGconn *conn, int forRead, int forWrite, time_t end_time) /* We will retry as long as we get EINTR */ do - result = pqSocketPoll(conn->sock, forRead, forWrite, end_time); + result = pqSocketPoll(sock, forRead, forWrite, end_time); while (result < 0 && SOCK_ERRNO == EINTR); if (result < 0) diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index defc415fa3..d095351c66 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -38,6 +38,8 @@ extern "C" #define LIBPQ_HAS_TRACE_FLAGS 1 /* Indicates that PQsslAttribute(NULL, "library") is useful */ #define LIBPQ_HAS_SSL_LIBRARY_DETECTION 1 +/* Indicates presence of the PQAUTHDATA_PROMPT_OAUTH_DEVICE authdata hook */ +#define LIBPQ_HAS_PROMPT_OAUTH_DEVICE 1 /* * Option flags for PQcopyResult @@ -78,7 +80,9 @@ typedef enum CONNECTION_CONSUME, /* Consuming any extra messages. */ CONNECTION_GSS_STARTUP, /* Negotiating GSSAPI. */ CONNECTION_CHECK_TARGET, /* Checking target server properties. */ - CONNECTION_CHECK_STANDBY /* Checking if server is in standby mode. */ + CONNECTION_CHECK_STANDBY, /* Checking if server is in standby mode. */ + CONNECTION_AUTHENTICATING, /* Authentication is in progress with some + * external system. */ } ConnStatusType; typedef enum @@ -160,6 +164,13 @@ typedef enum PQ_PIPELINE_ABORTED } PGpipelineStatus; +typedef enum +{ + PQAUTHDATA_PROMPT_OAUTH_DEVICE, /* user must visit a device-authorization + * URL */ + PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token */ +} PGAuthData; + /* PGconn encapsulates a connection to the backend. * The contents of this struct are not supposed to be known to applications. */ @@ -658,10 +669,74 @@ extern int PQenv2encoding(void); /* === in fe-auth.c === */ +typedef struct _PQpromptOAuthDevice +{ + const char *verification_uri; /* verification URI to visit */ + const char *user_code; /* user code to enter */ +} PQpromptOAuthDevice; + +typedef struct _PQoauthBearerRequest +{ + /* Hook inputs (constant across all calls) */ + const char *const openid_configuration; /* OIDC discovery URI */ + const char *const scope; /* required scope(s), or NULL */ + + /* Hook outputs */ + + /*--------- + * Callback implementing a custom asynchronous OAuth flow. + * + * The callback may return + * - PGRES_POLLING_READING/WRITING, to indicate that a file descriptor + * has been stored in *altsock and libpq should wait until it is + * readable or writable before calling back; + * - PGRES_POLLING_OK, to indicate that the flow is complete and + * request->token has been set; or + * - PGRES_POLLING_FAILED, to indicate that token retrieval has failed. + * + * This callback is optional. If the token can be obtained without + * blocking during the original call to the PQAUTHDATA_OAUTH_BEARER_TOKEN + * hook, it may be returned directly, but one of request->async or + * request->token must be set by the hook. + */ + PostgresPollingStatusType (*async) (PGconn *conn, + struct _PQoauthBearerRequest *request, + int *altsock); + + /* + * Callback to clean up custom allocations. A hook implementation may use + * this to free request->token and any resources in request->user. + * + * This is technically optional, but highly recommended, because there is + * no other indication as to when it is safe to free the token. + */ + void (*cleanup) (PGconn *conn, struct _PQoauthBearerRequest *request); + + /* + * The hook should set this to the Bearer token contents for the + * connection, once the flow is completed. The token contents must remain + * available to libpq until the hook's cleanup callback is called. + */ + char *token; + + /* + * Hook-defined data. libpq will not modify this pointer across calls to + * the async callback, so it can be used to keep track of + * application-specific state. Resources allocated here should be freed by + * the cleanup callback. + */ + void *user; +} PQoauthBearerRequest; + extern char *PQencryptPassword(const char *passwd, const char *user); extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm); extern PGresult *PQchangePassword(PGconn *conn, const char *user, const char *passwd); +typedef int (*PQauthDataHook_type) (PGAuthData type, PGconn *conn, void *data); +extern void PQsetAuthDataHook(PQauthDataHook_type hook); +extern PQauthDataHook_type PQgetAuthDataHook(void); +extern int PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data); + /* === in encnames.c === */ extern int pg_char_to_encoding(const char *name); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 82c18f870d..cf26c693e3 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -351,6 +351,8 @@ typedef struct pg_conn_host * found in password file. */ } pg_conn_host; +typedef PostgresPollingStatusType (*AsyncAuthFunc) (PGconn *conn, pgsocket *altsock); + /* * PGconn stores all the state data associated with a single connection * to a backend. @@ -409,6 +411,15 @@ struct pg_conn char *require_auth; /* name of the expected auth method */ char *load_balance_hosts; /* load balance over hosts */ + /* OAuth v2 */ + char *oauth_issuer; /* token issuer URL */ + char *oauth_discovery_uri; /* URI of the issuer's discovery + * document */ + char *oauth_client_id; /* client identifier */ + char *oauth_client_secret; /* client secret */ + char *oauth_scope; /* access token scope */ + bool oauth_want_retry; /* should we retry on failure? */ + /* Optional file to write trace info to */ FILE *Pfdebug; int traceFlags; @@ -477,6 +488,9 @@ struct pg_conn bool client_finished_auth; /* have we finished our half of the * authentication exchange? */ + AsyncAuthFunc async_auth; /* callback for external async authentication */ + pgsocket altsock; /* alternative socket for client to poll */ + /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ diff --git a/src/interfaces/libpq/meson.build b/src/interfaces/libpq/meson.build index be6fadaea2..ae90483319 100644 --- a/src/interfaces/libpq/meson.build +++ b/src/interfaces/libpq/meson.build @@ -40,6 +40,15 @@ if gssapi.found() ) endif +if oauth.found() + libpq_sources += files('fe-auth-oauth.c') + if oauth_library == 'iddawc' + libpq_sources += files('fe-auth-oauth-iddawc.c') + else + libpq_sources += files('fe-auth-oauth-curl.c') + endif +endif + export_file = custom_target('libpq.exports', kwargs: gen_export_kwargs, ) diff --git a/src/makefiles/meson.build b/src/makefiles/meson.build index b0f4178b3d..f803c1200b 100644 --- a/src/makefiles/meson.build +++ b/src/makefiles/meson.build @@ -231,6 +231,7 @@ pgxs_deps = { 'llvm': llvm, 'lz4': lz4, 'nls': libintl, + 'oauth': oauth, 'pam': pam, 'perl': perl_dep, 'python': python3_dep, diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 2461567026..6234fe66f1 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -153,6 +153,7 @@ ArrayMetaState ArraySubWorkspace ArrayToken ArrayType +AsyncAuthFunc AsyncQueueControl AsyncQueueEntry AsyncRequest @@ -354,6 +355,8 @@ CState CTECycleClause CTEMaterialize CTESearchClause +CURL +CURLM CV CachedExpression CachedPlan @@ -1653,6 +1656,7 @@ NumericDigit NumericSortSupport NumericSumAccum NumericVar +OAuthStep OM_uint32 OP OSAPerGroupState @@ -1718,6 +1722,7 @@ PFN PGAlignedBlock PGAlignedXLogBlock PGAsyncStatusType +PGAuthData PGCALL2 PGChecksummablePage PGContextVisibility @@ -1877,11 +1882,14 @@ PQArgBlock PQEnvironmentOption PQExpBuffer PQExpBufferData +PQauthDataHook_type PQcommMethods PQconninfoOption PQnoticeProcessor PQnoticeReceiver +PQoauthBearerRequest PQprintOpt +PQpromptOAuthDevice PQsslKeyPassHook_OpenSSL_type PREDICATELOCK PREDICATELOCKTAG @@ -3343,6 +3351,8 @@ explain_get_index_name_hook_type f_smgr fasthash_state fd_set +fe_oauth_state +fe_oauth_state_enum fe_scram_state fe_scram_state_enum fetch_range_request -- 2.34.1