diff --git a/headers/private/netservices/HttpAuthentication.h b/headers/private/netservices/HttpAuthentication.h index 1edf1acaba..18bfb488ce 100644 --- a/headers/private/netservices/HttpAuthentication.h +++ b/headers/private/netservices/HttpAuthentication.h @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 Haiku Inc. All rights reserved. + * Copyright 2010-2023 Haiku Inc. All rights reserved. * Distributed under the terms of the MIT License. */ #ifndef _B_HTTP_AUTHENTICATION_H_ @@ -24,8 +24,10 @@ enum BHttpAuthenticationMethod { // Basic base64 authentication method (unsecure) B_HTTP_AUTHENTICATION_DIGEST = 2, // Digest authentication - B_HTTP_AUTHENTICATION_IE_DIGEST = 4 + B_HTTP_AUTHENTICATION_IE_DIGEST = 4, // Slightly modified digest authentication to mimic old IE one + B_HTTP_AUTHENTICATION_BEARER = 5 + // Bearer authentication used to convey a token }; @@ -56,6 +58,7 @@ public: // Field modification void SetUserName(const BString& username); void SetPassword(const BString& password); + void SetToken(const BString& token); void SetMethod( BHttpAuthenticationMethod type); status_t Initialize(const BString& wwwAuthenticate); @@ -63,6 +66,7 @@ public: // Field access const BString& UserName() const; const BString& Password() const; + const BString& Token() const; BHttpAuthenticationMethod Method() const; BString Authorization(const BUrl& url, @@ -89,6 +93,7 @@ private: BHttpAuthenticationMethod fAuthenticationMethod; BString fUserName; BString fPassword; + BString fToken; BString fRealm; BString fDigestNonce; diff --git a/src/apps/haikudepot/HaikuDepot.rdef b/src/apps/haikudepot/HaikuDepot.rdef index 97be6f0263..5421895d91 100644 --- a/src/apps/haikudepot/HaikuDepot.rdef +++ b/src/apps/haikudepot/HaikuDepot.rdef @@ -8,7 +8,7 @@ resource app_flags B_SINGLE_LAUNCH; resource app_version { major = 0, middle = 0, - minor = 6, + minor = 7, variety = B_APPV_ALPHA, internal = 1, diff --git a/src/apps/haikudepot/HaikuDepotConstants.h b/src/apps/haikudepot/HaikuDepotConstants.h index 539f2c355d..e91c8542cf 100644 --- a/src/apps/haikudepot/HaikuDepotConstants.h +++ b/src/apps/haikudepot/HaikuDepotConstants.h @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021, Andrew Lindesay . + * Copyright 2018-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef HAIKU_DEPOT_CONSTANTS_H @@ -66,6 +66,7 @@ enum BitmapSize { #define HD_CLIENT_TOO_OLD (HD_ERROR_BASE + 2) #define HD_ERR_NOT_MODIFIED (HD_ERROR_BASE + 3) #define HD_ERR_NO_DATA (HD_ERROR_BASE + 4) +#define HD_AUTHENTICATION_FAILED (HD_ERROR_BASE + 5) #define REPOSITORY_NAME_SYSTEM "system" diff --git a/src/apps/haikudepot/Jamfile b/src/apps/haikudepot/Jamfile index 21381ea649..38134b3e8e 100644 --- a/src/apps/haikudepot/Jamfile +++ b/src/apps/haikudepot/Jamfile @@ -113,6 +113,7 @@ local textDocumentSources = local applicationSources = App.cpp + AccessToken.cpp BitmapView.cpp Captcha.cpp CreateUserDetail.cpp @@ -124,6 +125,7 @@ local applicationSources = IconTarPtr.cpp IncrementViewCounterProcess.cpp JobStateListener.cpp + JwtTokenHelper.cpp LanguageModel.cpp LinkView.cpp LinkedBitmapView.cpp diff --git a/src/apps/haikudepot/model/AccessToken.cpp b/src/apps/haikudepot/model/AccessToken.cpp new file mode 100644 index 0000000000..c6eb3aab67 --- /dev/null +++ b/src/apps/haikudepot/model/AccessToken.cpp @@ -0,0 +1,134 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ +#include "AccessToken.h" + +#include "JwtTokenHelper.h" +#include "Logger.h" + +// These are keys that are used to store this object's data into a BMessage instance. + +#define KEY_TOKEN "token" +#define KEY_EXPIRY_TIMESTAMP "expiryTimestamp" + + +AccessToken::AccessToken(BMessage* from) + : + fToken(""), + fExpiryTimestamp(0) +{ + if (from->FindString(KEY_TOKEN, &fToken) != B_OK) { + HDERROR("expected key [%s] in the message data when creating an access" + " token", KEY_TOKEN); + } + + if (from->FindUInt64(KEY_EXPIRY_TIMESTAMP, &fExpiryTimestamp) != B_OK) { + HDERROR("expected key [%s] in the message data when creating an access" + " token", KEY_EXPIRY_TIMESTAMP); + } +} + + +AccessToken::AccessToken() + : + fToken(""), + fExpiryTimestamp(0) +{ +} + + +AccessToken::~AccessToken() +{ +} + + +AccessToken& +AccessToken::operator=(const AccessToken& other) +{ + fToken = other.fToken; + fExpiryTimestamp = other.fExpiryTimestamp; + return *this; +} + + +bool +AccessToken::operator==(const AccessToken& other) const +{ + return fToken == other.fToken; +} + + +bool +AccessToken::operator!=(const AccessToken& other) const +{ + return !(*this == other); +} + + +const BString& +AccessToken::Token() const +{ + return fToken; +} + + +uint64 +AccessToken::ExpiryTimestamp() const +{ + return fExpiryTimestamp; +} + + +void +AccessToken::SetToken(const BString& value) +{ + fToken = value; +} + + +void +AccessToken::SetExpiryTimestamp(uint64 value) +{ + fExpiryTimestamp = value; +} + + +/*! The access token may have a value or may be the empty string. This method + will check that the token appears to be an access token. +*/ + +bool +AccessToken::IsValid() const +{ + return JwtTokenHelper::IsValid(fToken); +} + + +bool +AccessToken::IsValid(uint64 currentTimestamp) const +{ + return IsValid() && (fExpiryTimestamp == 0 || fExpiryTimestamp > currentTimestamp); +} + + +void +AccessToken::Clear() +{ + fToken = ""; + fExpiryTimestamp = 0; +} + + +status_t +AccessToken::Archive(BMessage* into, bool deep) const +{ + status_t result = B_OK; + if (result == B_OK && into == NULL) + result = B_ERROR; + if (result == B_OK) + result = into->AddString(KEY_TOKEN, fToken); + if (result == B_OK) + result = into->AddUInt64(KEY_EXPIRY_TIMESTAMP, fExpiryTimestamp); + return result; +} \ No newline at end of file diff --git a/src/apps/haikudepot/model/AccessToken.h b/src/apps/haikudepot/model/AccessToken.h new file mode 100644 index 0000000000..21c1a128c2 --- /dev/null +++ b/src/apps/haikudepot/model/AccessToken.h @@ -0,0 +1,51 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef ACCESS_TOKEN_H +#define ACCESS_TOKEN_H + + +#include +#include + +class BPositionIO; + +/*! When a user authenticates with the HDS system, the authentication API will + return a JWT access token which can then be later used with other APIs. This + object models the token. The reason why the token is modelled like + this is that the access token is not an opaque string; it contains a number + of key-value pairs that are known as "claims". Some of the claims are used to + detect, for example, when the access token has expired. +*/ + +class AccessToken : public BArchivable { +public: + AccessToken(BMessage* from); + AccessToken(); + virtual ~AccessToken(); + + AccessToken& operator=(const AccessToken& other); + bool operator==(const AccessToken& other) const; + bool operator!=(const AccessToken& other) const; + + const BString& Token() const; + uint64 ExpiryTimestamp() const; + + void SetToken(const BString& value); + void SetExpiryTimestamp(uint64 value); + + bool IsValid() const; + bool IsValid(uint64 currentTimestamp) const; + + void Clear(); + + status_t Archive(BMessage* into, bool deep = true) const; +private: + BString fToken; + uint64 fExpiryTimestamp; + // milliseconds since epoc UTC +}; + + +#endif // ACCESS_TOKEN_H diff --git a/src/apps/haikudepot/model/Model.cpp b/src/apps/haikudepot/model/Model.cpp index 7991fc93b0..8d176b7ae8 100644 --- a/src/apps/haikudepot/model/Model.cpp +++ b/src/apps/haikudepot/model/Model.cpp @@ -1,7 +1,7 @@ /* * Copyright 2013-2014, Stephan Aßmus . * Copyright 2014, Axel Dörfler . - * Copyright 2016-2022, Andrew Lindesay . + * Copyright 2016-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -567,7 +567,7 @@ Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) } status_t status = fWebAppInterface - .RetreiveUserRatingsForPackageForDisplay(packageName, + .RetrieveUserRatingsForPackageForDisplay(packageName, webAppRepositoryCode, webAppRepositorySourceCode, 0, PACKAGE_INFO_MAX_USER_RATINGS, info); if (status == B_OK) { @@ -780,19 +780,19 @@ Model::SetNickname(BString nickname) nickname = ""; } - SetAuthorization(nickname, password, false); + SetCredentials(nickname, password, false); } const BString& -Model::Nickname() const +Model::Nickname() { return fWebAppInterface.Nickname(); } void -Model::SetAuthorization(const BString& nickname, const BString& passwordClear, +Model::SetCredentials(const BString& nickname, const BString& passwordClear, bool storePassword) { BString existingNickname = Nickname(); @@ -820,7 +820,7 @@ Model::SetAuthorization(const BString& nickname, const BString& passwordClear, } BAutolock locker(&fLock); - fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear)); + fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear)); if (nickname != existingNickname) _NotifyAuthorizationChanged(); diff --git a/src/apps/haikudepot/model/Model.h b/src/apps/haikudepot/model/Model.h index 35b88fb95a..e50f344b38 100644 --- a/src/apps/haikudepot/model/Model.h +++ b/src/apps/haikudepot/model/Model.h @@ -1,6 +1,6 @@ /* * Copyright 2013-2014, Stephan Aßmus . - * Copyright 2016-2021, Andrew Lindesay . + * Copyright 2016-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef MODEL_H @@ -148,14 +148,13 @@ public: uint32 flags); void SetNickname(BString nickname); - const BString& Nickname() const; - void SetAuthorization(const BString& nickname, + const BString& Nickname(); + void SetCredentials(const BString& nickname, const BString& passwordClear, bool storePassword); - WebAppInterface& - GetWebAppInterface() - { return fWebAppInterface; } + WebAppInterface* GetWebAppInterface() + { return &fWebAppInterface; } status_t IconTarPath(BPath& path) const; status_t DumpExportReferenceDataPath(BPath& path); diff --git a/src/apps/haikudepot/model/UserCredentials.cpp b/src/apps/haikudepot/model/UserCredentials.cpp index a2def1725a..f42600cd20 100644 --- a/src/apps/haikudepot/model/UserCredentials.cpp +++ b/src/apps/haikudepot/model/UserCredentials.cpp @@ -55,6 +55,31 @@ UserCredentials::~UserCredentials() } +UserCredentials& +UserCredentials::operator=(const UserCredentials& other) +{ + fNickname = other.fNickname; + fPasswordClear = other.fPasswordClear; + fIsSuccessful = other.fIsSuccessful; + return *this; +} + + +bool +UserCredentials::operator==(const UserCredentials& other) const +{ + return fNickname == other.fNickname && fPasswordClear == other.fPasswordClear + && fIsSuccessful == other.fIsSuccessful; +} + + +bool +UserCredentials::operator!=(const UserCredentials& other) const +{ + return !(*this == other); +} + + const BString& UserCredentials::Nickname() const { @@ -104,16 +129,6 @@ UserCredentials::SetIsSuccessful(bool value) } -UserCredentials& -UserCredentials::operator=(const UserCredentials& other) -{ - fNickname = other.fNickname; - fPasswordClear = other.fPasswordClear; - fIsSuccessful = other.fIsSuccessful; - return *this; -} - - status_t UserCredentials::Archive(BMessage* into, bool deep) const { diff --git a/src/apps/haikudepot/model/UserCredentials.h b/src/apps/haikudepot/model/UserCredentials.h index c76c0e3429..06e36d86fa 100644 --- a/src/apps/haikudepot/model/UserCredentials.h +++ b/src/apps/haikudepot/model/UserCredentials.h @@ -25,6 +25,10 @@ public: UserCredentials(); virtual ~UserCredentials(); + UserCredentials& operator=(const UserCredentials& other); + bool operator==(const UserCredentials& other) const; + bool operator!=(const UserCredentials& other) const; + const BString& Nickname() const; const BString& PasswordClear() const; const bool IsSuccessful() const; @@ -34,8 +38,6 @@ public: void SetPasswordClear(const BString& value); void SetIsSuccessful(bool value); - UserCredentials& operator=(const UserCredentials& other); - status_t Archive(BMessage* into, bool deep = true) const; private: BString fNickname; diff --git a/src/apps/haikudepot/server/IncrementViewCounterProcess.cpp b/src/apps/haikudepot/server/IncrementViewCounterProcess.cpp index a6724e47a8..2560a7c8f4 100644 --- a/src/apps/haikudepot/server/IncrementViewCounterProcess.cpp +++ b/src/apps/haikudepot/server/IncrementViewCounterProcess.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022, Andrew Lindesay . + * Copyright 2021-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #include "IncrementViewCounterProcess.h" @@ -81,13 +81,11 @@ IncrementViewCounterProcess::RunInternal() while (attempts > 0 && !WasStopped()) { BMessage resultEnvelope; - WebAppInterface& webAppInterface = fModel->GetWebAppInterface(); - result = webAppInterface.IncrementViewCounter(fPackage, depot, - resultEnvelope); + WebAppInterface* webAppInterface = fModel->GetWebAppInterface(); + result = webAppInterface->IncrementViewCounter(fPackage, depot, resultEnvelope); if (result == B_OK) { - int32 errorCode = webAppInterface.ErrorCodeFromResponse( - resultEnvelope); + int32 errorCode = WebAppInterface::ErrorCodeFromResponse(resultEnvelope); switch (errorCode) { case ERROR_CODE_NONE: HDINFO("did increment the view counter for [%s]", diff --git a/src/apps/haikudepot/server/JwtTokenHelper.cpp b/src/apps/haikudepot/server/JwtTokenHelper.cpp new file mode 100644 index 0000000000..ea19c04554 --- /dev/null +++ b/src/apps/haikudepot/server/JwtTokenHelper.cpp @@ -0,0 +1,78 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + + +#include "JwtTokenHelper.h" + +#include "DataIOUtils.h" +#include "Json.h" +#include "JsonMessageWriter.h" + +#include + + +/*static*/ bool +JwtTokenHelper::IsValid(const BString& value) +{ + int countDots = 0; + + for (int i = 0; i < value.Length(); i++) { + char ch = value[i]; + + if ('.' == ch) + countDots++; + else { + if (!_IsBase64(ch)) + return false; + } + } + + return 2 == countDots; +} + + +/*! A JWT token is split into three parts separated by a '.' character. The + middle section is a base-64 encoded string and within the string is JSON + structured data. The JSON data contains key-value pairs which carry data + about the token. This method will take a JWT token, will find the middle + section and will parse the claims into the supplied 'message' parameter. +*/ + +/*static*/ status_t +JwtTokenHelper::ParseClaims(const BString& token, BMessage& message) +{ + int firstDot = token.FindFirst('.'); + + if (firstDot == B_ERROR) + return B_BAD_VALUE; + + // find the end of the first section by looking for the next dot. + + int secondDot = token.FindFirst('.', firstDot + 1); + + if (secondDot == B_ERROR) + return B_BAD_VALUE; + + BMemoryIO memoryIo(&(token.String())[firstDot + 1], (secondDot - firstDot) - 1); + Base64DecodingDataIO base64DecodingIo(&memoryIo, '-', '_'); + BJsonMessageWriter writer(message); + BJson::Parse(&base64DecodingIo, &writer); + + return writer.ErrorStatus(); +} + + +/*! Note this is base64 "url" standard that disallows "/" and "+" and instead + uses "-" and "_". +*/ + +/*static*/ bool +JwtTokenHelper::_IsBase64(char ch) +{ + return isalnum(ch) + || '=' == ch + || '-' == ch + || '_' == ch; +} diff --git a/src/apps/haikudepot/server/JwtTokenHelper.h b/src/apps/haikudepot/server/JwtTokenHelper.h new file mode 100644 index 0000000000..12c82159c8 --- /dev/null +++ b/src/apps/haikudepot/server/JwtTokenHelper.h @@ -0,0 +1,37 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef JWT_TOKEN_HELPER_H +#define JWT_TOKEN_HELPER_H + + +#include + + +class BMessage; + + +/*! The term "JWT" is short for "Java Web Token" and is a token format typically used to convey + proof of an authentication. The token is structured and contains three parts separated by the + full-stop character ".". The first part is a header, the middle part contains some data (which + is termed the "claims") and the last part is a signature proving the authenticity of the claims. + + The claims are base-64 encoded JSON and the JSON data typically conveys some key-value pairs + with a number of the key names being well-known through standards. + + This class contains a number of helper methods for working with JWT tokens. + */ + + class JwtTokenHelper { + public: + static bool IsValid(const BString& value); + + static status_t ParseClaims(const BString& token, + BMessage& message); + + private: + static bool _IsBase64(char ch); + }; + + #endif // JWT_TOKEN_HELPER_H diff --git a/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp b/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp index b7195cb4a6..3965abd3b5 100644 --- a/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp +++ b/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp @@ -99,18 +99,18 @@ UserDetailVerifierProcess::_ShouldVerify() status_t UserDetailVerifierProcess::_TryFetchUserDetail(UserDetail& userDetail) { - WebAppInterface interface = fModel->GetWebAppInterface(); + WebAppInterface* interface = fModel->GetWebAppInterface(); BMessage userDetailResponse; status_t result; - result = interface.RetrieveCurrentUserDetail(userDetailResponse); + result = interface->RetrieveCurrentUserDetail(userDetailResponse); if (result != B_OK) { HDERROR("a problem has arisen retrieving the current user detail: %s", strerror(result)); } if (result == B_OK) { - int32 errorCode = interface.ErrorCodeFromResponse(userDetailResponse); + int32 errorCode = WebAppInterface::ErrorCodeFromResponse(userDetailResponse); switch (errorCode) { case ERROR_CODE_NONE: break; @@ -132,7 +132,7 @@ UserDetailVerifierProcess::_TryFetchUserDetail(UserDetail& userDetail) // worked, it is now necessary to check to see that the user has agreed // to the most recent user-usage conditions. - result = interface.UnpackUserDetail(userDetailResponse, userDetail); + result = interface->UnpackUserDetail(userDetailResponse, userDetail); if (result != B_OK) HDERROR("it was not possible to unpack the user details."); } diff --git a/src/apps/haikudepot/server/WebAppInterface.cpp b/src/apps/haikudepot/server/WebAppInterface.cpp index 8bdfb5b232..86a148fb32 100644 --- a/src/apps/haikudepot/server/WebAppInterface.cpp +++ b/src/apps/haikudepot/server/WebAppInterface.cpp @@ -6,21 +6,24 @@ #include "WebAppInterface.h" -#include #include +#include +#include + +#include +#include #include #include #include #include #include -#include -#include #include #include #include #include "DataIOUtils.h" #include "HaikuDepotConstants.h" +#include "JwtTokenHelper.h" #include "Logger.h" #include "ServerSettings.h" #include "ServerHelper.h" @@ -103,10 +106,6 @@ make_http_request(const BUrl& url, BDataIO* output, } -int -WebAppInterface::fRequestIndex = 0; - - enum { NEEDS_AUTHORIZATION = 1 << 0, }; @@ -117,38 +116,26 @@ WebAppInterface::WebAppInterface() } -WebAppInterface::WebAppInterface(const WebAppInterface& other) - : - fCredentials(other.fCredentials) -{ -} - - WebAppInterface::~WebAppInterface() { } -WebAppInterface& -WebAppInterface::operator=(const WebAppInterface& other) -{ - if (this == &other) - return *this; - fCredentials = other.fCredentials; - return *this; -} - - void -WebAppInterface::SetAuthorization(const UserCredentials& value) +WebAppInterface::SetCredentials(const UserCredentials& value) { - fCredentials = value; + AutoLocker lock(&fLock); + if (fCredentials != value) { + fCredentials = value; + fAccessToken.Clear(); + } } const BString& -WebAppInterface::Nickname() const +WebAppInterface::Nickname() { + AutoLocker lock(&fLock); return fCredentials.Nickname(); } @@ -172,7 +159,7 @@ WebAppInterface::GetChangelog(const BString& packageName, BMessage& message) status_t -WebAppInterface::RetreiveUserRatingsForPackageForDisplay( +WebAppInterface::RetrieveUserRatingsForPackageForDisplay( const BString& packageName, const BString& webAppRepositoryCode, const BString& webAppRepositorySourceCode, @@ -209,7 +196,7 @@ WebAppInterface::RetreiveUserRatingsForPackageForDisplay( status_t -WebAppInterface::RetreiveUserRatingForPackageAndVersionByUser( +WebAppInterface::RetrieveUserRatingForPackageAndVersionByUser( const BString& packageName, const BPackageVersion& version, const BString& architecture, const BString& webAppRepositoryCode, @@ -283,19 +270,40 @@ WebAppInterface::RetrieveUserDetailForCredentials( "to obtain the user detail"); } - // BHttpRequest later takes ownership of this. - BMallocIO* requestEnvelopeData = new BMallocIO(); - BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); + status_t result = B_OK; - requestEnvelopeWriter.WriteObjectStart(); - requestEnvelopeWriter.WriteObjectName("nickname"); - requestEnvelopeWriter.WriteString(credentials.Nickname().String()); - requestEnvelopeWriter.WriteObjectEnd(); + // authenticate the user and obtain a token to use with the latter + // request. - status_t result = _SendJsonRequest("user/get-user", credentials, - requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), - NEEDS_AUTHORIZATION, message); - // note that the credentials used here are passed in as args. + BMessage authenticateResponseEnvelopeMessage; + + if (result == B_OK) { + result = AuthenticateUser( + credentials.Nickname(), + credentials.PasswordClear(), + authenticateResponseEnvelopeMessage); + } + + AccessToken accessToken; + + if (result == B_OK) + result = UnpackAccessToken(authenticateResponseEnvelopeMessage, accessToken); + + if (result == B_OK) { + // BHttpRequest later takes ownership of this. + BMallocIO* requestEnvelopeData = new BMallocIO(); + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); + + requestEnvelopeWriter.WriteObjectStart(); + requestEnvelopeWriter.WriteObjectName("nickname"); + requestEnvelopeWriter.WriteString(credentials.Nickname().String()); + requestEnvelopeWriter.WriteObjectEnd(); + + result = _SendJsonRequest("user/get-user", accessToken, + requestEnvelopeData, _LengthAndSeekToZero(requestEnvelopeData), + NEEDS_AUTHORIZATION, message); + // note that the credentials used here are passed in as args. + } return result; } @@ -308,7 +316,8 @@ WebAppInterface::RetrieveUserDetailForCredentials( status_t WebAppInterface::RetrieveCurrentUserDetail(BMessage& message) { - return RetrieveUserDetailForCredentials(fCredentials, message); + UserCredentials credentials = _Credentials(); + return RetrieveUserDetailForCredentials(credentials, message); } @@ -366,6 +375,66 @@ WebAppInterface::UnpackUserDetail(BMessage& responseEnvelopeMessage, } +/*! When an authentication API call is made, the response (if successful) will + return an access token in the response. This method will take the response + from the server and will parse out the access token data into the supplied + object. +*/ + +/*static*/ status_t +WebAppInterface::UnpackAccessToken(BMessage& responseEnvelopeMessage, + AccessToken& accessToken) +{ + status_t result; + + BMessage resultMessage; + result = responseEnvelopeMessage.FindMessage( + "result", &resultMessage); + + if (result != B_OK) { + HDERROR("bad response envelope missing 'result' entry"); + return result; + } + + BString token; + result = resultMessage.FindString("token", &token); + + if (result != B_OK || token.IsEmpty()) { + HDINFO("failure to authenticate"); + return HD_AUTHENTICATION_FAILED; + } + + // The token should be present in three parts; the header, the claims and + // then a digital signature. The logic here wants to extract some data + // from the claims part. + + BMessage claimsMessage; + result = JwtTokenHelper::ParseClaims(token, claimsMessage); + + if (Logger::IsTraceEnabled()) { + HDTRACE("start; token claims..."); + claimsMessage.PrintToStream(); + HDTRACE("...end; token claims"); + } + + if (B_OK == result) { + accessToken.SetToken(token); + accessToken.SetExpiryTimestamp(0); + + double expiryTimestampDouble; + + // The claims should have parsed but it could transpire that there is + // no expiry. This should not be the case, but it is theoretically + // possible. + + if (claimsMessage.FindDouble("exp", &expiryTimestampDouble) == B_OK) + accessToken.SetExpiryTimestamp(1000 * static_cast(expiryTimestampDouble)); + } + + return result; +} + + /*! \brief Returns data relating to the user usage conditions \param code defines the version of the data to return or if empty then the @@ -432,7 +501,7 @@ WebAppInterface::AgreeUserUsageConditions(const BString& code, requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode"); requestEnvelopeWriter.WriteString(code.String()); requestEnvelopeWriter.WriteObjectName("nickname"); - requestEnvelopeWriter.WriteString(fCredentials.Nickname()); + requestEnvelopeWriter.WriteString(Nickname()); requestEnvelopeWriter.WriteObjectEnd(); // now fetch this information into an object. @@ -504,7 +573,7 @@ WebAppInterface::CreateUserRating(const BString& packageName, requestEnvelopeWriter.WriteObjectName("pkgVersionType"); requestEnvelopeWriter.WriteString("SPECIFIC"); requestEnvelopeWriter.WriteObjectName("userNickname"); - requestEnvelopeWriter.WriteString(fCredentials.Nickname()); + requestEnvelopeWriter.WriteString(Nickname()); if (!version.Major().IsEmpty()) { requestEnvelopeWriter.WriteObjectName("pkgVersionMajor"); @@ -668,6 +737,47 @@ WebAppInterface::CreateUser(const BString& nickName, } +/*! This method will authenticate the user set in the credentials and will + retain the resultant access token for authenticating any latter API calls. +*/ + +status_t +WebAppInterface::AuthenticateUserRetainingAccessToken() +{ + UserCredentials userCredentials = _Credentials(); + + if (!userCredentials.IsValid()) { + HDINFO("unable to get a new access token as there are no credentials"); + return B_NOT_INITIALIZED; + } + + return _AuthenticateUserRetainingAccessToken(userCredentials.Nickname(), + userCredentials.PasswordClear()); +} + + +status_t +WebAppInterface::_AuthenticateUserRetainingAccessToken(const BString& nickName, + const BString& passwordClear) { + AutoLocker lock(&fLock); + + fAccessToken.Clear(); + + BMessage responseEnvelopeMessage; + status_t result = AuthenticateUser(nickName, passwordClear, responseEnvelopeMessage); + + AccessToken accessToken; + + if (result == B_OK) + result = UnpackAccessToken(responseEnvelopeMessage, accessToken); + + if (result == B_OK) + fAccessToken = accessToken; + + return result; +} + + status_t WebAppInterface::AuthenticateUser(const BString& nickName, const BString& passwordClear, BMessage& message) @@ -800,7 +910,7 @@ WebAppInterface::_RetrievePasswordRequirementsMeta(BMessage& message) not look like an error. */ -int32 +/*static*/ int32 WebAppInterface::ErrorCodeFromResponse(BMessage& responseEnvelopeMessage) { BMessage error; @@ -821,17 +931,23 @@ WebAppInterface::ErrorCodeFromResponse(BMessage& responseEnvelopeMessage) status_t WebAppInterface::_SendJsonRequest(const char* urlPathComponents, BPositionIO* requestData, size_t requestDataSize, uint32 flags, - BMessage& reply) const + BMessage& reply) { - return _SendJsonRequest(urlPathComponents, fCredentials, requestData, + bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0; + AccessToken accessToken; + + if (needsAuthorization) + accessToken = _ObtainValidAccessToken(); + + return _SendJsonRequest(urlPathComponents, accessToken, requestData, requestDataSize, flags, reply); } -status_t +/*static*/ status_t WebAppInterface::_SendJsonRequest(const char* urlPathComponents, - UserCredentials credentials, BPositionIO* requestData, - size_t requestDataSize, uint32 flags, BMessage& reply) const + const AccessToken& accessToken, BPositionIO* requestData, + size_t requestDataSize, uint32 flags, BMessage& reply) { if (requestDataSize == 0) { HDINFO("%s; empty request payload", PROTOCOL_NAME); @@ -852,6 +968,15 @@ WebAppInterface::_SendJsonRequest(const char* urlPathComponents, return HD_CLIENT_TOO_OLD; } + bool needsAuthorization = (flags & NEEDS_AUTHORIZATION) != 0; + + if (needsAuthorization && !accessToken.IsValid()) { + HDDEBUG("%s; dropping request to ...[%s] as access token is not valid", + PROTOCOL_NAME, urlPathComponents); + delete requestData; + return B_NOT_ALLOWED; + } + BUrl url = ServerSettings::CreateFullUrl(BString("/__api/v2/") << urlPathComponents); HDDEBUG("%s; will make request to [%s]", PROTOCOL_NAME, @@ -883,13 +1008,10 @@ WebAppInterface::_SendJsonRequest(const char* urlPathComponents, request->SetMethod(B_HTTP_POST); request->SetHeaders(headers); - // Authentication via Basic Authentication - // The other way would be to obtain a token and then use the Token Bearer - // header. - if (((flags & NEEDS_AUTHORIZATION) != 0) && credentials.IsValid()) { - BHttpAuthentication authentication(credentials.Nickname(), - credentials.PasswordClear()); - authentication.SetMethod(B_HTTP_AUTHENTICATION_BASIC); + if (needsAuthorization) { + BHttpAuthentication authentication; + authentication.SetMethod(B_HTTP_AUTHENTICATION_BEARER); + authentication.SetToken(accessToken.Token()); context.AddAuthentication(url, authentication); } @@ -948,7 +1070,7 @@ WebAppInterface::_SendJsonRequest(const char* urlPathComponents, status_t WebAppInterface::_SendJsonRequest(const char* urlPathComponents, - const BString& jsonString, uint32 flags, BMessage& reply) const + const BString& jsonString, uint32 flags, BMessage& reply) { // gets 'adopted' by the subsequent http request. BMemoryIO* data = new BMemoryIO(jsonString.String(), @@ -1042,3 +1164,32 @@ WebAppInterface::_LengthAndSeekToZero(BPositionIO* data) data->Seek(0, SEEK_SET); return dataSize; } + + +UserCredentials +WebAppInterface::_Credentials() +{ + AutoLocker lock(&fLock); + return fCredentials; +} + + +AccessToken +WebAppInterface::_ObtainValidAccessToken() +{ + AutoLocker lock(&fLock); + + uint64 now = static_cast(time(NULL)) * 1000; + + if (!fAccessToken.IsValid(now)) { + HDINFO("clearing cached access token as it is no longer valid"); + fAccessToken.Clear(); + } + + if (!fAccessToken.IsValid()) { + HDINFO("no cached access token present; will obtain a new one"); + AuthenticateUserRetainingAccessToken(); + } + + return fAccessToken; +} diff --git a/src/apps/haikudepot/server/WebAppInterface.h b/src/apps/haikudepot/server/WebAppInterface.h index e97148966d..da5942bc68 100644 --- a/src/apps/haikudepot/server/WebAppInterface.h +++ b/src/apps/haikudepot/server/WebAppInterface.h @@ -9,9 +9,11 @@ #include #include +#include #include #include +#include "AccessToken.h" #include "PackageInfo.h" #include "PasswordRequirements.h" #include "UserCredentials.h" @@ -43,26 +45,23 @@ using BPackageKit::BPackageVersion; class WebAppInterface { public: WebAppInterface(); - WebAppInterface(const WebAppInterface& other); virtual ~WebAppInterface(); - WebAppInterface& operator=(const WebAppInterface& other); - - void SetAuthorization(const UserCredentials& value); - const BString& Nickname() const; + void SetCredentials(const UserCredentials& value); + const BString& Nickname(); status_t GetChangelog( const BString& packageName, BMessage& message); - status_t RetreiveUserRatingsForPackageForDisplay( + status_t RetrieveUserRatingsForPackageForDisplay( const BString& packageName, const BString& webAppRepositoryCode, const BString& webAppRepositorySourceCode, int resultOffset, int maxResults, BMessage& message); - status_t RetreiveUserRatingForPackageAndVersionByUser( + status_t RetrieveUserRatingForPackageAndVersionByUser( const BString& packageName, const BPackageVersion& version, const BString& architecture, @@ -121,6 +120,8 @@ public: const BString& userUsageConditionsCode, BMessage& message); + status_t AuthenticateUserRetainingAccessToken(); + status_t AuthenticateUser(const BString& nickName, const BString& passwordClear, BMessage& message); @@ -139,7 +140,17 @@ public: static status_t UnpackUserDetail( BMessage& responseEnvelopeMessage, UserDetail& userDetail); + + static status_t UnpackAccessToken( + BMessage& responseEnvelopeMessage, + AccessToken& accessToken); private: + UserCredentials _Credentials(); + + AccessToken _ObtainValidAccessToken(); + + status_t _AuthenticateUserRetainingAccessToken(const BString& nickName, + const BString& passwordClear); status_t _RetrievePasswordRequirementsMeta( BMessage& message); @@ -151,16 +162,16 @@ private: status_t _SendJsonRequest(const char* urlPathComponents, const BString& jsonString, uint32 flags, - BMessage& reply) const; - status_t _SendJsonRequest(const char* urlPathComponents, - UserCredentials credentials, - BPositionIO* requestData, - size_t requestDataSize, uint32 flags, - BMessage& reply) const; + BMessage& reply); status_t _SendJsonRequest(const char* urlPathComponents, BPositionIO* requestData, size_t requestDataSize, uint32 flags, - BMessage& reply) const; + BMessage& reply); + static status_t _SendJsonRequest(const char* urlPathComponents, + const AccessToken& accessToken, + BPositionIO* requestData, + size_t requestDataSize, uint32 flags, + BMessage& reply); status_t _SendRawGetRequest( const BString urlPathComponents, @@ -171,7 +182,8 @@ private: private: UserCredentials fCredentials; - static int fRequestIndex; + AccessToken fAccessToken; + BLocker fLock; }; diff --git a/src/apps/haikudepot/ui/MainWindow.cpp b/src/apps/haikudepot/ui/MainWindow.cpp index 1d37c34e37..b93403dd39 100644 --- a/src/apps/haikudepot/ui/MainWindow.cpp +++ b/src/apps/haikudepot/ui/MainWindow.cpp @@ -3,7 +3,7 @@ * Copyright 2013-2014, Stephan Aßmus . * Copyright 2013, Rene Gollent, rene@gollent.com. * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. - * Copyright 2016-2022, Andrew Lindesay . + * Copyright 2016-2023, Andrew Lindesay . * Copyright 2017, Julian Harnath . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -653,7 +653,7 @@ main_window_str_to_package_list_view_mode(const BString& str) void -MainWindow::StoreSettings(BMessage& settings) const +MainWindow::StoreSettings(BMessage& settings) { settings.AddRect(_WindowFrameName(), Frame()); if (!fSinglePackageMode) { diff --git a/src/apps/haikudepot/ui/MainWindow.h b/src/apps/haikudepot/ui/MainWindow.h index a59380b66e..449e8d6fd3 100644 --- a/src/apps/haikudepot/ui/MainWindow.h +++ b/src/apps/haikudepot/ui/MainWindow.h @@ -2,7 +2,7 @@ * Copyright 2013-2014, Stephan Aßmus . * Copyright 2013, Rene Gollent . * Copyright 2017, Julian Harnath . - * Copyright 2017-2022, Andrew Lindesay . + * Copyright 2017-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef MAIN_WINDOW_H @@ -48,7 +48,7 @@ public: virtual bool QuitRequested(); virtual void MessageReceived(BMessage* message); - void StoreSettings(BMessage& message) const; + void StoreSettings(BMessage& message); // ProcessCoordinatorConsumer virtual void Consume(ProcessCoordinator *item); diff --git a/src/apps/haikudepot/ui/RatePackageWindow.cpp b/src/apps/haikudepot/ui/RatePackageWindow.cpp index 3941caf42b..f1b1211512 100644 --- a/src/apps/haikudepot/ui/RatePackageWindow.cpp +++ b/src/apps/haikudepot/ui/RatePackageWindow.cpp @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2016-2022, Andrew Lindesay . + * Copyright 2016-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -544,7 +544,8 @@ RatePackageWindow::_QueryRatingThread() return; } - WebAppInterface interface; + WebAppInterface* interface = fModel.GetWebAppInterface(); + BMessage info; const DepotInfo* depot = fModel.DepotForName(package->DepotName()); BString webAppRepositoryCode; @@ -561,16 +562,15 @@ RatePackageWindow::_QueryRatingThread() "code for depot; %s", package->DepotName().String()); BMessenger(this).SendMessage(B_QUIT_REQUESTED); } else { - status_t status = interface - .RetreiveUserRatingForPackageAndVersionByUser(package->Name(), - package->Version(), package->Architecture(), + status_t status = interface->RetrieveUserRatingForPackageAndVersionByUser( + package->Name(), package->Version(), package->Architecture(), webAppRepositoryCode, webAppRepositorySourceCode, nickname, info); if (status == B_OK) { // could be an error or could be a valid response envelope // containing data. - switch (interface.ErrorCodeFromResponse(info)) { + switch (WebAppInterface::ErrorCodeFromResponse(info)) { case ERROR_CODE_NONE: { //info.PrintToStream(); @@ -647,7 +647,7 @@ RatePackageWindow::_SendRatingThread() webAppRepositorySourceCode = depot->WebAppRepositorySourceCode(); } - WebAppInterface interface = fModel.GetWebAppInterface(); + WebAppInterface* interface = fModel.GetWebAppInterface(); Unlock(); @@ -672,11 +672,11 @@ RatePackageWindow::_SendRatingThread() BMessage info; if (ratingID.Length() > 0) { HDINFO("will update the existing user rating [%s]", ratingID.String()); - status = interface.UpdateUserRating(ratingID, + status = interface->UpdateUserRating(ratingID, languageCode, comment, stability, rating, active, info); } else { HDINFO("will create a new user rating for pkg [%s]", package.String()); - status = interface.CreateUserRating(package, fPackage->Version(), + status = interface->CreateUserRating(package, fPackage->Version(), architecture, webAppRepositoryCode, webAppRepositorySourceCode, languageCode, comment, stability, rating, info); } @@ -684,7 +684,7 @@ RatePackageWindow::_SendRatingThread() if (status == B_OK) { // could be an error or could be a valid response envelope // containing data. - switch (interface.ErrorCodeFromResponse(info)) { + switch (WebAppInterface::ErrorCodeFromResponse(info)) { case ERROR_CODE_NONE: { if (ratingID.Length() > 0) diff --git a/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp b/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp index d9ec72972e..0429027a6e 100644 --- a/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp +++ b/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp @@ -293,9 +293,9 @@ void ToLatestUserUsageConditionsWindow::_FetchDataPerform() { UserUsageConditions conditions; - WebAppInterface interface = fModel.GetWebAppInterface(); + WebAppInterface* interface = fModel.GetWebAppInterface(); - if (interface.RetrieveUserUsageConditions("", conditions) == B_OK) { + if (interface->RetrieveUserUsageConditions("", conditions) == B_OK) { BMessage userUsageConditionsMessage; conditions.Archive(&userUsageConditionsMessage, true); BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA); @@ -362,16 +362,15 @@ ToLatestUserUsageConditionsWindow::_AgreePerform() { BMessenger messenger(this); BMessage responsePayload; - WebAppInterface webApp = fModel.GetWebAppInterface(); - status_t result = webApp.AgreeUserUsageConditions( + WebAppInterface* webApp = fModel.GetWebAppInterface(); + status_t result = webApp->AgreeUserUsageConditions( fUserUsageConditions.Code(), responsePayload); if (result != B_OK) { ServerHelper::NotifyTransportError(result); messenger.SendMessage(MSG_AGREE_FAILED); } else { - int32 errorCode = WebAppInterface::ErrorCodeFromResponse( - responsePayload); + int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload); if (errorCode == ERROR_CODE_NONE) { AppUtils::NotifySimpleError( B_TRANSLATE("Usage conditions agreed"), diff --git a/src/apps/haikudepot/ui/UserLoginWindow.cpp b/src/apps/haikudepot/ui/UserLoginWindow.cpp index 2e185cf10d..76f864c6e1 100644 --- a/src/apps/haikudepot/ui/UserLoginWindow.cpp +++ b/src/apps/haikudepot/ui/UserLoginWindow.cpp @@ -518,14 +518,14 @@ void UserLoginWindow::_AuthenticateThread(UserCredentials& userCredentials) { BMessage responsePayload; - WebAppInterface interface = fModel.GetWebAppInterface(); - status_t status = interface.AuthenticateUser( + WebAppInterface* interface = fModel.GetWebAppInterface(); + status_t status = interface->AuthenticateUser( userCredentials.Nickname(), userCredentials.PasswordClear(), responsePayload); BString token; if (status == B_OK) { - int32 errorCode = interface.ErrorCodeFromResponse(responsePayload); + int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload); if (errorCode == ERROR_CODE_NONE) _UnpackAuthenticationToken(responsePayload, token); @@ -648,7 +648,7 @@ UserLoginWindow::_TakeUpCredentialsAndQuit(const UserCredentials& credentials) { { AutoLocker locker(fModel.Lock()); - fModel.SetAuthorization(credentials.Nickname(), + fModel.SetCredentials(credentials.Nickname(), credentials.PasswordClear(), true); } @@ -815,9 +815,8 @@ status_t UserLoginWindow::_CreateAccountUserUsageConditionsSetupThread( UserUsageConditions& userUsageConditions) { - WebAppInterface interface = fModel.GetWebAppInterface(); - status_t result = interface.RetrieveUserUsageConditions( - NULL, userUsageConditions); + WebAppInterface* interface = fModel.GetWebAppInterface(); + status_t result = interface->RetrieveUserUsageConditions(NULL, userUsageConditions); if (result != B_OK) { AppUtils::NotifySimpleError( @@ -836,9 +835,8 @@ status_t UserLoginWindow::_CreateAccountPasswordRequirementsSetupThread( PasswordRequirements& passwordRequirements) { - WebAppInterface interface = fModel.GetWebAppInterface(); - status_t result = interface.RetrievePasswordRequirements( - passwordRequirements); + WebAppInterface* interface = fModel.GetWebAppInterface(); + status_t result = interface->RetrievePasswordRequirements(passwordRequirements); if (result != B_OK) { AppUtils::NotifySimpleError( @@ -856,10 +854,10 @@ UserLoginWindow::_CreateAccountPasswordRequirementsSetupThread( status_t UserLoginWindow::_CreateAccountCaptchaSetupThread(Captcha& captcha) { - WebAppInterface interface = fModel.GetWebAppInterface(); + WebAppInterface* interface = fModel.GetWebAppInterface(); BMessage responsePayload; - status_t status = interface.RequestCaptcha(responsePayload); + status_t status = interface->RequestCaptcha(responsePayload); // check for transport related errors. @@ -873,7 +871,7 @@ UserLoginWindow::_CreateAccountCaptchaSetupThread(Captcha& captcha) // check for server-generated errors. if (status == B_OK) { - if (interface.ErrorCodeFromResponse(responsePayload) + if (WebAppInterface::ErrorCodeFromResponse(responsePayload) != ERROR_CODE_NONE) { ServerHelper::AlertTransportError(&responsePayload); status = B_ERROR; @@ -1307,11 +1305,11 @@ UserLoginWindow::_CreateAccountThreadEntry(void* data) void UserLoginWindow::_CreateAccountThread(CreateUserDetail* detail) { - WebAppInterface interface = fModel.GetWebAppInterface(); + WebAppInterface* interface = fModel.GetWebAppInterface(); BMessage responsePayload; BMessenger messenger(this); - status_t status = interface.CreateUser( + status_t status = interface->CreateUser( detail->Nickname(), detail->PasswordClear(), detail->Email(), @@ -1325,7 +1323,7 @@ UserLoginWindow::_CreateAccountThread(CreateUserDetail* detail) "There was a puzzling response from the web service."); if (status == B_OK) { - int32 errorCode = interface.ErrorCodeFromResponse(responsePayload); + int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload); switch (errorCode) { case ERROR_CODE_NONE: diff --git a/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp b/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp index f8272ca995..41a68f8946 100644 --- a/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp +++ b/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp @@ -270,7 +270,7 @@ UserUsageConditionsWindow::_FetchDataPerform() { UserDetail userDetail; UserUsageConditions conditions; - WebAppInterface interface = fModel.GetWebAppInterface(); + WebAppInterface* interface = fModel.GetWebAppInterface(); BString code; status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code); @@ -291,7 +291,7 @@ UserUsageConditionsWindow::_FetchDataPerform() } if (status == B_OK) { - if (interface.RetrieveUserUsageConditions(code, conditions) == B_OK) { + if (interface->RetrieveUserUsageConditions(code, conditions) == B_OK) { BMessage userUsageConditionsMessage; BMessage userDetailMessage; conditions.Archive(&userUsageConditionsMessage, true); @@ -334,20 +334,19 @@ status_t UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform( UserDetail& userDetail, BString& code) { - WebAppInterface interface = fModel.GetWebAppInterface(); + WebAppInterface* interface = fModel.GetWebAppInterface(); - if (interface.Nickname().IsEmpty()) + if (interface->Nickname().IsEmpty()) debugger("attempt to get user details for the current user, but" " there is no current user"); BMessage responseEnvelopeMessage; - status_t result = interface.RetrieveCurrentUserDetail( - responseEnvelopeMessage); + status_t result = interface->RetrieveCurrentUserDetail(responseEnvelopeMessage); if (result == B_OK) { // could be an error or could be a valid response envelope // containing data. - switch (interface.ErrorCodeFromResponse(responseEnvelopeMessage)) { + switch (WebAppInterface::ErrorCodeFromResponse(responseEnvelopeMessage)) { case ERROR_CODE_NONE: result = WebAppInterface::UnpackUserDetail( responseEnvelopeMessage, userDetail); @@ -368,12 +367,12 @@ UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform( if (result == B_OK) { BString userUsageConditionsCode = userDetail.Agreement().Code(); HDDEBUG("the user [%s] has agreed to uuc [%s]", - interface.Nickname().String(), + interface->Nickname().String(), userUsageConditionsCode.String()); code.SetTo(userUsageConditionsCode); } else { HDDEBUG("unable to get details of the user [%s]", - interface.Nickname().String()); + interface->Nickname().String()); } return result; diff --git a/src/apps/haikudepot/util/DataIOUtils.cpp b/src/apps/haikudepot/util/DataIOUtils.cpp index 06e36d6724..b603c21494 100644 --- a/src/apps/haikudepot/util/DataIOUtils.cpp +++ b/src/apps/haikudepot/util/DataIOUtils.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020, Andrew Lindesay . + * Copyright 2018-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #include "DataIOUtils.h" @@ -31,6 +31,8 @@ DataIOUtils::CopyAll(BDataIO* target, BDataIO* source) } +// #pragma mark - ConstraintedDataIO + ConstraintedDataIO::ConstraintedDataIO(BDataIO* delegate, size_t limit) : fDelegate(delegate), @@ -71,3 +73,120 @@ ConstraintedDataIO::Flush() { return B_OK; } + + +// #pragma mark - Base64DecodingDataIO + +Base64DecodingDataIO::Base64DecodingDataIO(BDataIO* delegate, char char62, char char63) + : + fDelegate(delegate), + fChar62(char62), + fChar63(char63), + fNextByteAssembly(0), + fNextByteAssemblyBits(0) +{ +} + + +Base64DecodingDataIO::~Base64DecodingDataIO() +{ +} + + +status_t +Base64DecodingDataIO::_CharToInt(uint8 ch, uint8* value) +{ + if (ch >= 0x41 && ch <= 0x5A) { + *value = (ch - 0x41); + return B_OK; + } + + if (ch >= 0x61 && ch <= 0x7a) { + *value = (ch - 0x61) + 26; + return B_OK; + } + + if (ch >= 0x30 && ch <= 0x39) { + *value = (ch - 0x30) + 52; + return B_OK; + } + + if (ch == fChar62) { + *value = 62; + return B_OK; + } + + if (ch == fChar63) { + *value = 63; + return B_OK; + } + + if (ch == '=') { + *value = 0; + return B_OK; + } + + return B_BAD_DATA; +} + + +status_t +Base64DecodingDataIO::_ReadSingleByte(void* buffer) +{ + uint8 delegateRead; + uint8 delegateReadInt; + status_t result = B_OK; + + if (result == B_OK) + result = fDelegate->ReadExactly(&delegateRead, 1); + + if (result == B_OK) + result = _CharToInt(delegateRead, &delegateReadInt); + + if (result == B_OK && 0 == fNextByteAssemblyBits) { + fNextByteAssembly = delegateReadInt; + fNextByteAssemblyBits = 6; + return _ReadSingleByte(buffer); + } + + if (result == B_OK) { + uint8 followingNextByteAssemblyBits = (6 - (8 - fNextByteAssemblyBits)); + *((uint8 *) buffer) = fNextByteAssembly << (8 - fNextByteAssemblyBits) + | (delegateReadInt >> followingNextByteAssemblyBits); + fNextByteAssembly = delegateReadInt & (0x3f >> (6 - followingNextByteAssemblyBits)); + fNextByteAssemblyBits = followingNextByteAssemblyBits; + } + + return result; +} + + +ssize_t +Base64DecodingDataIO::Read(void* buffer, size_t size) +{ + size_t readSize = 0; + status_t result = B_OK; + + while (result == B_OK && readSize < size) { + result = _ReadSingleByte(&((uint8_t*) buffer)[readSize]); + + if (result == B_OK) + readSize++; + } + + return readSize++; +} + + +ssize_t +Base64DecodingDataIO::Write(const void* buffer, size_t size) +{ + return B_NOT_SUPPORTED; +} + + +status_t +Base64DecodingDataIO::Flush() +{ + return B_OK; +} \ No newline at end of file diff --git a/src/apps/haikudepot/util/DataIOUtils.h b/src/apps/haikudepot/util/DataIOUtils.h index 887ee2b070..00d4d48339 100644 --- a/src/apps/haikudepot/util/DataIOUtils.h +++ b/src/apps/haikudepot/util/DataIOUtils.h @@ -1,5 +1,5 @@ /* - * Copyright 2018-2020, Andrew Lindesay . + * Copyright 2018-2023, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef DATA_IO_UTILS_H @@ -40,4 +40,29 @@ private: }; +class Base64DecodingDataIO : public BDataIO { +public: + Base64DecodingDataIO(BDataIO* delegate, + char char62 = '+', char char63 = '/'); + virtual ~Base64DecodingDataIO(); + + virtual ssize_t Read(void* buffer, size_t size); + virtual ssize_t Write(const void* buffer, size_t size); + + virtual status_t Flush(); + +private: + status_t _ReadSingleByte(void* buffer); + status_t _CharToInt(uint8 ch, uint8* value); + +private: + BDataIO* fDelegate; + char fChar62; + char fChar63; + + uint8 fNextByteAssembly; + uint8 fNextByteAssemblyBits; +}; + + #endif // DATA_IO_UTILS_H diff --git a/src/kits/network/libnetservices/HttpAuthentication.cpp b/src/kits/network/libnetservices/HttpAuthentication.cpp index 13adb71f55..b877b4b186 100644 --- a/src/kits/network/libnetservices/HttpAuthentication.cpp +++ b/src/kits/network/libnetservices/HttpAuthentication.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2010-2013 Haiku, Inc. All rights reserved. + * Copyright 2010-2023 Haiku, Inc. All rights reserved. * Distributed under the terms of the MIT License. * * Authors: @@ -60,6 +60,7 @@ BHttpAuthentication::BHttpAuthentication(const BHttpAuthentication& other) fAuthenticationMethod(other.fAuthenticationMethod), fUserName(other.fUserName), fPassword(other.fPassword), + fToken(other.fToken), fRealm(other.fRealm), fDigestNonce(other.fDigestNonce), fDigestCnonce(other.fDigestCnonce), @@ -79,6 +80,7 @@ BHttpAuthentication& BHttpAuthentication::operator=( fAuthenticationMethod = other.fAuthenticationMethod; fUserName = other.fUserName; fPassword = other.fPassword; + fToken = other.fToken; fRealm = other.fRealm; fDigestNonce = other.fDigestNonce; fDigestCnonce = other.fDigestCnonce; @@ -113,6 +115,15 @@ BHttpAuthentication::SetPassword(const BString& password) } +void +BHttpAuthentication::SetToken(const BString& token) +{ + fLock.Lock(); + fToken = token; + fLock.Unlock(); +} + + void BHttpAuthentication::SetMethod(BHttpAuthenticationMethod method) { @@ -151,7 +162,9 @@ BHttpAuthentication::Initialize(const BString& wwwAuthenticate) else if (authRequired == "digest") { fAuthenticationMethod = B_HTTP_AUTHENTICATION_DIGEST; fDigestAlgorithm = B_HTTP_AUTHENTICATION_ALGORITHM_MD5; - } else + } else if (authRequired == "bearer") + fAuthenticationMethod = B_HTTP_AUTHENTICATION_BEARER; + else return B_ERROR; @@ -206,6 +219,8 @@ BHttpAuthentication::Initialize(const BString& wwwAuthenticate) if (fAuthenticationMethod == B_HTTP_AUTHENTICATION_BASIC) return B_OK; + else if (fAuthenticationMethod == B_HTTP_AUTHENTICATION_BEARER) + return B_OK; else if (fAuthenticationMethod == B_HTTP_AUTHENTICATION_DIGEST && fDigestNonce.Length() > 0 && fDigestAlgorithm != B_HTTP_AUTHENTICATION_ALGORITHM_NONE) { @@ -260,6 +275,10 @@ BHttpAuthentication::Authorization(const BUrl& url, const BString& method) const break; } + case B_HTTP_AUTHENTICATION_BEARER: + authorizationString << "Bearer " << fToken; + break; + case B_HTTP_AUTHENTICATION_DIGEST: case B_HTTP_AUTHENTICATION_IE_DIGEST: authorizationString << "Digest " << "username=\"" << fUserName @@ -384,6 +403,7 @@ BHttpAuthentication::_DigestResponse(const BString& uri, const BString& method) PRINT(("HttpAuth: Computing digest response: \n")); PRINT(("HttpAuth: > username = %s\n", fUserName.String())); PRINT(("HttpAuth: > password = %s\n", fPassword.String())); + PRINT(("HttpAuth: > token = %s\n", fToken.String())); PRINT(("HttpAuth: > realm = %s\n", fRealm.String())); PRINT(("HttpAuth: > nonce = %s\n", fDigestNonce.String())); PRINT(("HttpAuth: > cnonce = %s\n", fDigestCnonce.String())); diff --git a/src/tests/apps/haikudepot/DataIOUtilsTest.cpp b/src/tests/apps/haikudepot/DataIOUtilsTest.cpp new file mode 100644 index 0000000000..9e72910cc7 --- /dev/null +++ b/src/tests/apps/haikudepot/DataIOUtilsTest.cpp @@ -0,0 +1,128 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + + +#include "DataIOUtilsTest.h" + +#include + +#include +#include + +#include + +#include "DataIOUtils.h" + + +DataIOUtilsTest::DataIOUtilsTest() +{ +} + + +DataIOUtilsTest::~DataIOUtilsTest() +{ +} + + +void +DataIOUtilsTest::TestReadBase64JwtClaims_1() +{ + const char* jwtToken = "eyJpc3MiOiJkZXYuaGRzIiwic3ViIjoiZXJpazY0QGhkcyIs" + "ImV4cCI6MTY5MzE5MTMzMiwiaWF0IjoxNjkzMTkxMDMyfQ"; + BMemoryIO memoryIo(jwtToken, strlen(jwtToken)); + Base64DecodingDataIO base64DecodingIo(&memoryIo, '-', '_'); + char actualOutputBuffer[71]; + size_t actualReadBytes; + + bzero(actualOutputBuffer, 71); + +// ---------------------- + status_t result = base64DecodingIo.ReadExactly(actualOutputBuffer, 70, &actualReadBytes); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL(B_OK, result); + CPPUNIT_ASSERT_EQUAL(70, actualReadBytes); + actualOutputBuffer[actualReadBytes] = 0; + + CPPUNIT_ASSERT_EQUAL(0x7b, (uint8) actualOutputBuffer[0]); + + CPPUNIT_ASSERT_EQUAL( + BString("{\"iss\":\"dev.hds\",\"sub\":\"erik64@hds\",\"exp\":1693191332,\"iat\"" + ":1693191032}"), + BString(actualOutputBuffer)); +} + + +void +DataIOUtilsTest::TestReadBase64JwtClaims_2() +{ + const char* jwtToken = "eyJpc3MiOiJkZXYuaGRzIiwic3ViIjoidG93ZWxkb3dudGVhQ" + "GhkcyIsImV4cCI6MTY5MzczODgyNiwiaWF0IjoxNjkzNzM4NTI2fQ"; + BMemoryIO memoryIo(jwtToken, strlen(jwtToken)); + Base64DecodingDataIO base64DecodingIo(&memoryIo, '-', '_'); + char actualOutputBuffer[77]; + size_t actualReadBytes; + + bzero(actualOutputBuffer, 77); + +// ---------------------- + status_t result = base64DecodingIo.ReadExactly(actualOutputBuffer, 76, &actualReadBytes); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL(B_OK, result); + CPPUNIT_ASSERT_EQUAL(76, actualReadBytes); + actualOutputBuffer[actualReadBytes] = 0; + + CPPUNIT_ASSERT_EQUAL(0x7b, (uint8) actualOutputBuffer[0]); + + CPPUNIT_ASSERT_EQUAL( + BString("{\"iss\":\"dev.hds\",\"sub\":\"toweldowntea@hds\",\"exp\":1693738826,\"iat\"" + ":1693738526}"), + BString(actualOutputBuffer)); +} + + +void +DataIOUtilsTest::TestCorrupt() +{ + const char* jwtToken = "QW5k$mV3"; + // note that '$' is not a valid base64 character + BMemoryIO memoryIo(jwtToken, strlen(jwtToken)); + Base64DecodingDataIO base64DecodingIo(&memoryIo, '-', '_'); + char actualOutputBuffer[7]; + size_t actualReadBytes; + + bzero(actualOutputBuffer, 7); + +// ---------------------- + status_t result = base64DecodingIo.ReadExactly(actualOutputBuffer, 6, &actualReadBytes); +// ---------------------- + + CPPUNIT_ASSERT(B_OK != result); +} + + +/*static*/ void +DataIOUtilsTest::AddTests(BTestSuite& parent) +{ + CppUnit::TestSuite& suite = *new CppUnit::TestSuite("DataIOUtilsTest"); + + suite.addTest( + new CppUnit::TestCaller( + "DataIOUtilsTest::TestReadBase64JwtClaims_1", + &DataIOUtilsTest::TestReadBase64JwtClaims_1)); + + suite.addTest( + new CppUnit::TestCaller( + "DataIOUtilsTest::TestReadBase64JwtClaims_2", + &DataIOUtilsTest::TestReadBase64JwtClaims_2)); + + suite.addTest( + new CppUnit::TestCaller( + "DataIOUtilsTest::TestCorrupt", + &DataIOUtilsTest::TestCorrupt)); + + parent.addTest("DataIOUtilsTest", &suite); +} \ No newline at end of file diff --git a/src/tests/apps/haikudepot/DataIOUtilsTest.h b/src/tests/apps/haikudepot/DataIOUtilsTest.h new file mode 100644 index 0000000000..81a894797d --- /dev/null +++ b/src/tests/apps/haikudepot/DataIOUtilsTest.h @@ -0,0 +1,26 @@ +/* + * Copyright 2023, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ +#ifndef DATA_IO_UTILS_TEST_H +#define DATA_IO_UTILS_TEST_H + + +#include +#include + + +class DataIOUtilsTest : public CppUnit::TestCase { +public: + DataIOUtilsTest(); + virtual ~DataIOUtilsTest(); + + void TestReadBase64JwtClaims_1(); + void TestReadBase64JwtClaims_2(); + void TestCorrupt(); + + static void AddTests(BTestSuite& suite); +}; + + +#endif // DATA_IO_UTILS_TEST_H diff --git a/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp b/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp index a6a47997cf..280cbf27e4 100644 --- a/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp +++ b/src/tests/apps/haikudepot/HaikuDepotTestAddon.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020, Andrew Lindesay, apl@lindesay.co.nz + * Copyright 2017-2023, Andrew Lindesay, apl@lindesay.co.nz * Distributed under the terms of the MIT License. */ @@ -8,7 +8,9 @@ #include #include "StandardMetaDataJsonEventListenerTest.h" +#include "DataIOUtilsTest.h" #include "DumpExportRepositoryJsonListenerTest.h" +#include "JwtTokenHelperTest.h" #include "ValidationFailureTest.h" #include "ValidationUtilsTest.h" #include "StorageUtilsTest.h" @@ -21,7 +23,10 @@ getTestSuite() BTestSuite* suite = new BTestSuite("HaikuDepot"); StandardMetaDataJsonEventListenerTest::AddTests(*suite); + DataIOUtilsTest::AddTests(*suite); DumpExportRepositoryJsonListenerTest::AddTests(*suite); + DumpExportRepositoryJsonListenerTest::AddTests(*suite); + JwtTokenHelperTest::AddTests(*suite); ValidationFailureTest::AddTests(*suite); ValidationUtilsTest::AddTests(*suite); StorageUtilsTest::AddTests(*suite); diff --git a/src/tests/apps/haikudepot/Jamfile b/src/tests/apps/haikudepot/Jamfile index b43564207a..84c0ff0b44 100644 --- a/src/tests/apps/haikudepot/Jamfile +++ b/src/tests/apps/haikudepot/Jamfile @@ -33,6 +33,7 @@ UnitTestLib haikudepottest.so : HaikuDepotTestAddon.cpp DataIOUtils.cpp + DataIOUtilsTest.cpp DumpExportRepositorySource.cpp DumpExportRepositorySourceMirror.cpp @@ -40,6 +41,9 @@ UnitTestLib haikudepottest.so : DumpExportRepositoryJsonListener.cpp DumpExportRepositoryJsonListenerTest.cpp + JwtTokenHelper.cpp + JwtTokenHelperTest.cpp + Logger.cpp StandardMetaData.cpp diff --git a/src/tests/apps/haikudepot/JwtTokenHelperTest.cpp b/src/tests/apps/haikudepot/JwtTokenHelperTest.cpp new file mode 100644 index 0000000000..a1e1e0ecdc --- /dev/null +++ b/src/tests/apps/haikudepot/JwtTokenHelperTest.cpp @@ -0,0 +1,102 @@ +/* + * Copyright 2023, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + + +#include "JwtTokenHelperTest.h" + +#include + +#include +#include + +#include + +#include "JwtTokenHelper.h" + + +JwtTokenHelperTest::JwtTokenHelperTest() +{ +} + + +JwtTokenHelperTest::~JwtTokenHelperTest() +{ +} + + +void +JwtTokenHelperTest::TestParseTokenClaims() +{ + const char* jwtToken = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJkZXYuaGRzIiwic3ViIj" + "oiZXJpazY0QGhkcyIsImV4cCI6MTY5MzkwNzM1NywiaWF0IjoxNjkzOTA3MDU3fQ.DJOz0" + "TmgN0Ya8De-oV0mBwWb-8FYavLbaFUFhCLqr-s"; + BMessage actualMessage; + +// ---------------------- + status_t result = JwtTokenHelper::ParseClaims(BString(jwtToken), actualMessage); +// ---------------------- + + CPPUNIT_ASSERT_EQUAL(B_OK, result); + + _AssertStringValue(actualMessage, "iss", "dev.hds"); + _AssertStringValue(actualMessage, "sub", "erik64@hds"); + _AssertDoubleValue(actualMessage, "exp", 1693907357); + _AssertDoubleValue(actualMessage, "iat", 1693907057); +} + + +void +JwtTokenHelperTest::TestCorrupt() +{ + const char* jwtToken = "application/json"; + BMessage actualMessage; + +// ---------------------- + status_t result = JwtTokenHelper::ParseClaims(BString(jwtToken), actualMessage); +// ---------------------- + + CPPUNIT_ASSERT(B_OK != result); +} + + +/*static*/ void +JwtTokenHelperTest::AddTests(BTestSuite& parent) +{ + CppUnit::TestSuite& suite = *new CppUnit::TestSuite("JwtTokenHelperTest"); + + suite.addTest( + new CppUnit::TestCaller( + "JwtTokenHelperTest::TestParseTokenClaims", + &JwtTokenHelperTest::TestParseTokenClaims)); + + suite.addTest( + new CppUnit::TestCaller( + "JwtTokenHelperTest::TestCorrupt", + &JwtTokenHelperTest::TestCorrupt)); + + parent.addTest("JwtTokenHelperTest", &suite); +} + + +void +JwtTokenHelperTest::_AssertStringValue(const BMessage& message, const char* key, + const char* expectedValue) const +{ + BString value; + status_t result = message.FindString(key, &value); + CPPUNIT_ASSERT_EQUAL(B_OK, result); + CPPUNIT_ASSERT_EQUAL(BString(expectedValue), value); +} + + +void +JwtTokenHelperTest::_AssertDoubleValue(const BMessage& message, const char* key, + int64 expectedValue) const +{ + double value; + status_t result = message.FindDouble(key, &value); + CPPUNIT_ASSERT_EQUAL(B_OK, result); + CPPUNIT_ASSERT_EQUAL((double) expectedValue, value); +} \ No newline at end of file diff --git a/src/tests/apps/haikudepot/JwtTokenHelperTest.h b/src/tests/apps/haikudepot/JwtTokenHelperTest.h new file mode 100644 index 0000000000..726e1304cd --- /dev/null +++ b/src/tests/apps/haikudepot/JwtTokenHelperTest.h @@ -0,0 +1,33 @@ +/* + * Copyright 2023, Andrew Lindesay + * Distributed under the terms of the MIT License. + */ +#ifndef JWT_TOKEN_HELPER_TEST_H +#define JWT_TOKEN_HELPER_TEST_H + + +#include + +#include +#include + + +class JwtTokenHelperTest : public CppUnit::TestCase { +public: + JwtTokenHelperTest(); + virtual ~JwtTokenHelperTest(); + + void TestParseTokenClaims(); + void TestCorrupt(); + + static void AddTests(BTestSuite& suite); + +private: + void _AssertStringValue(const BMessage& message, const char* key, + const char* expectedValue) const; + void _AssertDoubleValue(const BMessage& message, const char* key, + int64 expectedValue) const; +}; + + +#endif // JWT_TOKEN_HELPER_TEST_H