diff --git a/CMakeLists.txt b/CMakeLists.txt index 5336e83..c7ef2b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,7 +122,7 @@ add_executable( owsec src/storage/orm_actionLinks.cpp src/storage/orm_actionLinks.h src/storage/orm_avatar.cpp src/storage/orm_avatar.h src/SpecialUserHelpers.h - src/RESTAPI/RESTAPI_db_helpers.h src/storage/orm_logins.cpp src/storage/orm_logins.h src/RESTAPI/RESTAPI_totp_handler.cpp src/RESTAPI/RESTAPI_totp_handler.h src/TotpCache.h src/RESTAPI/RESTAPI_subtotp_handler.cpp src/RESTAPI/RESTAPI_subtotp_handler.h) + src/RESTAPI/RESTAPI_db_helpers.h src/storage/orm_logins.cpp src/storage/orm_logins.h src/RESTAPI/RESTAPI_totp_handler.cpp src/RESTAPI/RESTAPI_totp_handler.h src/TotpCache.h src/RESTAPI/RESTAPI_subtotp_handler.cpp src/RESTAPI/RESTAPI_subtotp_handler.h src/RESTAPI/RESTAPI_signup_handler.cpp src/RESTAPI/RESTAPI_signup_handler.h) if(NOT SMALL_BUILD) target_link_libraries(owsec PUBLIC diff --git a/build b/build index e440e5c..62f9457 100644 --- a/build +++ b/build @@ -1 +1 @@ -3 \ No newline at end of file +6 \ No newline at end of file diff --git a/openpapi/owsec.yaml b/openpapi/owsec.yaml index 6fa3189..064cb59 100644 --- a/openpapi/owsec.yaml +++ b/openpapi/owsec.yaml @@ -1484,6 +1484,32 @@ paths: 403: $ref: '#/components/responses/Unauthorized' + /signup: + post: + tags: + - Subscriber Registration + summary: This call allows a new subscriber to register themselves and their devices. + operationId: postSignup + parameters: + - in: query + name: email + schema: + type: string + format: email + required: true + - in: query + name: serialNumber + schema: + type: string + required: true + responses: + 200: + $ref: '#/components/responses/Success' + 400: + $ref: '#/components/responses/BadRequest' + 404: + $ref: '#/components/responses/Unauthorized' + ######################################################################################### ## ## These are endpoints that all services in the uCentral stack must provide diff --git a/src/ActionLinkManager.cpp b/src/ActionLinkManager.cpp index aaea6b0..5e1d75d 100644 --- a/src/ActionLinkManager.cpp +++ b/src/ActionLinkManager.cpp @@ -86,6 +86,14 @@ namespace OpenWifi { } break; + case OpenWifi::SecurityObjects::LinkActions::SUB_SIGNUP: { + if(AuthService::SendEmailToSubUser(i.id, UInfo.email, AuthService::SIGNUP_VERIFICATION)) { + Logger().information(Poco::format("Send new subscriber email verification link to %s",UInfo.email)); + } + StorageService()->ActionLinksDB().SentAction(i.id); + } + break; + default: { StorageService()->ActionLinksDB().SentAction(i.id); } diff --git a/src/ActionLinkManager.h b/src/ActionLinkManager.h index 3caae7a..043c8e6 100644 --- a/src/ActionLinkManager.h +++ b/src/ActionLinkManager.h @@ -16,17 +16,18 @@ namespace OpenWifi { FORGOT_PASSWORD, VERIFY_EMAIL, SUB_FORGOT_PASSWORD, - SUB_VERIFY_EMAIL + SUB_VERIFY_EMAIL, + SUB_SIGNUP }; */ static ActionLinkManager * instance() { - static auto * instance_ = new ActionLinkManager; + static auto instance_ = new ActionLinkManager; return instance_; } int Start() final; void Stop() final; - void run(); + void run() final; private: Poco::Thread Thr_; diff --git a/src/AuthService.cpp b/src/AuthService.cpp index 813af7d..d58904b 100644 --- a/src/AuthService.cpp +++ b/src/AuthService.cpp @@ -546,6 +546,17 @@ namespace OpenWifi { } break; + case SIGNUP_VERIFICATION: { + MessageAttributes Attrs; + Attrs[RECIPIENT_EMAIL] = UInfo.email; + Attrs[LOGO] = GetLogoAssetURI(); + Attrs[SUBJECT] = "EMail Address Verification"; + Attrs[ACTION_LINK] = MicroService::instance().GetPublicAPIEndPoint() + "/actionLink?action=signup_verification&id=" + LinkId ; + SMTPMailerService()->SendMessage(UInfo.email, "signup_verification.txt", Attrs); + UInfo.waitingForEmailCheck = true; + } + break; + default: break; } diff --git a/src/AuthService.h b/src/AuthService.h index 872adf8..edf0563 100644 --- a/src/AuthService.h +++ b/src/AuthService.h @@ -38,7 +38,8 @@ namespace OpenWifi{ enum EMAIL_REASON { FORGOT_PASSWORD, - EMAIL_VERIFICATION + EMAIL_VERIFICATION, + SIGNUP_VERIFICATION }; static ACCESS_TYPE IntToAccessType(int C); diff --git a/src/RESTAPI/RESTAPI_action_links.cpp b/src/RESTAPI/RESTAPI_action_links.cpp index 353c053..50ff337 100644 --- a/src/RESTAPI/RESTAPI_action_links.cpp +++ b/src/RESTAPI/RESTAPI_action_links.cpp @@ -25,6 +25,8 @@ namespace OpenWifi { return RequestResetPassword(Link); else if(Action=="email_verification") return DoEmailVerification(Link); + else if(Action=="signup_verification") + return DoNewSubVerification(Link); else return DoReturnA404(); } @@ -34,6 +36,8 @@ namespace OpenWifi { if(Action=="password_reset") return CompleteResetPassword(); + else if(Action=="signup_completion") + return CompleteSubVerification(); else return DoReturnA404(); } @@ -46,6 +50,14 @@ namespace OpenWifi { SendHTMLFileBack(FormFile,FormVars); } + void RESTAPI_action_links::DoNewSubVerification(SecurityObjects::ActionLink &Link) { + Logger_.information(Poco::format("REQUEST-SUB-SIGNUP(%s): For ID=%s", Request->clientAddress().toString(), Link.userId)); + Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification.html"}; + Types::StringPairVec FormVars{ {"UUID", Link.id}, + {"PASSWORD_VALIDATION", AuthService()->PasswordValidationExpression()}}; + SendHTMLFileBack(FormFile,FormVars); + } + void RESTAPI_action_links::CompleteResetPassword() { // form has been posted... RESTAPI_PartHandler PartHandler; @@ -53,7 +65,7 @@ namespace OpenWifi { if (!Form.empty()) { auto Password1 = Form.get("password1","bla"); - auto Password2 = Form.get("password1","blu"); + auto Password2 = Form.get("password2","blu"); auto Id = Form.get("id",""); auto Now = std::time(nullptr); @@ -118,6 +130,77 @@ namespace OpenWifi { } } + void RESTAPI_action_links::CompleteSubVerification() { + RESTAPI_PartHandler PartHandler; + Poco::Net::HTMLForm Form(*Request, Request->stream(), PartHandler); + + if (!Form.empty()) { + auto Password1 = Form.get("password1","bla"); + auto Password2 = Form.get("password2","blu"); + auto Id = Form.get("id",""); + auto Now = std::time(nullptr); + + SecurityObjects::ActionLink Link; + if(!StorageService()->ActionLinksDB().GetActionLink(Id,Link)) { + return DoReturnA404(); + } + + if(Now > Link.expires) { + StorageService()->ActionLinksDB().CancelAction(Id); + return DoReturnA404(); + } + + if(Password1!=Password2 || !AuthService()->ValidateSubPassword(Password1)) { + Poco::File FormFile{ Daemon()->AssetDir() + "/password_reset_error.html"}; + Types::StringPairVec FormVars{ {"UUID", Id}, + {"ERROR_TEXT", "For some reason, the passwords entered do not match or they do not comply with" + " accepted password creation restrictions. Please consult our on-line help" + " to look at the our password policy. If you would like to contact us, please mention" + " id(" + Id + ")"}}; + return SendHTMLFileBack(FormFile,FormVars); + } + + SecurityObjects::UserInfo UInfo; + bool Found = StorageService()->SubDB().GetUserById(Link.userId,UInfo); + if(!Found) { + Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_error.html"}; + Types::StringPairVec FormVars{ {"UUID", Id}, + {"ERROR_TEXT", "This request does not contain a valid user ID. Please contact your system administrator."}}; + return SendHTMLFileBack(FormFile,FormVars); + } + + if(UInfo.blackListed || UInfo.suspended) { + Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_error.html"}; + Types::StringPairVec FormVars{ {"UUID", Id}, + {"ERROR_TEXT", "Please contact our system administrators. We have identified an error in your account that must be resolved first."}}; + return SendHTMLFileBack(FormFile,FormVars); + } + + bool GoodPassword = AuthService()->SetSubPassword(Password1,UInfo); + if(!GoodPassword) { + Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_error.html"}; + Types::StringPairVec FormVars{ {"UUID", Id}, + {"ERROR_TEXT", "You cannot reuse one of your recent passwords."}}; + return SendHTMLFileBack(FormFile,FormVars); + } + + UInfo.modified = std::time(nullptr); + UInfo.changePassword = false; + UInfo.lastEmailCheck = std::time(nullptr); + UInfo.waitingForEmailCheck = false; + + StorageService()->SubDB().UpdateUserInfo(UInfo.email,Link.userId,UInfo); + + Poco::File FormFile{ Daemon()->AssetDir() + "/signup_verification_success.html"}; + Types::StringPairVec FormVars{ {"UUID", Id}, + {"USERNAME", UInfo.email} }; + StorageService()->ActionLinksDB().CompleteAction(Id); + SendHTMLFileBack(FormFile,FormVars); + } else { + DoReturnA404(); + } + } + void RESTAPI_action_links::DoEmailVerification(SecurityObjects::ActionLink &Link) { auto Now = std::time(nullptr); diff --git a/src/RESTAPI/RESTAPI_action_links.h b/src/RESTAPI/RESTAPI_action_links.h index 1763b42..fdc0ec7 100644 --- a/src/RESTAPI/RESTAPI_action_links.h +++ b/src/RESTAPI/RESTAPI_action_links.h @@ -23,8 +23,10 @@ namespace OpenWifi { static const std::list PathName() { return std::list{"/api/v1/actionLink"}; }; void RequestResetPassword(SecurityObjects::ActionLink &Link); void CompleteResetPassword(); + void CompleteSubVerification(); void DoEmailVerification(SecurityObjects::ActionLink &Link); void DoReturnA404(); + void DoNewSubVerification(SecurityObjects::ActionLink &Link); void DoGet() final; void DoPost() final; diff --git a/src/RESTAPI/RESTAPI_routers.cpp b/src/RESTAPI/RESTAPI_routers.cpp index 37ae081..c5e7c8f 100644 --- a/src/RESTAPI/RESTAPI_routers.cpp +++ b/src/RESTAPI/RESTAPI_routers.cpp @@ -24,6 +24,7 @@ #include "RESTAPI/RESTAPI_submfa_handler.h" #include "RESTAPI/RESTAPI_totp_handler.h" #include "RESTAPI/RESTAPI_subtotp_handler.h" +#include "RESTAPI/RESTAPI_signup_handler.h" namespace OpenWifi { @@ -48,7 +49,8 @@ namespace OpenWifi { RESTAPI_subusers_handler, RESTAPI_submfa_handler, RESTAPI_totp_handler, - RESTAPI_subtotp_handler + RESTAPI_subtotp_handler, + RESTAPI_signup_handler >(Path, Bindings, L, S,TransactionId); } @@ -67,7 +69,8 @@ namespace OpenWifi { RESTAPI_preferences, RESTAPI_subpreferences, RESTAPI_suboauth2_handler, - RESTAPI_submfa_handler + RESTAPI_submfa_handler, + RESTAPI_signup_handler >(Path, Bindings, L, S, TransactionId); } } \ No newline at end of file diff --git a/src/RESTAPI/RESTAPI_signup_handler.cpp b/src/RESTAPI/RESTAPI_signup_handler.cpp new file mode 100644 index 0000000..4dd64a6 --- /dev/null +++ b/src/RESTAPI/RESTAPI_signup_handler.cpp @@ -0,0 +1,68 @@ +// +// Created by stephane bourque on 2022-02-20. +// + +#include "RESTAPI_signup_handler.h" +#include "StorageService.h" +#include "RESTObjects/RESTAPI_SecurityObjects.h" + +namespace OpenWifi { + + void RESTAPI_signup_handler::DoPost() { + + auto UserName = GetParameter("email",""); + auto SerialNumber = GetParameter("serialNumber",""); + + if(UserName.empty() || SerialNumber.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + + if(!Utils::ValidEMailAddress(UserName)) { + return BadRequest(RESTAPI::Errors::InvalidEmailAddress); + } + + if(!Utils::ValidSerialNumber(SerialNumber)) { + return BadRequest(RESTAPI::Errors::InvalidSerialNumber); + } + + // Do we already exist? Can only signup once... + SecurityObjects::UserInfo Existing; + if(StorageService()->SubDB().GetUserByEmail(UserName,Existing)) { + + if(Existing.signingUp.empty()) { + return BadRequest(1, "Subscriber already signed up."); + } + + if(Existing.waitingForEmailCheck) { + return BadRequest(2, "Waiting for email check completion."); + } + + return BadRequest(3, "Waiting for device:" + Existing.signingUp); + } + + SecurityObjects::UserInfo NewSub; + NewSub.signingUp = SerialNumber; + NewSub.waitingForEmailCheck = true; + NewSub.modified = std::time(nullptr); + NewSub.creationDate = std::time(nullptr); + NewSub.id = MicroService::instance().CreateUUID(); + NewSub.email = UserName; + NewSub.userRole = SecurityObjects::SUBSCRIBER; + NewSub.changePassword = true; + + StorageService()->SubDB().CreateRecord(NewSub); + + Logger_.information(Poco::format("SIGNUP-PASSWORD(%s): Request for %s", Request->clientAddress().toString(), UserName)); + SecurityObjects::ActionLink NewLink; + + NewLink.action = OpenWifi::SecurityObjects::LinkActions::SUB_SIGNUP; + NewLink.id = MicroService::CreateUUID(); + NewLink.userId = NewSub.id; + NewLink.created = std::time(nullptr); + NewLink.expires = NewLink.created + (1*60*60); // 1 hour + NewLink.userAction = false; + StorageService()->ActionLinksDB().CreateAction(NewLink); + + return OK(); + } +} \ No newline at end of file diff --git a/src/RESTAPI/RESTAPI_signup_handler.h b/src/RESTAPI/RESTAPI_signup_handler.h new file mode 100644 index 0000000..44f9894 --- /dev/null +++ b/src/RESTAPI/RESTAPI_signup_handler.h @@ -0,0 +1,38 @@ +// +// Created by stephane bourque on 2022-02-20. +// + +#pragma once + +#include "framework/MicroService.h" + +namespace OpenWifi { + class RESTAPI_signup_handler : public RESTAPIHandler { + public: + RESTAPI_signup_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L, RESTAPI_GenericServer & Server, uint64_t TransactionId, bool Internal) + : RESTAPIHandler(bindings, L, + std::vector{ + Poco::Net::HTTPRequest::HTTP_POST, + Poco::Net::HTTPRequest::HTTP_OPTIONS}, + Server, + TransactionId, + Internal, false, true ){} + + static const std::list PathName() { return std::list{"/api/v1/action"}; }; + +/* inline bool RoleIsAuthorized(std::string & Reason) { + if(UserInfo_.userinfo.userRole != SecurityObjects::USER_ROLE::SUBSCRIBER) { + Reason = "User must be a subscriber"; + return false; + } + return true; + } +*/ + void DoGet() final {}; + void DoPost() final; + void DoPut() final {}; + void DoDelete() final {}; + private: + + }; +} diff --git a/src/RESTObjects/RESTAPI_SecurityObjects.cpp b/src/RESTObjects/RESTAPI_SecurityObjects.cpp index 0cccbf7..98b9bc3 100644 --- a/src/RESTObjects/RESTAPI_SecurityObjects.cpp +++ b/src/RESTObjects/RESTAPI_SecurityObjects.cpp @@ -257,6 +257,7 @@ namespace OpenWifi::SecurityObjects { field_to_json(Obj,"oauthType",oauthType); field_to_json(Obj,"oauthUserInfo",oauthUserInfo); field_to_json(Obj,"modified",modified); + field_to_json(Obj,"signingUp",signingUp); }; bool UserInfo::from_json(const Poco::JSON::Object::Ptr &Obj) { @@ -292,6 +293,7 @@ namespace OpenWifi::SecurityObjects { field_from_json(Obj,"oauthType",oauthType); field_from_json(Obj,"oauthUserInfo",oauthUserInfo); field_from_json(Obj,"modified",modified); + field_from_json(Obj,"signingUp",signingUp); return true; } catch (const Poco::Exception &E) { diff --git a/src/RESTObjects/RESTAPI_SecurityObjects.h b/src/RESTObjects/RESTAPI_SecurityObjects.h index 9b532ca..034dd74 100644 --- a/src/RESTObjects/RESTAPI_SecurityObjects.h +++ b/src/RESTObjects/RESTAPI_SecurityObjects.h @@ -138,6 +138,7 @@ namespace OpenWifi { std::string oauthType; std::string oauthUserInfo; uint64_t modified; + std::string signingUp; void to_json(Poco::JSON::Object &Obj) const; bool from_json(const Poco::JSON::Object::Ptr &Obj); @@ -233,7 +234,8 @@ namespace OpenWifi { FORGOT_PASSWORD=1, VERIFY_EMAIL, SUB_FORGOT_PASSWORD, - SUB_VERIFY_EMAIL + SUB_VERIFY_EMAIL, + SUB_SIGNUP }; struct ActionLink { diff --git a/src/storage/orm_users.cpp b/src/storage/orm_users.cpp index 1989b68..6fe140e 100644 --- a/src/storage/orm_users.cpp +++ b/src/storage/orm_users.cpp @@ -73,7 +73,8 @@ namespace OpenWifi { ORM::Field{"lastPasswords", ORM::FieldType::FT_TEXT}, ORM::Field{"oauthType", ORM::FieldType::FT_TEXT}, ORM::Field{"oauthUserInfo", ORM::FieldType::FT_TEXT}, - ORM::Field{"modified", ORM::FieldType::FT_TEXT} + ORM::Field{"modified", ORM::FieldType::FT_TEXT}, + ORM::Field{"signingUp", ORM::FieldType::FT_TEXT} }; static ORM::IndexVec MakeIndices(const std::string & shortname) { @@ -96,7 +97,8 @@ namespace OpenWifi { bool BaseUserDB::Upgrade(uint32_t from, uint32_t &to) { std::vector Statements{ - "alter table " + TableName_ + " add column modified BIGINT;" + "alter table " + TableName_ + " add column modified BIGINT;", + "alter table " + TableName_ + " add column signingUp TEXT default '';" }; RunScript(Statements); to = CurrentVersion; @@ -314,6 +316,7 @@ template<> void ORM::DB(); U.oauthUserInfo = T.get<29>(); U.modified = T.get<30>(); + U.signingUp = T.get<31>(); } template<> void ORM::DB< OpenWifi::UserInfoRecordTuple, @@ -350,4 +353,5 @@ template<> void ORM::DB< OpenWifi::UserInfoRecordTuple, T.set<28>(U.oauthType); T.set<29>(U.oauthUserInfo); T.set<30>(U.modified); + T.set<31>(U.signingUp); } diff --git a/src/storage/orm_users.h b/src/storage/orm_users.h index 3a79081..aead63a 100644 --- a/src/storage/orm_users.h +++ b/src/storage/orm_users.h @@ -40,7 +40,8 @@ namespace OpenWifi { std::string, // lastPasswords; std::string, // oauthType; std::string, // oauthUserInfo; - uint64_t // modified + uint64_t, // modified + std::string // signingUp; > UserInfoRecordTuple; typedef std::vector UserInfoRecordTupleList; @@ -62,7 +63,7 @@ namespace OpenWifi { class BaseUserDB : public ORM::DB { public: - const uint32_t CurrentVersion = 1; + const uint32_t CurrentVersion = 2; BaseUserDB( const std::string &name, const std::string &shortname, OpenWifi::DBType T, Poco::Data::SessionPool & P, Poco::Logger &L, UserCache * Cache, bool users); bool CreateUser(const std::string & Admin, SecurityObjects::UserInfo & NewUser, bool PasswordHashedAlready = false ); diff --git a/templates/signup_verification.html b/templates/signup_verification.html new file mode 100644 index 0000000..566549b --- /dev/null +++ b/templates/signup_verification.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file diff --git a/templates/signup_verification.txt b/templates/signup_verification.txt new file mode 100644 index 0000000..5b1a4bf --- /dev/null +++ b/templates/signup_verification.txt @@ -0,0 +1,10 @@ +Dear ${RECIPIENT_EMAIL}, + + Before you can access the system, you must validate your e-mail address. Please click on the link below + to complete this task. + + ${ACTION_LINK} + + And follow the instructions. + +Thank you! \ No newline at end of file diff --git a/wwwassets/signup_verification.html b/wwwassets/signup_verification.html new file mode 100644 index 0000000..4abef98 --- /dev/null +++ b/wwwassets/signup_verification.html @@ -0,0 +1,162 @@ + + + + + + + + + +
+ OpenWifi +
+ +
+ +
+

Reset Password

+
+ + +
+
+ + +
+
+ +
+ +
+
    +
  • Must be at least 8 characters long
  • +
  • Must contain 1 uppercase letter
  • +
  • Must contain 1 lowercase letter
  • +
  • Must contain 1 digit
  • +
  • Must contain 1 special character
  • +
+
+
+
+ + + + \ No newline at end of file diff --git a/wwwassets/signup_verification_error.html b/wwwassets/signup_verification_error.html new file mode 100644 index 0000000..869c668 --- /dev/null +++ b/wwwassets/signup_verification_error.html @@ -0,0 +1,116 @@ + + + + + + + + +
+ OpenWifi +
+ + +
+

Reset Password Failed

+
+

ID

+ ${UUID} +
+
+

Error

+ ${ERROR_TEXT} +
+
+ + + diff --git a/wwwassets/signup_verification_success.html b/wwwassets/signup_verification_success.html new file mode 100644 index 0000000..3a6516e --- /dev/null +++ b/wwwassets/signup_verification_success.html @@ -0,0 +1,76 @@ + + + + + + + +
+ Avatar +
+ +

Signup was successfully done

+
+

Thank you ${USERNAME} for signing up for service.

+
+
+ You can access your account using the mobile app. +
+ + \ No newline at end of file