From 361596e773e716dcb4098d13dd22552694a6547a Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 31 Aug 2017 16:33:07 +0200 Subject: [PATCH] Support 3PID lookups --- .../groovy/io/kamax/mxisd/GlobalProvider.java | 7 + .../GoogleFirebaseAuthenticator.groovy | 236 ++++++++++++++++++ .../provider/GoogleFirebaseAuthenticator.java | 117 --------- .../io/kamax/mxisd/config/FirebaseConfig.java | 14 +- 4 files changed, 253 insertions(+), 121 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/GlobalProvider.java create mode 100644 src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy delete mode 100644 src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java diff --git a/src/main/groovy/io/kamax/mxisd/GlobalProvider.java b/src/main/groovy/io/kamax/mxisd/GlobalProvider.java new file mode 100644 index 0000000..9e68b5d --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/GlobalProvider.java @@ -0,0 +1,7 @@ +package io.kamax.mxisd; + +import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.lookup.provider.IThreePidProvider; + +public interface GlobalProvider extends AuthenticatorProvider, IThreePidProvider { +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy new file mode 100644 index 0000000..9cbd439 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy @@ -0,0 +1,236 @@ +package io.kamax.mxisd.auth.provider + +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import com.google.firebase.auth.* +import com.google.firebase.internal.NonNull +import com.google.firebase.tasks.OnCompleteListener +import com.google.firebase.tasks.OnFailureListener +import com.google.firebase.tasks.OnSuccessListener +import com.google.firebase.tasks.Task +import io.kamax.matrix.MatrixID +import io.kamax.matrix.ThreePidMedium +import io.kamax.mxisd.GlobalProvider +import io.kamax.mxisd.auth.UserAuthResult +import io.kamax.mxisd.lookup.SingleLookupRequest +import io.kamax.mxisd.lookup.ThreePidMapping +import org.apache.commons.lang.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.function.Consumer +import java.util.regex.Matcher +import java.util.regex.Pattern + +public class GoogleFirebaseAuthenticator implements GlobalProvider { + + private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class); + + private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); + + private boolean isEnabled; + private String domain; + private FirebaseApp fbApp; + private FirebaseAuth fbAuth; + + 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)); + fbAuth = FirebaseAuth.getInstance(fbApp); + + log.info("Google Firebase Authentication is ready"); + } catch (IOException e) { + throw new RuntimeException("Error when initializing Firebase", e); + } + } + + private FirebaseCredential getCreds(String credsPath) throws IOException { + if (StringUtils.isNotBlank(credsPath)) { + return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath)); + } else { + return FirebaseCredentials.applicationDefault(); + } + } + + private FirebaseOptions getOpts(String credsPath, String db) throws IOException { + if (StringUtils.isBlank(db)) { + throw new IllegalArgumentException("Firebase database is not configured"); + } + + return new FirebaseOptions.Builder() + .setCredential(getCreds(credsPath)) + .setDatabaseUrl(db) + .build(); + } + + @Override + public boolean isEnabled() { + return isEnabled; + } + + @Override + public boolean isLocal() { + return true; + } + + @Override + public int getPriority() { + return 25; + } + + private void waitOnLatch(CountDownLatch l) { + try { + l.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for Firebase auth check"); + } + } + + private Optional findInternal(String medium, String address) { + UserRecord r; + CountDownLatch l = new CountDownLatch(1); + + OnSuccessListener success = new OnSuccessListener() { + @Override + void onSuccess(UserRecord result) { + r = result; + } + }; + + OnFailureListener failure = new OnFailureListener() { + @Override + void onFailure(@NonNull Exception e) { + r = null; + } + }; + + OnCompleteListener complete = new OnCompleteListener() { + @Override + void onComplete(@NonNull Task task) { + l.countDown(); + } + }; + + if (ThreePidMedium.Email.is(medium)) { + fbAuth.getUserByEmail(address) + .addOnSuccessListener(success) + .addOnFailureListener(failure) + .addOnCompleteListener(complete); + waitOnLatch(l); + } else if (ThreePidMedium.PhoneNumber.is(medium)) { + fbAuth.getUserByPhoneNumber(address) + .addOnSuccessListener(success) + .addOnFailureListener(failure) + .addOnCompleteListener(complete); + waitOnLatch(l); + } else { + log.info("{} is not a supported 3PID medium", medium); + r = null; + } + + return Optional.ofNullable(r); + } + + @Override + public Optional find(SingleLookupRequest request) { + Optional urOpt = findInternal(request.getType(), request.getThreePid()) + if (urOpt.isPresent()) { + return [ + address : request.getThreePid(), + medium : request.getType(), + mxid : new MatrixID(urOpt.get().getUid(), domain).getId(), + not_before: 0, + not_after : 9223372036854775807, + ts : 0 + ] + } else { + return Optional.empty(); + } + } + + @Override + public List populate(List mappings) { + List results = new ArrayList<>(); + mappings.parallelStream().forEach(new Consumer() { + @Override + void accept(ThreePidMapping o) { + Optional urOpt = findInternal(o.getMedium(), o.getValue()); + if (urOpt.isPresent()) { + ThreePidMapping result = new ThreePidMapping(); + result.setMedium(o.getMedium()) + result.setValue(o.getValue()) + result.setMxid(new MatrixID(urOpt.get().getUid(), domain).getId()) + results.add(result) + } + } + }); + return results; + } + + @Override + public UserAuthResult authenticate(String id, String password) { + if (!isEnabled()) { + throw new IllegalStateException(); + } + + final UserAuthResult result = new UserAuthResult(); + + log.info("Trying to authenticate {}", id); + Matcher m = matrixIdLaxPattern.matcher(id); + if (!m.matches()) { + log.warn("Could not validate {} as a Matrix ID", id); + result.failure(); + } + + String localpart = m.group(1); + + CountDownLatch l = new CountDownLatch(1); + fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener() { + @Override + void onSuccess(FirebaseToken token) { + 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(); + } + + log.info("{} was successfully authenticated", id); + result.success(id, token.getName()); + } + }).addOnFailureListener(new OnFailureListener() { + @Override + void onFailure(@NonNull Exception e) { + if (e instanceof IllegalArgumentException) { + log.info("Failure to authenticate {}: invalid firebase token", id); + } else { + log.info("Failure to authenticate {}: {}", id, e.getMessage(), e); + log.info("Exception", e); + } + + result.failure(); + } + }).addOnCompleteListener(new OnCompleteListener() { + @Override + void onComplete(@NonNull Task task) { + l.countDown() + } + }); + + try { + l.await(30, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.warn("Interrupted while waiting for Firebase auth check"); + result.failure(); + } + + return result; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java deleted file mode 100644 index 10c4cf1..0000000 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java +++ /dev/null @@ -1,117 +0,0 @@ -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 io.kamax.mxisd.auth.UserAuthResult; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.FileInputStream; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { - - private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class); - - private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); - - private boolean isEnabled; - private FirebaseApp fbApp; - private FirebaseAuth fbAuth; - - public GoogleFirebaseAuthenticator(boolean isEnabled) { - this.isEnabled = isEnabled; - } - - public GoogleFirebaseAuthenticator(String credsPath, String db) { - this(true); - try { - fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db)); - fbAuth = FirebaseAuth.getInstance(fbApp); - - log.info("Google Firebase Authentication is ready"); - } catch (IOException e) { - throw new RuntimeException("Error when initializing Firebase", e); - } - } - - private FirebaseCredential getCreds(String credsPath) throws IOException { - if (StringUtils.isNotBlank(credsPath)) { - return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath)); - } else { - return FirebaseCredentials.applicationDefault(); - } - } - - private FirebaseOptions getOpts(String credsPath, String db) throws IOException { - if (StringUtils.isBlank(db)) { - throw new IllegalArgumentException("Firebase database is not configured"); - } - - return new FirebaseOptions.Builder() - .setCredential(getCreds(credsPath)) - .setDatabaseUrl(db) - .build(); - } - - @Override - public boolean isEnabled() { - return isEnabled; - } - - @Override - public UserAuthResult authenticate(String id, String password) { - if (!isEnabled()) { - throw new IllegalStateException(); - } - - final UserAuthResult result = new UserAuthResult(); - - log.info("Trying to authenticate {}", id); - Matcher m = matrixIdLaxPattern.matcher(id); - if (!m.matches()) { - log.warn("Could not validate {} as a Matrix ID", id); - result.failure(); - } - - String localpart = m.group(1); - - CountDownLatch l = new CountDownLatch(1); - fbAuth.verifyIdToken(password).addOnSuccessListener(token -> { - 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(); - } - - log.info("{} was successfully authenticated", id); - result.success(id, token.getName()); - }).addOnFailureListener(e -> { - if (e instanceof IllegalArgumentException) { - log.info("Failure to authenticate {}: invalid firebase token", id); - } else { - log.info("Failure to authenticate {}: {}", id, e.getMessage(), e); - log.info("Exception", e); - } - - result.failure(); - }).addOnCompleteListener(t -> l.countDown()); - - try { - l.await(30, TimeUnit.SECONDS); - } catch (InterruptedException e) { - log.warn("Interrupted while waiting for Firebase auth check"); - result.failure(); - } - - 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 22ad824..6ad4791 100644 --- a/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java @@ -1,9 +1,10 @@ package io.kamax.mxisd.config; -import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.GlobalProvider; import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -16,6 +17,9 @@ public class FirebaseConfig { private Logger log = LoggerFactory.getLogger(FirebaseConfig.class); + @Autowired + private ServerConfig srvCfg; + private boolean enabled; private String credentials; private String database; @@ -52,14 +56,16 @@ public class FirebaseConfig { log.info("Credentials: {}", getCredentials()); log.info("Database: {}", getDatabase()); } + + } @Bean - public AuthenticatorProvider getProvider() { + public GlobalProvider getProvider() { if (!enabled) { return new GoogleFirebaseAuthenticator(false); + } else { + return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName()); } - - return new GoogleFirebaseAuthenticator(credentials, database); } }