diff --git a/src/main/groovy/io/kamax/mxisd/ThreePid.java b/src/main/groovy/io/kamax/mxisd/ThreePid.java index 04e685a..d51ecd6 100644 --- a/src/main/groovy/io/kamax/mxisd/ThreePid.java +++ b/src/main/groovy/io/kamax/mxisd/ThreePid.java @@ -39,4 +39,9 @@ public class ThreePid { return address; } + @Override + public String toString() { + return getMedium() + ":" + getAddress(); + } + } diff --git a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java index dd14f0b..61f33e7 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java +++ b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java @@ -20,7 +20,10 @@ package io.kamax.mxisd.auth; +import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.invitation.InvitationManager; +import io.kamax.mxisd.lookup.ThreePidMapping; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +40,9 @@ public class AuthManager { @Autowired private List providers = new ArrayList<>(); + @Autowired + private InvitationManager invMgr; + public UserAuthResult authenticate(String id, String password) { for (AuthenticatorProvider provider : providers) { if (!provider.isEnabled()) { @@ -45,6 +51,12 @@ public class AuthManager { UserAuthResult result = provider.authenticate(id, password); if (result.isSuccess()) { + log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName()); + for (ThreePid pid : result.getThreePids()) { + log.info("Processing {} for {}", pid, id); + invMgr.publishMappingIfInvited(new ThreePidMapping(pid, result.getMxid())); + } + return result; } } diff --git a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java index efa278c..f51f2bc 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java +++ b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java @@ -20,11 +20,19 @@ 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; + public class UserAuthResult { private boolean success; private String mxid; private String displayName; + private List threePids = new ArrayList<>(); public UserAuthResult failure() { success = false; @@ -66,4 +74,18 @@ public class UserAuthResult { this.displayName = displayName; } + public UserAuthResult withThreePid(ThreePidMedium medium, String address) { + return withThreePid(medium.getId(), address); + } + + public UserAuthResult withThreePid(String medium, String address) { + threePids.add(new ThreePid(medium, address)); + + return this; + } + + public List getThreePids() { + return Collections.unmodifiableList(threePids); + } + } diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy index 94ff111..7d57cce 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy @@ -22,17 +22,12 @@ package io.kamax.mxisd.auth.provider import com.google.firebase.FirebaseApp 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.FirebaseToken +import com.google.firebase.auth.* import com.google.firebase.internal.NonNull import com.google.firebase.tasks.OnFailureListener import com.google.firebase.tasks.OnSuccessListener import io.kamax.matrix.ThreePidMedium import io.kamax.mxisd.auth.UserAuthResult -import io.kamax.mxisd.invitation.InvitationManager -import io.kamax.mxisd.lookup.ThreePidMapping import org.apache.commons.lang.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -46,25 +41,31 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class); - private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); + private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); // FIXME use matrix-java-sdk private boolean isEnabled; private String domain; private FirebaseApp fbApp; private FirebaseAuth fbAuth; - private InvitationManager invMgr; - - public GoogleFirebaseAuthenticator(InvitationManager invMgr, boolean isEnabled) { - this.isEnabled = isEnabled; - this.invMgr = invMgr; + private void waitOnLatch(UserAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) { + try { + l.await(timeout, unit); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for " + purpose); + result.failure(); + } } - public GoogleFirebaseAuthenticator(InvitationManager invMgr, String credsPath, String db, String domain) { - this(invMgr, true); + public GoogleFirebaseAuthenticator(boolean isEnabled) { + this.isEnabled = isEnabled; + } + + public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) { + this(true); this.domain = domain; try { - fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db)); + fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider"); fbAuth = FirebaseAuth.getInstance(fbApp); log.info("Google Firebase Authentication is ready"); @@ -130,14 +131,42 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { if (!StringUtils.equals(localpart, token.getUid())) { log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid()); result.failure(); + return; } log.info("{} was successfully authenticated", id); result.success(id, token.getName()); - if (StringUtils.isNotBlank(token.getEmail())) { - invMgr.publishMappingIfInvited(new ThreePidMapping(ThreePidMedium.Email.getId(), token.getEmail(), id)) - } + log.info("Fetching profile for {}", id); + CountDownLatch userRecordLatch = new CountDownLatch(1); + fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener() { + @Override + void onSuccess(UserRecord user) { + try { + if (StringUtils.isNotBlank(user.getEmail())) { + result.withThreePid(ThreePidMedium.Email, user.getEmail()); + } + + if (StringUtils.isNotBlank(user.getPhoneNumber())) { + result.withThreePid(ThreePidMedium.PhoneNumber, user.getPhoneNumber()); + } + } finally { + userRecordLatch.countDown(); + } + } + }).addOnFailureListener(new OnFailureListener() { + @Override + void onFailure(@NonNull Exception e) { + try { + log.warn("Unable to fetch Firebase user profile for {}", id); + result.failure(); + } finally { + userRecordLatch.countDown(); + } + } + }); + + waitOnLatch(result, userRecordLatch, 30, TimeUnit.SECONDS, "Firebase user profile"); } finally { l.countDown() } @@ -160,13 +189,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { } }); - try { - l.await(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - log.warn("Interrupted while waiting for Firebase auth check"); - result.failure(); - } - + waitOnLatch(result, l, 30, TimeUnit.SECONDS, "Firebase auth check"); return result; } diff --git a/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java b/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java index 9480347..b3799f8 100644 --- a/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java @@ -22,7 +22,6 @@ package io.kamax.mxisd.config; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator; -import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.lookup.provider.GoogleFirebaseProvider; import io.kamax.mxisd.lookup.provider.IThreePidProvider; import org.slf4j.Logger; @@ -43,9 +42,6 @@ public class FirebaseConfig { @Autowired private ServerConfig srvCfg; - @Autowired - private InvitationManager invMgr; - private boolean enabled; private String credentials; private String database; @@ -87,9 +83,9 @@ public class FirebaseConfig { @Bean public AuthenticatorProvider getAuthProvider() { if (!enabled) { - return new GoogleFirebaseAuthenticator(invMgr, false); + return new GoogleFirebaseAuthenticator(false); } else { - return new GoogleFirebaseAuthenticator(invMgr, credentials, database, srvCfg.getName()); + return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName()); } } diff --git a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java index 7287030..5d7510c 100644 --- a/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java +++ b/src/main/groovy/io/kamax/mxisd/invitation/InvitationManager.java @@ -25,7 +25,6 @@ import io.kamax.matrix.ThreePid; import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.invitation.sender.IInviteSender; -import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.signature.SignatureManager; @@ -99,7 +98,7 @@ public class InvitationManager { // TODO use caching mechanism // TODO export in matrix-java-sdk - Optional findHomeserverForDomain(String domain) { + String findHomeserverForDomain(String domain) { log.debug("Performing SRV lookup for {}", domain); String lookupDns = getSrvRecordName(domain); log.info("Lookup name: {}", lookupDns); @@ -110,7 +109,7 @@ public class InvitationManager { Arrays.sort(records, Comparator.comparingInt(SRVRecord::getPriority)); for (SRVRecord record : records) { log.info("Found SRV record: {}", record.toString()); - return Optional.of("https://" + record.getTarget().toString(true) + ":" + record.getPort()); + return "https://" + record.getTarget().toString(true) + ":" + record.getPort(); } } else { log.info("No SRV record for {}", lookupDns); @@ -120,7 +119,7 @@ public class InvitationManager { } log.info("Performing basic lookup using domain name {}", domain); - return Optional.of("https://" + domain + ":8448"); + return "https://" + domain + ":8448"; } @Autowired @@ -143,13 +142,7 @@ public class InvitationManager { return invitations.get(pid); } - SingleLookupRequest request = new SingleLookupRequest(); - request.setType(invitation.getMedium()); - request.setThreePid(invitation.getAddress()); - request.setRecursive(true); - request.setRequester("Internal"); - - Optional result = lookupMgr.findRecursive(request); + Optional result = lookupMgr.find(invitation.getMedium(), invitation.getAddress(), true); if (result.isPresent()) { log.info("Mapping for {}:{} already exists, refusing to store invite", pid.getMedium(), pid.getAddress()); throw new MappingAlreadyExistsException(); @@ -180,60 +173,52 @@ public class InvitationManager { log.info("{}:{} has an invite pending, publishing mapping", threePid.getMedium(), threePid.getValue()); String domain = reply.getInvite().getSender().getDomain(); log.info("Discovering HS for domain {}", domain); - Optional hsUrlOpt = findHomeserverForDomain(domain); - if (!hsUrlOpt.isPresent()) { - log.warn("No HS found for domain {} - ignoring publishing", domain); - } else { - // TODO this is needed as this will block if called during authentication cycle due to synapse implementation + String hsUrlOpt = findHomeserverForDomain(domain); - new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool - HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind"); - // Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr - JSONObject obj = new JSONObject(); // TODO use Gson instead - obj.put("mxid", threePid.getMxid()); - obj.put("token", reply.getToken()); - obj.put("signatures", signMgr.signMessageJson(obj.toString())); + // TODO this is needed as this will block if called during authentication cycle due to synapse implementation + new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool + HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind"); + // Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr + JSONObject obj = new JSONObject(); // TODO use Gson instead + obj.put("mxid", threePid.getMxid()); + obj.put("token", reply.getToken()); + obj.put("signatures", signMgr.signMessageJson(obj.toString())); - JSONObject objUp = new JSONObject(); - objUp.put("mxid", threePid.getMxid()); - objUp.put("medium", threePid.getMedium()); - objUp.put("address", threePid.getValue()); - objUp.put("sender", reply.getInvite().getSender().getId()); - objUp.put("room_id", reply.getInvite().getRoomId()); - objUp.put("signed", obj); + JSONObject objUp = new JSONObject(); + objUp.put("mxid", threePid.getMxid()); + objUp.put("medium", threePid.getMedium()); + objUp.put("address", threePid.getValue()); + objUp.put("sender", reply.getInvite().getSender().getId()); + objUp.put("room_id", reply.getInvite().getRoomId()); + objUp.put("signed", obj); - String mapping = gson.toJson(objUp); // FIXME we shouldn't need to be doing this + JSONObject content = new JSONObject(); // TODO use Gson instead + JSONArray invites = new JSONArray(); + invites.put(objUp); + content.put("invites", invites); + content.put("medium", threePid.getMedium()); + content.put("address", threePid.getValue()); + content.put("mxid", threePid.getMxid()); - JSONObject content = new JSONObject(); // TODO use Gson instead - JSONArray invites = new JSONArray(); - invites.put(objUp); - content.put("invites", invites); - content.put("medium", threePid.getMedium()); - content.put("address", threePid.getValue()); - content.put("mxid", threePid.getMxid()); + content.put("signatures", signMgr.signMessageJson(content.toString())); - content.put("signatures", signMgr.signMessageJson(content.toString())); - - StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); - entity.setContentType("application/json"); - req.setEntity(entity); - try { - log.info("Posting onBind event to {}", req.getURI()); - CloseableHttpResponse response = client.execute(req); - int statusCode = response.getStatusLine().getStatusCode(); - log.info("Answer code: {}", statusCode); - if (statusCode >= 400) { - log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); - } - response.close(); - } catch (IOException e) { - log.warn("Unable to tell HS {} about invite being mapped", domain, e); + StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); + entity.setContentType("application/json"); + req.setEntity(entity); + try { + log.info("Posting onBind event to {}", req.getURI()); + CloseableHttpResponse response = client.execute(req); + int statusCode = response.getStatusLine().getStatusCode(); + log.info("Answer code: {}", statusCode); + if (statusCode >= 400) { + log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8)); } - invitations.remove(key); - }).start(); - } - - + response.close(); + } catch (IOException e) { + log.warn("Unable to tell HS {} about invite being mapped", domain, e); + } + invitations.remove(key); + }).start(); } } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java index c87d1b4..5777ad7 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java +++ b/src/main/groovy/io/kamax/mxisd/lookup/ThreePidMapping.java @@ -21,6 +21,7 @@ package io.kamax.mxisd.lookup; import groovy.json.JsonOutput; +import io.kamax.mxisd.ThreePid; public class ThreePidMapping { @@ -32,6 +33,10 @@ public class ThreePidMapping { // stub } + public ThreePidMapping(ThreePid threePid, String mxid) { + this(threePid.getMedium(), threePid.getAddress(), mxid); + } + public ThreePidMapping(String medium, String value, String mxid) { setMedium(medium); setValue(value); diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy index 30e89a7..077974b 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/GoogleFirebaseProvider.groovy @@ -60,7 +60,7 @@ public class GoogleFirebaseProvider implements IThreePidProvider { this(true); this.domain = domain; try { - fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db)); + fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider"); fbAuth = FirebaseAuth.getInstance(fbApp); log.info("Google Firebase Authentication is ready"); diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy index 1e1c8d0..a4fd5e7 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/LookupStrategy.groovy @@ -29,6 +29,8 @@ interface LookupStrategy { List getLocalProviders() + Optional find(String medium, String address, boolean recursive) + Optional find(SingleLookupRequest request) Optional findRecursive(SingleLookupRequest request) diff --git a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy index 8cfdd28..39a6337 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/strategy/RecursivePriorityLookupStrategy.groovy @@ -121,6 +121,16 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea }).collect(Collectors.toList()) } + @Override + Optional find(String medium, String address, boolean recursive) { + SingleLookupRequest req = new SingleLookupRequest(); + req.setType(medium) + req.setThreePid(address) + req.setRequester("Internal") + return find(req, recursive) + } + + @Override Optional find(SingleLookupRequest request, boolean forceRecursive) { for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) { Optional lookupDataOpt = provider.find(request)