diff --git a/src/main/groovy/io/kamax/mxisd/UserID.java b/src/main/groovy/io/kamax/mxisd/UserID.java new file mode 100644 index 0000000..5cf683a --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/UserID.java @@ -0,0 +1,46 @@ +/* + * 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; + +// FIXME consider integrating in matrix-java-sdk? +public class UserID { + + private String type; + private String value; + + protected UserID() { + // stub for (de)serialization + } + + public UserID(String type, String value) { + this.type = type; + this.value = value; + } + + public String getType() { + return type; + } + + public String getValue() { + return value; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/UserIdType.java b/src/main/groovy/io/kamax/mxisd/UserIdType.java new file mode 100644 index 0000000..e0625bf --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/UserIdType.java @@ -0,0 +1,47 @@ +/* + * 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; + +import org.apache.commons.lang.StringUtils; + +// FIXME consider integrating in matrix-java-sdk? +public enum UserIdType { + + Localpart("localpart"), + MatrixID("mxid"), + EmailLocalpart("email_localpart"), + Email("email"); + + private String id; + + UserIdType(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public boolean is(String id) { + return StringUtils.equalsIgnoreCase(this.id, id); + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java index 6dbfff2..aad2cbd 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java +++ b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java @@ -20,8 +20,12 @@ package io.kamax.mxisd.auth; +import io.kamax.matrix.MatrixID; import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.auth.provider.BackendAuthResult; +import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.lookup.ThreePidMapping; import org.slf4j.Logger; @@ -40,6 +44,9 @@ public class AuthManager { @Autowired private List providers = new ArrayList<>(); + @Autowired + private MatrixConfig mxCfg; + @Autowired private InvitationManager invMgr; @@ -49,17 +56,32 @@ public class AuthManager { continue; } - UserAuthResult result = provider.authenticate(id, password); + BackendAuthResult result = provider.authenticate(id, password); if (result.isSuccess()) { + + String mxId; + if (UserIdType.Localpart.is(result.getId().getType())) { + mxId = new MatrixID(result.getId().getValue(), mxCfg.getDomain()).getId(); + } else if (UserIdType.MatrixID.is(result.getId().getType())) { + mxId = new MatrixID(result.getId().getValue()).getId(); + } else { + log.warn("Unsupported User ID type {} for backend {}", result.getId().getType(), provider.getClass().getSimpleName()); + continue; + } + + UserAuthResult authResult = new UserAuthResult().success(mxId, result.getProfile().getDisplayName()); + for (ThreePid pid : result.getProfile().getThreePids()) { + authResult.withThreePid(pid.getMedium(), pid.getAddress()); + } log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName()); - for (ThreePid pid : result.getThreePids()) { + for (ThreePid pid : authResult.getThreePids()) { log.info("Processing {} for {}", pid, id); - invMgr.publishMappingIfInvited(new ThreePidMapping(pid, result.getMxid())); + invMgr.publishMappingIfInvited(new ThreePidMapping(pid, authResult.getMxid())); } invMgr.lookupMappingsForInvites(); - return result; + return authResult; } } diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java index f9aefb6..6103f36 100644 --- a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java @@ -20,12 +20,10 @@ package io.kamax.mxisd.auth.provider; -import io.kamax.mxisd.auth.UserAuthResult; - public interface AuthenticatorProvider { boolean isEnabled(); - UserAuthResult authenticate(String id, String password); + BackendAuthResult authenticate(String id, String password); } diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthReplyJson.java b/src/main/groovy/io/kamax/mxisd/auth/provider/BackendAuthResult.java similarity index 52% rename from src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthReplyJson.java rename to src/main/groovy/io/kamax/mxisd/auth/provider/BackendAuthResult.java index cb0cef7..79c1036 100644 --- a/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthReplyJson.java +++ b/src/main/groovy/io/kamax/mxisd/auth/provider/BackendAuthResult.java @@ -18,16 +18,18 @@ * along with this program. If not, see . */ -package io.kamax.mxisd.backend.rest; +package io.kamax.mxisd.auth.provider; import io.kamax.mxisd.ThreePid; +import io.kamax.mxisd.UserID; +import io.kamax.mxisd.UserIdType; import java.util.ArrayList; import java.util.List; -public class RestAuthReplyJson { +public class BackendAuthResult { - public static class RestAuthProfileData { + public static class BackendAuthProfile { private String displayName; private List threePids = new ArrayList<>(); @@ -36,46 +38,51 @@ public class RestAuthReplyJson { return displayName; } - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - public List getThreePids() { return threePids; } + } - public void setThreePids(List threePids) { - this.threePids = threePids; - } + public static BackendAuthResult failure() { + BackendAuthResult r = new BackendAuthResult(); + r.success = false; + return r; + } + public static BackendAuthResult success(String id, UserIdType type, String displayName) { + return success(id, type.getId(), displayName); + } + + public static BackendAuthResult success(String id, String type, String displayName) { + BackendAuthResult r = new BackendAuthResult(); + r.success = true; + r.id = new UserID(type, id); + r.profile = new BackendAuthProfile(); + r.profile.displayName = displayName; + + return r; } private Boolean success; - private String mxid; - private RestAuthProfileData profile; + private UserID id; + private BackendAuthProfile profile = new BackendAuthProfile(); - public boolean isSuccess() { + public Boolean isSuccess() { return success; } - public void setSuccess(boolean success) { - this.success = success; + public UserID getId() { + return id; } - public String getMxid() { - return mxid; - } - - public void setMxid(String mxid) { - this.mxid = mxid; - } - - public RestAuthProfileData getProfile() { + public BackendAuthProfile getProfile() { return profile; } - public void setProfile(RestAuthProfileData profile) { - this.profile = profile; + public BackendAuthResult withThreePid(ThreePid threePid) { + this.profile.threePids.add(threePid); + + return this; } } diff --git a/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy b/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy index ea1d843..7139f2a 100644 --- a/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy +++ b/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy @@ -27,8 +27,10 @@ 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.ThreePid +import io.kamax.mxisd.UserIdType import io.kamax.mxisd.auth.provider.AuthenticatorProvider +import io.kamax.mxisd.auth.provider.BackendAuthResult import org.apache.commons.lang.StringUtils import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -49,7 +51,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { private FirebaseApp fbApp; private FirebaseAuth fbAuth; - private void waitOnLatch(UserAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) { + private void waitOnLatch(BackendAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) { try { l.await(timeout, unit); } catch (InterruptedException e) { @@ -108,22 +110,21 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { } @Override - public UserAuthResult authenticate(String id, String password) { + public BackendAuthResult 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(); + BackendAuthResult.failure(); } - String localpart = m.group(1); + BackendAuthResult result = BackendAuthResult.failure(); + String localpart = m.group(1); CountDownLatch l = new CountDownLatch(1); fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener() { @Override @@ -131,13 +132,12 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { try { 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(); + result = BackendAuthResult.failure(); return; } + result = BackendAuthResult.success(id, UserIdType.MatrixID, token.getName()); log.info("{} was successfully authenticated", id); - result.success(id, token.getName()); - log.info("Fetching profile for {}", id); CountDownLatch userRecordLatch = new CountDownLatch(1); fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener() { @@ -145,12 +145,13 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { void onSuccess(UserRecord user) { try { if (StringUtils.isNotBlank(user.getEmail())) { - result.withThreePid(ThreePidMedium.Email, user.getEmail()); + result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail())); } if (StringUtils.isNotBlank(user.getPhoneNumber())) { - result.withThreePid(ThreePidMedium.PhoneNumber, user.getPhoneNumber()); + result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber())); } + } finally { userRecordLatch.countDown(); } @@ -160,7 +161,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { void onFailure(@NonNull Exception e) { try { log.warn("Unable to fetch Firebase user profile for {}", id); - result.failure(); + result = BackendAuthResult.failure(); } finally { userRecordLatch.countDown(); } @@ -183,7 +184,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { log.info("Exception", e); } - result.failure(); + result = BackendAuthResult.failure(); } finally { l.countDown() } diff --git a/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java b/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java index 30a0fc9..970b379 100644 --- a/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java +++ b/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java @@ -21,8 +21,9 @@ package io.kamax.mxisd.backend.ldap; import io.kamax.matrix.MatrixID; -import io.kamax.mxisd.auth.UserAuthResult; +import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.auth.provider.BackendAuthResult; import org.apache.commons.lang.StringUtils; import org.apache.directory.api.ldap.model.cursor.CursorException; import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException; @@ -53,7 +54,7 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato } @Override - public UserAuthResult authenticate(String id, String password) { + public BackendAuthResult authenticate(String id, String password) { log.info("Performing auth for {}", id); LdapConnection conn = getConn(); @@ -88,7 +89,7 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato conn.bind(entry.getDn(), password); } catch (LdapException e) { log.info("Unable to bind using {} because {}", entry.getDn().getName(), e.getMessage()); - return new UserAuthResult().failure(); + return BackendAuthResult.failure(); } Attribute nameAttribute = entry.get(getCfg().getAttribute().getName()); @@ -97,7 +98,8 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato log.info("Authentication successful for {}", entry.getDn().getName()); log.info("DN {} is a valid match", dn); - return new UserAuthResult().success(mxIdExt.getId(), name); + // TODO should we canonicalize the MXID? + return BackendAuthResult.success(mxIdExt.getId(), UserIdType.MatrixID, name); } } catch (CursorLdapReferralException e) { log.warn("Entity for {} is only available via referral, skipping", mxIdExt); @@ -106,7 +108,7 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato } log.info("No match were found for {}", id); - return new UserAuthResult().failure(); + return BackendAuthResult.failure(); } catch (LdapException | IOException | CursorException e) { throw new RuntimeException(e); } finally { diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java b/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java index 57859d7..8a07c4b 100644 --- a/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java +++ b/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java @@ -25,8 +25,8 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixID; -import io.kamax.mxisd.auth.UserAuthResult; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.auth.provider.BackendAuthResult; import io.kamax.mxisd.config.rest.RestBackendConfig; import io.kamax.mxisd.util.GsonParser; import io.kamax.mxisd.util.RestClientUtils; @@ -62,7 +62,7 @@ public class RestAuthProvider implements AuthenticatorProvider { } @Override - public UserAuthResult authenticate(String id, String password) { + public BackendAuthResult authenticate(String id, String password) { _MatrixID mxid = new MatrixID(id); RestAuthRequestJson auth = new RestAuthRequestJson(); auth.setMxid(id); @@ -72,19 +72,12 @@ public class RestAuthProvider implements AuthenticatorProvider { HttpUriRequest req = RestClientUtils.post(cfg.getEndpoints().getAuth(), gson, "auth", auth); try (CloseableHttpResponse res = client.execute(req)) { - UserAuthResult result = new UserAuthResult(); - int status = res.getStatusLine().getStatusCode(); if (status < 200 || status >= 300) { - return result.failure(); + return BackendAuthResult.failure(); } - RestAuthReplyJson reply = parser.parse(res, "auth", RestAuthReplyJson.class); - if (!reply.isSuccess()) { - return result.failure(); - } - - return result.success(reply.getMxid(), reply.getProfile().getDisplayName()); + return parser.parse(res, "auth", BackendAuthResult.class); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java b/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java index dd9e6ee..17714ab 100644 --- a/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java +++ b/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java @@ -20,8 +20,8 @@ package io.kamax.mxisd.backend.sql; -import io.kamax.mxisd.auth.UserAuthResult; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; +import io.kamax.mxisd.auth.provider.BackendAuthResult; import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.sql.SqlProviderConfig; import io.kamax.mxisd.invitation.InvitationManager; @@ -50,11 +50,11 @@ public class SqlAuthProvider implements AuthenticatorProvider { } @Override - public UserAuthResult authenticate(String id, String password) { + public BackendAuthResult authenticate(String id, String password) { log.info("Performing dummy authentication try to force invite mapping refresh"); invMgr.lookupMappingsForInvites(); - return new UserAuthResult().failure(); + return BackendAuthResult.failure(); } }