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:
@@ -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
|
||||
|
||||
@@ -206,3 +206,6 @@ PQsocketPoll 203
|
||||
PQsetChunkedRowsMode 204
|
||||
PQgetCurrentTimeUSec 205
|
||||
PQservice 206
|
||||
PQsetAuthDataHook 207
|
||||
PQgetAuthDataHook 208
|
||||
PQdefaultAuthDataHook 209
|
||||
|
||||
2883
src/interfaces/libpq/fe-auth-oauth-curl.c
Normal file
2883
src/interfaces/libpq/fe-auth-oauth-curl.c
Normal file
File diff suppressed because it is too large
Load Diff
1163
src/interfaces/libpq/fe-auth-oauth.c
Normal file
1163
src/interfaces/libpq/fe-auth-oauth.c
Normal file
File diff suppressed because it is too large
Load Diff
46
src/interfaces/libpq/fe-auth-oauth.h
Normal file
46
src/interfaces/libpq/fe-auth-oauth.h
Normal 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 */
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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? */
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user