1
0
mirror of https://github.com/postgres/postgres.git synced 2025-10-24 01:29:19 +03:00

Add support for OAUTHBEARER SASL mechanism

This commit implements OAUTHBEARER, RFC 7628, and OAuth 2.0 Device
Authorization Grants, RFC 8628.  In order to use this there is a
new pg_hba auth method called oauth.  When speaking to a OAuth-
enabled server, it looks a bit like this:

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

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

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

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

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

Author: Jacob Champion <jacob.champion@enterprisedb.com>
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
Co-authored-by: Thomas Munro <thomas.munro@gmail.com>
Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Antonin Houska <ah@cybertec.at>
Reviewed-by: Kashif Zeeshan <kashi.zeeshan@gmail.com>
Discussion: https://postgr.es/m/d1b467a78e0e36ed85a09adf979d04cf124a9d4b.camel@vmware.com
This commit is contained in:
Daniel Gustafsson
2025-02-20 16:25:17 +01:00
parent 1fd1bd8710
commit b3f0be788a
60 changed files with 9278 additions and 39 deletions

View File

@@ -31,6 +31,7 @@ endif
OBJS = \
$(WIN32RES) \
fe-auth-oauth.o \
fe-auth-scram.o \
fe-cancel.o \
fe-connect.o \
@@ -63,6 +64,10 @@ OBJS += \
fe-secure-gssapi.o
endif
ifeq ($(with_libcurl),yes)
OBJS += fe-auth-oauth-curl.o
endif
ifeq ($(PORTNAME), cygwin)
override shlib = cyg$(NAME)$(DLSUFFIX)
endif
@@ -81,7 +86,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 -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
@@ -110,6 +115,8 @@ backend_src = $(top_srcdir)/src/backend
# which seems to insert references to that even in pure C code. Excluding
# __tsan_func_exit is necessary when using ThreadSanitizer data race detector
# which use this function for instrumentation of function exit.
# libcurl registers an exit handler in the memory debugging code when running
# with LeakSanitizer.
# Skip the test when profiling, as gcc may insert exit() calls for that.
# Also skip the test on platforms where libpq infrastructure may be provided
# by statically-linked libraries, as we can't expect them to honor this
@@ -117,7 +124,7 @@ backend_src = $(top_srcdir)/src/backend
libpq-refs-stamp: $(shlib)
ifneq ($(enable_coverage), yes)
ifeq (,$(filter solaris,$(PORTNAME)))
@if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit | grep exit; then \
@if nm -A -u $< 2>/dev/null | grep -v -e __cxa_atexit -e __tsan_func_exit -e _atexit | grep exit; then \
echo 'libpq must not be calling any function which invokes exit'; exit 1; \
fi
endif

View File

@@ -206,3 +206,6 @@ PQsocketPoll 203
PQsetChunkedRowsMode 204
PQgetCurrentTimeUSec 205
PQservice 206
PQsetAuthDataHook 207
PQgetAuthDataHook 208
PQdefaultAuthDataHook 209

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
/*-------------------------------------------------------------------------
*
* fe-auth-oauth.h
*
* Definitions for OAuth authentication implementations
*
* Portions Copyright (c) 1996-2025, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
*
* 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"
enum fe_oauth_step
{
FE_OAUTH_INIT,
FE_OAUTH_BEARER_SENT,
FE_OAUTH_REQUESTING_TOKEN,
FE_OAUTH_SERVER_ERROR,
};
typedef struct
{
enum fe_oauth_step step;
PGconn *conn;
void *async_ctx;
} fe_oauth_state;
extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn);
extern void pg_fe_cleanup_oauth_flow(PGconn *conn);
extern void pqClearOAuthToken(PGconn *conn);
extern bool oauth_unsafe_debugging_enabled(void);
/* Mechanisms in fe-auth-oauth.c */
extern const pg_fe_sasl_mech pg_oauth_mech;
#endif /* FE_AUTH_OAUTH_H */

View File

@@ -40,9 +40,11 @@
#endif
#include "common/md5.h"
#include "common/oauth-common.h"
#include "common/scram-common.h"
#include "fe-auth.h"
#include "fe-auth-sasl.h"
#include "fe-auth-oauth.h"
#include "libpq-fe.h"
#ifdef ENABLE_GSS
@@ -535,6 +537,13 @@ pg_SASL_init(PGconn *conn, int payloadlen, bool *async)
conn->sasl = &pg_scram_mech;
conn->password_needed = true;
}
else if (strcmp(mechanism_buf.data, OAUTHBEARER_NAME) == 0 &&
!selected_mechanism)
{
selected_mechanism = OAUTHBEARER_NAME;
conn->sasl = &pg_oauth_mech;
conn->password_needed = false;
}
}
if (!selected_mechanism)
@@ -559,13 +568,6 @@ pg_SASL_init(PGconn *conn, int payloadlen, bool *async)
if (!allowed)
{
/*
* TODO: this is dead code until a second SASL mechanism is added;
* the connection can't have proceeded past check_expected_areq()
* if no SASL methods are allowed.
*/
Assert(false);
libpq_append_conn_error(conn, "authentication method requirement \"%s\" failed: server requested %s authentication",
conn->require_auth, selected_mechanism);
goto error;
@@ -1580,3 +1582,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 */
}

View File

@@ -18,6 +18,9 @@
#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,
bool *async);

View File

@@ -28,6 +28,7 @@
#include "common/scram-common.h"
#include "common/string.h"
#include "fe-auth.h"
#include "fe-auth-oauth.h"
#include "libpq-fe.h"
#include "libpq-int.h"
#include "mb/pg_wchar.h"
@@ -373,6 +374,23 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
{"scram_server_key", NULL, NULL, NULL, "SCRAM-Server-Key", "D", SCRAM_MAX_KEY_LEN * 2,
offsetof(struct pg_conn, scram_server_key)},
/* 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}
@@ -399,6 +417,7 @@ static const PQEnvironmentOption EnvironmentOptions[] =
static const pg_fe_sasl_mech *supported_sasl_mechs[] =
{
&pg_scram_mech,
&pg_oauth_mech,
};
#define SASL_MECHANISM_COUNT lengthof(supported_sasl_mechs)
@@ -655,6 +674,7 @@ pqDropServerData(PGconn *conn)
conn->write_failed = false;
free(conn->write_err_msg);
conn->write_err_msg = NULL;
conn->oauth_want_retry = false;
/*
* Cancel connections need to retain their be_pid and be_key across
@@ -1144,7 +1164,7 @@ static inline void
fill_allowed_sasl_mechs(PGconn *conn)
{
/*---
* We only support one mechanism at the moment, so rather than deal with a
* We only support two mechanisms at the moment, so rather than deal with a
* linked list, conn->allowed_sasl_mechs is an array of static length. We
* rely on the compile-time assertion here to keep us honest.
*
@@ -1519,6 +1539,10 @@ pqConnectOptions2(PGconn *conn)
{
mech = &pg_scram_mech;
}
else if (strcmp(method, "oauth") == 0)
{
mech = &pg_oauth_mech;
}
/*
* Final group: meta-options.
@@ -4111,7 +4135,19 @@ keep_going: /* We will come back to here until there is
conn->inStart = conn->inCursor;
if (res != STATUS_OK)
{
/*
* OAuth connections may perform two-step discovery, where
* the first connection is a dummy.
*/
if (conn->sasl == &pg_oauth_mech && conn->oauth_want_retry)
{
need_new_connection = true;
goto keep_going;
}
goto error_return;
}
/*
* Just make sure that any data sent by pg_fe_sendauth is
@@ -4390,6 +4426,9 @@ keep_going: /* We will come back to here until there is
}
}
/* Don't hold onto any OAuth tokens longer than necessary. */
pqClearOAuthToken(conn);
/*
* For non cancel requests we can release the address list
* now. For cancel requests we never actually resolve
@@ -5002,6 +5041,12 @@ freePGconn(PGconn *conn)
free(conn->load_balance_hosts);
free(conn->scram_client_key);
free(conn->scram_server_key);
free(conn->oauth_issuer);
free(conn->oauth_issuer_id);
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);
@@ -5155,6 +5200,7 @@ pqClosePGconn(PGconn *conn)
conn->asyncStatus = PGASYNC_IDLE;
conn->xactStatus = PQTRANS_IDLE;
conn->pipelineStatus = PQ_PIPELINE_OFF;
pqClearOAuthToken(conn);
pqClearAsyncResult(conn); /* deallocate result */
pqClearConnErrorState(conn);

View File

@@ -59,6 +59,8 @@ extern "C"
/* Features added in PostgreSQL v18: */
/* Indicates presence of PQfullProtocolVersion */
#define LIBPQ_HAS_FULL_PROTOCOL_VERSION 1
/* Indicates presence of the PQAUTHDATA_PROMPT_OAUTH_DEVICE authdata hook */
#define LIBPQ_HAS_PROMPT_OAUTH_DEVICE 1
/*
* Option flags for PQcopyResult
@@ -186,6 +188,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.
*/
@@ -720,10 +729,86 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
typedef struct _PGpromptOAuthDevice
{
const char *verification_uri; /* verification URI to visit */
const char *user_code; /* user code to enter */
const char *verification_uri_complete; /* optional combination of URI and
* code, or NULL */
int expires_in; /* seconds until user code expires */
} PGpromptOAuthDevice;
/* for PGoauthBearerRequest.async() */
#ifdef _WIN32
#define SOCKTYPE uintptr_t /* avoids depending on winsock2.h for SOCKET */
#else
#define SOCKTYPE int
#endif
typedef struct _PGoauthBearerRequest
{
/* 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 socket 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 _PGoauthBearerRequest *request,
SOCKTYPE * 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 _PGoauthBearerRequest *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;
} PGoauthBearerRequest;
#undef SOCKTYPE
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);

View File

@@ -437,6 +437,17 @@ struct pg_conn
* cancel request, instead of being a normal
* connection that's used for queries */
/* OAuth v2 */
char *oauth_issuer; /* token issuer/URL */
char *oauth_issuer_id; /* token issuer identifier */
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 */
char *oauth_token; /* access token */
bool oauth_want_retry; /* should we retry on failure? */
/* Optional file to write trace info to */
FILE *Pfdebug;
int traceFlags;
@@ -505,7 +516,7 @@ struct pg_conn
* the server? */
uint32 allowed_auth_methods; /* bitmask of acceptable AuthRequest
* codes */
const pg_fe_sasl_mech *allowed_sasl_mechs[1]; /* and acceptable SASL
const pg_fe_sasl_mech *allowed_sasl_mechs[2]; /* and acceptable SASL
* mechanisms */
bool client_finished_auth; /* have we finished our half of the
* authentication exchange? */

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2022-2025, PostgreSQL Global Development Group
libpq_sources = files(
'fe-auth-oauth.c',
'fe-auth-scram.c',
'fe-auth.c',
'fe-cancel.c',
@@ -37,6 +38,10 @@ if gssapi.found()
)
endif
if libcurl.found()
libpq_sources += files('fe-auth-oauth-curl.c')
endif
export_file = custom_target('libpq.exports',
kwargs: gen_export_kwargs,
)