Compare commits

...

3 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
10 changed files with 223 additions and 65 deletions

View File

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

View File

@@ -48,4 +48,22 @@ public class ThreePid {
return getMedium() + ":" + getAddress(); 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; continue;
} }
UserAuthResult authResult = new UserAuthResult().success(mxId, result.getProfile().getDisplayName()); UserAuthResult authResult = new UserAuthResult().success(result.getProfile().getDisplayName());
for (ThreePid pid : result.getProfile().getThreePids()) { for (ThreePid pid : result.getProfile().getThreePids()) {
authResult.withThreePid(pid.getMedium(), pid.getAddress()); authResult.withThreePid(pid.getMedium(), pid.getAddress());
} }
log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName()); log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName());
for (ThreePid pid : authResult.getThreePids()) { for (ThreePid pid : authResult.getThreePids()) {
log.info("Processing {} for {}", pid, id); log.info("Processing {} for {}", pid, id);
invMgr.publishMappingIfInvited(new ThreePidMapping(pid, authResult.getMxid())); invMgr.publishMappingIfInvited(new ThreePidMapping(pid, mxId));
} }
invMgr.lookupMappingsForInvites(); invMgr.lookupMappingsForInvites();

View File

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

View File

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

View File

@@ -25,6 +25,9 @@ import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential; import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials; 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.ThreePidMedium;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.ThreePid;
@@ -48,14 +51,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private FirebaseApp fbApp; private FirebaseApp fbApp;
private FirebaseAuth fbAuth; private FirebaseAuth fbAuth;
private void waitOnLatch(BackendAuthResult result, CountDownLatch l, String purpose) { private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for " + purpose);
result.fail();
}
}
public GoogleFirebaseAuthenticator(boolean isEnabled) { public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = 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 { private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) { if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath)); return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
@@ -131,14 +163,15 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
CountDownLatch userRecordLatch = new CountDownLatch(1); CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(user -> { fbAuth.getUser(token.getUid()).addOnSuccessListener(user -> {
try { try {
if (StringUtils.isNotBlank(user.getEmail())) { toEmail(result, user.getEmail());
result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail())); toMsisdn(result, user.getPhoneNumber());
}
for (UserInfo info : user.getProviderData()) {
if (StringUtils.isNotBlank(user.getPhoneNumber())) { toEmail(result, info.getEmail());
result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber())); toMsisdn(result, info.getPhoneNumber());
} }
log.info("Got {} 3PIDs in profile", result.getProfile().getThreePids().size());
} finally { } finally {
userRecordLatch.countDown(); userRecordLatch.countDown();
} }

View File

@@ -23,10 +23,12 @@ package io.kamax.mxisd.controller.v1;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.kamax.mxisd.auth.AuthManager; import io.kamax.mxisd.auth.AuthManager;
import io.kamax.mxisd.auth.UserAuthResult; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -38,7 +40,6 @@ import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
@RestController @RestController
@CrossOrigin @CrossOrigin
@@ -47,7 +48,8 @@ public class AuthController {
private Logger log = LoggerFactory.getLogger(AuthController.class); private Logger log = LoggerFactory.getLogger(AuthController.class);
private Gson gson = new Gson(); private Gson gson = GsonUtil.build();
private GsonParser parser = new GsonParser(gson);
@Autowired @Autowired
private AuthManager mgr; private AuthManager mgr;
@@ -55,14 +57,9 @@ public class AuthController {
@RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST) @RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST)
public String checkCredentials(HttpServletRequest req) { public String checkCredentials(HttpServletRequest req) {
try { try {
JsonElement el = new JsonParser().parse(IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8)); JsonObject authData = parser.parse(req.getInputStream(), "user");
if (!el.isJsonObject() || !el.getAsJsonObject().has("user")) {
throw new IllegalArgumentException("Missing user key");
}
JsonObject authData = el.getAsJsonObject().get("user").getAsJsonObject();
if (!authData.has("id") || !authData.has("password")) { 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(); String id = authData.get("id").getAsString();
@@ -70,16 +67,17 @@ public class AuthController {
String password = authData.get("password").getAsString(); String password = authData.get("password").getAsString();
UserAuthResult result = mgr.authenticate(id, password); UserAuthResult result = mgr.authenticate(id, password);
CredentialsValidationResponse response = new CredentialsValidationResponse(result.isSuccess());
JsonObject authObj = new JsonObject();
authObj.addProperty("success", result.isSuccess());
if (result.isSuccess()) { if (result.isSuccess()) {
authObj.addProperty("mxid", result.getMxid()); response.setDisplayName(result.getDisplayName());
authObj.addProperty("display_name", 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); return gson.toJson(obj);
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(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.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.*;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.exception.MatrixException;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -78,6 +75,18 @@ public class DefaultExceptionHandler {
return handle("M_INVALID_BODY", e.getMessage()); 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) @ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MappingAlreadyExistsException.class) @ExceptionHandler(MappingAlreadyExistsException.class)
public String handle(MappingAlreadyExistsException e) { 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

@@ -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();
}
}