Auth support with synapse REST auth module
This commit is contained in:
@@ -99,14 +99,37 @@ lookup:
|
|||||||
|
|
||||||
|
|
||||||
ldap:
|
ldap:
|
||||||
enabled: false
|
|
||||||
|
# Global enable/disable switch
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Connection configuration to the LDAP server
|
||||||
|
connection:
|
||||||
|
|
||||||
|
# If the connection should be secure
|
||||||
tls: false
|
tls: false
|
||||||
|
|
||||||
|
# Host to connect to
|
||||||
host: 'localhost'
|
host: 'localhost'
|
||||||
|
|
||||||
|
# Port to connect to
|
||||||
port: 389
|
port: 389
|
||||||
|
|
||||||
|
# Bind DN to use when performing lookups
|
||||||
bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org'
|
bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org'
|
||||||
|
|
||||||
|
# Bind password to use
|
||||||
bindPassword: 'password'
|
bindPassword: 'password'
|
||||||
|
|
||||||
|
# Base DN used in all queries
|
||||||
baseDn: 'CN=Users,DC=example,DC=org'
|
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.
|
# How should we resolve the Matrix ID in case of a match using the attribute.
|
||||||
#
|
#
|
||||||
# The following type are supported:
|
# The following type are supported:
|
||||||
@@ -122,13 +145,34 @@ ldap:
|
|||||||
# - For type 'uid': 'userPrincipalName' or 'uid' or 'saMAccountName'
|
# - 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
|
# - For type 'mxid', regardless of the directory type, we recommend using 'pager' as it is a standard attribute and
|
||||||
# is typically not used.
|
# is typically not used.
|
||||||
attribute: 'userPrincipalName'
|
value: 'userPrincipalName'
|
||||||
|
|
||||||
|
# The display name of the user
|
||||||
|
name: 'displayName'
|
||||||
|
|
||||||
|
# Configuration section relating the authentication of users performed via LDAP.
|
||||||
|
#
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# 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.
|
# Configure each 3PID type with a dedicated query.
|
||||||
mappings:
|
medium:
|
||||||
|
# E-mail query
|
||||||
email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))"
|
email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))"
|
||||||
|
|
||||||
# Phone numbers query.
|
# Phone numbers query
|
||||||
#
|
#
|
||||||
# Phone numbers use the MSISDN format: https://en.wikipedia.org/wiki/MSISDN
|
# 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.
|
# This format does not include international prefix (+ or 00) and therefore has to be put in the query.
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ buildscript {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
maven { url "https://kamax.io/maven/releases/" }
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +76,9 @@ dependencies {
|
|||||||
// Spring Boot - standalone app
|
// Spring Boot - standalone app
|
||||||
compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE'
|
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
|
// ed25519 handling
|
||||||
compile 'net.i2p.crypto:eddsa:0.1.0'
|
compile 'net.i2p.crypto:eddsa:0.1.0'
|
||||||
|
|
||||||
@@ -93,6 +97,9 @@ dependencies {
|
|||||||
// Phone numbers validation
|
// Phone numbers validation
|
||||||
compile 'com.googlecode.libphonenumber:libphonenumber:8.7.1'
|
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'
|
testCompile 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
src/main/groovy/io/kamax/mxisd/GlobalProvider.java
Normal file
7
src/main/groovy/io/kamax/mxisd/GlobalProvider.java
Normal 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 {
|
||||||
|
}
|
||||||
35
src/main/groovy/io/kamax/mxisd/auth/AuthManager.java
Normal file
35
src/main/groovy/io/kamax/mxisd/auth/AuthManager.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
49
src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java
Normal file
49
src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
71
src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java
Normal file
71
src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
125
src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy
Normal file
125
src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy
Normal 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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\"}}";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.lookup.provider
|
package io.kamax.mxisd.lookup.provider
|
||||||
|
|
||||||
import io.kamax.mxisd.config.LdapConfig
|
|
||||||
import io.kamax.mxisd.config.ServerConfig
|
import io.kamax.mxisd.config.ServerConfig
|
||||||
|
import io.kamax.mxisd.config.ldap.LdapConfig
|
||||||
import io.kamax.mxisd.lookup.SingleLookupRequest
|
import io.kamax.mxisd.lookup.SingleLookupRequest
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping
|
import io.kamax.mxisd.lookup.ThreePidMapping
|
||||||
import org.apache.commons.lang.StringUtils
|
import org.apache.commons.lang.StringUtils
|
||||||
@@ -53,7 +53,19 @@ class LdapProvider implements IThreePidProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isEnabled() {
|
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
|
@Override
|
||||||
@@ -67,20 +79,22 @@ class LdapProvider implements IThreePidProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Optional<String> lookup(LdapConnection conn, String medium, String value) {
|
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()) {
|
if (!queryOpt.isPresent()) {
|
||||||
log.warn("{} is not a configured 3PID type for LDAP lookup", medium)
|
log.warn("{} is not a configured 3PID type for LDAP lookup", medium)
|
||||||
return Optional.empty()
|
return Optional.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
String searchQuery = queryOpt.get().replaceAll("%3pid", value)
|
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 {
|
try {
|
||||||
while (cursor.next()) {
|
while (cursor.next()) {
|
||||||
Entry entry = cursor.get()
|
Entry entry = cursor.get()
|
||||||
log.info("Found possible match, DN: {}", entry.getDn().getName())
|
log.info("Found possible match, DN: {}", entry.getDn().getName())
|
||||||
|
|
||||||
Attribute attribute = entry.get(ldapCfg.getAttribute())
|
Attribute attribute = entry.get(uidAttribute)
|
||||||
if (attribute == null) {
|
if (attribute == null) {
|
||||||
log.info("DN {}: no attribute {}, skpping", entry.getDn(), ldapCfg.getAttribute())
|
log.info("DN {}: no attribute {}, skpping", entry.getDn(), ldapCfg.getAttribute())
|
||||||
continue
|
continue
|
||||||
@@ -94,12 +108,13 @@ class LdapProvider implements IThreePidProvider {
|
|||||||
|
|
||||||
StringBuilder matrixId = new StringBuilder()
|
StringBuilder matrixId = new StringBuilder()
|
||||||
// TODO Should we turn this block into a map of functions?
|
// 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())
|
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)
|
matrixId.append(data)
|
||||||
} else {
|
} 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,9 +134,9 @@ class LdapProvider implements IThreePidProvider {
|
|||||||
Optional<?> find(SingleLookupRequest request) {
|
Optional<?> find(SingleLookupRequest request) {
|
||||||
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}")
|
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 {
|
try {
|
||||||
conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword())
|
bind(conn)
|
||||||
|
|
||||||
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid())
|
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid())
|
||||||
if (mxid.isPresent()) {
|
if (mxid.isPresent()) {
|
||||||
@@ -147,9 +162,9 @@ class LdapProvider implements IThreePidProvider {
|
|||||||
log.info("Looking up {} mappings", mappings.size())
|
log.info("Looking up {} mappings", mappings.size())
|
||||||
List<ThreePidMapping> mappingsFound = new ArrayList<>()
|
List<ThreePidMapping> mappingsFound = new ArrayList<>()
|
||||||
|
|
||||||
LdapConnection conn = new LdapNetworkConnection(ldapCfg.getHost(), ldapCfg.getPort())
|
LdapConnection conn = getConn()
|
||||||
try {
|
try {
|
||||||
conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword())
|
bind(conn)
|
||||||
|
|
||||||
for (ThreePidMapping mapping : mappings) {
|
for (ThreePidMapping mapping : mappings) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user