First working prototype to proxy 3PID binds to central Matrix.org IS

This commit is contained in:
Maxime Dor
2017-09-23 04:27:14 +02:00
parent 5836965a1e
commit df81dda22d
26 changed files with 1200 additions and 158 deletions

View File

@@ -79,6 +79,9 @@ dependencies {
// Spring Boot - standalone app
compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE'
// Thymeleaf for HTML templates
compile "org.springframework.boot:spring-boot-starter-thymeleaf:1.5.3.RELEASE"
// Matrix Java SDK
compile 'io.kamax:matrix-java-sdk:0.0.2'

View File

@@ -0,0 +1,42 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.resourceresolver.FileResourceResolver;
import org.thymeleaf.templateresolver.TemplateResolver;
@Configuration
public class ThymeleafConfig {
@Bean
public TemplateResolver getFileSystemResolver() {
TemplateResolver resolver = new TemplateResolver();
resolver.setPrefix("");
resolver.setSuffix("");
resolver.setCacheable(false);
resolver.setOrder(1);
resolver.setResourceResolver(new FileResourceResolver());
return resolver;
}
}

View File

@@ -0,0 +1,144 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.config;
import com.google.gson.Gson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("view")
public class ViewConfig {
private Logger log = LoggerFactory.getLogger(ViewConfig.class);
public static class Session {
public static class Paths {
private String failure;
private String success;
public String getFailure() {
return failure;
}
public void setFailure(String failure) {
this.failure = failure;
}
public String getSuccess() {
return success;
}
public void setSuccess(String success) {
this.success = success;
}
}
public static class Local {
private Paths onTokenSubmit = new Paths();
public Paths getOnTokenSubmit() {
return onTokenSubmit;
}
public void setOnTokenSubmit(Paths onTokenSubmit) {
this.onTokenSubmit = onTokenSubmit;
}
}
public static class Remote {
private Paths onRequest = new Paths();
private Paths onCheck = new Paths();
public Paths getOnRequest() {
return onRequest;
}
public void setOnRequest(Paths onRequest) {
this.onRequest = onRequest;
}
public Paths getOnCheck() {
return onCheck;
}
public void setOnCheck(Paths onCheck) {
this.onCheck = onCheck;
}
}
private Local local = new Local();
private Local localRemote = new Local();
private Remote remote = new Remote();
public Local getLocal() {
return local;
}
public void setLocal(Local local) {
this.local = local;
}
public Local getLocalRemote() {
return localRemote;
}
public void setLocalRemote(Local localRemote) {
this.localRemote = localRemote;
}
public Remote getRemote() {
return remote;
}
public void setRemote(Remote remote) {
this.remote = remote;
}
}
private Session session = new Session();
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
@PostConstruct
public void build() {
log.info("--- View config ---");
log.info("Session: {}", new Gson().toJson(session));
}
}

View File

@@ -20,146 +20,62 @@
package io.kamax.mxisd.controller.v1
import com.google.gson.Gson
import com.google.gson.JsonObject
import io.kamax.matrix.ThreePidMedium
import io.kamax.mxisd.ThreePid
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson
import io.kamax.mxisd.exception.BadRequestException
import io.kamax.mxisd.exception.SessionNotValidatedException
import io.kamax.mxisd.invitation.InvitationManager
import io.kamax.mxisd.lookup.ThreePidValidation
import io.kamax.mxisd.config.ServerConfig
import io.kamax.mxisd.config.ViewConfig
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1
import io.kamax.mxisd.session.SessionMananger
import org.apache.commons.io.IOUtils
import org.apache.http.HttpStatus
import io.kamax.mxisd.session.ValidationResult
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.nio.charset.StandardCharsets
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@Controller
@RequestMapping(path = IdentityAPIv1.BASE)
class SessionController {
private Logger log = LoggerFactory.getLogger(SessionController.class)
@Autowired
private ServerConfig srvCfg;
@Autowired
private SessionMananger mgr
@Autowired
private InvitationManager invMgr;
private Gson gson = new Gson()
private Logger log = LoggerFactory.getLogger(SessionController.class)
private <T> T fromJson(HttpServletRequest req, Class<T> obj) {
gson.fromJson(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8), obj)
}
@RequestMapping(value = "/validate/{medium}/requestToken")
String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString())
if (ThreePidMedium.Email.is(medium)) {
SessionEmailTokenRequestJson req = fromJson(request, SessionEmailTokenRequestJson.class)
return gson.toJson(new Sid(mgr.create(
request.getRemoteHost(),
new ThreePid(req.getMedium(), req.getValue()),
req.getSecret(),
req.getAttempt(),
req.getNextLink())));
}
if (ThreePidMedium.PhoneNumber) {
SessionPhoneTokenRequestJson req = fromJson(request, SessionPhoneTokenRequestJson.class)
return gson.toJson(new Sid(mgr.create(
request.getRemoteHost(),
new ThreePid(req.getMedium(), req.getValue()),
req.getSecret(),
req.getAttempt(),
req.getNextLink())));
}
JsonObject obj = new JsonObject();
obj.addProperty("errcode", "M_INVALID_3PID_TYPE")
obj.addProperty("error", medium + " is not supported as a 3PID type")
response.setStatus(HttpStatus.SC_BAD_REQUEST)
return gson.toJson(obj)
}
private ViewConfig viewCfg;
@RequestMapping(value = "/validate/{medium}/submitToken")
String validate(HttpServletRequest request,
String validate(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam String sid,
@RequestParam("client_secret") String secret, @RequestParam String token) {
@RequestParam("client_secret") String secret,
@RequestParam String token,
Model model
) {
log.info("Requested: {}?{}", request.getRequestURL(), request.getQueryString())
mgr.validate(sid, secret, token)
return "{}"
ValidationResult r = mgr.validate(sid, secret, token)
log.info("Session {} was validated", sid)
if (r.getNextUrl().isPresent()) {
String url = srvCfg.getPublicUrl() + r.getNextUrl().get()
log.info("Session {} validation: next URL is present, redirecting to {}", sid, url)
response.sendRedirect(url)
} else {
if (r.isCanRemote()) {
String url = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.getRequestToken(r.getSession().getId(), r.getSession().getSecret());
model.addAttribute("remoteSessionLink", url)
return viewCfg.getSession().getLocalRemote().getOnTokenSubmit().getSuccess()
} else {
return viewCfg.getSession().getLocal().getOnTokenSubmit().getSuccess()
}
@RequestMapping(value = "/3pid/getValidated3pid")
String check(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret) {
log.info("Requested: {}", request.getRequestURL(), request.getQueryString())
try {
ThreePidValidation pid = mgr.getValidated(sid, secret)
JsonObject obj = new JsonObject()
obj.addProperty("medium", pid.getMedium())
obj.addProperty("address", pid.getAddress())
obj.addProperty("validated_at", pid.getValidation().toEpochMilli())
return gson.toJson(obj);
} catch (SessionNotValidatedException e) {
log.info("Session {} was requested but has not yet been validated", sid);
throw e;
}
}
@RequestMapping(value = "/3pid/bind")
String bind(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) {
String data = IOUtils.toString(request.getReader())
log.info("Requested: {}", request.getRequestURL(), request.getQueryString())
try {
mgr.bind(sid, secret, mxid)
return "{}"
} catch (BadRequestException e) {
log.info("requested session was not validated")
JsonObject obj = new JsonObject()
obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED")
obj.addProperty("error", e.getMessage())
response.setStatus(HttpStatus.SC_BAD_REQUEST)
return gson.toJson(obj)
} finally {
// If a user registers, there is no standard login event. Instead, this is the only way to trigger
// resolution at an appropriate time. Meh at synapse/Riot!
invMgr.lookupMappingsForInvites()
}
}
private class Sid {
private String sid;
public Sid(String sid) {
setSid(sid);
}
String getSid() {
return sid
}
void setSid(String sid) {
this.sid = sid
}
}

View File

@@ -0,0 +1,159 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.controller.v1.io.SessionEmailTokenRequestJson;
import io.kamax.mxisd.controller.v1.io.SessionPhoneTokenRequestJson;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.util.GsonParser;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@CrossOrigin
@RequestMapping(path = IdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class SessionRestController {
private Logger log = LoggerFactory.getLogger(SessionRestController.class);
private class Sid { // FIXME replace with RequestTokenResponse
private String sid;
public Sid(String sid) {
setSid(sid);
}
String getSid() {
return sid;
}
void setSid(String sid) {
this.sid = sid;
}
}
@Autowired
private ServerConfig srvCfg;
@Autowired
private SessionMananger mgr;
@Autowired
private InvitationManager invMgr;
@Autowired
private ViewConfig viewCfg;
private Gson gson = new Gson();
private GsonParser parser = new GsonParser(gson);
@RequestMapping(value = "/validate/{medium}/requestToken")
String init(HttpServletRequest request, HttpServletResponse response, @PathVariable String medium) throws IOException {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString());
if (ThreePidMedium.Email.is(medium)) {
SessionEmailTokenRequestJson req = parser.parse(request, SessionEmailTokenRequestJson.class);
return gson.toJson(new Sid(mgr.create(
request.getRemoteHost(),
new ThreePid(req.getMedium(), req.getValue()),
req.getSecret(),
req.getAttempt(),
req.getNextLink())));
}
if (ThreePidMedium.PhoneNumber.is(medium)) {
SessionPhoneTokenRequestJson req = parser.parse(request, SessionPhoneTokenRequestJson.class);
return gson.toJson(new Sid(mgr.create(
request.getRemoteHost(),
new ThreePid(req.getMedium(), req.getValue()),
req.getSecret(),
req.getAttempt(),
req.getNextLink())));
}
JsonObject obj = new JsonObject();
obj.addProperty("errcode", "M_INVALID_3PID_TYPE");
obj.addProperty("error", medium + " is not supported as a 3PID type");
response.setStatus(HttpStatus.SC_BAD_REQUEST);
return gson.toJson(obj);
}
@RequestMapping(value = "/3pid/getValidated3pid")
String check(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret) {
log.info("Requested: {}", request.getRequestURL(), request.getQueryString());
try {
ThreePidValidation pid = mgr.getValidated(sid, secret);
JsonObject obj = new JsonObject();
obj.addProperty("medium", pid.getMedium());
obj.addProperty("address", pid.getAddress());
obj.addProperty("validated_at", pid.getValidation().toEpochMilli());
return gson.toJson(obj);
} catch (SessionNotValidatedException e) {
log.info("Session {} was requested but has not yet been validated", sid);
throw e;
}
}
@RequestMapping(value = "/3pid/bind")
String bind(HttpServletRequest request, HttpServletResponse response,
@RequestParam String sid, @RequestParam("client_secret") String secret, @RequestParam String mxid) {
log.info("Requested: {}", request.getRequestURL(), request.getQueryString());
try {
mgr.bind(sid, secret, mxid);
return "{}";
} catch (BadRequestException e) {
log.info("requested session was not validated");
JsonObject obj = new JsonObject();
obj.addProperty("errcode", "M_SESSION_NOT_VALIDATED");
obj.addProperty("error", e.getMessage());
response.setStatus(HttpStatus.SC_BAD_REQUEST);
return gson.toJson(obj);
} finally {
// If a user registers, there is no standard login event. Instead, this is the only way to trigger
// resolution at an appropriate time. Meh at synapse/Riot!
invMgr.lookupMappingsForInvites();
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.controller.v1.io;
public class RequestTokenResponse {
private String sid;
public String getSid() {
return sid;
}
}

View File

@@ -22,6 +22,16 @@ package io.kamax.mxisd.controller.v1.remote;
public class RemoteIdentityAPIv1 {
public static final String BASE = "/_matrix/identity-remote/api/v1";
public static final String BASE = "/_matrix/identity/remote/api/v1";
public static final String SESSION_REQUEST_TOKEN = BASE + "/validate/requestToken";
public static final String SESSION_CHECK = BASE + "/validate/check";
public static String getRequestToken(String id, String secret) {
return SESSION_REQUEST_TOKEN + "?sid=" + id + "&client_secret=" + secret;
}
public static String getSessionCheck(String id, String secret) {
return SESSION_CHECK + "?sid=" + id + "&client_secret=" + secret;
}
}

View File

@@ -1,37 +1,59 @@
package io.kamax.mxisd.controller.v1.remote;
import io.kamax.mxisd.config.ViewConfig;
import io.kamax.mxisd.exception.SessionNotValidatedException;
import io.kamax.mxisd.session.SessionMananger;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@CrossOrigin
@RequestMapping(path = RemoteIdentityAPIv1.BASE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_CHECK;
import static io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1.SESSION_REQUEST_TOKEN;
@Controller
public class RemoteSessionController {
private Logger log = LoggerFactory.getLogger(RemoteSessionController.class);
@Autowired
private ViewConfig viewCfg;
@Autowired
private SessionMananger mgr;
@RequestMapping(path = "/validate/requestToken")
@RequestMapping(path = SESSION_REQUEST_TOKEN)
public String requestToken(
HttpServletRequest request,
@RequestParam String sid,
@RequestParam("client_secret") String secret,
@RequestParam String token) {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL(), request.getQueryString());
mgr.createRemote(sid, secret, token);
Model model
) {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL());
IThreePidSession session = mgr.createRemote(sid, secret);
model.addAttribute("checkLink", RemoteIdentityAPIv1.getSessionCheck(session.getId(), session.getSecret()));
return viewCfg.getSession().getRemote().getOnRequest().getSuccess();
}
return "{}";
@RequestMapping(path = SESSION_CHECK)
public String check(
HttpServletRequest request,
@RequestParam String sid,
@RequestParam("client_secret") String secret) {
log.info("Request {}: {}", request.getMethod(), request.getRequestURL());
try {
mgr.validateRemote(sid, secret);
return viewCfg.getSession().getRemote().getOnCheck().getSuccess();
} catch (SessionNotValidatedException e) {
return viewCfg.getSession().getRemote().getOnCheck().getFailure();
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception;
public class SessionUnknownException extends MatrixException {
public SessionUnknownException() {
this("No valid session was found matching that sid and client secret");
}
public SessionUnknownException(String error) {
super(200, "M_NO_VALID_SESSION", error);
}
}

View File

@@ -21,10 +21,14 @@
package io.kamax.mxisd.session;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.SessionConfig;
import io.kamax.mxisd.controller.v1.io.RequestTokenResponse;
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
import io.kamax.mxisd.exception.*;
import io.kamax.mxisd.lookup.ThreePidValidation;
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
@@ -32,20 +36,28 @@ import io.kamax.mxisd.matrix.IdentityServerUtils;
import io.kamax.mxisd.notification.NotificationManager;
import io.kamax.mxisd.storage.IStorage;
import io.kamax.mxisd.storage.dao.IThreePidSessionDao;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import io.kamax.mxisd.threepid.session.ThreePidSession;
import io.kamax.mxisd.util.GsonParser;
import io.kamax.mxisd.util.RestClientUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.RandomStringUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -91,7 +103,7 @@ public class SessionMananger {
private ThreePidSession getSession(String sid, String secret) {
Optional<IThreePidSessionDao> dao = storage.getThreePidSession(sid);
if (!dao.isPresent() || !StringUtils.equals(dao.get().getSecret(), secret)) {
throw new InvalidCredentialsException();
throw new SessionUnknownException();
}
return new ThreePidSession(dao.get());
@@ -165,13 +177,28 @@ public class SessionMananger {
}
}
public Optional<String> validate(String sid, String secret, String token) {
public ValidationResult validate(String sid, String secret, String token) {
ThreePidSession session = getSession(sid, secret);
log.info("Attempting validation for session {} from {}", session.getId(), session.getServer());
boolean isLocal = isLocal(session.getThreePid());
PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal);
if (!policy.isEnabled()) {
throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed");
}
session.validate(token);
storage.updateThreePidSession(session.getDao());
log.info("Session {} has been validated", session.getId());
return session.getNextLink();
// FIXME definitely doable in a nicer way
ValidationResult r = new ValidationResult(session, policy.toRemote());
if (!policy.toLocal()) {
r.setNextUrl(RemoteIdentityAPIv1.getRequestToken(sid, secret));
} else {
session.getNextLink().ifPresent(r::setNextUrl);
}
return r;
}
public ThreePidValidation getValidated(String sid, String secret) {
@@ -179,34 +206,74 @@ public class SessionMananger {
return new ThreePidValidation(session.getThreePid(), session.getValidationTime());
}
public void bind(String sid, String secret, String mxid) {
public void bind(String sid, String secret, String mxidRaw) {
_MatrixID mxid = new MatrixID(mxidRaw);
ThreePidSession session = getSessionIfValidated(sid, secret);
log.info("Accepting bind of {} on session {} from server {}", mxid, session.getId(), session.getServer());
// TODO perform this if request was proxied
if (!session.isRemote()) {
log.info("Session {} for {}: MXID {} was bound locally", sid, session.getThreePid(), mxid);
return;
}
public void createRemote(String sid, String secret, String token) {
log.info("Session {} for {}: MXID {} bind is remote", sid, session.getThreePid(), mxid);
if (!session.isRemoteValidated()) {
log.error("Session {} for {}: Not validated remotely", sid, session.getThreePid());
throw new SessionNotValidatedException();
}
log.info("Session {} for {}: Performing remote bind", sid, session.getThreePid());
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(
Arrays.asList(
new BasicNameValuePair("sid", session.getRemoteId()),
new BasicNameValuePair("client_secret", session.getRemoteSecret()),
new BasicNameValuePair("mxid", mxid.getId())
), StandardCharsets.UTF_8);
HttpPost bindReq = new HttpPost(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/bind");
bindReq.setEntity(entity);
try (CloseableHttpResponse response = client.execute(bindReq)) {
int status = response.getStatusLine().getStatusCode();
if (status < 200 || status >= 300) {
String body = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
log.error("Session {} for {}: Remote IS {} failed when trying to bind {} for remote session {}\n{}",
sid, session.getThreePid(), session.getRemoteServer(), mxid, session.getRemoteId(), body);
throw new RemoteIdentityServerException(body);
}
log.error("Session {} for {}: MXID {} was bound remotely", sid, session.getThreePid(), mxid);
} catch (IOException e) {
log.error("Session {} for {}: I/O Error when trying to bind mxid {}", sid, session.getThreePid(), mxid);
throw new RemoteIdentityServerException(e.getMessage());
}
}
public IThreePidSession createRemote(String sid, String secret) {
ThreePidSession session = getSessionIfValidated(sid, secret);
log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid);
boolean isLocal = isLocal(session.getThreePid());
PolicySource policy = cfg.getPolicy().getValidation().forIf(isLocal);
if (!policy.isEnabled() || !policy.toRemote()) {
throw new NotAllowedException("Validating " + (isLocal ? "local" : "remote") + " 3PID is not allowed");
}
log.info("Remote 3PID is allowed by policy");
List<String> servers = mxCfg.getIdentity().getServers(policy.getToRemote().getServer());
if (servers.isEmpty()) {
throw new InternalServerError();
}
String url = IdentityServerUtils.findIsUrlForDomain(servers.get(0)).orElseThrow(InternalServerError::new);
log.info("Will use IS endpoint {}", url);
String remoteSecret = session.isRemote() ? session.getRemoteSecret() : RandomStringUtils.randomAlphanumeric(16);
JsonObject body = new JsonObject();
body.addProperty("client_secret", RandomStringUtils.randomAlphanumeric(16));
body.addProperty("client_secret", remoteSecret);
body.addProperty(session.getThreePid().getMedium(), session.getThreePid().getAddress());
body.addProperty("send_attempt", 1);
body.addProperty("send_attempt", session.increaseAndGetRemoteAttempt());
log.info("Creating remote 3PID session for {} with local session [{}] to {}", session.getThreePid(), sid);
log.info("Requesting remote session with attempt {}", session.getRemoteAttempt());
HttpPost tokenReq = RestClientUtils.post(url + "/_matrix/identity/api/v1/validate/" + session.getThreePid().getMedium() + "/requestToken", body);
try (CloseableHttpResponse response = client.execute(tokenReq)) {
int status = response.getStatusLine().getStatusCode();
@@ -214,13 +281,66 @@ public class SessionMananger {
throw new RemoteIdentityServerException("Remote identity server returned with status " + status);
}
// TODO finish
RequestTokenResponse data = new GsonParser().parse(response, RequestTokenResponse.class);
log.info("Remote Session ID: {}", data.getSid());
session.setRemoteData(url, data.getSid(), remoteSecret, 1);
storage.updateThreePidSession(session.getDao());
log.info("Updated Session {} with remote data", sid);
return session;
} catch (IOException e) {
log.warn("Failed to create remote session with {} for {}: {}", url, session.getThreePid(), e.getMessage());
throw new RemoteIdentityServerException(e.getMessage());
}
}
public void validateRemote(String sid, String secret) {
ThreePidSession session = getSessionIfValidated(sid, secret);
if (!session.isRemote()) {
throw new NotAllowedException("Cannot remotely validate a local session");
}
log.info("Session {} for {}: Validating remote 3PID session {} on {}", sid, session.getThreePid(), session.getRemoteId(), session.getRemoteServer());
if (session.isRemoteValidated()) {
log.info("Session {} for {}: Already remotely validated", sid, session.getThreePid());
return;
}
HttpGet validateReq = new HttpGet(session.getRemoteServer() + "/_matrix/identity/api/v1/3pid/getValidated3pid?sid=" + session.getRemoteId() + "&client_secret=" + session.getRemoteSecret());
try (CloseableHttpResponse response = client.execute(validateReq)) {
int status = response.getStatusLine().getStatusCode();
if (status < 200 || status >= 300) {
throw new RemoteIdentityServerException("Remote identity server returned with status " + status);
}
JsonObject o = new GsonParser().parse(response.getEntity().getContent());
if (o.has("errcode")) {
String errcode = o.get("errcode").getAsString();
if (StringUtils.equals("M_SESSION_NOT_VALIDATED", errcode)) {
throw new SessionNotValidatedException();
} else if (StringUtils.equals("M_NO_VALID_SESSION", errcode)) {
throw new SessionUnknownException();
} else {
throw new RemoteIdentityServerException("Unknown error while validating Remote 3PID session: " + errcode + " - " + o.get("error").getAsString());
}
}
if (o.has("validated_at")) {
ThreePid remoteThreePid = new ThreePid(o.get("medium").getAsString(), o.get("address").getAsString());
if (session.getThreePid().equals(remoteThreePid)) { // sanity check
throw new InternalServerError("Local 3PID " + session.getThreePid() + " and remote 3PID " + remoteThreePid + " do not match for session " + session.getId());
}
log.info("Session {} for {}: Remotely validated successfully", sid, session.getThreePid());
session.validateRemote();
storage.updateThreePidSession(session.getDao());
log.info("Session {} was updated in storage", sid);
}
} catch (IOException e) {
log.warn("Session {} for {}: Failed to validated remotely on {}: {}", sid, session.getThreePid(), session.getRemoteServer(), e.getMessage());
throw new RemoteIdentityServerException(e.getMessage());
}
}
}

View File

@@ -0,0 +1,54 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
*
* https://max.kamax.io/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.session;
import io.kamax.mxisd.threepid.session.IThreePidSession;
import java.util.Optional;
public class ValidationResult {
private IThreePidSession session;
private boolean canRemote;
private String nextUrl;
public ValidationResult(IThreePidSession session, boolean canRemote) {
this.session = session;
this.canRemote = canRemote;
}
public IThreePidSession getSession() {
return session;
}
public boolean isCanRemote() {
return canRemote;
}
public Optional<String> getNextUrl() {
return Optional.ofNullable(nextUrl);
}
public void setNextUrl(String nextUrl) {
this.nextUrl = nextUrl;
}
}

View File

@@ -44,4 +44,16 @@ public interface IThreePidSessionDao {
long getValidationTime();
boolean isRemote();
String getRemoteServer();
String getRemoteId();
String getRemoteSecret();
int getRemoteAttempt();
boolean isRemoteValidated();
}

View File

@@ -61,6 +61,24 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
@DatabaseField
private long validationTime;
@DatabaseField(canBeNull = false)
private boolean isRemote;
@DatabaseField
private String remoteServer;
@DatabaseField
private String remoteId;
@DatabaseField
private String remoteSecret;
@DatabaseField
private Integer remoteAttempt;
@DatabaseField(canBeNull = false)
private boolean isRemoteValidated;
public ThreePidSessionDao() {
// stub for ORMLite
}
@@ -77,7 +95,12 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
setToken(session.getToken());
setValidated(session.getValidated());
setValidationTime(session.getValidationTime());
setRemote(session.isRemote());
setRemoteServer(session.getRemoteServer());
setRemoteId(session.getRemoteId());
setRemoteSecret(session.getRemoteSecret());
setRemoteAttempt(session.getRemoteAttempt());
setRemoteValidated(session.isRemoteValidated());
}
public ThreePidSessionDao(ThreePid tpid, String secret) {
@@ -180,6 +203,60 @@ public class ThreePidSessionDao implements IThreePidSessionDao {
return validationTime;
}
@Override
public boolean isRemote() {
return isRemote;
}
public void setRemote(boolean remote) {
isRemote = remote;
}
@Override
public String getRemoteServer() {
return remoteServer;
}
public void setRemoteServer(String remoteServer) {
this.remoteServer = remoteServer;
}
@Override
public String getRemoteId() {
return remoteId;
}
public void setRemoteId(String remoteId) {
this.remoteId = remoteId;
}
@Override
public String getRemoteSecret() {
return remoteSecret;
}
public void setRemoteSecret(String remoteSecret) {
this.remoteSecret = remoteSecret;
}
@Override
public int getRemoteAttempt() {
return remoteAttempt;
}
@Override
public boolean isRemoteValidated() {
return isRemoteValidated;
}
public void setRemoteValidated(boolean remoteValidated) {
isRemoteValidated = remoteValidated;
}
public void setRemoteAttempt(int remoteAttempt) {
this.remoteAttempt = remoteAttempt;
}
public void setValidationTime(long validationTime) {
this.validationTime = validationTime;
}

View File

@@ -26,7 +26,6 @@ import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.threepid.medium.EmailConfig;
import io.kamax.mxisd.config.threepid.medium.EmailTemplateConfig;
import io.kamax.mxisd.controller.v1.IdentityAPIv1;
import io.kamax.mxisd.controller.v1.remote.RemoteIdentityAPIv1;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.invitation.IThreePidInviteReply;
import io.kamax.mxisd.threepid.session.IThreePidSession;
@@ -140,16 +139,14 @@ public class EmailNotificationGenerator implements IEmailNotificationGenerator {
log.info("Generating notification content for remote-only 3PID session");
String templateBody = getTemplateAndPopulate(templateCfg.getSession().getValidation().getRemote(), session.getThreePid());
// FIXME should have a global link builder, specific to mxisd
String nextStepLink = srvCfg.getPublicUrl() + RemoteIdentityAPIv1.BASE +
"/validate/requestToken?sid=" + session.getId() +
"&client_secret=" + session.getSecret() +
// FIXME should have a global link builder, most likely in the SDK?
String validationLink = srvCfg.getPublicUrl() + IdentityAPIv1.BASE +
"/validate/" + session.getThreePid().getMedium() +
"/submitToken?sid=" + session.getId() + "&client_secret=" + session.getSecret() +
"&token=" + session.getToken();
templateBody = templateBody.replace("%SESSION_ID%", session.getId());
templateBody = templateBody.replace("%SESSION_SECRET%", session.getSecret());
templateBody = templateBody.replace("%SESSION_TOKEN%", session.getToken());
templateBody = templateBody.replace("%NEXT_STEP_LINK%", nextStepLink);
templateBody = templateBody.replace("%VALIDATION_LINK%", validationLink);
templateBody = templateBody.replace("%VALIDATION_TOKEN%", session.getToken());
return templateBody;
}

View File

@@ -51,4 +51,16 @@ public interface IThreePidSession {
Instant getValidationTime();
boolean isRemote();
String getRemoteServer();
String getRemoteId();
String getRemoteSecret();
int getRemoteAttempt();
void setRemoteData(String server, String id, String secret, int attempt);
}

View File

@@ -42,6 +42,12 @@ public class ThreePidSession implements IThreePidSession {
private int attempt;
private Instant validationTimestamp;
private boolean isValidated;
private boolean isRemote;
private String remoteServer;
private String remoteId;
private String remoteSecret;
private int remoteAttempt;
private boolean isRemoteValidated;
public ThreePidSession(IThreePidSessionDao dao) {
this(
@@ -58,6 +64,13 @@ public class ThreePidSession implements IThreePidSession {
if (isValidated) {
validationTimestamp = Instant.ofEpochMilli(dao.getValidationTime());
}
isRemote = dao.isRemote();
remoteServer = dao.getRemoteServer();
remoteId = dao.getRemoteId();
remoteSecret = dao.getRemoteSecret();
remoteAttempt = dao.getRemoteAttempt();
isRemoteValidated = dao.isRemoteValidated();
}
public ThreePidSession(String id, String server, ThreePid tPid, String secret, int attempt, String nextLink, String token) {
@@ -129,6 +142,44 @@ public class ThreePidSession implements IThreePidSession {
return validationTimestamp;
}
@Override
public boolean isRemote() {
return isRemote;
}
@Override
public String getRemoteServer() {
return remoteServer;
}
@Override
public String getRemoteId() {
return remoteId;
}
@Override
public String getRemoteSecret() {
return remoteSecret;
}
@Override
public int getRemoteAttempt() {
return remoteAttempt;
}
public int increaseAndGetRemoteAttempt() {
return ++remoteAttempt;
}
@Override
public void setRemoteData(String server, String id, String secret, int attempt) {
this.remoteServer = server;
this.remoteId = id;
this.remoteSecret = secret;
this.attempt = attempt;
this.isRemote = true;
}
@Override
public boolean isValidated() {
return isValidated;
@@ -151,6 +202,14 @@ public class ThreePidSession implements IThreePidSession {
isValidated = true;
}
public boolean isRemoteValidated() {
return isRemoteValidated;
}
public void validateRemote() {
this.isRemoteValidated = true;
}
public IThreePidSessionDao getDao() {
return new IThreePidSessionDao() {
@@ -209,6 +268,36 @@ public class ThreePidSession implements IThreePidSession {
return isValidated ? validationTimestamp.toEpochMilli() : 0;
}
@Override
public boolean isRemote() {
return isRemote;
}
@Override
public String getRemoteServer() {
return remoteServer;
}
@Override
public String getRemoteId() {
return remoteId;
}
@Override
public String getRemoteSecret() {
return remoteSecret;
}
@Override
public int getRemoteAttempt() {
return remoteAttempt;
}
@Override
public boolean isRemoteValidated() {
return isRemoteValidated;
}
};
}

View File

@@ -26,6 +26,7 @@ import io.kamax.mxisd.exception.JsonMemberNotFoundException;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -53,6 +54,10 @@ public class GsonParser {
return el.getAsJsonObject();
}
public <T> T parse(HttpServletRequest req, Class<T> type) throws IOException {
return gson.fromJson(parse(req.getInputStream()), type);
}
public <T> T parse(HttpResponse res, Class<T> type) throws IOException {
return gson.fromJson(parse(res.getEntity().getContent()), type);
}

View File

@@ -106,6 +106,24 @@ session.policy.validation:
enabled: true
server: 'root'
view:
session:
local:
onTokenSubmit:
success: 'session/local/tokenSubmitSuccess'
failure: 'session/local/tokenSubmitFailure'
localRemote:
onTokenSubmit:
success: 'session/localRemote/tokenSubmitSuccess'
failure: 'session/local/tokenSubmitFailure'
remote:
onRequest:
success: 'session/remote/requestSuccess'
failure: 'session/remote/requestFailure'
onCheck:
success: 'session/remote/checkSuccess'
failure: 'session/remote/checkFailure'
storage:
backend: 'sqlite'

View File

@@ -17,7 +17,7 @@ If you continue, your e-mail address and Matrix ID association will be made publ
If you would still like to continue, you will need to:
1. Go to your private Public registration process page:
%NEXT_STEP_LINK%
%VALIDATION_LINK%
2. Follow the registration process of the central Identity Server, usually another email with similar content
3. Once your email address validated with the central Identity Server, click on "Continue" on page of step #1
@@ -81,7 +81,7 @@ pre, code {
<p>If you would still like to continue, you will need to:
<ol>
<li>Go to your private <a href="%NEXT_STEP_LINK%">Public registration process page</a></li>
<li>Go to your private <a href="%VALIDATION_LINK%">Public registration process page</a></li>
<li>Follow the registration process of the central Identity Server, usually another email with similar content</li>
<li>Once your email address validated with the central Identity Server, click on "Continue" on page of step #1</li>
<li>If your public association is found by our Identity server, the next step will be given to you.</li>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix Token Verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification failed: you may need to request another verification email</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix Token Verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification successful: return to your Matrix client to complete the process.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<title>Matrix Token Verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification successful!</p>
<p>Your email will remain private and you will only be discoverable with it on your own server, or any related
servers configured by your system admin.<br/>
If you would like to be globally discoverable, start the process <a th:href="${remoteSessionLink}">here</a>.
<br/>If you chose to start the global publication process, wait until it is done before returning to your
client.</p>
<p>If the remote process is finished, or if you do not wish to start it at this time, you can now return to your
Matrix client to complete the process.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>You do not seem to have validated your session with the global server. Please check your messages for one similar
to the one you received initially.<br/>
Once this is done, <a href="#">click here to continue</a></p>
<p>If this problem persists, contact your system administrator with the following info: Reference #ABC</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>Verification successful!</p>
<p>Return to your Matrix client to complete the process and make yourself globally discoverable.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>The process to be globally discoverable has failed!<br/>You can try to refresh this page in a few seconds or
minutes.</p>
<p>If this problem persists, contact your system administrator with the following info: Reference #ABC</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8"/>
<title>Matrix global token verification</title>
<style>
body {
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
font-size: 12pt;
margin: 1em;
}
#message {
width: 1200px;
text-align: left;
padding: 1em;
margin-bottom: 40px;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
-webkit-border-radius: 10px;
-moz-border-radius: 10px;
border-radius: 10px;
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
background-color: #f8f8f8;
border: 1px #ccc solid;
}
</style>
</head>
<body>
<div id="message">
<p>The process to be globally discoverable has started. A verification token has been requested on your behalf.</p>
<p>You will receive a similar communication as the first verification message.<br/>
Follow the instructions and come back to this page once you are told to return to your Matrix client or that the
verification was successful.</p>
<p>Once the validation was successful with the global server, please follow <a th:href="${checkLink}">this link</a>
to validate it with us.</p>
</div>
</body>
</html>