From 7f6b02652cd771a93ce4269607d498f4ac574e7f Mon Sep 17 00:00:00 2001 From: Jacob Champion Date: Tue, 13 Apr 2021 10:27:27 -0700 Subject: [PATCH v3 5/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. The client implementation requires libiddawc and its development headers. Configure --with-oauth (and --with-includes/--with-libraries to point at the iddawc installation, if it's in a custom location). 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 - ...and more. --- configure | 100 ++++ configure.ac | 19 + src/Makefile.global.in | 1 + src/include/common/oauth-common.h | 19 + src/include/pg_config.h.in | 6 + src/interfaces/libpq/Makefile | 7 +- src/interfaces/libpq/fe-auth-oauth.c | 744 +++++++++++++++++++++++++++ src/interfaces/libpq/fe-auth-sasl.h | 5 +- src/interfaces/libpq/fe-auth-scram.c | 6 +- src/interfaces/libpq/fe-auth.c | 42 +- src/interfaces/libpq/fe-auth.h | 3 + src/interfaces/libpq/fe-connect.c | 38 ++ src/interfaces/libpq/libpq-int.h | 8 + 13 files changed, 979 insertions(+), 19 deletions(-) create mode 100644 src/include/common/oauth-common.h create mode 100644 src/interfaces/libpq/fe-auth-oauth.c diff --git a/configure b/configure index f3cb5c2b51..cd0c50a951 100755 --- a/configure +++ b/configure @@ -718,6 +718,7 @@ with_uuid with_readline with_systemd with_selinux +with_oauth with_ldap with_krb_srvnam krb_srvtab @@ -861,6 +862,7 @@ with_krb_srvnam with_pam with_bsd_auth with_ldap +with_oauth with_bonjour with_selinux with_systemd @@ -1570,6 +1572,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 build with OAuth 2.0 support --with-bonjour build with Bonjour support --with-selinux build with SELinux support --with-systemd build with systemd support @@ -8377,6 +8380,42 @@ $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_echo "#define USE_OAUTH 1" >>confdefs.h + + ;; + no) + : + ;; + *) + as_fn_error $? "no argument expected for --with-oauth option" "$LINENO" 5 + ;; + esac + +else + with_oauth=no + +fi + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $with_oauth" >&5 +$as_echo "$with_oauth" >&6; } + + + # # Bonjour # @@ -13500,6 +13539,56 @@ fi +if test "$with_oauth" = yes ; 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 OAuth support" "$LINENO" 5 +fi + +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 @@ -14513,6 +14602,17 @@ fi done +fi + +if test "$with_oauth" != no; 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 19d1a80367..922608065f 100644 --- a/configure.ac +++ b/configure.ac @@ -887,6 +887,17 @@ AC_MSG_RESULT([$with_ldap]) AC_SUBST(with_ldap) +# +# OAuth 2.0 +# +AC_MSG_CHECKING([whether to build with OAuth support]) +PGAC_ARG_BOOL(with, oauth, no, + [build with OAuth 2.0 support], + [AC_DEFINE([USE_OAUTH], 1, [Define to 1 to build with OAuth 2.0 support. (--with-oauth)])]) +AC_MSG_RESULT([$with_oauth]) +AC_SUBST(with_oauth) + + # # Bonjour # @@ -1385,6 +1396,10 @@ fi AC_SUBST(LDAP_LIBS_FE) AC_SUBST(LDAP_LIBS_BE) +if test "$with_oauth" = yes ; then + AC_CHECK_LIB(iddawc, i_init_session, [], [AC_MSG_ERROR([library 'iddawc' is required for OAuth support])]) +fi + # for contrib/sepgsql if test "$with_selinux" = yes; then AC_CHECK_LIB(selinux, security_compute_create_name, [], @@ -1603,6 +1618,10 @@ elif test "$with_uuid" = ossp ; then [AC_MSG_ERROR([header file or is required for OSSP UUID])])]) fi +if test "$with_oauth" != no; 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/src/Makefile.global.in b/src/Makefile.global.in index bbdc1c4bda..c9c61a9c99 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..3fa95ac7e8 --- /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 635fbb2181..1b3332601e 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -319,6 +319,9 @@ /* Define to 1 if you have the `crypto' library (-lcrypto). */ #undef HAVE_LIBCRYPTO +/* 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 @@ -922,6 +925,9 @@ /* 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 build with OpenSSL support. (--with-ssl=openssl) */ #undef USE_OPENSSL diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 3c53393fa4..727305c578 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -62,6 +62,11 @@ OBJS += \ fe-secure-gssapi.o endif +ifeq ($(with_oauth),yes) +OBJS += \ + fe-auth-oauth.o +endif + ifeq ($(PORTNAME), cygwin) override shlib = cyg$(NAME)$(DLSUFFIX) endif @@ -83,7 +88,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 -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/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c new file mode 100644 index 0000000000..383c9d4bdb --- /dev/null +++ b/src/interfaces/libpq/fe-auth-oauth.c @@ -0,0 +1,744 @@ +/*------------------------------------------------------------------------- + * + * fe-auth-oauth.c + * The front-end (client) implementation of OAuth/OIDC authentication. + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-auth-oauth.c + * + *------------------------------------------------------------------------- + */ + +#include + +#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 "mb/pg_wchar.h" + +/* The exported OAuth callback mechanism. */ +static void *oauth_init(PGconn *conn, const char *password, + const char *sasl_mechanism); +static void oauth_exchange(void *opaq, bool final, + char *input, int inputlen, + char **output, int *outputlen, + bool *done, bool *success); +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, +}; + +typedef enum +{ + FE_OAUTH_INIT, + FE_OAUTH_BEARER_SENT, + FE_OAUTH_SERVER_ERROR, +} fe_oauth_state_enum; + +typedef struct +{ + fe_oauth_state_enum state; + + PGconn *conn; +} fe_oauth_state; + +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)); + + state = malloc(sizeof(*state)); + if (!state) + return NULL; + + state->state = FE_OAUTH_INIT; + state->conn = conn; + + return state; +} + +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); +} + +static char * +get_auth_token(PGconn *conn) +{ + PQExpBuffer token_buf = NULL; + struct _i_session session; + 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; + + if (!conn->oauth_discovery_uri) + return strdup(""); /* ask the server for one */ + + if (!conn->oauth_client_id) + { + /* We can't talk to a server without a client identifier. */ + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("no oauth_client_id is set for the connection")); + return NULL; + } + + i_init_session(&session); + + token_buf = createPQExpBuffer(); + if (!token_buf) + goto cleanup; + + err = i_set_str_parameter(&session, I_OPT_OPENID_CONFIG_ENDPOINT, conn->oauth_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, + "OAuth token retrieval failed"); + goto cleanup; + } + + if (!user_prompted) + { + /* + * Now that we know the token endpoint isn't broken, give the user + * the login instructions. + */ + pqInternalNotice(&conn->noticeHooks, + "Visit %s and enter the code: %s", + verification_uri, user_code); + + 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; +} + +#define kvsep "\x01" + +static char * +client_initial_response(PGconn *conn) +{ + static const char * const resp_format = "n,," kvsep "auth=%s" kvsep kvsep; + + PQExpBuffer token_buf; + PQExpBuffer discovery_buf = NULL; + char *token = NULL; + char *response = NULL; + + token_buf = createPQExpBuffer(); + if (!token_buf) + goto cleanup; + + /* + * 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. + */ + if (!conn->oauth_discovery_uri && conn->oauth_issuer) + { + discovery_buf = createPQExpBuffer(); + if (!discovery_buf) + goto cleanup; + + appendPQExpBufferStr(discovery_buf, conn->oauth_issuer); + appendPQExpBufferStr(discovery_buf, "/.well-known/openid-configuration"); + + if (PQExpBufferBroken(discovery_buf)) + goto cleanup; + + conn->oauth_discovery_uri = strdup(discovery_buf->data); + } + + token = get_auth_token(conn); + if (!token) + goto cleanup; + + appendPQExpBuffer(token_buf, resp_format, token); + if (PQExpBufferBroken(token_buf)) + goto cleanup; + + response = strdup(token_buf->data); + +cleanup: + if (token) + free(token); + if (discovery_buf) + destroyPQExpBuffer(discovery_buf); + if (token_buf) + destroyPQExpBuffer(token_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 void +oauth_json_object_start(void *state) +{ + struct json_ctx *ctx = state; + + if (oauth_json_has_error(ctx)) + return; /* short-circuit */ + + 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; +} + +static void +oauth_json_object_end(void *state) +{ + struct json_ctx *ctx = state; + + if (oauth_json_has_error(ctx)) + return; /* short-circuit */ + + --ctx->nested; +} + +static void +oauth_json_object_field_start(void *state, char *name, bool isnull) +{ + struct json_ctx *ctx = state; + + if (oauth_json_has_error(ctx)) + { + /* short-circuit */ + free(name); + return; + } + + if (ctx->nested == 1) + { + if (!strcmp(name, ERROR_STATUS_FIELD)) + { + ctx->target_field_name = ERROR_STATUS_FIELD; + ctx->target_field = &ctx->status; + } + else if (!strcmp(name, ERROR_SCOPE_FIELD)) + { + ctx->target_field_name = ERROR_SCOPE_FIELD; + ctx->target_field = &ctx->scope; + } + else if (!strcmp(name, ERROR_OPENID_CONFIGURATION_FIELD)) + { + ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD; + ctx->target_field = &ctx->discovery_uri; + } + } + + free(name); +} + +static void +oauth_json_array_start(void *state) +{ + struct json_ctx *ctx = state; + + if (oauth_json_has_error(ctx)) + return; /* short-circuit */ + + 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); + } +} + +static void +oauth_json_scalar(void *state, char *token, JsonTokenType type) +{ + struct json_ctx *ctx = state; + + if (oauth_json_has_error(ctx)) + { + /* short-circuit */ + free(token); + return; + } + + 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; /* 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); +} + +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")); + return false; + } + + initJsonLexContextCstringLen(&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_SUCCESS) + { + errmsg = json_errdetail(err, &lex); + } + else if (PQExpBufferDataBroken(ctx.errbuf)) + { + errmsg = libpq_gettext("out of memory"); + } + else if (ctx.errmsg) + { + errmsg = ctx.errmsg; + } + + 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); + termJsonLexContext(&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")) + { + /* + * 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 +oauth_exchange(void *opaq, bool final, + char *input, int inputlen, + char **output, int *outputlen, + bool *done, bool *success) +{ + fe_oauth_state *state = opaq; + PGconn *conn = state->conn; + + *done = false; + *success = false; + *output = NULL; + *outputlen = 0; + + switch (state->state) + { + case FE_OAUTH_INIT: + Assert(inputlen == -1); + + *output = client_initial_response(conn); + if (!*output) + goto error; + + *outputlen = strlen(*output); + state->state = FE_OAUTH_BEARER_SENT; + + break; + + case FE_OAUTH_BEARER_SENT: + if (final) + { + /* TODO: ensure there is no message content here. */ + *done = true; + *success = true; + + break; + } + + /* + * Error message sent by the server. + */ + if (!handle_oauth_sasl_error(conn, input, inputlen)) + goto error; + + /* + * 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; + break; + + 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")); + goto error; + + default: + appendPQExpBufferStr(&conn->errorMessage, + libpq_gettext("invalid OAuth exchange state\n")); + goto error; + } + + return; + +error: + *done = true; + *success = false; +} + +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); +} diff --git a/src/interfaces/libpq/fe-auth-sasl.h b/src/interfaces/libpq/fe-auth-sasl.h index da3c30b87b..b1bb382f70 100644 --- a/src/interfaces/libpq/fe-auth-sasl.h +++ b/src/interfaces/libpq/fe-auth-sasl.h @@ -65,6 +65,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 @@ -92,7 +94,8 @@ typedef struct pg_fe_sasl_mech * Ignored if *done is false. *-------- */ - void (*exchange) (void *state, char *input, int inputlen, + void (*exchange) (void *state, bool final, + char *input, int inputlen, char **output, int *outputlen, bool *done, bool *success); diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index e616200704..681b76adbe 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 void scram_exchange(void *opaq, char *input, int inputlen, +static void scram_exchange(void *opaq, bool final, + char *input, int inputlen, char **output, int *outputlen, bool *done, bool *success); static bool scram_channel_bound(void *opaq); @@ -206,7 +207,8 @@ scram_free(void *opaq) * Exchange a SCRAM message with backend. */ static void -scram_exchange(void *opaq, char *input, int inputlen, +scram_exchange(void *opaq, bool final, + char *input, int inputlen, char **output, int *outputlen, bool *done, bool *success) { diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 6fceff561b..2567a34023 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -38,6 +38,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" @@ -422,7 +423,7 @@ pg_SASL_init(PGconn *conn, int payloadlen) bool success; const char *selected_mechanism; PQExpBufferData mechanism_buf; - char *password; + char *password = NULL; initPQExpBuffer(&mechanism_buf); @@ -444,8 +445,7 @@ pg_SASL_init(PGconn *conn, int payloadlen) /* * Parse the list of SASL authentication mechanisms in the * AuthenticationSASL message, and select the best mechanism that we - * support. SCRAM-SHA-256-PLUS and SCRAM-SHA-256 are the only ones - * supported at the moment, listed by order of decreasing importance. + * support. Mechanisms are listed by order of decreasing importance. */ selected_mechanism = NULL; for (;;) @@ -485,6 +485,7 @@ pg_SASL_init(PGconn *conn, int payloadlen) { selected_mechanism = SCRAM_SHA_256_PLUS_NAME; conn->sasl = &pg_scram_mech; + conn->password_needed = true; } #else /* @@ -522,7 +523,17 @@ pg_SASL_init(PGconn *conn, int payloadlen) { selected_mechanism = SCRAM_SHA_256_NAME; 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) @@ -547,18 +558,19 @@ pg_SASL_init(PGconn *conn, int payloadlen) /* * First, select the password to use for the exchange, complaining if - * there isn't one. Currently, all supported SASL mechanisms require a - * password, so we can just go ahead here without further distinction. + * there isn't one and the SASL mechanism needs it. */ - conn->password_needed = true; - password = conn->connhost[conn->whichhost].password; - if (password == NULL) - password = conn->pgpass; - if (password == NULL || password[0] == '\0') + if (conn->password_needed) { - appendPQExpBufferStr(&conn->errorMessage, - PQnoPasswordSupplied); - goto error; + password = conn->connhost[conn->whichhost].password; + if (password == NULL) + password = conn->pgpass; + if (password == NULL || password[0] == '\0') + { + appendPQExpBufferStr(&conn->errorMessage, + PQnoPasswordSupplied); + goto error; + } } Assert(conn->sasl); @@ -576,7 +588,7 @@ pg_SASL_init(PGconn *conn, int payloadlen) goto oom_error; /* Get the mechanism-specific Initial Client Response, if any */ - conn->sasl->exchange(conn->sasl_state, + conn->sasl->exchange(conn->sasl_state, false, NULL, -1, &initialresponse, &initialresponselen, &done, &success); @@ -657,7 +669,7 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final) /* For safety and convenience, ensure the buffer is NULL-terminated. */ challenge[payloadlen] = '\0'; - conn->sasl->exchange(conn->sasl_state, + conn->sasl->exchange(conn->sasl_state, final, challenge, payloadlen, &output, &outputlen, &done, &success); diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index 049a8bb1a1..2a56774019 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -28,4 +28,7 @@ extern const pg_fe_sasl_mech pg_scram_mech; extern char *pg_fe_scram_build_secret(const char *password, 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 1c5a2b43e9..5f78439586 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -344,6 +344,23 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */ offsetof(struct pg_conn, target_session_attrs)}, + /* 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} @@ -606,6 +623,7 @@ pqDropServerData(PGconn *conn) conn->write_err_msg = NULL; conn->be_pid = 0; conn->be_key = 0; + /* conn->oauth_want_retry = false; TODO */ } @@ -3381,6 +3399,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 /* @@ -4161,6 +4189,16 @@ freePGconn(PGconn *conn) free(conn->rowBuf); if (conn->target_session_attrs) free(conn->target_session_attrs); + if (conn->oauth_issuer) + free(conn->oauth_issuer); + if (conn->oauth_discovery_uri) + free(conn->oauth_discovery_uri); + if (conn->oauth_client_id) + free(conn->oauth_client_id); + if (conn->oauth_client_secret) + free(conn->oauth_client_secret); + if (conn->oauth_scope) + free(conn->oauth_scope); termPQExpBuffer(&conn->errorMessage); termPQExpBuffer(&conn->workBuffer); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index e0cee4b142..0dff13505a 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -394,6 +394,14 @@ struct pg_conn char *ssl_max_protocol_version; /* maximum TLS protocol version */ char *target_session_attrs; /* desired session properties */ + /* 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; -- 2.25.1