mirror of
https://github.com/postgres/postgres.git
synced 2026-03-12 05:32:27 -04:00
libpq: Introduce PQAUTHDATA_OAUTH_BEARER_TOKEN_V2
For the libpq-oauth module to eventually make use of the PGoauthBearerRequest API, it needs some additional functionality: the derived Issuer ID for the authorization server needs to be provided, and error messages need to be built without relying on PGconn internals. These features seem useful for application hooks, too, so that they don't each have to reinvent the wheel. The original plan was for additions to PGoauthBearerRequest to be made without a version bump to the PGauthData type. Applications would simply check a LIBPQ_HAS_* macro at compile time to decide whether they could use the new features. That theoretically works for applications linked against libpq, since it's not safe to downgrade libpq from the version you've compiled against. We've since found that this strategy won't work for plugins, due to a complication first noticed during the libpq-oauth module split: it's normal for a plugin on disk to be *newer* than the libpq that's loading it, because you might have upgraded your installation while an application was running. (In other words, a plugin architecture causes the compile-time and run-time dependency arrows to point in opposite directions, so plugins won't be able to rely on the LIBPQ_HAS_* macros to determine what APIs are available to them.) Instead, extend the original PGoauthBearerRequest (now retroactively referred to as "v1" in the code) with a v2 subclass-style struct. When an application implements and accepts PQAUTHDATA_OAUTH_BEARER_TOKEN_V2, it may safely cast the base request pointer it receives in its callbacks to v2 in order to make use of the new functionality. libpq will query the application for a v2 hook first, then v1 to maintain backwards compatibility, before giving up and using the builtin flow. Reviewed-by: Chao Li <li.evan.chao@gmail.com> Reviewed-by: Zsolt Parragi <zsolt.parragi@percona.com> Discussion: https://postgr.es/m/CAOYmi%2BmrGg%2Bn_X2MOLgeWcj3v_M00gR8uz_D7mM8z%3DdX1JYVbg%40mail.gmail.com
This commit is contained in:
parent
b2898baaf7
commit
e982331b52
6 changed files with 323 additions and 36 deletions
|
|
@ -10362,6 +10362,7 @@ PQauthDataHook_type PQgetAuthDataHook(void);
|
|||
<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
|
||||
|
|
@ -10414,6 +10415,7 @@ typedef struct _PGpromptOAuthDevice
|
|||
<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>.
|
||||
|
|
@ -10421,6 +10423,13 @@ typedef struct _PGpromptOAuthDevice
|
|||
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
|
||||
|
|
@ -10516,6 +10525,81 @@ typedef struct PGoauthBearerRequest
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -675,6 +675,25 @@ cleanup:
|
|||
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.
|
||||
|
|
@ -687,20 +706,23 @@ static PostgresPollingStatusType
|
|||
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)
|
||||
|
|
@ -710,14 +732,14 @@ run_user_oauth_flow(PGconn *conn)
|
|||
* 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");
|
||||
|
|
@ -739,19 +761,20 @@ run_user_oauth_flow(PGconn *conn)
|
|||
}
|
||||
|
||||
/*
|
||||
* 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);
|
||||
|
|
@ -975,8 +998,8 @@ use_builtin_flow(PGconn *conn, fe_oauth_state *state)
|
|||
* 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
|
||||
|
|
@ -990,27 +1013,37 @@ static bool
|
|||
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");
|
||||
|
|
@ -1018,8 +1051,8 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
|
|||
}
|
||||
|
||||
/* short-circuit */
|
||||
if (request.cleanup)
|
||||
request.cleanup(conn, &request);
|
||||
if (request.v1.cleanup)
|
||||
request.v1.cleanup(conn, (PGoauthBearerRequest *) &request);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1038,7 +1071,7 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
|
|||
}
|
||||
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))
|
||||
|
|
@ -1050,8 +1083,8 @@ setup_token_request(PGconn *conn, fe_oauth_state *state)
|
|||
return true;
|
||||
|
||||
fail:
|
||||
if (request.cleanup)
|
||||
request.cleanup(conn, &request);
|
||||
if (request.v1.cleanup)
|
||||
request.v1.cleanup(conn, (PGoauthBearerRequest *) &request);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ extern "C"
|
|||
/* 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
|
||||
|
|
@ -197,7 +199,9 @@ typedef enum
|
|||
{
|
||||
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.
|
||||
|
|
@ -735,6 +739,7 @@ extern int PQenv2encoding(void);
|
|||
|
||||
/* === in fe-auth.c === */
|
||||
|
||||
/* Authdata for PQAUTHDATA_PROMPT_OAUTH_DEVICE */
|
||||
typedef struct _PGpromptOAuthDevice
|
||||
{
|
||||
const char *verification_uri; /* verification URI to visit */
|
||||
|
|
@ -755,6 +760,7 @@ typedef struct _PGpromptOAuthDevice
|
|||
#define PQ_SOCKTYPE int
|
||||
#endif
|
||||
|
||||
/* Authdata for PQAUTHDATA_OAUTH_BEARER_TOKEN */
|
||||
typedef struct PGoauthBearerRequest
|
||||
{
|
||||
/* Hook inputs (constant across all calls) */
|
||||
|
|
@ -788,7 +794,8 @@ typedef struct PGoauthBearerRequest
|
|||
|
||||
/*
|
||||
* 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.
|
||||
|
|
@ -813,6 +820,26 @@ typedef struct PGoauthBearerRequest
|
|||
|
||||
#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);
|
||||
|
|
|
|||
|
|
@ -36,13 +36,16 @@ usage(char *argv[])
|
|||
|
||||
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");
|
||||
}
|
||||
|
||||
|
|
@ -51,9 +54,12 @@ static bool no_hook = false;
|
|||
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[])
|
||||
|
|
@ -68,6 +74,8 @@ 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}
|
||||
};
|
||||
|
||||
|
|
@ -75,7 +83,7 @@ main(int argc, char *argv[])
|
|||
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)
|
||||
{
|
||||
|
|
@ -83,6 +91,18 @@ main(int argc, char *argv[])
|
|||
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;
|
||||
|
|
@ -111,6 +131,14 @@ main(int argc, char *argv[])
|
|||
stress_async = true;
|
||||
break;
|
||||
|
||||
case 1007: /* --expected-issuer */
|
||||
expected_issuer = optarg;
|
||||
break;
|
||||
|
||||
case 1008: /* --error */
|
||||
errmsg = optarg;
|
||||
break;
|
||||
|
||||
default:
|
||||
usage(argv);
|
||||
return 1;
|
||||
|
|
@ -167,16 +195,24 @@ main(int argc, char *argv[])
|
|||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
if (no_hook || (type != PQAUTHDATA_OAUTH_BEARER_TOKEN))
|
||||
Assert(hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN ||
|
||||
hook_version == PQAUTHDATA_OAUTH_BEARER_TOKEN_V2);
|
||||
|
||||
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. */
|
||||
|
|
@ -221,6 +257,44 @@ handle_auth_data(PGauthData type, PGconn *conn, void *data)
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -273,6 +347,20 @@ misbehave_cb(PGconn *conn, PGoauthBearerRequest *req, pgsocket *altsock)
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -78,9 +78,9 @@ sub test
|
|||
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}))
|
||||
|
|
@ -110,11 +110,41 @@ test(
|
|||
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.
|
||||
|
|
@ -126,6 +156,15 @@ if ($ENV{with_libcurl} ne 'yes')
|
|||
);
|
||||
}
|
||||
|
||||
# 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(
|
||||
|
|
@ -163,6 +202,21 @@ foreach my $c (@cases)
|
|||
"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();
|
||||
|
|
|
|||
|
|
@ -1926,6 +1926,7 @@ PGdataValue
|
|||
PGlobjfuncs
|
||||
PGnotify
|
||||
PGoauthBearerRequest
|
||||
PGoauthBearerRequestV2
|
||||
PGpipelineStatus
|
||||
PGpromptOAuthDevice
|
||||
PGresAttDesc
|
||||
|
|
|
|||
Loading…
Reference in a new issue