Auth support with synapse REST auth module

This commit is contained in:
Maxime Dor
2017-09-05 21:36:33 +02:00
19 changed files with 1039 additions and 199 deletions

View File

@@ -99,41 +99,85 @@ lookup:
ldap:
enabled: false
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.
# Global enable/disable switch
enabled: true
# 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)
#
# /!\ Currently NOT supported due to a possible bug in LDAP library /!\
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))"

View File

@@ -62,6 +62,7 @@ buildscript {
}
repositories {
maven { url "https://kamax.io/maven/releases/" }
mavenCentral()
}
@@ -75,6 +76,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.2'
// ed25519 handling
compile 'net.i2p.crypto:eddsa:0.1.0'
@@ -93,6 +97,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'
}

View File

@@ -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 {
}

View File

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

View File

@@ -0,0 +1,49 @@
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 UserAuthResult success(String mxid, String displayName) {
setSuccess(true);
setMxid(mxid);
setDisplayName(displayName);
return this;
}
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;
}
}

View File

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

View File

@@ -0,0 +1,227 @@
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.OnFailureListener
import com.google.firebase.tasks.OnSuccessListener
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<UserRecord> findInternal(String medium, String address) {
UserRecord r;
CountDownLatch l = new CountDownLatch(1);
OnSuccessListener<UserRecord> success = new OnSuccessListener<UserRecord>() {
@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;
l.countDown()
}
};
if (ThreePidMedium.Email.is(medium)) {
log.info("Performing E-mail 3PID lookup for {}", address)
fbAuth.getUserByEmail(address)
.addOnSuccessListener(success)
.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);
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<UserRecord> urOpt = findInternal(request.getType(), request.getThreePid())
if (urOpt.isPresent()) {
return [
address : request.getThreePid(),
medium : request.getType(),
mxid : "@${urOpt.get().getUid()}:${domain}",
not_before: 0,
not_after : 9223372036854775807,
ts : 0
]
} else {
return Optional.empty();
}
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<ThreePidMapping> results = new ArrayList<>();
mappings.parallelStream().forEach(new Consumer<ThreePidMapping>() {
@Override
void accept(ThreePidMapping o) {
Optional<UserRecord> urOpt = findInternal(o.getMedium(), o.getValue());
if (urOpt.isPresent()) {
ThreePidMapping result = new ThreePidMapping();
result.setMedium(o.getMedium())
result.setValue(o.getValue())
result.setMxid("@${urOpt.get().getUid()}:${domain}")
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<FirebaseToken>() {
@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());
l.countDown()
}
}).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();
l.countDown()
}
});
try {
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for Firebase auth check");
result.failure();
}
return result;
}
}

View File

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

View File

@@ -0,0 +1,71 @@
package io.kamax.mxisd.config;
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;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("firebase")
public class FirebaseConfig {
private Logger log = LoggerFactory.getLogger(FirebaseConfig.class);
@Autowired
private ServerConfig srvCfg;
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 GlobalProvider getProvider() {
if (!enabled) {
return new GoogleFirebaseAuthenticator(false);
} else {
return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName());
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> 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<String, String> getMappings() {
return mappings
}
void setMappings(Map<String, String> mappings) {
this.mappings = mappings
}
Optional<String> 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!")
}
}
}

View File

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

View File

@@ -0,0 +1,40 @@
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 {
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;
}
@PostConstruct
public void postConstruct() {
if (!StringUtils.equals(LdapProvider.UID, getType()) && !StringUtils.equals(LdapProvider.MATRIX_ID, getType())) {
throw new IllegalArgumentException("Unsupported LDAP UID type: " + getType());
}
}
}

View File

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

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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))
}
}

View File

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

View File

@@ -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<String, String> medium = new HashMap<>();
public Map<String, String> getMedium() {
return medium;
}
public Optional<String> getQuery(String key) {
return Optional.ofNullable(medium.get(key));
}
public void setMedium(Map<String, String> medium) {
this.medium = medium;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -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\"}}";
}
}

View File

@@ -20,8 +20,8 @@
package io.kamax.mxisd.lookup.provider
import io.kamax.mxisd.config.LdapConfig
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
@@ -53,7 +53,19 @@ 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())
}
private String getUidAttribute() {
return ldapCfg.getAttribute().getUid().getValue();
}
@Override
@@ -67,20 +79,22 @@ class LdapProvider implements IThreePidProvider {
}
Optional<String> lookup(LdapConnection conn, String medium, String value) {
Optional<String> queryOpt = ldapCfg.getMapping(medium)
String uidAttribute = getUidAttribute()
Optional<String> 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 +108,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 +134,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<String> mxid = lookup(conn, request.getType(), request.getThreePid())
if (mxid.isPresent()) {
@@ -147,9 +162,9 @@ class LdapProvider implements IThreePidProvider {
log.info("Looking up {} mappings", mappings.size())
List<ThreePidMapping> 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 {