From 7fe7a70fcf7540beb6d7b4847e53a425de66c6bf Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Wed, 3 Nov 2021 18:34:59 +0100
Subject: [PATCH] Implement and fix 3pid UIA stages

---
 include/mtx/requests.hpp          | 68 +++++++++++++++++++++++++++++++
 include/mtx/responses/common.hpp  | 28 +++++++++++++
 include/mtx/user_interactive.hpp  | 10 +++--
 include/mtxclient/http/client.hpp | 33 ++++++++++++---
 lib/http/client.cpp               | 50 ++++++++++++++++++++++-
 lib/structs/requests.cpp          | 25 ++++++++++++
 lib/structs/responses/common.cpp  | 15 +++++++
 lib/structs/user_interactive.cpp  |  8 ++--
 tests/requests.cpp                | 64 ++++++++++++-----------------
 9 files changed, 248 insertions(+), 53 deletions(-)

diff --git a/include/mtx/requests.hpp b/include/mtx/requests.hpp
index c091a3a6f..c3b5127ef 100644
--- a/include/mtx/requests.hpp
+++ b/include/mtx/requests.hpp
@@ -111,6 +111,74 @@ struct Login
 void
 to_json(json &obj, const Login &request);
 
+/// @brief Request payload for
+/// `POST /_matrix/client/r0/{register,account/password}/email/requestToken`
+///
+/// The homeserver should validate the email itself, either by sending a validation email itself or
+/// by using a service it has control over.
+struct RequestEmailToken
+{
+    //! Required. A unique string generated by the client, and used to identify the validation
+    //! attempt. It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must
+    //! not exceed 255 characters and it must not be empty.
+    std::string client_secret;
+    //! Required. The email address to validate.
+    std::string email;
+
+    //! Required. The server will only send an email if the send_attempt is a number greater than
+    //! the most recent one which it has seen, scoped to that email + client_secret pair. This is to
+    //! avoid repeatedly sending the same email in the case of request retries between the POSTing
+    //! user and the identity server. The client should increment this value if they desire a new
+    //! email (e.g. a reminder) to be sent. If they do not, the server should respond with success
+    //! but not resend the email.
+    int send_attempt = 0;
+};
+
+void
+to_json(json &obj, const RequestEmailToken &request);
+
+/// @brief Request payload for
+/// `POST /_matrix/client/r0/{register,account/password}/msisdn/requestToken`
+///
+/// The homeserver should validate the email itself, either by sending a validation email itself or
+/// by using a service it has control over.
+struct RequestMSISDNToken
+{
+    //! Required. A unique string generated by the client, and used to identify the validation
+    //! attempt. It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must
+    //! not exceed 255 characters and it must not be empty.
+    std::string client_secret;
+    //! Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in
+    //! phone_number should be parsed as if it were dialled from.
+    std::string country;
+    //! Required. The phone number to validate.
+    std::string phone_number;
+
+    //! Required. The server will only send an SMS if the send_attempt is a number greater than the
+    //! most recent one which it has seen, scoped to that country + phone_number + client_secret
+    //! triple. This is to avoid repeatedly sending the same SMS in the case of request retries
+    //! between the POSTing user and the identity server. The client should increment this value if
+    //! they desire a new SMS (e.g. a reminder) to be sent.
+    int send_attempt = 0;
+};
+
+void
+to_json(json &obj, const RequestMSISDNToken &request);
+
+//! Validate ownership of an email address/phone number.
+struct IdentitySubmitToken
+{
+    //! Required. The session ID, generated by the requestToken call.
+    std::string sid;
+    //! Required. The client secret that was supplied to the requestToken call.
+    std::string client_secret;
+    //! Required. The token generated by the requestToken call and emailed to the user.
+    std::string token;
+};
+
+void
+to_json(json &obj, const IdentitySubmitToken &request);
+
 //! Request payload for the `PUT /_matrix/client/r0/sendToDevice/{eventType}/{transcationID}`
 template<typename EventContent>
 using ToDeviceMessages = std::map<mtx::identifiers::User, std::map<std::string, EventContent>>;
diff --git a/include/mtx/responses/common.hpp b/include/mtx/responses/common.hpp
index fdf7b3951..cdbc4d763 100644
--- a/include/mtx/responses/common.hpp
+++ b/include/mtx/responses/common.hpp
@@ -66,6 +66,34 @@ struct Version
 
 void
 from_json(const nlohmann::json &obj, Version &response);
+
+//! Some endpoints return this to indicate success in addition to the http code.
+struct Success
+{
+    //! Required. Whether the validation was successful or not.
+    bool success;
+};
+
+void
+from_json(const nlohmann::json &obj, Success &response);
+
+//! Responses to the `/requestToken` endpoints
+struct RequestToken
+{
+    //! Required. The session ID. Session IDs are opaque strings that must consist entirely of the
+    //! characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be
+    //! empty.
+    std::string sid;
+    //! An optional field containing a URL where the client must submit the validation token to,
+    //! with identical parameters to the Identity Service API's POST /validate/email/submitToken
+    //! endpoint (without the requirement for an access token). The homeserver must send this token
+    //! to the user (if applicable), who should then be prompted to provide it to the client.
+    std::string submit_url;
+};
+
+void
+from_json(const nlohmann::json &obj, RequestToken &response);
+
 //! Different helper for parsing responses.
 namespace utils {
 //! Multiple account_data events.
diff --git a/include/mtx/user_interactive.hpp b/include/mtx/user_interactive.hpp
index b60647045..28c17eecb 100644
--- a/include/mtx/user_interactive.hpp
+++ b/include/mtx/user_interactive.hpp
@@ -172,15 +172,17 @@ struct ThreePIDCred
 //! Email authentication stage.
 struct EmailIdentity
 {
-    // The 3rd party ids
-    std::vector<ThreePIDCred> threepidCreds;
+    //! The 3rd party id
+    //! See https://github.com/matrix-org/matrix-doc/pull/3471 for context.
+    ThreePIDCred threepidCred;
 };
 
 //! SMS authentication stage.
 struct MSISDN
 {
-    // The 3rd party ids
-    std::vector<ThreePIDCred> threepidCreds;
+    //! The 3rd party id
+    //! See https://github.com/matrix-org/matrix-doc/pull/3471 for context.
+    ThreePIDCred threepidCred;
 };
 
 //! Registration token authentication stage.
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index c75096424..35ba83b19 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -55,10 +55,9 @@ struct AvatarUrl;
 struct ClaimKeys;
 struct ContentURI;
 struct CreateRoom;
+struct Device;
 struct EventId;
-struct RoomId;
 struct FilterId;
-struct Version;
 struct GroupId;
 struct GroupProfile;
 struct JoinedGroups;
@@ -69,18 +68,21 @@ struct LoginFlows;
 struct Messages;
 struct Notifications;
 struct Profile;
+struct PublicRoomVisibility;
+struct PublicRooms;
+struct QueryDevices;
 struct QueryKeys;
 struct Register;
 struct RegistrationTokenValidity;
+struct RequestToken;
+struct RoomId;
+struct Success;
 struct Sync;
 struct TurnServer;
 struct UploadKeys;
+struct Version;
 struct Versions;
 struct WellKnown;
-struct PublicRoomVisibility;
-struct PublicRooms;
-struct QueryDevices;
-struct Device;
 namespace backup {
 struct SessionBackup;
 struct RoomKeysBackup;
@@ -279,6 +281,25 @@ public:
     void registration_token_validity(const std::string token,
                                      Callback<mtx::responses::RegistrationTokenValidity> cb);
 
+    //! Validate an unused email address.
+    void register_email_request_token(const requests::RequestEmailToken &r,
+                                      Callback<mtx::responses::RequestToken> cb);
+    //! Validate a used email address.
+    void verify_email_request_token(const requests::RequestEmailToken &r,
+                                    Callback<mtx::responses::RequestToken> cb);
+
+    //! Validate an unused phone number.
+    void register_phone_request_token(const requests::RequestMSISDNToken &r,
+                                      Callback<mtx::responses::RequestToken> cb);
+    //! Validate a used phone number.
+    void verify_phone_request_token(const requests::RequestMSISDNToken &r,
+                                    Callback<mtx::responses::RequestToken> cb);
+
+    //! Validate ownership of an email address/phone number.
+    void validate_submit_token(const std::string &url,
+                               const requests::IdentitySubmitToken &r,
+                               Callback<mtx::responses::Success>);
+
     //! Paginate through the list of events that the user has been,
     //! or would have been notified about.
     void notifications(uint64_t limit,
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index 34141f6cf..38fc62fa4 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -2,6 +2,7 @@
 #include "mtx/log.hpp"
 #include "mtxclient/http/client_impl.hpp"
 
+#include <iostream>
 #include <mutex>
 #include <thread>
 
@@ -917,9 +918,10 @@ Client::registration(const std::string &user,
           "/client/r0/register",
           request,
           [cb, h](auto &r, RequestErr e) {
-              if (e && e->status_code == 401)
+              if (e && e->status_code == 401) {
+                  std::cout << e->matrix_error.error << "\n";
                   h.prompt(h, e->matrix_error.unauthorized);
-              else
+              } else
                   cb(r, e);
           },
           false);
@@ -943,6 +945,50 @@ Client::registration_token_validity(const std::string token,
       });
 }
 
+void
+Client::register_email_request_token(const requests::RequestEmailToken &r,
+                                     Callback<mtx::responses::RequestToken> cb)
+{
+    post("/client/r0/register/email/requestToken", r, cb);
+}
+void
+Client::verify_email_request_token(const requests::RequestEmailToken &r,
+                                   Callback<mtx::responses::RequestToken> cb)
+{
+    post("/client/r0/account/password/email/requestToken", r, cb);
+}
+
+void
+Client::register_phone_request_token(const requests::RequestMSISDNToken &r,
+                                     Callback<mtx::responses::RequestToken> cb)
+{
+    post("/client/r0/register/msisdn/requestToken", r, cb);
+}
+void
+Client::verify_phone_request_token(const requests::RequestMSISDNToken &r,
+                                   Callback<mtx::responses::RequestToken> cb)
+{
+    post("/client/r0/account/password/msisdn/requestToken", r, cb);
+}
+
+void
+Client::validate_submit_token(const std::string &url,
+                              const requests::IdentitySubmitToken &r,
+                              Callback<mtx::responses::Success> cb)
+{
+    // some dancing to send to an arbitrary, server provided url
+    auto callback = prepare_callback<mtx::responses::Success>(
+      [cb](const mtx::responses::Success &res, HeaderFields, RequestErr err) { cb(res, err); });
+    p->client.post(
+      url,
+      json(r).dump(),
+      "application/json",
+      [callback](const coeurl::Request &r) {
+          callback(r.response_headers(), r.response(), r.error_code(), r.response_code());
+      },
+      prepare_headers(false));
+}
+
 void
 Client::send_state_event(const std::string &room_id,
                          const std::string &event_type,
diff --git a/lib/structs/requests.cpp b/lib/structs/requests.cpp
index a55dfccdd..de4ab9704 100644
--- a/lib/structs/requests.cpp
+++ b/lib/structs/requests.cpp
@@ -84,6 +84,31 @@ to_json(json &obj, const Login &request)
     obj["type"] = request.type;
 }
 
+void
+to_json(json &obj, const RequestEmailToken &request)
+{
+    obj["client_secret"] = request.client_secret;
+    obj["email"]         = request.email;
+    obj["send_attempt"]  = request.send_attempt;
+}
+
+void
+to_json(json &obj, const RequestMSISDNToken &request)
+{
+    obj["client_secret"] = request.client_secret;
+    obj["country"]       = request.country;
+    obj["phone_number"]  = request.phone_number;
+    obj["send_attempt"]  = request.send_attempt;
+}
+
+void
+to_json(json &obj, const IdentitySubmitToken &request)
+{
+    obj["sid"]           = request.sid;
+    obj["client_secret"] = request.client_secret;
+    obj["token"]         = request.token;
+}
+
 void
 to_json(json &obj, const AvatarUrl &request)
 {
diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp
index 366815f7d..158854f29 100644
--- a/lib/structs/responses/common.cpp
+++ b/lib/structs/responses/common.cpp
@@ -59,6 +59,21 @@ from_json(const nlohmann::json &obj, Version &response)
     response.version = obj.at("version");
 }
 
+void
+from_json(const nlohmann::json &obj, Success &success)
+{
+    success.success = obj.at("success");
+}
+
+void
+from_json(const nlohmann::json &obj, RequestToken &r)
+{
+    r.sid = obj.at("sid");
+
+    if (obj.contains("submit_url"))
+        r.submit_url = obj.at("submit_url");
+}
+
 namespace utils {
 
 void
diff --git a/lib/structs/user_interactive.cpp b/lib/structs/user_interactive.cpp
index 61b2ffa27..e79914420 100644
--- a/lib/structs/user_interactive.cpp
+++ b/lib/structs/user_interactive.cpp
@@ -110,12 +110,12 @@ to_json(nlohmann::json &obj, const Auth &auth)
                      obj["txn_id"] = token.txn_id;
                  },
                  [&obj](const auth::EmailIdentity &id) {
-                     obj["type"]          = auth_types::email_identity;
-                     obj["threepidCreds"] = id.threepidCreds;
+                     obj["type"]           = auth_types::email_identity;
+                     obj["threepid_creds"] = id.threepidCred;
                  },
                  [&obj](const auth::MSISDN &id) {
-                     obj["type"]          = auth_types::msisdn;
-                     obj["threepidCreds"] = id.threepidCreds;
+                     obj["type"]           = auth_types::msisdn;
+                     obj["threepid_creds"] = id.threepidCred;
                  },
                  [&obj](const auth::RegistrationToken &registration_token) {
                      obj["type"]  = auth_types::registration_token;
diff --git a/tests/requests.cpp b/tests/requests.cpp
index 072de255b..a348ee055 100644
--- a/tests/requests.cpp
+++ b/tests/requests.cpp
@@ -225,58 +225,48 @@ TEST(Requests, UserInteractiveAuth)
   "session": "<session ID>"
 })"_json);
 
-    a.content = auth::EmailIdentity{{
-      {"<identity server session id>",
-       "<identity server client secret>",
-       "<url of identity server authed with, e.g. 'matrix.org:8090'>",
-       "<access token previously registered with the identity server>"},
-    }};
+    a.content =
+      auth::EmailIdentity{"<identity server session id>",
+                          "<identity server client secret>",
+                          "<url of identity server authed with, e.g. 'matrix.org:8090'>",
+                          "<access token previously registered with the identity server>"};
 
     EXPECT_EQ(nlohmann::json(a), R"({
   "type": "m.login.email.identity",
-  "threepidCreds": [
-    {
-      "sid": "<identity server session id>",
-      "client_secret": "<identity server client secret>",
-      "id_server": "<url of identity server authed with, e.g. 'matrix.org:8090'>",
-      "id_access_token": "<access token previously registered with the identity server>"
-    }
-  ],
+  "threepid_creds": {
+    "sid": "<identity server session id>",
+    "client_secret": "<identity server client secret>",
+    "id_server": "<url of identity server authed with, e.g. 'matrix.org:8090'>",
+    "id_access_token": "<access token previously registered with the identity server>"
+  },
   "session": "<session ID>"
 })"_json);
 
-    a.content = auth::MSISDN{{
-      {"<identity server session id>",
-       "<identity server client secret>",
-       "<url of identity server authed with, e.g. 'matrix.org:8090'>",
-       "<access token previously registered with the identity server>"},
-    }};
+    a.content = auth::MSISDN{"<identity server session id>",
+                             "<identity server client secret>",
+                             "<url of identity server authed with, e.g. 'matrix.org:8090'>",
+                             "<access token previously registered with the identity server>"};
 
     EXPECT_EQ(nlohmann::json(a), R"({
   "type": "m.login.msisdn",
-  "threepidCreds": [
-    {
-      "sid": "<identity server session id>",
-      "client_secret": "<identity server client secret>",
-      "id_server": "<url of identity server authed with, e.g. 'matrix.org:8090'>",
-      "id_access_token": "<access token previously registered with the identity server>"
-    }
-  ],
+  "threepid_creds": {
+    "sid": "<identity server session id>",
+    "client_secret": "<identity server client secret>",
+    "id_server": "<url of identity server authed with, e.g. 'matrix.org:8090'>",
+    "id_access_token": "<access token previously registered with the identity server>"
+  },
   "session": "<session ID>"
 })"_json);
 
-    a.content = auth::MSISDN{{
-      {"<identity server session id>", "<identity server client secret>", "", ""},
-    }};
+    a.content =
+      auth::MSISDN{"<identity server session id>", "<identity server client secret>", "", ""};
 
     EXPECT_EQ(nlohmann::json(a), R"({
   "type": "m.login.msisdn",
-  "threepidCreds": [
-    {
-      "sid": "<identity server session id>",
-      "client_secret": "<identity server client secret>"
-    }
-  ],
+  "threepid_creds": {
+    "sid": "<identity server session id>",
+    "client_secret": "<identity server client secret>"
+  },
   "session": "<session ID>"
 })"_json);
 
-- 
GitLab