Howto be notified of an error from QOAuth2AuthorizationCodeFlow?

Viewed 73

I am using QOAuth2AuthorizationCodeFlow to perform OIDC authentication. I can connect to the signal QAbstractOAuth::granted() and be notified when it worked with success. Fine.

My problem is: how to be notified when something wrong happened?

I tried to connect to the QAbstractOAuth2::error() signal but I am not notified. I used:

QObject::connect(this, SIGNAL(error(const QString &, const QString &, const QUrl &)), this, SLOT(catchAll()));

In the application console I see the error reported by the server:

qt.networkauth.replyhandler: Error transferring https://idp.safenetid.com/auth/realms/2H31DFOIEQ-STA/protocol/openid-connect/token - server replied: Fake Bad request

So Qt detected the problem. It looks like the probem is detected in QHttpThreadDelegate::finishedSlot():

if (httpReply->statusCode() >= 400) {
        // it's an error reply
        QString msg = QLatin1String(QT_TRANSLATE_NOOP("QNetworkReply",
                                                      "Error transferring %1 - server replied: %2"));
        msg = msg.arg(httpRequest.url().toString(), httpReply->reasonPhrase());
        emit error(statusCodeFromHttp(httpReply->statusCode(), httpRequest.url()), msg);
    }

An error() signal is emitted. But it is catched by Qt itself and not reported upstream to the application?

Note: Sometimes the server I use replies with an HTTP error code 400 and message "User not found". Here I faked the problem using mitmproxy to be able to reproduce the issue easily.

PS: I think it is a missing feature in Qt. So I created an issue for QOAuth2AuthorizationCodeFlow::requestAccessToken() at https://bugreports.qt.io/browse/QTBUG-102279

2 Answers

You can get notified by implementing your own OAuthHttpServerReplyHandler. This handler has to be set to your QOAuth2AuthorizationCodeFlow by using its setReplyHandler(_).

The OAuthHttpServerReplyHandler has to override the void networkReplyFinished(QNetworkReply *reply) which will get called by QT.

tl;dr: Connect to QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived, and write your own handler.

Looking at the source code the problem seems to be that only one of these case statements will actually emit the error signal. They all return without a Q_EMIT error(...) call.

void QOAuth2AuthorizationCodeFlowPrivate::_q_handleCallback(const QVariantMap &data)
{
    Q_Q(QOAuth2AuthorizationCodeFlow);
    using Key = QAbstractOAuth2Private::OAuth2KeyString;

    if (status != QAbstractOAuth::Status::NotAuthenticated) {
        qCWarning(loggingCategory, "Unexpected call");
        return;
    }

    Q_ASSERT(!state.isEmpty());

    const QString error = data.value(Key::error).toString();
    const QString code = data.value(Key::code).toString();
    const QString receivedState = data.value(Key::state).toString();
    if (error.size()) {
        const QString uri = data.value(Key::errorUri).toString();
        const QString description = data.value(Key::errorDescription).toString();
        qCWarning(loggingCategory, "AuthenticationError: %s(%s): %s",
                 qPrintable(error), qPrintable(uri), qPrintable(description));
        Q_EMIT q->error(error, description, uri);
        return;
    }
    if (code.isEmpty()) {
        qCWarning(loggingCategory, "AuthenticationError: Code not received");
        return;
    }
    if (receivedState.isEmpty()) {
        qCWarning(loggingCategory, "State not received");
        return;
    }
    if (state != receivedState) {
        qCWarning(loggingCategory, "State mismatch");
        return;
    }

    setStatus(QAbstractOAuth::Status::TemporaryCredentialsReceived);

    QVariantMap copy(data);
    copy.remove(Key::code);
    extraTokens = copy;
    q->requestAccessToken(code);
}

But there also seems to be a misunderstanding as to what the error signal is for; Based on the parameters in its declaration,

void QAbstractOAuth2::error(const QString &error, const QString &errorDescription, const QUrl &uri)

The error signal is specifically for the HTTP 400 "Bad Request" response as described in rfc6749, rather than as a signal for all errors, which is why it won't emit signal on (client-side) errors for things like state mismatch (which could be a quite serious error).

If you are still keen on propagating the error signal, however, you can connect to the &QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived signal as the private class does, then emit the error signal. It takes a bit of creativity because members in the code flow private class are not available, but it can be done.

For example, if you want to propagate error on a state mismatch, that might look like this:

{
   static QString last_state;
   QObject::connect(
      &oauth2, &QOAuth2AuthorizationCodeFlow::stateChanged,
      [&](const QString& state) {
         last_state = state;
      });
   QObject::connect(
      &oauth2, QOAuth2AuthorizationCodeFlow::authorizationCallbackReceived,
      [&](const QVariantMap& data) {
         const QString state = data.value("state").toString();

         if(last_state != state) {
             Q_EMIT oauth2.error(
                 "The state in request does not match the state in reply!",                      
                 "State Mismatch!",
                 QUrl());
         }
       });
}

Since this is an abuse of the error function, however, I would suggest putting all this in a different object or class; One that would have stateUpdated and onCallbackReceived slots and be capable of emitting a stateMismatch signal, maybe.

Related