<indexterm><primary>PQAUTHDATA_PROMPT_OAUTH_DEVICE</primary></indexterm>
</term>
<listitem>
+ <para><emphasis>Available in PostgreSQL 18 and later.</emphasis></para>
<para>
Replaces the default user prompt during the builtin device
authorization client flow. <replaceable>data</replaceable> points to
<indexterm><primary>PQAUTHDATA_OAUTH_BEARER_TOKEN</primary></indexterm>
</term>
<listitem>
+ <para><emphasis>Available in PostgreSQL 18 and later.</emphasis></para>
<para>
Adds a custom implementation of a flow, replacing the builtin flow if
it is <link linkend="configure-option-with-libcurl">installed</link>.
user/issuer/scope combination, if one is available without blocking, or
else set up an asynchronous callback to retrieve one.
</para>
+ <note>
+ <para>
+ For <productname>PostgreSQL</productname> releases 19 and later,
+ applications should prefer
+ <link linkend="libpq-oauth-authdata-oauth-bearer-token-v2"><literal>PQAUTHDATA_OAUTH_BEARER_TOKEN_V2</literal></link>.
+ </para>
+ </note>
<para>
<replaceable>data</replaceable> points to an instance
of <symbol>PGoauthBearerRequest</symbol>, which should be filled in
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="libpq-oauth-authdata-oauth-bearer-token-v2">
+ <term>
+ <symbol>PQAUTHDATA_OAUTH_BEARER_TOKEN_V2</symbol>
+ <indexterm>
+ <primary>PQAUTHDATA_OAUTH_BEARER_TOKEN_V2</primary>
+ <secondary>PQAUTHDATA_OAUTH_BEARER_TOKEN</secondary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para><emphasis>Available in PostgreSQL 19 and later.</emphasis></para>
+ <para>
+ Provides all the functionality of
+ <link linkend="libpq-oauth-authdata-oauth-bearer-token"><literal>PQAUTHDATA_OAUTH_BEARER_TOKEN</literal></link>,
+ as well as the ability to set custom error messages and retrieve the
+ OAuth issuer ID that the client has trusted.
+ </para>
+ <para>
+ <replaceable>data</replaceable> points to an instance
+ of <symbol>PGoauthBearerRequestV2</symbol>:
+<synopsis>
+typedef struct
+{
+ PGoauthBearerRequest v1; /* see the PGoauthBearerRequest struct, above */
+
+ /* Hook inputs (constant across all calls) */
+ const char *issuer; /* the issuer identifier (RFC 9207) in use */
+
+ /* Hook outputs */
+ const char *error; /* hook-defined error message */
+} PGoauthBearerRequestV2;
+</synopsis>
+ </para>
+ <para>
+ Applications must first use the <replaceable>v1</replaceable> struct
+ member to implement the base API, as described
+ <link linkend="libpq-oauth-authdata-oauth-bearer-token">above</link>.
+ <application>libpq</application> additionally guarantees that the
+ <literal>request</literal> pointer passed to the
+ <replaceable>v1.async</replaceable> and <replaceable>v1.cleanup</replaceable>
+ callbacks may be safely cast to <literal>(PGoauthBearerRequestV2 *)</literal>,
+ to make use of the additional members described below.
+ </para>
+ <warning>
+ <para>
+ Casting to <literal>(PGoauthBearerRequestV2 *)</literal> is
+ <emphasis>only</emphasis> safe when the hook type is
+ <literal>PQAUTHDATA_OAUTH_BEARER_TOKEN_V2</literal>. Applications may
+ crash or misbehave if a hook implementation attempts to access v2
+ members when handling a v1 (<literal>PQAUTHDATA_OAUTH_BEARER_TOKEN</literal>)
+ hook request.
+ </para>
+ </warning>
+ <para>
+ In addition to the functionality of the version 1 API, the v2 struct
+ provides an additional input and output for the hook:
+ </para>
+ <para>
+ <replaceable>issuer</replaceable> contains the issuer identifier, as
+ defined in <ulink url="https://datatracker.ietf.org/doc/html/rfc9207">RFC 9207</ulink>,
+ that is in use for the current connection. This identifier is
+ derived from <xref linkend="libpq-connect-oauth-issuer"/>.
+ To avoid mix-up attacks, custom flows should ensure that any discovery
+ metadata provided by the authorization server matches this issuer ID.
+ </para>
+ <para>
+ <replaceable>error</replaceable> may be set to point to a custom
+ error message when a flow fails. The message will be included as part
+ of <xref linkend="libpq-PQerrorMessage"/>. Hooks must free any error
+ message allocations during the <replaceable>v1.cleanup</replaceable>
+ callback.
+ </para>
+ </listitem>
+ </varlistentry>
+
</variablelist>
</para>
</sect3>
return success;
}
+/*
+ * Helper for handling flow failures. If anything was put into request->error,
+ * it's added to conn->errorMessage here.
+ */
+static void
+report_user_flow_error(PGconn *conn, const PGoauthBearerRequestV2 *request)
+{
+ appendPQExpBufferStr(&conn->errorMessage,
+ libpq_gettext("user-defined OAuth flow failed"));
+
+ if (request->error)
+ {
+ appendPQExpBufferStr(&conn->errorMessage, ": ");
+ appendPQExpBufferStr(&conn->errorMessage, request->error);
+ }
+
+ appendPQExpBufferChar(&conn->errorMessage, '\n');
+}
+
/*
* Callback implementation of conn->async_auth() for a user-defined OAuth flow.
* Delegates the retrieval of the token to the application's async callback.
run_user_oauth_flow(PGconn *conn)
{
fe_oauth_state *state = conn->sasl_state;
- PGoauthBearerRequest *request = state->async_ctx;
+ PGoauthBearerRequestV2 *request = state->async_ctx;
PostgresPollingStatusType status;
- if (!request->async)
+ if (!request->v1.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, &conn->altsock);
+ status = request->v1.async(conn,
+ (PGoauthBearerRequest *) request,
+ &conn->altsock);
+
if (status == PGRES_POLLING_FAILED)
{
- libpq_append_conn_error(conn, "user-defined OAuth flow failed");
+ report_user_flow_error(conn, request);
return status;
}
else if (status == PGRES_POLLING_OK)
* onto the original string, since it may not be safe for us to free()
* it.)
*/
- if (!request->token)
+ if (!request->v1.token)
{
libpq_append_conn_error(conn,
"user-defined OAuth flow did not provide a token");
return PGRES_POLLING_FAILED;
}
- conn->oauth_token = strdup(request->token);
+ conn->oauth_token = strdup(request->v1.token);
if (!conn->oauth_token)
{
libpq_append_conn_error(conn, "out of memory");
}
/*
- * Cleanup callback for the async user flow. Delegates most of its job to the
- * user-provided cleanup implementation, then disconnects the altsock.
+ * Cleanup callback for the async user flow. Delegates most of its job to
+ * PGoauthBearerRequest.cleanup(), then disconnects the altsock and frees the
+ * request itself.
*/
static void
cleanup_user_oauth_flow(PGconn *conn)
{
fe_oauth_state *state = conn->sasl_state;
- PGoauthBearerRequest *request = state->async_ctx;
+ PGoauthBearerRequestV2 *request = state->async_ctx;
Assert(request);
- if (request->cleanup)
- request->cleanup(conn, request);
+ if (request->v1.cleanup)
+ request->v1.cleanup(conn, (PGoauthBearerRequest *) request);
conn->altsock = PGINVALID_SOCKET;
free(request);
* token for presentation to the server.
*
* If the application has registered a custom flow handler using
- * PQAUTHDATA_OAUTH_BEARER_TOKEN, it may either return a token immediately (e.g.
- * if it has one cached for immediate use), or set up for a series of
+ * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2], it may either return a token immediately
+ * (e.g. if it has one cached for immediate use), or set up for a series of
* asynchronous callbacks which will be managed by run_user_oauth_flow().
*
* If the default handler is used instead, a Device Authorization flow is used
setup_token_request(PGconn *conn, fe_oauth_state *state)
{
int res;
- PGoauthBearerRequest request = {
- .openid_configuration = conn->oauth_discovery_uri,
- .scope = conn->oauth_scope,
+ PGoauthBearerRequestV2 request = {
+ .v1 = {
+ .openid_configuration = conn->oauth_discovery_uri,
+ .scope = conn->oauth_scope,
+ },
+ .issuer = conn->oauth_issuer_id,
};
- Assert(request.openid_configuration);
+ Assert(request.v1.openid_configuration);
+ Assert(request.issuer);
+
+ /*
+ * The client may have overridden the OAuth flow. Try the v2 hook first,
+ * then fall back to the v1 implementation.
+ */
+ res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, conn, &request);
+ if (res == 0)
+ res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
- /* The client may have overridden the OAuth flow. */
- res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
if (res > 0)
{
- PGoauthBearerRequest *request_copy;
+ PGoauthBearerRequestV2 *request_copy;
- if (request.token)
+ if (request.v1.token)
{
/*
* We already have a token, so copy it into the conn. (We can't
* hold onto the original string, since it may not be safe for us
* to free() it.)
*/
- conn->oauth_token = strdup(request.token);
+ conn->oauth_token = strdup(request.v1.token);
if (!conn->oauth_token)
{
libpq_append_conn_error(conn, "out of memory");
}
/* short-circuit */
- if (request.cleanup)
- request.cleanup(conn, &request);
+ if (request.v1.cleanup)
+ request.v1.cleanup(conn, (PGoauthBearerRequest *) &request);
return true;
}
}
else if (res < 0)
{
- libpq_append_conn_error(conn, "user-defined OAuth flow failed");
+ report_user_flow_error(conn, &request);
goto fail;
}
else if (!use_builtin_flow(conn, state))
return true;
fail:
- if (request.cleanup)
- request.cleanup(conn, &request);
+ if (request.v1.cleanup)
+ request.v1.cleanup(conn, (PGoauthBearerRequest *) &request);
return false;
}
/* Features added in PostgreSQL v19: */
/* Indicates presence of PQgetThreadLock */
#define LIBPQ_HAS_GET_THREAD_LOCK 1
+/* Indicates presence of the PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 authdata hook */
+#define LIBPQ_HAS_OAUTH_BEARER_TOKEN_V2 1
/*
* Option flags for PQcopyResult
{
PQAUTHDATA_PROMPT_OAUTH_DEVICE, /* user must visit a device-authorization
* URL */
- PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token */
+ PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token
+ * (v2 is preferred; see below) */
+ PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, /* newest API for OAuth Bearer tokens */
} PGauthData;
/* PGconn encapsulates a connection to the backend.
/* === in fe-auth.c === */
+/* Authdata for PQAUTHDATA_PROMPT_OAUTH_DEVICE */
typedef struct _PGpromptOAuthDevice
{
const char *verification_uri; /* verification URI to visit */
#define PQ_SOCKTYPE int
#endif
+/* Authdata for PQAUTHDATA_OAUTH_BEARER_TOKEN */
typedef struct PGoauthBearerRequest
{
/* Hook inputs (constant across all calls) */
/*
* Callback to clean up custom allocations. A hook implementation may use
- * this to free request->token and any resources in request->user.
+ * this to free request->token and any resources in request->user. V2
+ * implementations should additionally free request->error, if set.
*
* This is technically optional, but highly recommended, because there is
* no other indication as to when it is safe to free the token.
#undef PQ_SOCKTYPE
+/* Authdata for PQAUTHDATA_OAUTH_BEARER_TOKEN_V2 */
+typedef struct
+{
+ PGoauthBearerRequest v1; /* see the PGoauthBearerRequest struct, above */
+
+ /* Hook inputs (constant across all calls) */
+ const char *issuer; /* the issuer identifier (RFC 9207) in use, as
+ * derived from the connection's oauth_issuer */
+
+ /* Hook outputs */
+
+ /*
+ * Hook-defined error message which will be included in the connection's
+ * PQerrorMessage() output when the flow fails. libpq does not take
+ * ownership of this pointer; any allocations should be freed during the
+ * cleanup callback.
+ */
+ const char *error;
+} PGoauthBearerRequestV2;
+
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);
printf("recognized flags:\n");
printf(" -h, --help show this message\n");
+ printf(" -v VERSION select the hook API version (default 2)\n");
printf(" --expected-scope SCOPE fail if received scopes do not match SCOPE\n");
printf(" --expected-uri URI fail if received configuration link does not match URI\n");
+ printf(" --expected-issuer ISS fail if received issuer does not match ISS (v2 only)\n");
printf(" --misbehave=MODE have the hook fail required postconditions\n"
" (MODEs: no-hook, fail-async, no-token, no-socket)\n");
printf(" --no-hook don't install OAuth hooks\n");
printf(" --hang-forever don't ever return a token (combine with connect_timeout)\n");
printf(" --token TOKEN use the provided TOKEN value\n");
+ printf(" --error ERRMSG fail instead, with the given ERRMSG (v2 only)\n");
printf(" --stress-async busy-loop on PQconnectPoll rather than polling\n");
}
static bool hang_forever = false;
static bool stress_async = false;
static const char *expected_uri = NULL;
+static const char *expected_issuer = NULL;
static const char *expected_scope = NULL;
static const char *misbehave_mode = NULL;
static char *token = NULL;
+static char *errmsg = NULL;
+static int hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
int
main(int argc, char *argv[])
{"hang-forever", no_argument, NULL, 1004},
{"misbehave", required_argument, NULL, 1005},
{"stress-async", no_argument, NULL, 1006},
+ {"expected-issuer", required_argument, NULL, 1007},
+ {"error", required_argument, NULL, 1008},
{0}
};
PGconn *conn;
int c;
- while ((c = getopt_long(argc, argv, "h", long_options, NULL)) != -1)
+ while ((c = getopt_long(argc, argv, "hv:", long_options, NULL)) != -1)
{
switch (c)
{
usage(argv);
return 0;
+ case 'v':
+ if (strcmp(optarg, "1") == 0)
+ hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN;
+ else if (strcmp(optarg, "2") == 0)
+ hook_version = PQAUTHDATA_OAUTH_BEARER_TOKEN_V2;
+ else
+ {
+ usage(argv);
+ return 1;
+ }
+ break;
+
case 1000: /* --expected-scope */
expected_scope = optarg;
break;
stress_async = true;
break;
+ case 1007: /* --expected-issuer */
+ expected_issuer = optarg;
+ break;
+
+ case 1008: /* --error */
+ errmsg = optarg;
+ break;
+
default:
usage(argv);
return 1;
/*
* PQauthDataHook implementation. Replaces the default client flow by handling
- * PQAUTHDATA_OAUTH_BEARER_TOKEN.
+ * PQAUTHDATA_OAUTH_BEARER_TOKEN[_V2].
*/
static int
handle_auth_data(PGauthData type, PGconn *conn, void *data)
{
- PGoauthBearerRequest *req = data;
+ PGoauthBearerRequest *req;
+ PGoauthBearerRequestV2 *req2 = NULL;
+
+ Assert(hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN ||
+ hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2);
- if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
+ if (no_hook || type != hook_version)
return 0;
+ req = data;
+ if (type == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2)
+ req2 = data;
+
if (hang_forever)
{
/* Start asynchronous processing. */
}
}
+ if (expected_issuer)
+ {
+ if (!req2)
+ {
+ fprintf(stderr, "--expected-issuer cannot be combined with -v1\n");
+ return -1;
+ }
+
+ if (!req2->issuer)
+ {
+ fprintf(stderr, "expected issuer \"%s\", got NULL\n", expected_issuer);
+ return -1;
+ }
+
+ if (strcmp(expected_issuer, req2->issuer) != 0)
+ {
+ fprintf(stderr, "expected issuer \"%s\", got \"%s\"\n", expected_issuer, req2->issuer);
+ return -1;
+ }
+ }
+
+ if (errmsg)
+ {
+ if (token)
+ {
+ fprintf(stderr, "--error cannot be combined with --token\n");
+ return -1;
+ }
+ else if (!req2)
+ {
+ fprintf(stderr, "--error cannot be combined with -v1\n");
+ return -1;
+ }
+
+ req2->error = errmsg;
+ return -1;
+ }
+
req->token = token;
return 1;
}
if (strcmp(misbehave_mode, "fail-async") == 0)
{
/* Just fail "normally". */
+ if (errmsg)
+ {
+ PGoauthBearerRequestV2 *req2;
+
+ if (hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN)
+ {
+ fprintf(stderr, "--error cannot be combined with -v1\n");
+ exit(1);
+ }
+
+ req2 = (PGoauthBearerRequestV2 *) req;
+ req2->error = errmsg;
+ }
+
return PGRES_POLLING_FAILED;
}
else if (strcmp(misbehave_mode, "no-token") == 0)
my $log_start = -s $node->logfile;
my ($stdout, $stderr) = run_command(\@cmd);
- if (defined($params{expected_stdout}))
+ if ($params{expect_success})
{
- like($stdout, $params{expected_stdout}, "$test_name: stdout matches");
+ like($stdout, qr/connection succeeded/, "$test_name: stdout matches");
}
if (defined($params{expected_stderr}))
flags => [
"--token", "my-token",
"--expected-uri", "$issuer/.well-known/openid-configuration",
+ "--expected-issuer", "$issuer",
"--expected-scope", $scope,
],
- expected_stdout => qr/connection succeeded/,
+ expect_success => 1,
log_like => [qr/oauth_validator: token="my-token", role="$user"/]);
+# The issuer ID provided to the hook is based on, but not equal to,
+# oauth_issuer. Make sure the correct string is passed.
+$common_connstr =
+ "$base_connstr oauth_issuer=$issuer/.well-known/openid-configuration oauth_client_id=myID oauth_scope='$scope'";
+test(
+ "derived issuer ID is correctly provided",
+ flags => [
+ "--token", "my-token",
+ "--expected-uri", "$issuer/.well-known/openid-configuration",
+ "--expected-issuer", "$issuer",
+ "--expected-scope", $scope,
+ ],
+ expect_success => 1,
+ log_like => [qr/oauth_validator: token="my-token", role="$user"/]);
+
+$common_connstr = "$base_connstr oauth_issuer=$issuer oauth_client_id=myID";
+
+# Make sure the v1 hook continues to work.
+test(
+ "v1 synchronous hook can provide a token",
+ flags => [
+ "-v1",
+ "--token" => "my-token-v1",
+ "--expected-uri" => "$issuer/.well-known/openid-configuration",
+ "--expected-scope" => $scope,
+ ],
+ expect_success => 1,
+ log_like => [qr/oauth_validator: token="my-token-v1", role="$user"/]);
+
if ($ENV{with_libcurl} ne 'yes')
{
# libpq should help users out if no OAuth support is built in.
);
}
+# v2 synchronous flows should be able to set custom error messages.
+test(
+ "basic synchronous hook can set error messages",
+ flags => [
+ "--error" => "a custom error message",
+ ],
+ expected_stderr =>
+ qr/user-defined OAuth flow failed: a custom error message/);
+
# connect_timeout should work if the flow doesn't respond.
$common_connstr = "$common_connstr connect_timeout=1";
test(
"hook misbehavior: $c->{'flag'}",
flags => [ $c->{'flag'} ],
expected_stderr => $c->{'expected_error'});
+
+ test(
+ "hook misbehavior: $c->{'flag'} (v1)",
+ flags => [ '-v1', $c->{'flag'} ],
+ expected_stderr => $c->{'expected_error'});
}
+# v2 async flows should be able to set error messages, too.
+test(
+ "asynchronous hook can set error messages",
+ flags => [
+ "--misbehave" => "fail-async",
+ "--error" => "async error message",
+ ],
+ expected_stderr =>
+ qr/user-defined OAuth flow failed: async error message/);
+
done_testing();
PGlobjfuncs
PGnotify
PGoauthBearerRequest
+PGoauthBearerRequestV2
PGpipelineStatus
PGpromptOAuthDevice
PGresAttDesc