From 1e178ec5bc65bee13c920ea6041c3fe7df1e031f Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Thu, 16 Jan 2014 17:59:07 -0500 Subject: [PATCH] Update Hatchet account type to current protocol workflow. --- .../hatchet/account/HatchetAccount.cpp | 222 +++++++++++++----- src/accounts/hatchet/account/HatchetAccount.h | 12 +- .../hatchet/account/HatchetAccountConfig.cpp | 6 +- src/accounts/hatchet/sip/HatchetSip.cpp | 40 +--- 4 files changed, 187 insertions(+), 93 deletions(-) diff --git a/src/accounts/hatchet/account/HatchetAccount.cpp b/src/accounts/hatchet/account/HatchetAccount.cpp index 145caad9d..46bc09eb6 100644 --- a/src/accounts/hatchet/account/HatchetAccount.cpp +++ b/src/accounts/hatchet/account/HatchetAccount.cpp @@ -86,7 +86,7 @@ HatchetAccount::HatchetAccount( const QString& accountId ) setAccountServiceName( "Hatchet" ); // We're connecting peers. setTypes( SipType ); - +/* QFile pemFile( ":/hatchet-account/mandella.pem" ); pemFile.open( QIODevice::ReadOnly ); tDebug( LOGVERBOSE ) << Q_FUNC_INFO << "certs/mandella.pem: " << pemFile.readAll(); @@ -100,6 +100,7 @@ HatchetAccount::HatchetAccount( const QString& accountId ) return; } m_publicKey = new QCA::PublicKey( publicKey ); +*/ } @@ -223,66 +224,128 @@ uint HatchetAccount::refreshTokenExpiration() const { bool ok; - return credentials().value( "expiration" ).toUInt( &ok ); + return credentials().value( "refresh_token_expiration" ).toUInt( &ok ); +} + + +QByteArray +HatchetAccount::mandellaAccessToken() const +{ + return credentials().value( "mandella_access_token" ).toByteArray(); +} + + +uint +HatchetAccount::mandellaAccessTokenExpiration() const +{ + bool ok; + return credentials().value( "mandella_access_token_expiration" ).toUInt( &ok ); +} + + +QByteArray +HatchetAccount::mandellaTokenType() const +{ + return credentials().value( "mandella_token_type" ).toByteArray(); } void HatchetAccount::loginWithPassword( const QString& username, const QString& password, const QString &otp ) { - if ( username.isEmpty() || password.isEmpty() || !m_publicKey ) + //if ( username.isEmpty() || password.isEmpty() || !m_publicKey ) + if ( username.isEmpty() || password.isEmpty() ) { tLog() << "No tomahawk account username or pw or public key, not logging in"; return; } + /* m_uuid = QUuid::createUuid().toString(); QCA::SecureArray sa( m_uuid.toLatin1() ); QCA::SecureArray result = m_publicKey->encrypt( sa, QCA::EME_PKCS1_OAEP ); - - QVariantMap params; - params[ "password" ] = password; - params[ "username" ] = username; - if ( !otp.isEmpty() ) - params[ "otp" ] = otp; - params[ "client" ] = "Tomahawk (" + QHostInfo::localHostName() + ")"; params[ "nonce" ] = QString( result.toByteArray().toBase64() ); + */ - QJson::Serializer s; - const QByteArray msgJson = s.serialize( params ); + QNetworkRequest req( QUrl( c_loginServer + "/authentication/password") ); + req.setHeader( QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded" ); + + QUrl params; + params.addQueryItem( "username", username ); + params.addQueryItem( "password", password ); + params.addQueryItem( "grant_type", "password" ); + if ( !otp.isEmpty() ) + params.addQueryItem( "otp", otp ); - QNetworkRequest req( QUrl( c_loginServer + "/auth/credentials") ); - req.setHeader( QNetworkRequest::ContentTypeHeader, "application/json; charset=utf-8" ); - QNetworkReply* reply = Tomahawk::Utils::nam()->post( req, msgJson ); + QByteArray data = params.encodedQuery(); + + QNetworkReply* reply = Tomahawk::Utils::nam()->post( req, data ); NewClosure( reply, SIGNAL( finished() ), this, SLOT( onPasswordLoginFinished( QNetworkReply*, const QString& ) ), reply, username ); } void -HatchetAccount::fetchAccessTokens( const QString& type ) +HatchetAccount::fetchAccessToken( const QString& type ) { - if ( username().isEmpty() || refreshToken().isEmpty() ) + if ( username().isEmpty() ) { - tLog() << "No refresh token, not logging in"; + tLog() << "No username, not logging in"; + return; + } + if ( mandellaAccessToken().isEmpty() || + (mandellaAccessTokenExpiration() < QDateTime::currentDateTime().toTime_t() && + (refreshToken().isEmpty() || + (refreshTokenExpiration() != 0 && refreshTokenExpiration() < QDateTime::currentDateTime().toTime_t()))) ) + { + tLog() << "No valid combination of access/refresh tokens, not logging in"; + tLog() << "Mandella access token expiration:" << mandellaAccessTokenExpiration() << ", refresh token expiration:" << refreshTokenExpiration(); + emit authError( "No valid credentials are stored locally, please log in again.", 401, QVariantMap() ); return; } - if ( refreshTokenExpiration() < ( QDateTime::currentMSecsSinceEpoch() / 1000 ) ) - tLog() << "Refresh token has expired, but may still be valid on the server"; + uint matExpiration = mandellaAccessTokenExpiration(); + bool interceptionNeeded = false; - tLog() << "Fetching access tokens"; - QNetworkRequest req( QUrl( c_accessTokenServer + "/tokens/" + type + "?username=" + username() + "&refresh_token=" + refreshToken() ) ); + if ( matExpiration < QDateTime::currentDateTime().toTime_t() ) + { + interceptionNeeded = true; + tLog() << "Mandella access token has expired, fetching new ones first"; + } + else + { + tLog() << "Fetching access tokens of type" << type; + } - QNetworkReply* reply = Tomahawk::Utils::nam()->get( req ); + QNetworkRequest req( QUrl( c_accessTokenServer + "/tokens/" + (interceptionNeeded ? "refresh/" + QString::fromUtf8(mandellaTokenType()).toLower() : "fetch/" + type) ) ); + QNetworkReply* reply; - connect( reply, SIGNAL( finished() ), this, SLOT( onFetchAccessTokensFinished() ) ); + if ( interceptionNeeded ) + { + tLog() << "Intercepting; new mandella access token needed"; + req.setHeader( QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded" ); + QUrl params; + params.addQueryItem( "grant_type", "refresh_token" ); + params.addQueryItem( "refresh_token", refreshToken() ); + QByteArray data = params.encodedQuery(); + reply = Tomahawk::Utils::nam()->post( req, data ); + reply->setProperty( "originalType", type ); + } + else + { + tLog() << "Fetching token of type" << type; + req.setRawHeader( "Authorization", QString( mandellaTokenType() + " " + mandellaAccessToken()).toUtf8() ); + reply = Tomahawk::Utils::nam()->get( req ); + } + + NewClosure( reply, SIGNAL( finished() ), this, SLOT( onFetchAccessTokenFinished( QNetworkReply*, const QString& ) ), reply, type ); } void HatchetAccount::onPasswordLoginFinished( QNetworkReply* reply, const QString& username ) { + tLog() << Q_FUNC_INFO; Q_ASSERT( reply ); bool ok; int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt( &ok ); @@ -307,12 +370,13 @@ HatchetAccount::onPasswordLoginFinished( QNetworkReply* reply, const QString& us } if ( statusCode >= 400 ) { - QString errString = resp.value( "result" ).toMap().value( "errorinfo" ).toMap().value( "description" ).toString(); + QString errString = resp.value( "error_description" ).toString(); tLog() << Q_FUNC_INFO << "An error was returned from the authentication server: " << errString; emit authError( errString, statusCode, resp ); return; } + /* const QString nonce = resp.value( "result" ).toMap().value( "nonce" ).toString(); if ( nonce != m_uuid ) { @@ -320,32 +384,59 @@ HatchetAccount::onPasswordLoginFinished( QNetworkReply* reply, const QString& us emit authError( "The nonce value was incorrect. YOUR ACCOUNT MAY BE COMPROMISED.", statusCode, resp ); return; } + */ - const QByteArray refreshTokenBytes = resp.value( "result" ).toMap().value( "refresh_token" ).toByteArray(); - uint expiration = resp.value( "result" ).toMap().value( "expiration" ).toUInt( &ok ); + const QByteArray refreshTokenBytes = resp.value( "refresh_token" ).toByteArray(); + uint refreshTokenExpiration = resp.value( "refresh_token_expires_in" ).toUInt( &ok ); + if ( refreshTokenBytes.isEmpty() || !ok ) + { + tLog() << Q_FUNC_INFO << "Error reading refresh token or its expiration"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; + } + const QByteArray accessTokenBytes = resp.value( "access_token" ).toByteArray(); + uint accessTokenExpiration = resp.value( "expires_in" ).toUInt( &ok ); + if ( accessTokenBytes.isEmpty() || !ok ) + { + tLog() << Q_FUNC_INFO << "Error reading access token or its expiration"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; + } + const QByteArray tokenTypeBytes = resp.value( "token_type" ).toByteArray(); + if ( tokenTypeBytes.isEmpty() ) + { + tLog() << Q_FUNC_INFO << "Error reading access token type"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; + } QVariantHash creds = credentials(); creds[ "username" ] = username; creds[ "refresh_token" ] = refreshTokenBytes; - creds[ "expiration" ] = expiration; + creds[ "refresh_token_expiration" ] = refreshTokenExpiration == 0 ? 0 : QDateTime::currentDateTime().toTime_t() + refreshTokenExpiration; + creds[ "mandella_access_token" ] = accessTokenBytes; + creds[ "mandella_access_token_expiration" ] = QDateTime::currentDateTime().toTime_t() + accessTokenExpiration; + creds[ "mandella_token_type" ] = tokenTypeBytes; setCredentials( creds ); syncConfig(); - if ( !refreshTokenBytes.isEmpty() ) - { - if ( sipPlugin() ) - sipPlugin()->connectPlugin(); - } + if ( sipPlugin() ) + sipPlugin()->connectPlugin(); } void -HatchetAccount::onFetchAccessTokensFinished() +HatchetAccount::onFetchAccessTokenFinished( QNetworkReply* reply, const QString& type ) { tLog() << Q_FUNC_INFO; - QNetworkReply* reply = qobject_cast< QNetworkReply* >( sender() ); Q_ASSERT( reply ); + QString originalType; + if ( reply->property( "originalType" ).isValid() ) + { + originalType = reply->property( "originalType" ).toString(); + } + bool ok; int statusCode = reply->attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt( &ok ); if ( !ok ) @@ -369,36 +460,59 @@ HatchetAccount::onFetchAccessTokensFinished() } if ( statusCode >= 400 ) { - QString errString = resp.value( "result" ).toMap().value( "errorinfo" ).toMap().value( "description" ).toString(); + QString errString = resp.value( "error_description" ).toString(); tLog() << Q_FUNC_INFO << "An error was returned from the authentication server: " << errString; emit authError( errString, statusCode, resp ); return; } QVariantHash creds = credentials(); - QStringList tokenTypesFound; - tDebug() << Q_FUNC_INFO << "resp: " << resp; - - if ( resp[ "result" ].toMap().contains( "refresh_token_expiration" ) ) + if ( !originalType.isEmpty() ) { - bool ok; - uint expiration = resp.value( "result" ).toMap().value( "refresh_token_expiration" ).toUInt( &ok ); - creds[ "expiration" ] = expiration; - } - - foreach( QVariant tokenVariant, resp[ "result" ].toMap()[ "tokens" ].toList() ) - { - QVariantMap tokenMap = tokenVariant.toMap(); - QString tokenTypeName = tokenMap[ "type" ].toString() + "tokens"; - if ( !tokenTypesFound.contains( tokenTypeName ) ) + const QByteArray accessTokenBytes = resp.value( "access_token" ).toByteArray(); + uint accessTokenExpiration = resp.value( "expires_in" ).toUInt( &ok ); + if ( accessTokenBytes.isEmpty() || !ok ) { - creds[ tokenTypeName ] = QVariantList(); - tokenTypesFound.append( tokenTypeName ); + tLog() << Q_FUNC_INFO << "Error reading access token or its expiration"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; } - creds[ tokenTypeName ] = creds[ tokenTypeName ].toList() << tokenMap; + const QByteArray tokenTypeBytes = resp.value( "token_type" ).toByteArray(); + if ( tokenTypeBytes.isEmpty() ) + { + tLog() << Q_FUNC_INFO << "Error reading access token type"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; + } + creds[ "mandella_access_token" ] = accessTokenBytes; + creds[ "mandella_access_token_expiration" ] = QDateTime::currentDateTime().toTime_t() + accessTokenExpiration; + creds[ "mandella_token_type" ] = tokenTypeBytes; + setCredentials( creds ); + syncConfig(); + + fetchAccessToken( originalType ); + return; } + const QByteArray accessTokenBytes = resp.value( "access_token" ).toByteArray(); + uint accessTokenExpiration = resp.value( "expires_in" ).toUInt( &ok ); + if ( accessTokenBytes.isEmpty() || !ok ) + { + tLog() << Q_FUNC_INFO << "Error reading access token or its expiration"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; + } + const QByteArray tokenTypeBytes = resp.value( "token_type" ).toByteArray(); + if ( tokenTypeBytes.isEmpty() ) + { + tLog() << Q_FUNC_INFO << "Error reading access token type"; + emit authError( "An error encountered parsing the authentication server's response", 0, QVariantMap() ); + return; + } + + creds[ type + "_access_token" ] = accessTokenBytes; + tDebug() << Q_FUNC_INFO << "Creds: " << creds; setCredentials( creds ); @@ -406,7 +520,7 @@ HatchetAccount::onFetchAccessTokensFinished() tLog() << Q_FUNC_INFO << "Access tokens fetched successfully"; - emit accessTokensFetched(); + emit accessTokenFetched(); } diff --git a/src/accounts/hatchet/account/HatchetAccount.h b/src/accounts/hatchet/account/HatchetAccount.h index e476e5833..06c10c122 100644 --- a/src/accounts/hatchet/account/HatchetAccount.h +++ b/src/accounts/hatchet/account/HatchetAccount.h @@ -79,7 +79,6 @@ public: void setConnectionState( Account::ConnectionState connectionState ); ConnectionState connectionState() const; - virtual Tomahawk::InfoSystem::InfoPluginPtr infoPlugin() { return Tomahawk::InfoSystem::InfoPluginPtr(); } SipPlugin* sipPlugin( bool create = true ); @@ -88,24 +87,29 @@ public: QString username() const; - void fetchAccessTokens( const QString& type = "dreamcatcher" ); + void fetchAccessToken( const QString& type = "dreamcatcher" ); QString authUrlForService( const Service& service ) const; signals: void authError( QString error, int statusCode, const QVariantMap ); void deauthenticated(); - void accessTokensFetched(); + void accessTokenFetched(); private slots: void onPasswordLoginFinished( QNetworkReply*, const QString& username ); - void onFetchAccessTokensFinished(); + void onFetchAccessTokenFinished( QNetworkReply*, const QString& type ); void authUrlDiscovered( Tomahawk::Accounts::HatchetAccount::Service service, const QString& authUrl ); private: QByteArray refreshToken() const; uint refreshTokenExpiration() const; + QByteArray mandellaAccessToken() const; + uint mandellaAccessTokenExpiration() const; + + QByteArray mandellaTokenType() const; + void loginWithPassword( const QString& username, const QString& password, const QString &otp ); QVariantMap parseReply( QNetworkReply* reply, bool& ok ) const; diff --git a/src/accounts/hatchet/account/HatchetAccountConfig.cpp b/src/accounts/hatchet/account/HatchetAccountConfig.cpp index 17b2fda52..6d35320ee 100644 --- a/src/accounts/hatchet/account/HatchetAccountConfig.cpp +++ b/src/accounts/hatchet/account/HatchetAccountConfig.cpp @@ -56,7 +56,7 @@ HatchetAccountConfig::HatchetAccountConfig( HatchetAccount* account ) connect( m_account, SIGNAL( authError( QString, int, QVariantMap ) ), this, SLOT( authError( QString, int, QVariantMap ) ) ); connect( m_account, SIGNAL( deauthenticated() ), this, SLOT( showLoggedOut() ) ); - connect( m_account, SIGNAL( accessTokensFetched() ), this, SLOT( accountInfoUpdated() ) ); + connect( m_account, SIGNAL( accessTokenFetched() ), this, SLOT( accountInfoUpdated() ) ); if ( !m_account->refreshToken().isEmpty() ) accountInfoUpdated(); @@ -170,7 +170,7 @@ HatchetAccountConfig::accountInfoUpdated() void HatchetAccountConfig::authError( const QString &error, int statusCode, const QVariantMap& resp ) { - if ( statusCode == 401 && resp["result"].toMap()["errorinfo"].toMap().contains("missingotp") ) + if ( statusCode == 400 && error == "otp_needed" ) { m_ui->usernameLabel->hide(); m_ui->usernameEdit->hide(); @@ -182,7 +182,7 @@ HatchetAccountConfig::authError( const QString &error, int statusCode, const QVa return; } if ( statusCode == 401 ) - m_account->deauthenticate(); + m_account->deauthenticate(); QMessageBox::critical( this, "An error was encountered:", error ); } diff --git a/src/accounts/hatchet/sip/HatchetSip.cpp b/src/accounts/hatchet/sip/HatchetSip.cpp index 381a35a52..99fc16e56 100644 --- a/src/accounts/hatchet/sip/HatchetSip.cpp +++ b/src/accounts/hatchet/sip/HatchetSip.cpp @@ -48,7 +48,7 @@ HatchetSipPlugin::HatchetSipPlugin( Tomahawk::Accounts::Account *account ) { tLog() << Q_FUNC_INFO; - connect( m_account, SIGNAL( accessTokensFetched() ), this, SLOT( connectWebSocket() ) ); + connect( m_account, SIGNAL( accessTokenFetched() ), this, SLOT( connectWebSocket() ) ); connect( Servent::instance(), SIGNAL( dbSyncTriggered() ), this, SLOT( dbSyncTriggered() )); QFile pemFile( ":/hatchet-account/dreamcatcher.pem" ); @@ -113,7 +113,7 @@ HatchetSipPlugin::connectPlugin() } hatchetAccount()->setConnectionState( Tomahawk::Accounts::Account::Connecting ); - hatchetAccount()->fetchAccessTokens(); + hatchetAccount()->fetchAccessToken( "dreamcatcher" ); } @@ -156,42 +156,18 @@ HatchetSipPlugin::connectWebSocket() return; } - m_token.clear(); + m_token = m_account->credentials()[ "dreamcatcher_access_token" ].toString(); - QVariantList tokensCreds = m_account->credentials()[ "dreamcatchertokens" ].toList(); - //FIXME: Don't blindly pick the first one that matches? Most likely, cycle through if the first one fails - QVariantMap connectVals; - foreach ( QVariant credObj, tokensCreds ) + if ( m_token.isEmpty() ) { - QVariantMap creds = credObj.toMap(); - if ( creds.contains( "type" ) && creds[ "type" ].toString() == "dreamcatcher" ) - { - connectVals = creds; - m_token = creds[ "token" ].toString(); - break; - } - } - - QString url; - if ( !connectVals.isEmpty() ) - { - QString port = connectVals[ "port" ].toString(); - if ( port == "443" ) - url = "wss://"; - else - url = "ws://"; - url += connectVals[ "host" ].toString() + ':' + connectVals[ "port" ].toString(); - } - - if ( url.isEmpty() || m_token.isEmpty() ) - { - tLog() << Q_FUNC_INFO << "Unable to find a proper connection endpoint; bailing"; + tLog() << Q_FUNC_INFO << "Unable to find an access token"; disconnectPlugin(); return; } - else - tLog() << Q_FUNC_INFO << "Connecting to Dreamcatcher endpoint at: " << url; + + QString url( "wss://dreamcatcher.hatchet.is:443" ); + tLog() << Q_FUNC_INFO << "Connecting to Dreamcatcher endpoint at: " << url; m_webSocketThreadController->setUrl( url ); m_webSocketThreadController->start();