v1-0001-oauth-flows-validation-hook-approach.patch
application/octet-stream
Filename: v1-0001-oauth-flows-validation-hook-approach.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: unified
Series: patch v1-0001
| File | + | − |
|---|---|---|
| src/backend/libpq/auth-oauth.c | 134 | 195 |
| src/include/libpq/auth.h | 35 | 0 |
| src/include/libpq/libpq-be.h | 11 | 0 |
| src/interfaces/libpq/fe-auth-oauth.c | 182 | 23 |
| src/interfaces/libpq/fe-connect.c | 46 | 0 |
| src/interfaces/libpq/fe-exec.c | 52 | 0 |
| src/interfaces/libpq/libpq-fe.h | 3 | 0 |
| src/interfaces/libpq/libpq-int.h | 37 | 0 |
diff --git a/src/backend/libpq/auth-oauth.c b/src/backend/libpq/auth-oauth.c
index 3a625847f3..f213a40b65 100644
--- a/src/backend/libpq/auth-oauth.c
+++ b/src/backend/libpq/auth-oauth.c
@@ -24,15 +24,23 @@
#include "libpq/hba.h"
#include "libpq/oauth.h"
#include "libpq/sasl.h"
+#include "miscadmin.h"
#include "storage/fd.h"
/* GUC */
char *oauth_validator_command;
+static OAuthProvider* oauth_provider = NULL;
+
+/*----------------------------------------------------------------
+ * OAuth Authentication
+ *----------------------------------------------------------------
+ */
+static List *oauth_providers = NIL;
static void oauth_get_mechanisms(Port *port, StringInfo buf);
static void *oauth_init(Port *port, const char *selected_mech, const char *shadow_pass);
static int oauth_exchange(void *opaq, const char *input, int inputlen,
- char **output, int *outputlen, char **logdetail);
+ char **output, int *outputlen, const char **logdetail);
/* Mechanism declaration */
const pg_be_sasl_mech pg_be_oauth_mech = {
@@ -43,7 +51,6 @@ const pg_be_sasl_mech pg_be_oauth_mech = {
PG_MAX_AUTH_TOKEN_LENGTH,
};
-
typedef enum
{
OAUTH_STATE_INIT = 0,
@@ -62,7 +69,7 @@ struct oauth_ctx
static char *sanitize_char(char c);
static char *parse_kvpairs_for_auth(char **input);
static void generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen);
-static bool validate(Port *port, const char *auth, char **logdetail);
+static bool validate(Port *port, const char *auth, const char **logdetail);
static bool run_validator_command(Port *port, const char *token);
static bool check_exit(FILE **fh, const char *command);
static bool unset_cloexec(int fd);
@@ -72,6 +79,86 @@ static bool username_ok_for_shell(const char *username);
#define AUTH_KEY "auth"
#define BEARER_SCHEME "Bearer "
+#include "utils/memutils.h"
+
+/*----------------------------------------------------------------
+ * OAuth Token Validator
+ *----------------------------------------------------------------
+ */
+
+/*
+ * RegistorOAuthProvider registers a OAuth Token Validator to be
+ * used for oauth token validation. It validates the token and adds the valiator
+ * name and it's hooks to a list of loaded token validator. The right validator's
+ * hooks can then be called based on the validator name specified in
+ * pg_hba.conf.
+ *
+ * This function should be called in _PG_init() by any extension looking to
+ * add a custom authentication method.
+ */
+void
+RegistorOAuthProvider(
+ const char *provider_name,
+ OAuthProviderCheck_hook_type OAuthProviderCheck_hook,
+ OAuthProviderError_hook_type OAuthProviderError_hook,
+ OAuthProviderOptions_hook_type OAuthProviderOptions_hook
+)
+{
+ if (!process_shared_preload_libraries_in_progress)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+ errmsg("RegistorOAuthProvider can only be called by a shared_preload_library")));
+ return;
+ }
+
+ MemoryContext oldcxt;
+ if (oauth_provider == NULL)
+ {
+ oldcxt = MemoryContextSwitchTo(TopMemoryContext);
+ oauth_provider = palloc(sizeof(OAuthProvider));
+ oauth_provider->name = pstrdup(provider_name);
+ oauth_provider->oauth_provider_hook = OAuthProviderCheck_hook;
+ oauth_provider->oauth_error_hook = OAuthProviderError_hook;
+ oauth_provider->oauth_options_hook = OAuthProviderOptions_hook;
+ oauth_providers = lappend(oauth_providers, oauth_provider);
+ MemoryContextSwitchTo(oldcxt);
+ }
+ else
+ {
+ if (oauth_provider && oauth_provider->name)
+ {
+ ereport(ERROR,
+ (errmsg("OAuth provider \"%s\" is already loaded.",
+ oauth_provider->name)));
+ }
+ else
+ {
+ ereport(ERROR,
+ (errmsg("OAuth provider is already loaded.")));
+ }
+ }
+}
+
+/*
+ * Returns the oauth provider (which includes it's
+ * callback functions) based on name specified.
+ */
+OAuthProvider *get_provider_by_name(const char *name)
+{
+ ListCell *lc;
+ foreach(lc, oauth_providers)
+ {
+ OAuthProvider *provider = (OAuthProvider *) lfirst(lc);
+ if (strcmp(provider->name, name) == 0)
+ {
+ return provider;
+ }
+ }
+
+ return NULL;
+}
+
static void
oauth_get_mechanisms(Port *port, StringInfo buf)
{
@@ -102,9 +189,32 @@ oauth_init(Port *port, const char *selected_mech, const char *shadow_pass)
return ctx;
}
+static void process_oauth_flow_type(pg_oauth_flow_type flow_type, struct oauth_ctx *ctx, char **output, int *outputlen)
+{
+ StringInfoData buf;
+ initStringInfo(&buf);
+
+ OAuthProviderOptions *oauth_options = oauth_provider->oauth_options_hook(flow_type);
+ ctx->scope = oauth_options->scope;
+ ctx->issuer = oauth_options->issuer_url;
+ appendStringInfo(&buf,
+ "{ "
+ "\"status\": \"invalid_token\", "
+ "\"openid-configuration\": \"%s/.well-known/openid-configuration\","
+ "\"scope\": \"%s\""
+ "}",
+ oauth_options->issuer_url,
+ oauth_options->scope);
+
+ *output = buf.data;
+ *outputlen = buf.len;
+
+ pfree(oauth_options);
+}
+
static int
oauth_exchange(void *opaq, const char *input, int inputlen,
- char **output, int *outputlen, char **logdetail)
+ char **output, int *outputlen, const char **logdetail)
{
char *p;
char cbind_flag;
@@ -247,11 +357,17 @@ oauth_exchange(void *opaq, const char *input, int inputlen,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("malformed OAUTHBEARER message"),
errdetail("Message contains additional data after the final terminator.")));
-
- if (!validate(ctx->port, auth, logdetail))
+
+ /* if not Bearer, process flow_type*/
+ if (strncasecmp(auth, BEARER_SCHEME, strlen(BEARER_SCHEME)))
+ {
+ process_oauth_flow_type(atoi(auth), ctx, output, outputlen);
+ ctx->state = OAUTH_STATE_ERROR;
+ return PG_SASL_EXCHANGE_CONTINUE;
+ }
+ else if(!validate(ctx->port, auth, logdetail))
{
generate_error_response(ctx, output, outputlen);
-
ctx->state = OAUTH_STATE_ERROR;
return PG_SASL_EXCHANGE_CONTINUE;
}
@@ -415,7 +531,7 @@ generate_error_response(struct oauth_ctx *ctx, char **output, int *outputlen)
}
static bool
-validate(Port *port, const char *auth, char **logdetail)
+validate(Port *port, const char *auth, const char **logdetail)
{
static const char * const b64_set = "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
@@ -508,7 +624,7 @@ validate(Port *port, const char *auth, char **logdetail)
return true;
}
- /* Make sure the validator authenticated the user. */
+ /* Make sure the validator authenticated the user. */
if (!MyClientConnectionInfo.authn_id)
{
/* TODO: use logdetail; reduce message duplication */
@@ -518,199 +634,22 @@ validate(Port *port, const char *auth, char **logdetail)
return false;
}
- /* Finally, check the user map. */
- ret = check_usermap(port->hba->usermap, port->user_name,
- MyClientConnectionInfo.authn_id, false);
+ /* Finally, check the user map. */
+ ret = check_usermap(port->hba->usermap, port->user_name,
+ MyClientConnectionInfo.authn_id, false);
return (ret == STATUS_OK);
}
static bool
run_validator_command(Port *port, const char *token)
{
- bool success = false;
- int rc;
- int pipefd[2];
- int rfd = -1;
- int wfd = -1;
-
- StringInfoData command = { 0 };
- char *p;
- FILE *fh = NULL;
-
- ssize_t written;
- char *line = NULL;
- size_t size = 0;
- ssize_t len;
-
- Assert(oauth_validator_command);
-
- if (!oauth_validator_command[0])
- {
- ereport(COMMERROR,
- (errmsg("oauth_validator_command is not set"),
- errhint("To allow OAuth authenticated connections, set "
- "oauth_validator_command in postgresql.conf.")));
- return false;
- }
-
- /*
- * Since popen() is unidirectional, open up a pipe for the other direction.
- * Use CLOEXEC to ensure that our write end doesn't accidentally get copied
- * into child processes, which would prevent us from closing it cleanly.
- *
- * XXX this is ugly. We should just read from the child process's stdout,
- * but that's a lot more code.
- * XXX by bypassing the popen API, we open the potential of process
- * deadlock. Clearly document child process requirements (i.e. the child
- * MUST read all data off of the pipe before writing anything).
- * TODO: port to Windows using _pipe().
- */
- rc = pipe2(pipefd, O_CLOEXEC);
- if (rc < 0)
- {
- ereport(COMMERROR,
- (errcode_for_file_access(),
- errmsg("could not create child pipe: %m")));
- return false;
- }
-
- rfd = pipefd[0];
- wfd = pipefd[1];
-
- /* Allow the read pipe be passed to the child. */
- if (!unset_cloexec(rfd))
- {
- /* error message was already logged */
- goto cleanup;
- }
-
- /*
- * Construct the command, substituting any recognized %-specifiers:
- *
- * %f: the file descriptor of the input pipe
- * %r: the role that the client wants to assume (port->user_name)
- * %%: a literal '%'
- */
- initStringInfo(&command);
-
- for (p = oauth_validator_command; *p; p++)
- {
- if (p[0] == '%')
- {
- switch (p[1])
- {
- case 'f':
- appendStringInfo(&command, "%d", rfd);
- p++;
- break;
- case 'r':
- /*
- * TODO: decide how this string should be escaped. The role
- * is controlled by the client, so if we don't escape it,
- * command injections are inevitable.
- *
- * This is probably an indication that the role name needs
- * to be communicated to the validator process in some other
- * way. For this proof of concept, just be incredibly strict
- * about the characters that are allowed in user names.
- */
- if (!username_ok_for_shell(port->user_name))
- goto cleanup;
-
- appendStringInfoString(&command, port->user_name);
- p++;
- break;
- case '%':
- appendStringInfoChar(&command, '%');
- p++;
- break;
- default:
- appendStringInfoChar(&command, p[0]);
- }
- }
- else
- appendStringInfoChar(&command, p[0]);
- }
-
- /* Execute the command. */
- fh = OpenPipeStream(command.data, "re");
- /* TODO: handle failures */
-
- /* We don't need the read end of the pipe anymore. */
- close(rfd);
- rfd = -1;
-
- /* Give the command the token to validate. */
- written = write(wfd, token, strlen(token));
- if (written != strlen(token))
- {
- /* TODO must loop for short writes, EINTR et al */
- ereport(COMMERROR,
- (errcode_for_file_access(),
- errmsg("could not write token to child pipe: %m")));
- goto cleanup;
- }
-
- close(wfd);
- wfd = -1;
-
- /*
- * Read the command's response.
- *
- * TODO: getline() is probably too new to use, unfortunately.
- * TODO: loop over all lines
- */
- if ((len = getline(&line, &size, fh)) >= 0)
- {
- /* TODO: fail if the authn_id doesn't end with a newline */
- if (len > 0)
- line[len - 1] = '\0';
-
- set_authn_id(port, line);
- }
- else if (ferror(fh))
- {
- ereport(COMMERROR,
- (errcode_for_file_access(),
- errmsg("could not read from command \"%s\": %m",
- command.data)));
- goto cleanup;
- }
-
- /* Make sure the command exits cleanly. */
- if (!check_exit(&fh, command.data))
+ int result = oauth_provider->oauth_provider_hook(port, token);
+ if(result == STATUS_OK)
{
- /* error message already logged */
- goto cleanup;
- }
-
- /* Done. */
- success = true;
-
-cleanup:
- if (line)
- free(line);
-
- /*
- * In the successful case, the pipe fds are already closed. For the error
- * case, always close out the pipe before waiting for the command, to
- * prevent deadlock.
- */
- if (rfd >= 0)
- close(rfd);
- if (wfd >= 0)
- close(wfd);
-
- if (fh)
- {
- Assert(!success);
- check_exit(&fh, command.data);
+ set_authn_id(port, port->user_name);
+ return true;
}
-
- if (command.data)
- pfree(command.data);
-
- return success;
+ return false;
}
static bool
@@ -780,7 +719,7 @@ username_ok_for_shell(const char *username)
/* This set is borrowed from fe_utils' appendShellStringNoError(). */
static const char * const allowed = "abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- "0123456789-_./:";
+ "0123456789-_./@:";
size_t span;
Assert(username && username[0]); /* should have already been checked */
diff --git a/src/include/libpq/auth.h b/src/include/libpq/auth.h
index b62457d57c..7b7b6ff9aa 100644
--- a/src/include/libpq/auth.h
+++ b/src/include/libpq/auth.h
@@ -28,6 +28,41 @@ extern void set_authn_id(Port *port, const char *id);
/* Hook for plugins to get control in ClientAuthentication() */
typedef void (*ClientAuthentication_hook_type) (Port *, int);
extern PGDLLIMPORT ClientAuthentication_hook_type ClientAuthentication_hook;
+/* Declarations for oAuth authentication providers */
+typedef int (*OAuthProviderCheck_hook_type) (Port *, const char*);
+
+/* Hook for plugins to report error messages in validation_failed() */
+typedef const char * (*OAuthProviderError_hook_type) (Port *);
+
+/* Hook for plugins to validate oauth provider options */
+typedef bool (*OAuthProviderValidateOptions_hook_type)
+ (char *, char *, HbaLine *, char **);
+
+typedef struct OAuthProviderOptions
+{
+ char *issuer_url;
+ char *scope;
+} OAuthProviderOptions;
+
+/* Hook for plugins to get oauth params */
+typedef OAuthProviderOptions *(*OAuthProviderOptions_hook_type) (pg_oauth_flow_type);
+
+typedef struct OAuthProvider
+{
+ const char *name;
+ OAuthProviderCheck_hook_type oauth_provider_hook;
+ OAuthProviderError_hook_type oauth_error_hook;
+ OAuthProviderOptions_hook_type oauth_options_hook;
+} OAuthProvider;
+
+extern void RegistorOAuthProvider
+ (const char *provider_name,
+ OAuthProviderCheck_hook_type OAuthProviderCheck_hook,
+ OAuthProviderError_hook_type OAuthProviderError_hook,
+ OAuthProviderOptions_hook_type OAuthProviderParams_hook
+ );
+
+extern OAuthProvider *get_provider_by_name(const char *name);
#define PG_MAX_AUTH_TOKEN_LENGTH 65535
#endif /* AUTH_H */
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 6d452ec6d9..f7bbb9dcf4 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -68,6 +68,17 @@ typedef enum CAC_state
CAC_TOOMANY
} CAC_state;
+/* OAuth flow types */
+typedef enum pg_oauth_flow_type
+{
+ OAUTH_DEVICE_CODE,
+ OAUTH_CLIENT_CREDENTIALS,
+ OAUTH_AUTH,
+ OAUTH_AUTH_PKCE,
+ OAUTH_REFRESH_TOKEN,
+ OAUTH_NONE
+} pg_oauth_flow_type;
+
/*
* GSSAPI specific state information
diff --git a/src/interfaces/libpq/fe-auth-oauth.c b/src/interfaces/libpq/fe-auth-oauth.c
index 91d2c69f16..1ba2e033c4 100644
--- a/src/interfaces/libpq/fe-auth-oauth.c
+++ b/src/interfaces/libpq/fe-auth-oauth.c
@@ -142,6 +142,43 @@ iddawc_request_error(PGconn *conn, struct _i_session *i, int err, const char *ms
appendPQExpBuffer(&conn->errorMessage, "(%s)\n", error_code);
}
+static pg_oauth_flow_type oauth_get_flow_type(const char *oauthflow)
+{
+ pg_oauth_flow_type flow_type;
+
+ if(!oauthflow)
+ {
+ return OAUTH_NONE;
+ }
+
+ /* client_secret, device_code, auth_code_pkce, refresh_token */
+ if(strcmp(oauthflow, "device_code") == 0)
+ {
+ flow_type = OAUTH_DEVICE_CODE;
+ }
+ else if(strcmp(oauthflow, "client_secret") == 0)
+ {
+ flow_type = OAUTH_CLIENT_CREDENTIALS;
+ }
+ else if(strcmp(oauthflow, "auth_code_pkce") == 0)
+ {
+ flow_type = OAUTH_AUTH_PKCE;
+ }
+ else if(strcmp(oauthflow, "refresh_token") == 0)
+ {
+ flow_type = OAUTH_REFRESH_TOKEN;
+ }
+ else if(strcmp(oauthflow, "auth_code"))
+ {
+ flow_type = OAUTH_AUTH_CODE;
+ }
+ else
+ {
+ flow_type = OAUTH_NONE;
+ }
+ return flow_type;
+}
+
static char *
get_auth_token(PGconn *conn)
{
@@ -150,29 +187,44 @@ get_auth_token(PGconn *conn)
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;
-
+ char *verification_uri;
+ char *user_code;
+ char *access_token;
+ char *refresh_token;
+ char *token_type;
+ pg_oauth_flow_type flow_type;
+ char *token = NULL;
+ uint session_response_type;
+ PGOAuthMsgObj oauthMsgObj;
+
+ MemSet(&oauthMsgObj, 0x00, sizeof(PGOAuthMsgObj));
+
if (!conn->oauth_discovery_uri)
return strdup(""); /* ask the server for one */
- i_init_session(&session);
-
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"));
- goto cleanup;
+ return NULL;
}
- token_buf = createPQExpBuffer();
+ i_init_session(&session);
+ token_buf = createPQExpBuffer();
if (!token_buf)
goto cleanup;
+
+ if(conn->oauth_bearer_token)
+ {
+ appendPQExpBufferStr(token_buf, "Bearer ");
+ appendPQExpBufferStr(token_buf, conn->oauth_bearer_token);
+ if (PQExpBufferBroken(token_buf))
+ goto cleanup;
+ token = strdup(token_buf->data);
+ goto cleanup;
+ }
err = i_set_str_parameter(&session, I_OPT_OPENID_CONFIG_ENDPOINT, conn->oauth_discovery_uri);
if (err)
@@ -181,6 +233,8 @@ get_auth_token(PGconn *conn)
goto cleanup;
}
+ flow_type = oauth_get_flow_type(conn->oauth_flow_type);
+
err = i_get_openid_config(&session);
if (err)
{
@@ -201,18 +255,64 @@ get_auth_token(PGconn *conn)
libpq_gettext("issuer does not support device authorization"));
goto cleanup;
}
+ auth_method = I_TOKEN_AUTH_METHOD_NONE;
+
+ /* for refresh token flow, do not run auth request*/
+ if(flow_type == OAUTH_REFRESH_TOKEN && conn->oauth_refresh_token)
+ {
+ err = i_set_parameter_list(&session,
+ I_OPT_CLIENT_ID, conn->oauth_client_id,
+ I_OPT_REFRESH_TOKEN, conn->oauth_refresh_token,
+ I_OPT_RESPONSE_TYPE, I_RESPONSE_TYPE_REFRESH_TOKEN,
+ I_OPT_TOKEN_METHOD, auth_method,
+ I_OPT_CLIENT_SECRET, conn->oauth_client_secret,
+ I_OPT_SCOPE, conn->oauth_scope,
+ I_OPT_NONE
+ );
+
+ if (err)
+ {
+ iddawc_error(conn, err, "failed to set refresh token flow parameters");
+ goto cleanup;
+ }
- err = i_set_response_type(&session, I_RESPONSE_TYPE_DEVICE_CODE);
+ err = i_run_token_request(&session);
+ if (err)
+ {
+ iddawc_request_error(conn, &session, err,
+ "failed to obtain token authorization with refresh token flow");
+ goto cleanup;
+ }
+
+ 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);
+ return token;
+ }
+
+ //default device flow
+ session_response_type = I_RESPONSE_TYPE_DEVICE_CODE;
+ err = i_set_response_type(&session, session_response_type);
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,
@@ -225,7 +325,7 @@ get_auth_token(PGconn *conn)
iddawc_error(conn, err, "failed to set client identifier");
goto cleanup;
}
-
+
err = i_run_device_auth_request(&session);
if (err)
{
@@ -278,14 +378,15 @@ get_auth_token(PGconn *conn)
if (!user_prompted)
{
+ oauthMsgObj.verification_uri = verification_uri;
+ oauthMsgObj.user_code = user_code;
+ conn->oauthNoticeHooks.noticeRecArg = (void*) &oauthMsgObj;
+
/*
* 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);
-
+ */
+ pqInternalOAuthNotice(&conn->oauthNoticeHooks, "");
user_prompted = true;
}
@@ -300,7 +401,7 @@ get_auth_token(PGconn *conn)
* 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"))
+ //if (!strcmp(error_code, "slow_down"))
{
interval += 5;
i_set_int_parameter(&session, I_OPT_DEVICE_AUTH_INTERVAL, interval);
@@ -323,6 +424,14 @@ get_auth_token(PGconn *conn)
access_token = i_get_str_parameter(&session, I_OPT_ACCESS_TOKEN);
token_type = i_get_str_parameter(&session, I_OPT_TOKEN_TYPE);
+ refresh_token = i_get_str_parameter(&session, I_OPT_REFRESH_TOKEN);
+
+ if(refresh_token)
+ {
+ MemSet(&oauthMsgObj, 0x00, sizeof(PGOAuthMsgObj));
+ oauthMsgObj.refresh_token = refresh_token;
+ pqInternalOAuthNotice(&conn->oauthNoticeHooks, "");
+ }
if (!access_token || !token_type || strcasecmp(token_type, "Bearer"))
{
@@ -358,6 +467,8 @@ client_initial_response(PGconn *conn)
PQExpBuffer discovery_buf = NULL;
char *token = NULL;
char *response = NULL;
+ pg_oauth_flow_type flow_type;
+ char oauth_flow_str[3];
token_buf = createPQExpBuffer();
if (!token_buf)
@@ -385,8 +496,26 @@ client_initial_response(PGconn *conn)
token = get_auth_token(conn);
if (!token)
goto cleanup;
-
+
+ if(strcmp(token, "") == 0)
+ {
+ flow_type = oauth_get_flow_type(conn->oauth_flow_type);
+ if(flow_type == OAUTH_NONE)
+ {
+ appendPQExpBufferStr(&conn->errorMessage,
+ libpq_gettext("value passed in oauth_flow_type is not valid."\
+ "supported flows: client_secret, device_code, auth_code_pkce, refresh_token\n"));
+ goto cleanup;
+ }
+ else
+ {
+ sprintf(oauth_flow_str, "%d", flow_type);
+ token = strdup(oauth_flow_str);
+ }
+ }
appendPQExpBuffer(token_buf, resp_format, token);
+// elog(INFO, "fe-flowtype: %s", token);
+
if (PQExpBufferBroken(token_buf))
goto cleanup;
@@ -406,6 +535,9 @@ cleanup:
#define ERROR_STATUS_FIELD "status"
#define ERROR_SCOPE_FIELD "scope"
#define ERROR_OPENID_CONFIGURATION_FIELD "openid-configuration"
+#define ERROR_ISSUER_URL_FIELD "issuer"
+#define ERROR_AUTH_ENDPOINT_FIELD "authorization_endpoint"
+#define ERROR_TOKEN_ENDPOINT_FIELD "token_endpoint"
struct json_ctx
{
@@ -420,6 +552,9 @@ struct json_ctx
char *status;
char *scope;
char *discovery_uri;
+ char *issuer_url;
+ char *auth_endpoint;
+ char *token_endpoint;
};
#define oauth_json_has_error(ctx) \
@@ -491,6 +626,21 @@ oauth_json_object_field_start(void *state, char *name, bool isnull)
ctx->target_field_name = ERROR_OPENID_CONFIGURATION_FIELD;
ctx->target_field = &ctx->discovery_uri;
}
+ else if(!strcmp(name, ERROR_ISSUER_URL_FIELD))
+ {
+ ctx->target_field_name = ERROR_ISSUER_URL_FIELD;
+ ctx->target_field = &ctx->issuer_url;
+ }
+ else if(!strcmp(name, ERROR_AUTH_ENDPOINT_FIELD))
+ {
+ ctx->target_field_name = ERROR_AUTH_ENDPOINT_FIELD;
+ ctx->target_field = &ctx->auth_endpoint;
+ }
+ else if(!strcmp(name, ERROR_TOKEN_ENDPOINT_FIELD))
+ {
+ ctx->target_field_name = ERROR_TOKEN_ENDPOINT_FIELD;
+ ctx->target_field = &ctx->token_endpoint;
+ }
}
free(name);
@@ -627,6 +777,15 @@ handle_oauth_sasl_error(PGconn *conn, char *msg, int msglen)
conn->oauth_scope = ctx.scope;
}
+
+ if(ctx.issuer_url)
+ {
+ if(conn->oauth_issuer)
+ free(conn->oauth_issuer);
+
+ conn->oauth_issuer = ctx.issuer_url;
+ }
+
/* TODO: missing error scope should clear any existing connection scope */
if (!ctx.status)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 64f27fee18..e6e8dc48e2 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -358,6 +358,18 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"OAuth-Scope", "", 15,
offsetof(struct pg_conn, oauth_scope)},
+ {"oauth_bearer_token", NULL, NULL, NULL,
+ "OAuth-Bearer", "", 20,
+ offsetof(struct pg_conn, oauth_bearer_token)},
+
+ {"oauth_flow_type", NULL, NULL, NULL,
+ "OAuth-Flow-Type", "", 20,
+ offsetof(struct pg_conn, oauth_flow_type)},
+
+ {"oauth_refresh_token", NULL, NULL, NULL,
+ "OAuth-Refresh-Token", "", 40,
+ offsetof(struct pg_conn, oauth_refresh_token)},
+
/* Terminating entry --- MUST BE LAST */
{NULL, NULL, NULL, NULL,
NULL, NULL, 0}
@@ -427,6 +439,7 @@ static PQconninfoOption *conninfo_find(PQconninfoOption *connOptions,
const char *keyword);
static void defaultNoticeReceiver(void *arg, const PGresult *res);
static void defaultNoticeProcessor(void *arg, const char *message);
+static void OAuthMsgObjReceiver(void *arg, const PGresult *res);
static int parseServiceInfo(PQconninfoOption *options,
PQExpBuffer errorMessage);
static int parseServiceFile(const char *serviceFile,
@@ -3926,6 +3939,7 @@ makeEmptyPGconn(void)
/* install default notice hooks */
conn->noticeHooks.noticeRec = defaultNoticeReceiver;
conn->noticeHooks.noticeProc = defaultNoticeProcessor;
+ conn->oauthNoticeHooks.noticeRec = OAuthMsgObjReceiver;
conn->status = CONNECTION_BAD;
conn->asyncStatus = PGASYNC_IDLE;
@@ -4073,6 +4087,12 @@ freePGconn(PGconn *conn)
free(conn->oauth_client_secret);
if (conn->oauth_scope)
free(conn->oauth_scope);
+ if(conn->oauth_bearer_token)
+ free(conn->oauth_bearer_token);
+ if(conn->oauth_flow_type)
+ free(conn->oauth_flow_type);
+ if(conn->oauth_refresh_token)
+ free(conn->oauth_refresh_token);
termPQExpBuffer(&conn->errorMessage);
termPQExpBuffer(&conn->workBuffer);
@@ -6991,6 +7011,32 @@ defaultNoticeProcessor(void *arg, const char *message)
fprintf(stderr, "%s", message);
}
+static void
+OAuthMsgObjReceiver(void *arg, const PGresult *res)
+{
+ PGOAuthMsgObj *oauthMsg = (PGOAuthMsgObj *) arg;
+
+ if(oauthMsg->message)
+ {
+ fprintf(stderr, "%s\n", oauthMsg->message);
+ }
+
+ if(oauthMsg->verification_uri)
+ {
+ fprintf(stderr, "Visit: %s\n", oauthMsg->verification_uri);
+ }
+
+ if(oauthMsg->user_code)
+ {
+ fprintf(stderr, "Enter: %s\n", oauthMsg->user_code);
+ }
+
+ if(oauthMsg->refresh_token)
+ {
+ fprintf(stderr, "Refresh Token: %s\n", oauthMsg->refresh_token);
+ }
+}
+
/*
* returns a pointer to the next token or NULL if the current
* token doesn't match
diff --git a/src/interfaces/libpq/fe-exec.c b/src/interfaces/libpq/fe-exec.c
index da229d632a..4789c1a1fe 100644
--- a/src/interfaces/libpq/fe-exec.c
+++ b/src/interfaces/libpq/fe-exec.c
@@ -976,6 +976,58 @@ pqInternalNotice(const PGNoticeHooks *hooks, const char *fmt,...)
PQclear(res);
}
+/*
+ * pqInternalOAuthNotice - it is similar to pqInternalNotice
+ * except that OAuthNoticeHooks are invoked.
+ */
+void
+pqInternalOAuthNotice(const PGOAuthNoticeHooks *hooks, const char *fmt,...)
+{
+ char msgBuf[1024];
+ va_list args;
+ PGresult *res;
+
+ if (hooks->noticeRec == NULL)
+ return; /* nobody home to receive notice? */
+
+ /* Format the message */
+ va_start(args, fmt);
+ vsnprintf(msgBuf, sizeof(msgBuf), libpq_gettext(fmt), args);
+ va_end(args);
+ msgBuf[sizeof(msgBuf) - 1] = '\0'; /* make real sure it's terminated */
+
+ /* Make a PGresult to pass to the notice receiver */
+ res = PQmakeEmptyPGresult(NULL, PGRES_NONFATAL_ERROR);
+ if (!res)
+ return;
+ res->oauthNoticeHooks = *hooks;
+ res->oauthNoticeHooks.noticeRecArg = hooks->noticeRecArg;
+
+ /*
+ * Set up fields of notice.
+ */
+ pqSaveMessageField(res, PG_DIAG_MESSAGE_PRIMARY, msgBuf);
+ pqSaveMessageField(res, PG_DIAG_SEVERITY, libpq_gettext("NOTICE"));
+ pqSaveMessageField(res, PG_DIAG_SEVERITY_NONLOCALIZED, "NOTICE");
+ /* XXX should provide a SQLSTATE too? */
+
+ /*
+ * Result text is always just the primary message + newline. If we can't
+ * allocate it, substitute "out of memory", as in pqSetResultError.
+ */
+ res->errMsg = (char *) pqResultAlloc(res, strlen(msgBuf) + 2, false);
+ if (res->errMsg)
+ sprintf(res->errMsg, "%s\n", msgBuf);
+ else
+ res->errMsg = libpq_gettext("out of memory\n");
+
+ /*
+ * Pass to receiver, then free it.
+ */
+ res->oauthNoticeHooks.noticeRec(res->oauthNoticeHooks.noticeRecArg, res);
+ PQclear(res);
+}
+
/*
* pqAddTuple
* add a row pointer to the PGresult structure, growing it if necessary
diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h
index b7df3224c0..ee5b2e2b59 100644
--- a/src/interfaces/libpq/libpq-fe.h
+++ b/src/interfaces/libpq/libpq-fe.h
@@ -197,6 +197,9 @@ typedef struct pgNotify
typedef void (*PQnoticeReceiver) (void *arg, const PGresult *res);
typedef void (*PQnoticeProcessor) (void *arg, const char *message);
+/* Function types for notice-handling callbacks */
+typedef void (*PQOAuthNoticeReceiver) (void *arg, const PGresult *res);
+
/* Print options for PQprint() */
typedef char pqbool;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index ae76ae0e8f..3155d81e00 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -157,6 +157,24 @@ typedef struct
void *noticeProcArg;
} PGNoticeHooks;
+typedef struct
+{
+ char *verification_uri; /* URI the user should go to with the user_code in order to sign in */
+ char *user_code; /* used to identify the session on a secondary device */
+ char *refresh_token;
+ char *message; /* string with instructions for the user. */
+ char *response_error; /*JSON error response (400 Bad Request) */
+ uint expires_in; /* number of seconds before the device_code expire */
+ uint interval; /* number of seconds the client should wait between polling requests */
+} PGOAuthMsgObj;
+
+/* Fields needed for oauth callback handling */
+typedef struct
+{
+ PQOAuthNoticeReceiver noticeRec; /* OAuth notice message receiver */
+ void *noticeRecArg;
+} PGOAuthNoticeHooks;
+
typedef struct PGEvent
{
PGEventProc proc; /* the function to call on events */
@@ -186,6 +204,7 @@ struct pg_result
* on the PGresult don't have to reference the PGconn.
*/
PGNoticeHooks noticeHooks;
+ PGOAuthNoticeHooks oauthNoticeHooks;
PGEvent *events;
int nEvents;
int client_encoding; /* encoding id */
@@ -343,6 +362,17 @@ typedef struct pg_conn_host
* found in password file. */
} pg_conn_host;
+typedef enum pg_oauth_flow_type
+{
+ OAUTH_DEVICE_CODE,
+ OAUTH_CLIENT_CREDENTIALS,
+ OAUTH_AUTH,
+ OAUTH_AUTH_PKCE,
+ OAUTH_REFRESH_TOKEN,
+ OAUTH_AUTH_CODE,
+ OAUTH_NONE
+} pg_oauth_flow_type;
+
/*
* PGconn stores all the state data associated with a single connection
* to a backend.
@@ -403,6 +433,9 @@ struct pg_conn
char *oauth_client_id; /* client identifier */
char *oauth_client_secret; /* client secret */
char *oauth_scope; /* access token scope */
+ char *oauth_bearer_token; /* oauth token */
+ char *oauth_flow_type; /* oauth flow type */
+ char *oauth_refresh_token; /* refresh token */
bool oauth_want_retry; /* should we retry on failure? */
/* Optional file to write trace info to */
@@ -412,6 +445,9 @@ struct pg_conn
/* Callback procedures for notice message processing */
PGNoticeHooks noticeHooks;
+ /* Callback procedures for notifying messages during oauth flows*/
+ PGOAuthNoticeHooks oauthNoticeHooks;
+
/* Event procs registered via PQregisterEventProc */
PGEvent *events; /* expandable array of event data */
int nEvents; /* number of active events */
@@ -677,6 +713,7 @@ extern void pqClearAsyncResult(PGconn *conn);
extern void pqSaveErrorResult(PGconn *conn);
extern PGresult *pqPrepareAsyncResult(PGconn *conn);
extern void pqInternalNotice(const PGNoticeHooks *hooks, const char *fmt,...) pg_attribute_printf(2, 3);
+extern void pqInternalOAuthNotice(const PGOAuthNoticeHooks *hooks, const char *fmt,...);
extern void pqSaveMessageField(PGresult *res, char code,
const char *value);
extern void pqSaveParameterStatus(PGconn *conn, const char *name,