Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 64 additions & 42 deletions src/auth/oauth2/core/qgsauthoauth2method.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ QgsO2 *QgsOAuth2Factory::createO2Private( const QString &authcfg, QgsAuthOAuth2C
QgsAuthOAuth2Method::QgsAuthOAuth2Method()
{
setVersion( 1 );
setExpansions( QgsAuthMethod::NetworkRequest | QgsAuthMethod::NetworkReply );
setExpansions( QgsAuthMethod::NetworkRequest | QgsAuthMethod::NetworkReply | QgsAuthMethod::DataSourceUri );
setDataProviders( QStringList() << u"ows"_s << u"wfs"_s // convert to lowercase
<< u"wcs"_s << u"wms"_s );
<< u"wcs"_s << u"wms"_s << u"postgres"_s );

const QStringList cachedirpaths = QStringList()
<< QgsAuthOAuth2Config::tokenCacheDirectory()
Expand Down Expand Up @@ -336,57 +336,66 @@ bool QgsAuthOAuth2Method::updateNetworkRequest( QNetworkRequest &request, const
QUrl url = request.url();
QUrlQuery query( url );

switch ( accessmethod )
if ( dataprovider == "postgres"_L1 )
{
case QgsAuthOAuth2Config::AccessMethod::Header:
// allow easy token "exfiltration" via QNetworkRequest, so that we can do without
// a postgres/oauth-specific update* method
request.setRawHeader( "Token"_ba, o2->token().toLatin1() );
}
else
{
switch ( accessmethod )
{
const QString header = o2->oauth2config()->customHeader().isEmpty() ? QString( O2_HTTP_AUTHORIZATION_HEADER ) : o2->oauth2config()->customHeader();
request.setRawHeader( header.toLatin1(), u"Bearer %1"_s.arg( o2->token() ).toLatin1() );

const QVariantMap extraTokens = o2->oauth2config()->extraTokens();
if ( !extraTokens.isEmpty() )
case QgsAuthOAuth2Config::AccessMethod::Header:
{
const QVariantMap receivedExtraTokens = o2->extraTokens();
const QStringList extraTokenNames = extraTokens.keys();
for ( const QString &extraTokenName : extraTokenNames )
const QString header = o2->oauth2config()->customHeader().isEmpty() ? QString( O2_HTTP_AUTHORIZATION_HEADER ) : o2->oauth2config()->customHeader();
request.setRawHeader( header.toLatin1(), u"Bearer %1"_s.arg( o2->token() ).toLatin1() );

const QVariantMap extraTokens = o2->oauth2config()->extraTokens();
if ( !extraTokens.isEmpty() )
{
if ( receivedExtraTokens.contains( extraTokenName ) )
const QVariantMap receivedExtraTokens = o2->extraTokens();
const QStringList extraTokenNames = extraTokens.keys();
for ( const QString &extraTokenName : extraTokenNames )
{
request.setRawHeader( extraTokens[extraTokenName].toString().replace( '_', '-' ).toLatin1(), receivedExtraTokens[extraTokenName].toString().toLatin1() );
if ( receivedExtraTokens.contains( extraTokenName ) )
{
request.setRawHeader( extraTokens[extraTokenName].toString().replace( '_', '-' ).toLatin1(), receivedExtraTokens[extraTokenName].toString().toLatin1() );
}
}
}
}

#ifdef QGISDEBUG
msg = u"Updated request HEADER with access token for authcfg: %1"_s.arg( authcfg );
QgsDebugMsgLevel( msg, 2 );
msg = u"Updated request HEADER with access token for authcfg: %1"_s.arg( authcfg );
QgsDebugMsgLevel( msg, 2 );
#endif
break;
}
case QgsAuthOAuth2Config::AccessMethod::Form:
// FIXME: what to do here if the parent request is not POST?
// probably have to skip this until auth system support is moved into QgsNetworkAccessManager
msg = u"Update request FAILED for authcfg %1: form POST token update is unsupported"_s.arg( authcfg );
QgsMessageLog::logMessage( msg, AUTH_METHOD_KEY, Qgis::MessageLevel::Warning );
break;
case QgsAuthOAuth2Config::AccessMethod::Query:
if ( !query.hasQueryItem( O2_OAUTH2_ACCESS_TOKEN ) )
{
query.addQueryItem( O2_OAUTH2_ACCESS_TOKEN, o2->token() );
url.setQuery( query );
request.setUrl( url );
break;
}
case QgsAuthOAuth2Config::AccessMethod::Form:
// FIXME: what to do here if the parent request is not POST?
// probably have to skip this until auth system support is moved into QgsNetworkAccessManager
msg = u"Update request FAILED for authcfg %1: form POST token update is unsupported"_s.arg( authcfg );
QgsMessageLog::logMessage( msg, AUTH_METHOD_KEY, Qgis::MessageLevel::Warning );
break;
case QgsAuthOAuth2Config::AccessMethod::Query:
if ( !query.hasQueryItem( O2_OAUTH2_ACCESS_TOKEN ) )
{
query.addQueryItem( O2_OAUTH2_ACCESS_TOKEN, o2->token() );
url.setQuery( query );
request.setUrl( url );
#ifdef QGISDEBUG
msg = u"Updated request QUERY with access token for authcfg: %1"_s.arg( authcfg );
msg = u"Updated request QUERY with access token for authcfg: %1"_s.arg( authcfg );
#endif
}
else
{
}
else
{
#ifdef QGISDEBUG
msg = u"Updated request QUERY with access token SKIPPED (existing token) for authcfg: %1"_s.arg( authcfg );
msg = u"Updated request QUERY with access token SKIPPED (existing token) for authcfg: %1"_s.arg( authcfg );
#endif
}
QgsDebugMsgLevel( msg, 2 );
break;
}
QgsDebugMsgLevel( msg, 2 );
break;
}
}

return true;
Expand Down Expand Up @@ -588,9 +597,22 @@ void QgsAuthOAuth2Method::onAuthCode()

bool QgsAuthOAuth2Method::updateDataSourceUriItems( QStringList &connectionItems, const QString &authcfg, const QString &dataprovider )
{
Q_UNUSED( connectionItems )
Q_UNUSED( authcfg )
Q_UNUSED( dataprovider )
if ( dataprovider == "postgres"_L1 )
{
QgsAuthOAuth2Config *config = getOAuth2Bundle( authcfg, true )->oauth2config();

connectionItems << u"oauth_issuer=%1"_s.arg( QUrl( config->requestUrl() ).toString( QUrl::RemovePath ) )
<< u"oauth_client_id=%1"_s.arg( config->clientId() );

if ( !config->clientSecret().isEmpty() )
{
connectionItems << u"oauth_client_secret=%1"_s.arg( config->clientSecret() );
}
if ( !config->scope().isEmpty() )
{
connectionItems << u"oauth_scope=%1"_s.arg( config->scope().replace( ' '_L1, u"%20"_s ) );
}
}

return true;
}
Expand Down
2 changes: 2 additions & 0 deletions src/providers/postgres/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ set(PG_SRCS
qgspostgresutils.cpp
qgspostgresprovider.cpp
qgspostgresconn.cpp
qgspostgresconnoauthhandler.cpp
qgspostgresconnpool.cpp
qgspostgresdataitems.cpp
qgspostgresfeatureiterator.cpp
Expand Down Expand Up @@ -87,6 +88,7 @@ set(PGRASTER_SRCS
raster/qgspostgresrastershareddata.cpp
raster/qgspostgresrasterutils.cpp
qgspostgresconn.cpp
qgspostgresconnoauthhandler.cpp
qgspostgresconnpool.cpp
qgspostgresprovidermetadatautils.cpp
)
Expand Down
33 changes: 31 additions & 2 deletions src/providers/postgres/qgspostgresconn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
#include <nlohmann/json.hpp>

#include "qgsapplication.h"
#include "qgsauthmanager.h"
#include "qgscredentials.h"
#include "qgsdatasourceuri.h"
#include "qgsdbquerylog.h"
#include "qgsdbquerylog_p.h"
#include "qgsjsonutils.h"
#include "qgslogger.h"
#include "qgsmessagelog.h"
#include "qgspostgresconnoauthhandler.h"
#include "qgspostgresconnpool.h"
#include "qgspostgresstringutils.h"
#include "qgssettings.h"
Expand All @@ -50,6 +52,11 @@
#include <netinet/in.h>
#endif

extern "C"
{
#include <pg_config.h>
}

const int PG_DEFAULT_TIMEOUT = 30;

static QString quotedString( const QString &v )
Expand Down Expand Up @@ -303,8 +310,30 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s
};
addDefaultTimeoutAndClientEncoding( expandedConnectionInfo );

bool isOAuth = false;
if ( !mUri.authConfigId().isEmpty() )
{
QgsAuthMethod *method = QgsApplication::authManager()->configAuthMethod( mUri.authConfigId() );
QStringList items;
if ( method->key() == "OAuth2"_L1 && method->updateDataSourceUriItems( items, mUri.authConfigId(), u"postgres"_s ) )
{
#if PG_MAJORVERSION_NUM >= 18
isOAuth = true;
expandedConnectionInfo += u" %1"_s.arg( items.join( " " ) );
#else
QgsMessageLog::logMessage(
tr( "OAuth authentication requires PostgreSQL 18, this QGIS is built using the PostgreSQL client library version %1.%2, falling back to basic authentication" ).arg( PG_MAJORVERSION_NUM ).arg( PG_MINORVERSION_NUM ),
tr( "PostGIS" )
);
#endif
}
}

auto logWrapper = std::make_unique<QgsDatabaseQueryLogWrapper>( u"libpq::PQconnectdb()"_s, expandedConnectionInfo.toUtf8(), u"postgres"_s, u"QgsPostgresConn"_s, QGS_QUERY_LOG_ORIGIN_PG_CON );
mConn = PQconnectdb( expandedConnectionInfo.toUtf8() );
{
const auto auth = QgsPostgresConnOAuthHandler( mUri.authConfigId() );
mConn = PQconnectdb( expandedConnectionInfo.toUtf8() );
}

// remove temporary cert/key/CA if they exist
QgsDataSourceUri expandedUri( expandedConnectionInfo );
Expand Down Expand Up @@ -343,7 +372,7 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s
}

// check the connection status
if ( PQstatus() != CONNECTION_OK )
if ( PQstatus() != CONNECTION_OK && !isOAuth )
{
QString username = mUri.username();
QString password = mUri.password();
Expand Down
115 changes: 115 additions & 0 deletions src/providers/postgres/qgspostgresconnoauthhandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/***************************************************************************
qgspostgresconnoauthhandler.cpp

---------------------
begin : 27.12.2025
copyright : (C) 2025 by Jan Dalheimer
email : [email protected]
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/

#include "qgspostgresconnoauthhandler.h"

#include <mutex>

#include "qgsapplication.h"
#include "qgsauthmanager.h"
#include "qgsauthmethod.h"
#include "qgslogger.h"
#include "qgsmessagelog.h"

extern "C"
{
#include <libpq-fe.h>
#include <pg_config.h>
}

#ifdef _WIN32
#define SOCKTYPE uintptr_t /* avoids depending on winsock2.h for SOCKET */
#else
#define SOCKTYPE int
#endif

#if PG_MAJORVERSION_NUM >= 18
std::once_flag postgresInitFlag;
int postgresAuthDataHook( PGauthData type, PGconn *conn, void *data )
{
if ( type != PQAUTHDATA_OAUTH_BEARER_TOKEN )
{
return -1;
}

PGoauthBearerRequest *request = static_cast<PGoauthBearerRequest *>( data );

if ( !QgsPostgresConnOAuthHandler::currentlyInScope() )
{
// might happen when connecting from outside of QgsPostgresConn, such as from psycopg in Python
// in practice the connection will likely fail, but at least we won't make anything worse/more confusing this way
return PQdefaultAuthDataHook( type, conn, data );
}

const QString authcfg = QgsPostgresConnOAuthHandler::currentAuthCfg();
QgsAuthMethod *method = QgsApplication::authManager()->configAuthMethod( authcfg );
if ( method && method->key() == "OAuth2"_L1 )
{
QNetworkRequest req;
// this _is_ a blocking call, however since it does run an event loop (so that the UI
// stays responsive, in case the connection is established from the main thread) and
// we never actually use the ability in libpq to connect asynchronously (at least not
// in QgsPostgresConn) we should be fine to keep this as a blocking call
const bool success = method->updateNetworkRequest( req, authcfg, u"postgres"_s );
if ( success )
{
const QByteArray token = req.rawHeader( "Token"_ba );
char *tokenStr = new char[token.size() + 1];
strncpy( tokenStr, token.constData(), token.size() + 1 );
request->token = tokenStr;
// libpq does not deallocate the token for us
request->cleanup = []( PGconn *, PGoauthBearerRequest *request ) { delete static_cast<const char *>( request->token ); };
return 1;
}
else
{
QgsLogger::warning( u"PostgreSQL: OAuth flow failed"_s );
return -1;
}
}
else
{
QgsMessageLog::logMessage( QgsPostgresConnOAuthHandler::tr( "PostgreSQL server request OAuth authentication, however connection in QGIS is not configured to use OAuth" ), QgsPostgresConnOAuthHandler::tr( "PostGIS" ) );
return -1;
}
// Success is indicated by returning an integer greater than zero.
return 1;
}
#endif

thread_local QString QgsPostgresConnOAuthHandler::sAuthCfg;
thread_local bool QgsPostgresConnOAuthHandler::sInScope = false;
QgsPostgresConnOAuthHandler::QgsPostgresConnOAuthHandler( const QString &authcfg )
{
#if PG_MAJORVERSION_NUM >= 18
std::call_once( postgresInitFlag, []() {
PQsetAuthDataHook( postgresAuthDataHook );
} );
#endif

if ( sInScope )
{
QgsLogger::warning( u"starting second postgres connection attempt while previous is not yet finished"_s );
}
sAuthCfg = authcfg;
sInScope = true;
}

QgsPostgresConnOAuthHandler::~QgsPostgresConnOAuthHandler()
{
sAuthCfg = QString();
sInScope = false;
}
Loading
Loading