Compare commits

..

6 Commits

Author SHA1 Message Date
Maxime Dor
61addd297a Use the correct formatting for MSISDN 2017-09-26 04:26:39 +02:00
Maxime Dor
1de0951733 Support 3PID listing during auth with Google Firebase 2017-09-26 03:11:15 +02:00
Maxime Dor
d348ebd813 Improved README to point to dedicated documents 2017-09-25 18:25:58 +02:00
Maxime Dor
0499c10a2c Better sample config, better README 2017-09-25 18:20:18 +02:00
Maxime Dor
13e248c71e Do not enforce Twilio config by default 2017-09-25 18:04:21 +02:00
Maxime Dor
d221b2c5de Fix groovy rollback issue 2017-09-25 17:56:06 +02:00
14 changed files with 240 additions and 119 deletions

View File

@@ -171,7 +171,8 @@ systemctl start mxisd
After following the specific instructions to create a config file from the sample:
1. Set the `matrix.domain` value to the domain value used in your Home Server configuration
2. Set an absolute location for the signing keys using `key.path`
3. Configure the E-mail invite sender with items starting in `invite.sender.email`
3. Configure the E-mail notification sender following [the documentation](docs/threepids/medium/email/smtp-connector.md)
4. If you would like to support Phone number validation, see the [Twilio configuration](docs/threepids/medium/msisdn/twilio-connector.md)
In case your IS public domain does not match your Matrix domain, see `server.name` and `server.publicUrl`
config items.

View File

@@ -301,18 +301,19 @@ key.path: '/path/to/sign.key'
#############################
# 3PID invites config items #
#############################
###################################
# 3PID notifications config items #
###################################
# If you would like to change the content, see https://github.com/kamax-io/mxisd/blob/master/docs/threepids/notifications/template-generator.md
#
#### E-mail invite sender
#
# SMTP host
invite.sender.email.host: "smtp.example.org"
threepid.medium.email.connectors.smtp.host: "smtp.example.org"
# SMTP port
invite.sender.email.port: 587
threepid.medium.email.connectors.smtp.port: 587
# TLS mode for the connection.
@@ -322,51 +323,19 @@ invite.sender.email.port: 587
# 1 Enable TLS if supported by server
# 2 Force TLS and fail if not available
#
#invite.sender.email.tls: 1
#threepid.medium.email.connectors.smtp.tls: 1
# Login for SMTP
invite.sender.email.login: "matrix-identity@example.org"
threepid.medium.email.connectors.smtp.login: "matrix-identity@example.org"
# Password for the account
invite.sender.email.password: "ThePassword"
threepid.medium.email.connectors.smtp.password: "ThePassword"
# The e-mail to send as. If empty, will be the same as login
invite.sender.email.email: "matrix-identity@example.org"
# The display name used in the e-mail
#
#invite.sender.email.name: "mxisd Identity Server"
# The E-mail template to use, using built-in template by default
#
# The template is expected to be a full e-mail body, including client headers, using MIME and UTF-8 encoding.
# The following headers will be set by mxisd directly and should not be present in the template:
# - From
# - To
# - Date
# - Message-Id
# - X-Mailer
#
# The following placeholders are available:
# - %DOMAIN% Domain name as per server.name config item
# - %DOMAIN_PRETTY% Word capitalize version of the domain. e.g. example.org -> Example.org
# - %FROM_EMAIL% Value of this section's email config item
# - %FROM_NAME% Value of this section's name config item
# - %SENDER_ID% Matrix ID of the invitation sender
# - %SENDER_NAME% Display name of the invitation sender, empty if not available
# - %SENDER_NAME_OR_ID% Value of %SENDER_NAME% or, if empty, value of %SENDER_ID%
# - %INVITE_MEDIUM% Medium of the invite (e.g. email, msisdn)
# - %INVITE_ADDRESS% Address used to invite
# - %ROOM_ID% ID of the room where the invitation took place
# - %ROOM_NAME% Name of the room, empty if not available
# - %ROOM_NAME_OR_ID% Value of %ROOM_NAME% or, if empty, value of %ROOM_ID%
#
#invite.sender.email.template: "/absolute/path/to/file"
threepid.medium.email.identity.from: "matrix-identity@example.org"

View File

@@ -24,7 +24,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
class MatrixIdentityServerApplication {
public class MatrixIdentityServerApplication {
public static void main(String[] args) {
SpringApplication.run(MatrixIdentityServerApplication.class, args);

View File

@@ -48,4 +48,22 @@ public class ThreePid {
return getMedium() + ":" + getAddress();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ThreePid threePid = (ThreePid) o;
if (!medium.equals(threePid.medium)) return false;
return address.equals(threePid.address);
}
@Override
public int hashCode() {
int result = medium.hashCode();
result = 31 * result + address.hashCode();
return result;
}
}

View File

@@ -71,14 +71,14 @@ public class AuthManager {
continue;
}
UserAuthResult authResult = new UserAuthResult().success(mxId, result.getProfile().getDisplayName());
UserAuthResult authResult = new UserAuthResult().success(result.getProfile().getDisplayName());
for (ThreePid pid : result.getProfile().getThreePids()) {
authResult.withThreePid(pid.getMedium(), pid.getAddress());
}
log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName());
for (ThreePid pid : authResult.getThreePids()) {
log.info("Processing {} for {}", pid, id);
invMgr.publishMappingIfInvited(new ThreePidMapping(pid, authResult.getMxid()));
invMgr.publishMappingIfInvited(new ThreePidMapping(pid, mxId));
}
invMgr.lookupMappingsForInvites();

View File

@@ -20,31 +20,30 @@
package io.kamax.mxisd.auth;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
public class UserAuthResult {
private boolean success;
private String mxid;
private String displayName;
private List<ThreePid> threePids = new ArrayList<>();
private String photo;
private Set<ThreePid> threePids = new HashSet<>();
public UserAuthResult failure() {
success = false;
mxid = null;
displayName = null;
photo = null;
threePids.clear();
return this;
}
public UserAuthResult success(String mxid, String displayName) {
public UserAuthResult success(String displayName) {
setSuccess(true);
setMxid(mxid);
setDisplayName(displayName);
return this;
@@ -58,14 +57,6 @@ public class UserAuthResult {
this.success = success;
}
public String getMxid() {
return mxid;
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getDisplayName() {
return displayName;
}
@@ -74,8 +65,12 @@ public class UserAuthResult {
this.displayName = displayName;
}
public UserAuthResult withThreePid(ThreePidMedium medium, String address) {
return withThreePid(medium.getId(), address);
public String getPhoto() {
return photo;
}
public void setPhoto(String photo) {
this.photo = photo;
}
public UserAuthResult withThreePid(String medium, String address) {
@@ -84,8 +79,8 @@ public class UserAuthResult {
return this;
}
public List<ThreePid> getThreePids() {
return Collections.unmodifiableList(threePids);
public Set<ThreePid> getThreePids() {
return Collections.unmodifiableSet(threePids);
}
}

View File

@@ -24,21 +24,21 @@ import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.UserID;
import io.kamax.mxisd.UserIdType;
import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
public class BackendAuthResult {
public static class BackendAuthProfile {
private String displayName;
private List<ThreePid> threePids = new ArrayList<>();
private Set<ThreePid> threePids = new HashSet<>();
public String getDisplayName() {
return displayName;
}
public List<ThreePid> getThreePids() {
public Set<ThreePid> getThreePids() {
return threePids;
}
}

View File

@@ -25,6 +25,9 @@ import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials;
import com.google.firebase.auth.UserInfo;
import com.google.i18n.phonenumbers.NumberParseException;
import com.google.i18n.phonenumbers.PhoneNumberUtil;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid;
@@ -48,14 +51,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, String purpose) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.fail();
}
}
private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = isEnabled;
@@ -73,6 +69,42 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
}
}
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, String purpose) {
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.fail();
}
}
private void toEmail(BackendAuthResult result, String email) {
if (StringUtils.isBlank(email)) {
return;
}
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), email));
}
private void toMsisdn(BackendAuthResult result, String phoneNumber) {
if (StringUtils.isBlank(phoneNumber)) {
return;
}
try {
String number = phoneUtil.format(
phoneUtil.parse(
phoneNumber,
null // No default region
),
PhoneNumberUtil.PhoneNumberFormat.E164
).substring(1); // We want without the leading +
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), number));
} catch (NumberParseException e) {
log.warn("Invalid phone number: {}", phoneNumber);
}
}
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
@@ -131,14 +163,15 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(user -> {
try {
if (StringUtils.isNotBlank(user.getEmail())) {
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
}
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
toEmail(result, user.getEmail());
toMsisdn(result, user.getPhoneNumber());
for (UserInfo info : user.getProviderData()) {
toEmail(result, info.getEmail());
toMsisdn(result, info.getPhoneNumber());
}
log.info("Got {} 3PIDs in profile", result.getProfile().getThreePids().size());
} finally {
userRecordLatch.countDown();
}

View File

@@ -20,8 +20,6 @@
package io.kamax.mxisd.config.threepid.connector;
import io.kamax.mxisd.exception.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -68,19 +66,6 @@ public class PhoneTwilioConfig {
@PostConstruct
public void build() {
log.info("--- Phone SMS Twilio connector config ---");
if (StringUtils.isBlank(getAccountSid())) {
throw new ConfigurationException(NAMESPACE + ".accountSid");
}
if (StringUtils.isBlank(getAuthToken())) {
throw new ConfigurationException(NAMESPACE + ".authToken");
}
if (StringUtils.isBlank(getNumber())) {
throw new ConfigurationException(NAMESPACE + ".number");
}
log.info("Account SID: {}", getAccountSid());
log.info("Sender number: {}", getNumber());
}

View File

@@ -23,10 +23,12 @@ package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.kamax.mxisd.auth.AuthManager;
import io.kamax.mxisd.auth.UserAuthResult;
import org.apache.commons.io.IOUtils;
import io.kamax.mxisd.controller.v1.io.CredentialsValidationResponse;
import io.kamax.mxisd.exception.JsonMemberNotFoundException;
import io.kamax.mxisd.util.GsonParser;
import io.kamax.mxisd.util.GsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -38,7 +40,6 @@ import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@RestController
@CrossOrigin
@@ -47,7 +48,8 @@ public class AuthController {
private Logger log = LoggerFactory.getLogger(AuthController.class);
private Gson gson = new Gson();
private Gson gson = GsonUtil.build();
private GsonParser parser = new GsonParser(gson);
@Autowired
private AuthManager mgr;
@@ -55,14 +57,9 @@ public class AuthController {
@RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST)
public String checkCredentials(HttpServletRequest req) {
try {
JsonElement el = new JsonParser().parse(IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8));
if (!el.isJsonObject() || !el.getAsJsonObject().has("user")) {
throw new IllegalArgumentException("Missing user key");
}
JsonObject authData = el.getAsJsonObject().get("user").getAsJsonObject();
JsonObject authData = parser.parse(req.getInputStream(), "user");
if (!authData.has("id") || !authData.has("password")) {
throw new IllegalArgumentException("Missing id or password keys");
throw new JsonMemberNotFoundException("Missing id or password keys");
}
String id = authData.get("id").getAsString();
@@ -70,16 +67,17 @@ public class AuthController {
String password = authData.get("password").getAsString();
UserAuthResult result = mgr.authenticate(id, password);
CredentialsValidationResponse response = new CredentialsValidationResponse(result.isSuccess());
JsonObject authObj = new JsonObject();
authObj.addProperty("success", result.isSuccess());
if (result.isSuccess()) {
authObj.addProperty("mxid", result.getMxid());
authObj.addProperty("display_name", result.getDisplayName());
response.setDisplayName(result.getDisplayName());
response.getProfile().setThreePids(result.getThreePids());
}
JsonObject obj = new JsonObject();
JsonElement authObj = gson.toJsonTree(response);
obj.add("authentication", authObj);
JsonObject obj = new JsonObject();
obj.add("auth", authObj);
obj.add("authentication", authObj); // TODO remove later, legacy support
return gson.toJson(obj);
} catch (IOException e) {
throw new RuntimeException(e);

View File

@@ -22,10 +22,7 @@ package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.exception.MatrixException;
import io.kamax.mxisd.exception.*;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -78,6 +75,18 @@ public class DefaultExceptionHandler {
return handle("M_INVALID_BODY", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(InvalidResponseJsonException.class)
public String handle(InvalidResponseJsonException e) {
return handle("M_INVALID_JSON", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(JsonMemberNotFoundException.class)
public String handle(JsonMemberNotFoundException e) {
return handle("M_JSON_MISSING_KEYS", e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MappingAlreadyExistsException.class)
public String handle(MappingAlreadyExistsException e) {

View File

@@ -0,0 +1,74 @@
/*
* 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;
import io.kamax.mxisd.ThreePid;
import java.util.HashSet;
import java.util.Set;
public class CredentialsValidationResponse {
public static class Profile {
private String displayName;
private Set<ThreePid> threePids = new HashSet<>();
public String getDisplayName() {
return displayName;
}
public Set<ThreePid> getThreePids() {
return threePids;
}
public void setThreePids(Set<ThreePid> threePids) {
this.threePids = new HashSet<>(threePids);
}
}
private boolean success;
private String displayName; // TODO remove later, legacy support
private Profile profile = new Profile();
public CredentialsValidationResponse(boolean success) {
this.success = success;
}
public boolean isSuccess() {
return success;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
this.profile.displayName = displayName;
}
public Profile getProfile() {
return profile;
}
}

View File

@@ -24,6 +24,8 @@ import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import io.kamax.mxisd.config.threepid.connector.PhoneTwilioConfig;
import io.kamax.mxisd.exception.BadRequestException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -51,6 +53,10 @@ public class PhoneSmsTwilioConnector implements IPhoneConnector {
@Override
public void send(String recipient, String content) {
if (StringUtils.isBlank(cfg.getAccountSid()) || StringUtils.isBlank(cfg.getAuthToken()) || StringUtils.isBlank(cfg.getNumber())) {
throw new BadRequestException("Phone numbers cannot be validated at this time. Contact your administrator.");
}
recipient = "+" + recipient;
log.info("Sending SMS notification from {} to {} with {} characters", cfg.getNumber(), recipient, content.length());
Message.creator(new PhoneNumber("+" + recipient), new PhoneNumber(cfg.getNumber()), content).create();

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.util;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class GsonUtil {
public static Gson build() {
return new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
}
}