I've got a need to implement SPNEGO with a Qt client. The server responds with 401/Unauthorized and sends the WWW-Authenticate: Negotiate header.
2 Answers
First, get a feel for the protocol through the RFC here: https://tools.ietf.org/html/rfc4559
Another great reference is https://github.com/requests/requests-kerberos
Understand that GSSAPI, while there is a cross-platform implementation, does not exist out of the box on Windows and instead you'll need to use SSPI. This answer will show how to implement it on Windows. You can map the functions onto GSSAPI for the other platforms. You will never write code like this for real, but I created this for people who need to implement SPNEGO without too much abstraction.
This example uses the fantastic Kerberos environment created here: https://github.com/Brandon-Godwin/vagrant-kerberos-environment
#include <QCoreApplication>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QtDebug>
#include <QNetworkReply>
#include <QAuthenticator>
#include <QNetworkRequest>
#include <QNetworkProxy>
#include <QNetworkCookieJar>
#include <QNetworkCookie>
#define SECURITY_WIN32
#include <windows.h>
#include <security.h>
#pragma comment(lib,"secur32.lib")
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
QNetworkAccessManager manager;
manager.connect(&manager,&QNetworkAccessManager::authenticationRequired,
[](QNetworkReply * reply, QAuthenticator * authenticator) {
qDebug() << "AUTH REQUIRED" << reply << authenticator;
});
manager.connect(&manager,&QNetworkAccessManager::proxyAuthenticationRequired,
[](const QNetworkProxy & proxy, QAuthenticator * authenticator) {
qDebug() << "AUTH REQUIRED" << proxy << authenticator;
});
qDebug() << "RUNNING";
QNetworkRequest request;
request.setUrl(QUrl("http://dc.testdomain.lan:8080/hello"));
auto reply = manager.get(request);
reply->connect(reply,&QNetworkReply::finished,
[reply,request,&manager](){
reply->deleteLater();
if(reply->rawHeader("www-authenticate") == "Negotiate") {
qDebug() << reply->rawHeaderList();
qDebug() << reply->rawHeader("set-cookie");
CredHandle cred = {0};
TimeStamp exp;
qDebug() << "Acquire"
<< AcquireCredentialsHandleA(NULL,(LPSTR)"Kerberos",SECPKG_CRED_OUTBOUND,NULL,NULL,NULL,NULL,&cred,&exp);
CtxtHandle ctx;
SecBufferDesc outputBufferDesc;
SecBuffer outputBuffers[1];
outputBuffers[0].pvBuffer = NULL;
outputBuffers[0].BufferType = SECBUFFER_TOKEN;
outputBuffers[0].cbBuffer = 0;
outputBufferDesc.ulVersion = SECBUFFER_VERSION;
outputBufferDesc.cBuffers = 1;
outputBufferDesc.pBuffers = outputBuffers;
ULONG contextAttr;
auto ret = InitializeSecurityContextA(&cred,NULL,(LPSTR)"HTTP/dc.testdomain.lan:8080",ISC_REQ_ALLOCATE_MEMORY,0,
SECURITY_NATIVE_DREP,NULL,0,&ctx,&outputBufferDesc,&contextAttr,&exp);
// TODO: De-allocate outputBufferDesc.pBuffers[0]
#define CASE(X) case X: qDebug() << #X; return;
switch(ret) {
case SEC_E_OK:
break;
CASE(SEC_E_INSUFFICIENT_MEMORY);
CASE(SEC_E_INTERNAL_ERROR);
CASE(SEC_E_INVALID_HANDLE);
CASE(SEC_E_INVALID_TOKEN);
CASE(SEC_E_LOGON_DENIED);
CASE(SEC_E_NO_AUTHENTICATING_AUTHORITY);
CASE(SEC_E_NO_CREDENTIALS);
CASE(SEC_E_TARGET_UNKNOWN);
CASE(SEC_E_UNSUPPORTED_FUNCTION);
CASE(SEC_E_WRONG_PRINCIPAL);
default:
qDebug() << "WAT" << ret;
return;
}
auto pBuffer = outputBufferDesc.pBuffers[0];
QByteArray array((const char *)pBuffer.pvBuffer,pBuffer.cbBuffer);
QNetworkRequest request2 = request;
request2.setRawHeader("Authorization","Negotiate " + array.toBase64());
auto reply2 = manager.get(request2);
reply2->connect(reply2,&QNetworkReply::finished,
[&manager,request,reply2]() {
qDebug() << reply2->rawHeaderList();
qDebug() << reply2->rawHeader("set-cookie");
qDebug() << reply2->readAll();
reply2->deleteLater();
auto reply3 = manager.get(request);
reply3->connect(reply3,&QNetworkReply::finished,
[reply3]() {
qDebug() << reply3->readAll();
qDebug() << reply3->rawHeaderList();
qDebug() << reply3->rawHeader("set-cookie");
reply3->deleteLater();
});
});
}
});
return a.exec();
}
I'll offer you an extremely simple patch for the Qt5Network library that adds support for the Negotiate method. I do not know why the Qt development team refused to support Kerberos. So,
diff -bur network0/access/qhttpnetworkconnection.cpp network/access/qhttpnetworkconnection.cpp
--- network0/access/qhttpnetworkconnection.cpp 2019-08-31 13:29:31.000000000 +0500
+++ network/access/qhttpnetworkconnection.cpp 2019-11-26 16:24:00.832160300 +0500
@@ -52,6 +52,7 @@
#include <qbuffer.h>
#include <qpair.h>
#include <qdebug.h>
+#include <qurl.h>
#ifndef QT_NO_SSL
# include <private/qsslsocket_p.h>
@@ -587,9 +588,14 @@
int i = indexOf(socket);
+ QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(channels[i].authenticator);
+ priv->host = QUrl(request.uri(true)).host();
+
// Send "Authorization" header, but not if it's NTLM and the socket is already authenticated.
if (channels[i].authMethod != QAuthenticatorPrivate::None) {
- if ((channels[i].authMethod != QAuthenticatorPrivate::Ntlm && request.headerField("Authorization").isEmpty()) || channels[i].lastStatus == 401) {
+ if ((channels[i].authMethod != QAuthenticatorPrivate::Ntlm
+ && channels[i].authMethod != QAuthenticatorPrivate::Negotiate
+ && request.headerField("Authorization").isEmpty()) || channels[i].lastStatus == 401) {
QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(channels[i].authenticator);
if (priv && priv->method != QAuthenticatorPrivate::None) {
QByteArray response = priv->calculateResponse(request.methodName(), request.uri(false));
@@ -601,7 +607,9 @@
// Send "Proxy-Authorization" header, but not if it's NTLM and the socket is already authenticated.
if (channels[i].proxyAuthMethod != QAuthenticatorPrivate::None) {
- if (!(channels[i].proxyAuthMethod == QAuthenticatorPrivate::Ntlm && channels[i].lastStatus != 407)) {
+ if (!((channels[i].proxyAuthMethod == QAuthenticatorPrivate::Ntlm
+ || channels[i].proxyAuthMethod == QAuthenticatorPrivate::Negotiate)
+ && channels[i].lastStatus != 407)) {
QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(channels[i].proxyAuthenticator);
if (priv && priv->method != QAuthenticatorPrivate::None) {
QByteArray response = priv->calculateResponse(request.methodName(), request.uri(false));
diff -bur network0/access/qhttpnetworkreply.cpp network/access/qhttpnetworkreply.cpp
--- network0/access/qhttpnetworkreply.cpp 2019-08-31 13:29:31.000000000 +0500
+++ network/access/qhttpnetworkreply.cpp 2019-11-01 15:35:31.207753700 +0500
@@ -421,7 +421,6 @@
for (int i = 0; i<challenges.size(); i++) {
QByteArray line = challenges.at(i);
// todo use qstrincmp
- if (!line.toLower().startsWith("negotiate"))
challenge = line;
}
return !challenge.isEmpty();
@@ -444,6 +443,9 @@
} else if (method < QAuthenticatorPrivate::DigestMd5
&& line.startsWith("digest")) {
method = QAuthenticatorPrivate::DigestMd5;
+ } else if (method < QAuthenticatorPrivate::Negotiate
+ && line.startsWith("negotiate")) {
+ method = QAuthenticatorPrivate::Negotiate;
}
}
return method;
diff -bur network0/kernel/qauthenticator.cpp network/kernel/qauthenticator.cpp
--- network0/kernel/qauthenticator.cpp 2019-08-31 13:29:31.000000000 +0500
+++ network/kernel/qauthenticator.cpp 2019-11-01 15:38:43.108057000 +0500
@@ -377,6 +377,7 @@
switch (method) {
case QAuthenticatorPrivate::Ntlm:
+ case QAuthenticatorPrivate::Negotiate:
if ((separatorPosn = user.indexOf(QLatin1String("\\"))) != -1) {
//domain name is present
realm.clear();
@@ -424,6 +425,9 @@
} else if (method < DigestMd5 && str.startsWith("digest")) {
method = DigestMd5;
headerVal = current.second.mid(7);
+ } else if (method < Negotiate && str.startsWith("negotiate")) {
+ method = Negotiate;
+ headerVal = current.second.mid(10);
}
}
@@ -439,6 +443,7 @@
phase = Done;
break;
case Ntlm:
+ case Negotiate:
// work is done in calculateResponse()
break;
case DigestMd5: {
@@ -477,7 +482,8 @@
phase = Done;
break;
case QAuthenticatorPrivate::Ntlm:
- methodString = "NTLM ";
+ case QAuthenticatorPrivate::Negotiate:
+ methodString = (method == Ntlm) ? "NTLM " : "Negotiate ";
if (challenge.isEmpty()) {
#if defined(Q_OS_WIN) && !defined(Q_OS_WINRT)
QByteArray phase1Token;
@@ -1457,6 +1463,17 @@
{
QByteArray result;
+ QString pkg;
+ QString host;
+ if(ctx->method == ctx->Ntlm) {
+ pkg = QString::fromLatin1("NTLM");
+ host.clear();
+ }
+ else if(ctx->method == ctx->Negotiate) {
+ pkg = QString::fromLatin1("Negotiate");
+ host = QString::fromLatin1("host/%1").arg( ctx->host );
+ }
+
if (!q_NTLM_SSPI_library_load())
return result;
@@ -1467,7 +1484,7 @@
memset(&ctx->ntlmWindowsHandles->credHandle, 0, sizeof(CredHandle));
TimeStamp tsDummy;
SECURITY_STATUS secStatus = pSecurityFunctionTable->AcquireCredentialsHandle(
- NULL, (SEC_WCHAR*)L"NTLM", SECPKG_CRED_OUTBOUND, NULL, NULL,
+ NULL, const_cast<SEC_WCHAR*>( (WCHAR*)pkg.utf16() ), SECPKG_CRED_OUTBOUND, NULL, NULL,
NULL, NULL, &ctx->ntlmWindowsHandles->credHandle, &tsDummy);
if (secStatus != SEC_E_OK) {
delete ctx->ntlmWindowsHandles;
@@ -1489,7 +1506,7 @@
ULONG attrs;
secStatus = pSecurityFunctionTable->InitializeSecurityContext(&ctx->ntlmWindowsHandles->credHandle, NULL,
- const_cast<SEC_WCHAR*>(L"") /* host */,
+ const_cast<SEC_WCHAR*>( (WCHAR*)host.utf16() ) /* host */,
ISC_REQ_ALLOCATE_MEMORY,
0, SECURITY_NETWORK_DREP,
NULL, 0,
diff -bur network0/kernel/qauthenticator_p.h network/kernel/qauthenticator_p.h
--- network0/kernel/qauthenticator_p.h 2019-08-31 13:29:31.000000000 +0500
+++ network/kernel/qauthenticator_p.h 2019-11-01 15:44:20.445370000 +0500
@@ -68,7 +68,7 @@
class Q_AUTOTEST_EXPORT QAuthenticatorPrivate
{
public:
- enum Method { None, Basic, Ntlm, DigestMd5 };
+ enum Method { None, Basic, Ntlm, DigestMd5, Negotiate };
QAuthenticatorPrivate();
~QAuthenticatorPrivate();
@@ -79,6 +79,7 @@
Method method;
QString realm;
QByteArray challenge;
+ QString host;
#ifdef Q_OS_WIN
QNtlmWindowsHandles *ntlmWindowsHandles;
#endif
This is a patch for qt5.13.1 version. If anyone is interested in patches of other versions of qt5 or already compiled libraries, you can find them at the anselm.ru.