From 0033d0dc1d5e6f4d716ca85adbdb799fa69fc1bf Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 31 Aug 2017 02:10:36 +0200 Subject: [PATCH 1/8] Experimental support for synapse REST auth module - See https://github.com/maxidor/matrix-synapse-rest-auth - Include Google Firebase backend using UID as login and user token as password --- build.gradle | 7 ++ .../io/kamax/mxisd/auth/AuthManager.java | 35 ++++++ .../io/kamax/mxisd/auth/UserAuthResult.java | 47 ++++++++ .../auth/provider/AuthenticatorProvider.java | 11 ++ .../provider/GoogleFirebaseAuthenticator.java | 103 ++++++++++++++++++ .../io/kamax/mxisd/config/FirebaseConfig.java | 65 +++++++++++ .../mxisd/controller/v1/AuthController.java | 89 +++++++++++++++ 7 files changed, 357 insertions(+) create mode 100644 src/main/groovy/io/kamax/mxisd/auth/AuthManager.java create mode 100644 src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java create mode 100644 src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java create mode 100644 src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/controller/v1/AuthController.java diff --git a/build.gradle b/build.gradle index b9de6ad..d54ab8f 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ buildscript { } repositories { + maven { url "https://kamax.io/maven/releases/" } mavenCentral() } @@ -45,6 +46,9 @@ dependencies { // Spring Boot - standalone app compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE' + // Matrix Java SDK + compile 'io.kamax:matrix-java-sdk:0.0.1' + // ed25519 handling compile 'net.i2p.crypto:eddsa:0.1.0' @@ -63,6 +67,9 @@ dependencies { // Phone numbers validation compile 'com.googlecode.libphonenumber:libphonenumber:8.7.1' + // Google Firebase Authentication backend + compile 'com.google.firebase:firebase-admin:5.3.0' + testCompile 'junit:junit:4.12' } diff --git a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java new file mode 100644 index 0000000..bb6e367 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java @@ -0,0 +1,35 @@ +package io.kamax.mxisd.auth; + +import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class AuthManager { + + private Logger log = LoggerFactory.getLogger(AuthManager.class); + + @Autowired + private List providers = new ArrayList<>(); + + public UserAuthResult authenticate(String id, String password) { + for (AuthenticatorProvider provider : providers) { + if (!provider.isEnabled()) { + continue; + } + + UserAuthResult result = provider.authenticate(id, password); + if (result.isSuccess()) { + return result; + } + } + + return new UserAuthResult().failure(); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java new file mode 100644 index 0000000..61100e4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java @@ -0,0 +1,47 @@ +package io.kamax.mxisd.auth; + +public class UserAuthResult { + + private boolean success; + private String mxid; + private String displayName; + + public UserAuthResult failure() { + success = false; + mxid = null; + displayName = null; + + return this; + } + + public void success(String mxid, String displayName) { + setSuccess(true); + setMxid(mxid); + setDisplayName(displayName); + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMxid() { + return mxid; + } + + public void setMxid(String mxid) { + this.mxid = mxid; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java new file mode 100644 index 0000000..bdcf72a --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java @@ -0,0 +1,11 @@ +package io.kamax.mxisd.auth.provider; + +import io.kamax.mxisd.auth.UserAuthResult; + +public interface AuthenticatorProvider { + + boolean isEnabled(); + + UserAuthResult authenticate(String id, String password); + +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java new file mode 100644 index 0000000..56d60b3 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java @@ -0,0 +1,103 @@ +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.matrix.MatrixID; +import io.kamax.matrix._MatrixID; +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; + +public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { + + private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class); + + 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(); + + try { + log.info("Trying to authenticate {}", id); + _MatrixID mxId = new MatrixID(id); + fbAuth.verifyIdToken(password).addOnSuccessListener(token -> { + if (!StringUtils.equals(mxId.getLocalPart(), token.getUid())) { + log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, mxId.getLocalPart(), 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()); + log.debug("Exception", e); + } + + result.failure(); + }); + } catch (IllegalArgumentException e) { + log.warn("Could not validate {} as a Matrix ID: {}", id, e.getMessage()); + 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 new file mode 100644 index 0000000..22ad824 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java @@ -0,0 +1,65 @@ +package io.kamax.mxisd.config; + +import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; + +@Configuration +@ConfigurationProperties("firebase") +public class FirebaseConfig { + + private Logger log = LoggerFactory.getLogger(FirebaseConfig.class); + + private boolean enabled; + private String credentials; + private String database; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getCredentials() { + return credentials; + } + + public void setCredentials(String credentials) { + this.credentials = credentials; + } + + public String getDatabase() { + return database; + } + + public void setDatabase(String database) { + this.database = database; + } + + @PostConstruct + private void postConstruct() { + log.info("--- Firebase configuration ---"); + log.info("Enabled: {}", isEnabled()); + if (isEnabled()) { + log.info("Credentials: {}", getCredentials()); + log.info("Database: {}", getDatabase()); + } + } + + @Bean + public AuthenticatorProvider getProvider() { + if (!enabled) { + return new GoogleFirebaseAuthenticator(false); + } + + return new GoogleFirebaseAuthenticator(credentials, database); + } +} diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/AuthController.java b/src/main/groovy/io/kamax/mxisd/controller/v1/AuthController.java new file mode 100644 index 0000000..70dd1bb --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/AuthController.java @@ -0,0 +1,89 @@ +/* + * 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 . + */ + +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 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.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@RestController +@CrossOrigin +@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class AuthController { + + private Logger log = LoggerFactory.getLogger(AuthController.class); + + private Gson gson = new Gson(); + + @Autowired + private AuthManager mgr; + + @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(); + if (!authData.has("id") || !authData.has("password")) { + throw new IllegalArgumentException("Missing id or password keys"); + } + + String id = authData.get("id").getAsString(); + log.info("Requested to check credentials for {}", id); + String password = authData.get("password").getAsString(); + + UserAuthResult result = mgr.authenticate(id, password); + + JsonObject authObj = new JsonObject(); + authObj.addProperty("success", result.isSuccess()); + if (result.isSuccess()) { + authObj.addProperty("mxid", result.getMxid()); + authObj.addProperty("display_name", result.getDisplayName()); + } + JsonObject obj = new JsonObject(); + + obj.add("authentication", authObj); + return gson.toJson(obj); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} From d57aef36ea758453842e8fd6a89ff1eb8e951e8b Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 31 Aug 2017 03:34:08 +0200 Subject: [PATCH 2/8] Wait for async calls --- .../provider/GoogleFirebaseAuthenticator.java | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java index 56d60b3..10c4cf1 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.java @@ -5,8 +5,6 @@ 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.matrix.MatrixID; -import io.kamax.matrix._MatrixID; import io.kamax.mxisd.auth.UserAuthResult; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -14,11 +12,17 @@ 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; @@ -71,29 +75,39 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { final UserAuthResult result = new UserAuthResult(); - try { - log.info("Trying to authenticate {}", id); - _MatrixID mxId = new MatrixID(id); - fbAuth.verifyIdToken(password).addOnSuccessListener(token -> { - if (!StringUtils.equals(mxId.getLocalPart(), token.getUid())) { - log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, mxId.getLocalPart(), token.getUid()); - result.failure(); - } + 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(); + } - 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()); - log.debug("Exception", e); - } + 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(); - }); - } catch (IllegalArgumentException e) { - log.warn("Could not validate {} as a Matrix ID: {}", id, e.getMessage()); + } + + 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(); } From 361596e773e716dcb4098d13dd22552694a6547a Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 31 Aug 2017 16:33:07 +0200 Subject: [PATCH 3/8] 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); } } From 05d1594ac2c406eb09b41a62a6f9be6091826b47 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Thu, 31 Aug 2017 18:52:06 +0200 Subject: [PATCH 4/8] Fix race condition and add more log statements --- .../GoogleFirebaseAuthenticator.groovy | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) 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 9cbd439..38c6079 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy @@ -4,10 +4,8 @@ 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 @@ -101,35 +99,32 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { OnSuccessListener success = new OnSuccessListener() { @Override void onSuccess(UserRecord result) { + log.info("Found 3PID match for {}:{} - UID is {}", medium, address, result.getUid()) r = result; + l.countDown() } }; OnFailureListener failure = new OnFailureListener() { @Override void onFailure(@NonNull Exception e) { + log.info("No 3PID match for {}:{} - {}", medium, address, e.getMessage()) r = null; - } - }; - - OnCompleteListener complete = new OnCompleteListener() { - @Override - void onComplete(@NonNull Task task) { - l.countDown(); + l.countDown() } }; if (ThreePidMedium.Email.is(medium)) { + log.info("Performing E-mail 3PID lookup for {}", address) fbAuth.getUserByEmail(address) .addOnSuccessListener(success) - .addOnFailureListener(failure) - .addOnCompleteListener(complete); + .addOnFailureListener(failure); waitOnLatch(l); } else if (ThreePidMedium.PhoneNumber.is(medium)) { + log.info("Performing msisdn 3PID lookup for {}", address) fbAuth.getUserByPhoneNumber(address) .addOnSuccessListener(success) - .addOnFailureListener(failure) - .addOnCompleteListener(complete); + .addOnFailureListener(failure); waitOnLatch(l); } else { log.info("{} is not a supported 3PID medium", medium); @@ -203,6 +198,7 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { log.info("{} was successfully authenticated", id); result.success(id, token.getName()); + l.countDown() } }).addOnFailureListener(new OnFailureListener() { @Override @@ -215,10 +211,6 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { } result.failure(); - } - }).addOnCompleteListener(new OnCompleteListener() { - @Override - void onComplete(@NonNull Task task) { l.countDown() } }); From 61399c77055acb97488a1202c73dee8387a98393 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Fri, 1 Sep 2017 18:28:40 +0200 Subject: [PATCH 5/8] Add status endpoint placeholder --- .../mxisd/controller/v1/StatusController.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java diff --git a/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java b/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java new file mode 100644 index 0000000..516ff03 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/controller/v1/StatusController.java @@ -0,0 +1,19 @@ +package io.kamax.mxisd.controller.v1; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@CrossOrigin +@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE) +public class StatusController { + + @RequestMapping(value = "/_matrix/identity/status") + public String getStatus() { + // TODO link to backend + return "{\"status\":{\"health\":\"OK\"}}"; + } + +} From 694e62edee46223fa3d6cf54ea92d88d9769b735 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Sat, 2 Sep 2017 03:11:22 +0200 Subject: [PATCH 6/8] Firebase UID is case sensitive, must not alter --- .../mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 38c6079..0d00c08 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/GoogleFirebaseAuthenticator.groovy @@ -6,7 +6,6 @@ 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.MatrixID import io.kamax.matrix.ThreePidMedium import io.kamax.mxisd.GlobalProvider import io.kamax.mxisd.auth.UserAuthResult @@ -141,7 +140,7 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { return [ address : request.getThreePid(), medium : request.getType(), - mxid : new MatrixID(urOpt.get().getUid(), domain).getId(), + mxid : "@${urOpt.get().getUid()}:${domain}", not_before: 0, not_after : 9223372036854775807, ts : 0 @@ -162,7 +161,7 @@ public class GoogleFirebaseAuthenticator implements GlobalProvider { ThreePidMapping result = new ThreePidMapping(); result.setMedium(o.getMedium()) result.setValue(o.getValue()) - result.setMxid(new MatrixID(urOpt.get().getUid(), domain).getId()) + result.setMxid("@${urOpt.get().getUid()}:${domain}") results.add(result) } } From 85236793e155ce3a6044767d029a2e93131d5995 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Mon, 4 Sep 2017 03:08:19 +0200 Subject: [PATCH 7/8] Skeleton to support LDAP Auth --- application.example.yaml | 102 ++++++++---- .../io/kamax/mxisd/config/LdapConfig.groovy | 156 ------------------ .../config/ldap/LdapAttributeConfig.java | 29 ++++ .../config/ldap/LdapAttributeUidConfig.java | 29 ++++ .../mxisd/config/ldap/LdapAuthConfig.java | 20 +++ .../kamax/mxisd/config/ldap/LdapConfig.groovy | 125 ++++++++++++++ .../config/ldap/LdapConnectionConfig.java | 65 ++++++++ .../mxisd/config/ldap/LdapIdentityConfig.java | 28 ++++ .../mxisd/lookup/provider/LdapProvider.groovy | 52 ++++-- 9 files changed, 407 insertions(+), 199 deletions(-) delete mode 100644 src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java diff --git a/application.example.yaml b/application.example.yaml index 36908d4..0d6307c 100644 --- a/application.example.yaml +++ b/application.example.yaml @@ -97,41 +97,83 @@ lookup: ldap: + + # Global enable/disable switch enabled: true - tls: false - host: 'localhost' - port: 389 - bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org' - bindPassword: 'password' - baseDn: 'CN=Users,DC=example,DC=org' - # How should we resolve the Matrix ID in case of a match using the attribute. + # Connection configuration to the LDAP server + connection: + + # If the connection should be secure + tls: false + + # Host to connect to + host: 'localhost' + + # Port to connect to + port: 389 + + # Bind DN to use when performing lookups + bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org' + + # Bind password to use + bindPassword: 'password' + + # Base DN used in all queries + baseDn: 'CN=Users,DC=example,DC=org' + + # How to map Matrix attributes with LDAP attributes when performing lookup/auth + attributes: + + # The username/login that will be looked up or used to build Matrix IDs + uid: + + # How should we resolve the Matrix ID in case of a match using the attribute. + # + # The following type are supported: + # - uid : the attribute only contains the UID part of the Matrix ID. e.g. 'john.doe' in @john.doe:example.org + # - mxid : the attribute contains the full Matrix ID - e.g. '@john.doe:example.org' + type: 'uid' + + # The attribute containing the binding itself. This value will be used differently depending on the type. + # + # /!\ This should match the synapse LDAP Authenticator 'uid' configuration /!\ + # + # Typical values: + # - For type 'uid': 'userPrincipalName' or 'uid' or 'saMAccountName' + # - For type 'mxid', regardless of the directory type, we recommend using 'pager' as it is a standard attribute and + # is typically not used. + value: 'userPrincipalName' + + # The display name of the user + name: 'displayName' + + # Configuration section relating the authentication of users performed via LDAP. # - # The following type are supported: - # - uid : the attribute only contains the UID part of the Matrix ID. e.g. 'john.doe' in @john.doe:example.org - # - mxid : the attribute contains the full Matrix ID - e.g. '@john.doe:example.org' - type: 'uid' + # This can be done using the REST Auth module for synapse and pointing it to the identity server. + # See https://github.com/maxidor/matrix-synapse-rest-auth + auth: - # The attribute containing the binding itself. This value will be used differently depending on the type. - # - # /!\ This should match the synapse LDAP Authenticator 'uid' configuration /!\ - # - # Typical values: - # - For type 'uid': 'userPrincipalName' or 'uid' or 'saMAccountName' - # - For type 'mxid', regardless of the directory type, we recommend using 'pager' as it is a standard attribute and - # is typically not used. - attribute: 'userPrincipalName' - - # Configure each 3PID type with a dedicated query. - mappings: - email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))" - - # Phone numbers query. + # What to filter potential users by, typically by using a dedicated group. + # If this value is not set, login check will be performed for all entities within the LDAP # - # Phone numbers use the MSISDN format: https://en.wikipedia.org/wiki/MSISDN - # This format does not include international prefix (+ or 00) and therefore has to be put in the query. - # Adapt this to your needs for each attribute. - msisdn: "(|(telephoneNumber=+%3pid)(mobile=+%3pid)(homePhone=+%3pid)(otherTelephone=+%3pid)(otherMobile=+%3pid)(otherHomePhone=+%3pid))" + # Example: (memberOf=CN=Matrix Users,CN=Users,DC=example,DC=org) + filter: '' + + # Configuration section relating to identity lookups + identity: + + # Configure each 3PID type with a dedicated query. + medium: + # E-mail query + email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))" + + # Phone numbers query + # + # Phone numbers use the MSISDN format: https://en.wikipedia.org/wiki/MSISDN + # This format does not include international prefix (+ or 00) and therefore has to be put in the query. + # Adapt this to your needs for each attribute. + msisdn: "(|(telephoneNumber=+%3pid)(mobile=+%3pid)(homePhone=+%3pid)(otherTelephone=+%3pid)(otherMobile=+%3pid)(otherHomePhone=+%3pid))" diff --git a/src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy deleted file mode 100644 index e90046e..0000000 --- a/src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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 . - */ - -package io.kamax.mxisd.config - -import org.apache.commons.lang.StringUtils -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.InitializingBean -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.annotation.Configuration - -@Configuration -@ConfigurationProperties(prefix = "ldap") -class LdapConfig implements InitializingBean { - - private Logger log = LoggerFactory.getLogger(LdapConfig.class) - - private boolean enabled - private boolean tls - private String host - private int port - private String baseDn - private String type - private String attribute - private String bindDn - private String bindPassword - private Map mappings - - boolean getEnabled() { - return enabled - } - - void setEnabled(boolean enabled) { - this.enabled = enabled - } - - boolean getTls() { - return tls - } - - void setTls(boolean tls) { - this.tls = tls - } - - String getHost() { - return host - } - - void setHost(String host) { - this.host = host - } - - int getPort() { - return port - } - - void setPort(int port) { - this.port = port - } - - String getBaseDn() { - return baseDn - } - - void setBaseDn(String baseDn) { - this.baseDn = baseDn - } - - String getType() { - return type - } - - void setType(String type) { - this.type = type - } - - String getAttribute() { - return attribute - } - - void setAttribute(String attribute) { - this.attribute = attribute - } - - String getBindDn() { - return bindDn - } - - void setBindDn(String bindDn) { - this.bindDn = bindDn - } - - String getBindPassword() { - return bindPassword - } - - void setBindPassword(String bindPassword) { - this.bindPassword = bindPassword - } - - Map getMappings() { - return mappings - } - - void setMappings(Map mappings) { - this.mappings = mappings - } - - Optional getMapping(String type) { - if (mappings == null) { - return Optional.empty() - } - - return Optional.ofNullable(mappings.get(type)) - } - - @Override - void afterPropertiesSet() throws Exception { - log.info("LDAP enabled: {}", getEnabled()) - - if (!getEnabled()) { - return - } - - log.info("Matrix ID type: {}", getType()) - log.info("LDAP Host: {}", getHost()) - log.info("LDAP Bind DN: {}", getBindDn()) - log.info("LDAP Attribute: {}", getAttribute()) - - if (StringUtils.isBlank(getHost())) { - throw new IllegalStateException("LDAP Host must be configured!") - } - if (StringUtils.isBlank(getAttribute())) { - throw new IllegalStateException("LDAP attribute must be configured!") - } - } - -} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java new file mode 100644 index 0000000..f8139ee --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java @@ -0,0 +1,29 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.attribute") +public class LdapAttributeConfig { + + private LdapAttributeUidConfig uid; + private String name; + + public LdapAttributeUidConfig getUid() { + return uid; + } + + public void setUid(LdapAttributeUidConfig uid) { + this.uid = uid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java new file mode 100644 index 0000000..771a630 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java @@ -0,0 +1,29 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.attribute.uid") +public class LdapAttributeUidConfig { + + private String type; + private String value; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java new file mode 100644 index 0000000..011500e --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java @@ -0,0 +1,20 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.auth") +public class LdapAuthConfig { + + private String filter; + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy new file mode 100644 index 0000000..55fd3d7 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy @@ -0,0 +1,125 @@ +/* + * 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 . + */ + +package io.kamax.mxisd.config.ldap + +import groovy.json.JsonOutput +import org.apache.commons.lang.StringUtils +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.Configuration + +import javax.annotation.PostConstruct + +@Configuration +@ConfigurationProperties(prefix = "ldap") +class LdapConfig { + + private Logger log = LoggerFactory.getLogger(LdapConfig.class) + + private boolean enabled + + @Autowired + private LdapConnectionConfig conn + private LdapAttributeConfig attribute + private LdapAuthConfig auth + private LdapIdentityConfig identity + + boolean isEnabled() { + return enabled + } + + void setEnabled(boolean enabled) { + this.enabled = enabled + } + + LdapConnectionConfig getConn() { + return conn + } + + void setConn(LdapConnectionConfig conn) { + this.conn = conn + } + + LdapAttributeConfig getAttribute() { + return attribute + } + + void setAttribute(LdapAttributeConfig attribute) { + this.attribute = attribute + } + + LdapAuthConfig getAuth() { + return auth + } + + void setAuth(LdapAuthConfig auth) { + this.auth = auth + } + + LdapIdentityConfig getIdentity() { + return identity + } + + void setIdentity(LdapIdentityConfig identity) { + this.identity = identity + } + + @PostConstruct + void afterPropertiesSet() { + log.info("--- LDAP Config ---") + log.info("Enabled: {}", isEnabled()) + + if (!isEnabled()) { + return + } + + if (StringUtils.isBlank(conn.getHost())) { + throw new IllegalStateException("LDAP Host must be configured!") + } + + if (1 > conn.getPort() || 65535 < conn.getPort()) { + throw new IllegalStateException("LDAP port is not valid") + } + + if (StringUtils.isBlank(attribute.getUid().getType())) { + throw new IllegalStateException("Attribute UID Type cannot be empty") + } + + + if (StringUtils.isBlank(attribute.getUid().getValue())) { + throw new IllegalStateException("Attribute UID value cannot be empty") + } + + + log.info("Conn: {}", JsonOutput.toJson(conn)) + log.info("Host: {}", conn.getHost()) + log.info("Port: {}", conn.getPort()) + log.info("Bind DN: {}", conn.getBindDn()) + log.info("Base DN: {}", conn.getBaseDn()) + + log.info("Attribute: {}", JsonOutput.toJson(attribute)) + log.info("Auth: {}", JsonOutput.toJson(auth)) + log.info("Identity: {}", JsonOutput.toJson(identity)) + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java new file mode 100644 index 0000000..7f85e5d --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java @@ -0,0 +1,65 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.connection") +public class LdapConnectionConfig { + + private boolean tls; + private String host; + private int port; + private String bindDn; + private String bindPassword; + private String baseDn; + + public boolean isTls() { + return tls; + } + + public void setTls(boolean tls) { + this.tls = tls; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getBindDn() { + return bindDn; + } + + public void setBindDn(String bindDn) { + this.bindDn = bindDn; + } + + public String getBindPassword() { + return bindPassword; + } + + public void setBindPassword(String bindPassword) { + this.bindPassword = bindPassword; + } + + public String getBaseDn() { + return baseDn; + } + + public void setBaseDn(String baseDn) { + this.baseDn = baseDn; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java new file mode 100644 index 0000000..c6839e4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java @@ -0,0 +1,28 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Configuration +@ConfigurationProperties(prefix = "ldap.identity") +public class LdapIdentityConfig { + + private Map medium = new HashMap<>(); + + public Map getMedium() { + return medium; + } + + public Optional getQuery(String key) { + return Optional.ofNullable(medium.get(key)); + } + + public void setMedium(Map medium) { + this.medium = medium; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy index 281fb82..392e5ff 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy @@ -20,8 +20,10 @@ package io.kamax.mxisd.lookup.provider -import io.kamax.mxisd.config.LdapConfig +import io.kamax.mxisd.auth.UserAuthResult +import io.kamax.mxisd.auth.provider.AuthenticatorProvider import io.kamax.mxisd.config.ServerConfig +import io.kamax.mxisd.config.ldap.LdapConfig import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import org.apache.commons.lang.StringUtils @@ -38,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component -class LdapProvider implements IThreePidProvider { +class LdapProvider implements IThreePidProvider, AuthenticatorProvider { public static final String UID = "uid" public static final String MATRIX_ID = "mxid" @@ -53,7 +55,28 @@ class LdapProvider implements IThreePidProvider { @Override boolean isEnabled() { - return ldapCfg.getEnabled() + return ldapCfg.isEnabled() + } + + private LdapConnection getConn() { + return new LdapNetworkConnection(ldapCfg.getConn().getHost(), ldapCfg.getConn().getPort(), ldapCfg.getConn().isTls()) + } + + private void bind(LdapConnection conn) { + conn.bind(ldapCfg.getConn().getBindDn(), ldapCfg.getConn().getBindPassword()) + } + + @Override + UserAuthResult authenticate(String id, String password) { + LdapConnection conn = getConn() + try { + bind(conn) + + // TODO finish this + return new UserAuthResult().failure() + } finally { + conn.close() + } } @Override @@ -67,20 +90,22 @@ class LdapProvider implements IThreePidProvider { } Optional lookup(LdapConnection conn, String medium, String value) { - Optional queryOpt = ldapCfg.getMapping(medium) + String uidAttribute = ldapCfg.getAttribute().getUid().getValue() + + Optional queryOpt = ldapCfg.getIdentity().getQuery(medium) if (!queryOpt.isPresent()) { log.warn("{} is not a configured 3PID type for LDAP lookup", medium) return Optional.empty() } String searchQuery = queryOpt.get().replaceAll("%3pid", value) - EntryCursor cursor = conn.search(ldapCfg.getBaseDn(), searchQuery, SearchScope.SUBTREE, ldapCfg.getAttribute()) + EntryCursor cursor = conn.search(ldapCfg.getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute) try { while (cursor.next()) { Entry entry = cursor.get() log.info("Found possible match, DN: {}", entry.getDn().getName()) - Attribute attribute = entry.get(ldapCfg.getAttribute()) + Attribute attribute = entry.get(uidAttribute) if (attribute == null) { log.info("DN {}: no attribute {}, skpping", entry.getDn(), ldapCfg.getAttribute()) continue @@ -94,12 +119,13 @@ class LdapProvider implements IThreePidProvider { StringBuilder matrixId = new StringBuilder() // TODO Should we turn this block into a map of functions? - if (StringUtils.equals(UID, ldapCfg.getType())) { + String uidType = ldapCfg.getAttribute().getUid().getType() + if (StringUtils.equals(UID, uidType)) { matrixId.append("@").append(data).append(":").append(srvCfg.getName()) - } else if (StringUtils.equals(MATRIX_ID, ldapCfg.getType())) { + } else if (StringUtils.equals(MATRIX_ID, uidType)) { matrixId.append(data) } else { - log.warn("Bind was found but type {} is not supported", ldapCfg.getType()) + log.warn("Bind was found but type {} is not supported", uidType) continue } @@ -119,9 +145,9 @@ class LdapProvider implements IThreePidProvider { Optional find(SingleLookupRequest request) { log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}") - LdapConnection conn = new LdapNetworkConnection(ldapCfg.getHost(), ldapCfg.getPort(), ldapCfg.getTls()) + LdapConnection conn = getConn() try { - conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword()) + bind(conn) Optional mxid = lookup(conn, request.getType(), request.getThreePid()) if (mxid.isPresent()) { @@ -147,9 +173,9 @@ class LdapProvider implements IThreePidProvider { log.info("Looking up {} mappings", mappings.size()) List mappingsFound = new ArrayList<>() - LdapConnection conn = new LdapNetworkConnection(ldapCfg.getHost(), ldapCfg.getPort()) + LdapConnection conn = getConn() try { - conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword()) + bind(conn) for (ThreePidMapping mapping : mappings) { try { From 4b0d549dd67ea2aea645ab3f245aa25921b7a3e2 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Tue, 5 Sep 2017 21:31:36 +0200 Subject: [PATCH 8/8] Add LDAP Auth support with synapse REST Auth module --- application.example.yaml | 2 + build.gradle | 2 +- .../io/kamax/mxisd/auth/UserAuthResult.java | 4 +- .../mxisd/auth/provider/LdapAuthProvider.java | 115 ++++++++++++++++++ .../config/ldap/LdapAttributeUidConfig.java | 11 ++ .../mxisd/lookup/provider/LdapProvider.groovy | 19 +-- 6 files changed, 136 insertions(+), 17 deletions(-) create mode 100644 src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java diff --git a/application.example.yaml b/application.example.yaml index 0d6307c..c4ad5dc 100644 --- a/application.example.yaml +++ b/application.example.yaml @@ -158,6 +158,8 @@ ldap: # If this value is not set, login check will be performed for all entities within the LDAP # # Example: (memberOf=CN=Matrix Users,CN=Users,DC=example,DC=org) + # + # /!\ Currently NOT supported due to a possible bug in LDAP library /!\ filter: '' # Configuration section relating to identity lookups diff --git a/build.gradle b/build.gradle index d54ab8f..f506d2b 100644 --- a/build.gradle +++ b/build.gradle @@ -47,7 +47,7 @@ dependencies { compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE' // Matrix Java SDK - compile 'io.kamax:matrix-java-sdk:0.0.1' + compile 'io.kamax:matrix-java-sdk:0.0.2' // ed25519 handling compile 'net.i2p.crypto:eddsa:0.1.0' diff --git a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java index 61100e4..16e1cf6 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java +++ b/src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java @@ -14,10 +14,12 @@ public class UserAuthResult { return this; } - public void success(String mxid, String displayName) { + public UserAuthResult success(String mxid, String displayName) { setSuccess(true); setMxid(mxid); setDisplayName(displayName); + + return this; } public boolean isSuccess() { diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java b/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java new file mode 100644 index 0000000..ff2ddc4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/LdapAuthProvider.java @@ -0,0 +1,115 @@ +package io.kamax.mxisd.auth.provider; + +import io.kamax.matrix.MatrixID; +import io.kamax.mxisd.auth.UserAuthResult; +import io.kamax.mxisd.config.ldap.LdapConfig; +import io.kamax.mxisd.lookup.provider.LdapProvider; +import org.apache.commons.lang.StringUtils; +import org.apache.directory.api.ldap.model.cursor.CursorException; +import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException; +import org.apache.directory.api.ldap.model.cursor.EntryCursor; +import org.apache.directory.api.ldap.model.entry.Attribute; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.exception.LdapException; +import org.apache.directory.api.ldap.model.message.SearchScope; +import org.apache.directory.ldap.client.api.LdapConnection; +import org.apache.directory.ldap.client.api.LdapNetworkConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +public class LdapAuthProvider implements AuthenticatorProvider { + + private Logger log = LoggerFactory.getLogger(LdapAuthProvider.class); + + @Autowired + private LdapConfig ldapCfg; + + private LdapConnection getConn() { + return new LdapNetworkConnection(ldapCfg.getConn().getHost(), ldapCfg.getConn().getPort(), ldapCfg.getConn().isTls()); + } + + private void bind(LdapConnection conn) throws LdapException { + conn.bind(ldapCfg.getConn().getBindDn(), ldapCfg.getConn().getBindPassword()); + } + + private String getUidAttribute() { + return ldapCfg.getAttribute().getUid().getValue(); + } + + @Override + public boolean isEnabled() { + return ldapCfg.isEnabled(); + } + + @Override + public UserAuthResult authenticate(String id, String password) { + log.info("Performing auth for {}", id); + + LdapConnection conn = getConn(); + try { + bind(conn); + + String uidType = ldapCfg.getAttribute().getUid().getType(); + MatrixID mxIdExt = new MatrixID(id); + String userFilterValue = StringUtils.equals(LdapProvider.UID, uidType) ? mxIdExt.getLocalPart() : mxIdExt.getId(); + String userFilter = "(" + ldapCfg.getAttribute().getUid().getValue() + "=" + userFilterValue + ")"; + EntryCursor cursor = conn.search(ldapCfg.getConn().getBaseDn(), userFilter, SearchScope.SUBTREE, getUidAttribute(), ldapCfg.getAttribute().getName()); + try { + while (cursor.next()) { + Entry entry = cursor.get(); + String dn = entry.getDn().getName(); + log.info("Checking possible match, DN: {}", dn); + + Attribute attribute = entry.get(getUidAttribute()); + if (attribute == null) { + log.info("DN {}: no attribute {}, skpping", dn, getUidAttribute()); + continue; + } + + String data = attribute.get().toString(); + if (data.length() < 1) { + log.info("DN {}: empty attribute {}, skipping", getUidAttribute()); + continue; + } + + log.info("Attempting authentication on LDAP for {}", dn); + try { + conn.bind(entry.getDn(), password); + } catch (LdapException e) { + log.info("Unable to bind using {} because {}", entry.getDn().getName(), e.getMessage()); + return new UserAuthResult().failure(); + } + + Attribute nameAttribute = entry.get(ldapCfg.getAttribute().getName()); + String name = nameAttribute != null ? nameAttribute.get().toString() : null; + + log.info("Authentication successful for {}", entry.getDn().getName()); + log.info("DN {} is a valid match", dn); + + return new UserAuthResult().success(mxIdExt.getId(), name); + } + } catch (CursorLdapReferralException e) { + log.warn("Entity for {} is only available via referral, skipping", mxIdExt); + } finally { + cursor.close(); + } + + log.info("No match were found for {}", id); + return new UserAuthResult().failure(); + } catch (LdapException | IOException | CursorException e) { + throw new RuntimeException(e); + } finally { + try { + conn.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java index 771a630..a56044a 100644 --- a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java @@ -1,8 +1,12 @@ package io.kamax.mxisd.config.ldap; +import io.kamax.mxisd.lookup.provider.LdapProvider; +import org.apache.commons.lang.StringUtils; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; +import javax.annotation.PostConstruct; + @Configuration @ConfigurationProperties(prefix = "ldap.attribute.uid") public class LdapAttributeUidConfig { @@ -26,4 +30,11 @@ public class LdapAttributeUidConfig { this.value = value; } + @PostConstruct + public void postConstruct() { + if (!StringUtils.equals(LdapProvider.UID, getType()) && !StringUtils.equals(LdapProvider.MATRIX_ID, getType())) { + throw new IllegalArgumentException("Unsupported LDAP UID type: " + getType()); + } + } + } diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy index 392e5ff..1984f32 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy @@ -20,8 +20,6 @@ package io.kamax.mxisd.lookup.provider -import io.kamax.mxisd.auth.UserAuthResult -import io.kamax.mxisd.auth.provider.AuthenticatorProvider import io.kamax.mxisd.config.ServerConfig import io.kamax.mxisd.config.ldap.LdapConfig import io.kamax.mxisd.lookup.SingleLookupRequest @@ -40,7 +38,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component -class LdapProvider implements IThreePidProvider, AuthenticatorProvider { +class LdapProvider implements IThreePidProvider { public static final String UID = "uid" public static final String MATRIX_ID = "mxid" @@ -66,17 +64,8 @@ class LdapProvider implements IThreePidProvider, AuthenticatorProvider { conn.bind(ldapCfg.getConn().getBindDn(), ldapCfg.getConn().getBindPassword()) } - @Override - UserAuthResult authenticate(String id, String password) { - LdapConnection conn = getConn() - try { - bind(conn) - - // TODO finish this - return new UserAuthResult().failure() - } finally { - conn.close() - } + private String getUidAttribute() { + return ldapCfg.getAttribute().getUid().getValue(); } @Override @@ -90,7 +79,7 @@ class LdapProvider implements IThreePidProvider, AuthenticatorProvider { } Optional lookup(LdapConnection conn, String medium, String value) { - String uidAttribute = ldapCfg.getAttribute().getUid().getValue() + String uidAttribute = getUidAttribute() Optional queryOpt = ldapCfg.getIdentity().getQuery(medium) if (!queryOpt.isPresent()) {