Experimental support for synapse REST auth module

- See https://github.com/maxidor/matrix-synapse-rest-auth
- Include Google Firebase backend using UID as login and user token as password
This commit is contained in:
Maxime Dor
2017-08-31 02:10:36 +02:00
parent 1c43ca7666
commit 0033d0dc1d
7 changed files with 357 additions and 0 deletions

View File

@@ -32,6 +32,7 @@ buildscript {
} }
repositories { repositories {
maven { url "https://kamax.io/maven/releases/" }
mavenCentral() mavenCentral()
} }
@@ -45,6 +46,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.1'
// ed25519 handling // ed25519 handling
compile 'net.i2p.crypto:eddsa:0.1.0' compile 'net.i2p.crypto:eddsa:0.1.0'
@@ -63,6 +67,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'
} }

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,47 @@
package io.kamax.mxisd.auth;
public class UserAuthResult {
private boolean success;
private String mxid;
private String displayName;
public UserAuthResult failure() {
success = false;
mxid = null;
displayName = null;
return this;
}
public void success(String mxid, String displayName) {
setSuccess(true);
setMxid(mxid);
setDisplayName(displayName);
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMxid() {
return mxid;
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
}

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,103 @@
package io.kamax.mxisd.auth.provider;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseCredential;
import com.google.firebase.auth.FirebaseCredentials;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.auth.UserAuthResult;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileInputStream;
import java.io.IOException;
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
private boolean isEnabled;
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
public GoogleFirebaseAuthenticator(boolean isEnabled) {
this.isEnabled = isEnabled;
}
public GoogleFirebaseAuthenticator(String credsPath, String db) {
this(true);
try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db));
fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready");
} catch (IOException e) {
throw new RuntimeException("Error when initializing Firebase", e);
}
}
private FirebaseCredential getCreds(String credsPath) throws IOException {
if (StringUtils.isNotBlank(credsPath)) {
return FirebaseCredentials.fromCertificate(new FileInputStream(credsPath));
} else {
return FirebaseCredentials.applicationDefault();
}
}
private FirebaseOptions getOpts(String credsPath, String db) throws IOException {
if (StringUtils.isBlank(db)) {
throw new IllegalArgumentException("Firebase database is not configured");
}
return new FirebaseOptions.Builder()
.setCredential(getCreds(credsPath))
.setDatabaseUrl(db)
.build();
}
@Override
public boolean isEnabled() {
return isEnabled;
}
@Override
public UserAuthResult authenticate(String id, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
final UserAuthResult result = new UserAuthResult();
try {
log.info("Trying to authenticate {}", id);
_MatrixID mxId = new MatrixID(id);
fbAuth.verifyIdToken(password).addOnSuccessListener(token -> {
if (!StringUtils.equals(mxId.getLocalPart(), token.getUid())) {
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, mxId.getLocalPart(), token.getUid());
result.failure();
}
log.info("{} was successfully authenticated", id);
result.success(id, token.getName());
}).addOnFailureListener(e -> {
if (e instanceof IllegalArgumentException) {
log.info("Failure to authenticate {}: invalid firebase token", id);
} else {
log.info("Failure to authenticate {}", id, e.getMessage());
log.debug("Exception", e);
}
result.failure();
});
} catch (IllegalArgumentException e) {
log.warn("Could not validate {} as a Matrix ID: {}", id, e.getMessage());
result.failure();
}
return result;
}
}

View File

@@ -0,0 +1,65 @@
package io.kamax.mxisd.config;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
@ConfigurationProperties("firebase")
public class FirebaseConfig {
private Logger log = LoggerFactory.getLogger(FirebaseConfig.class);
private boolean enabled;
private String credentials;
private String database;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getCredentials() {
return credentials;
}
public void setCredentials(String credentials) {
this.credentials = credentials;
}
public String getDatabase() {
return database;
}
public void setDatabase(String database) {
this.database = database;
}
@PostConstruct
private void postConstruct() {
log.info("--- Firebase configuration ---");
log.info("Enabled: {}", isEnabled());
if (isEnabled()) {
log.info("Credentials: {}", getCredentials());
log.info("Database: {}", getDatabase());
}
}
@Bean
public AuthenticatorProvider getProvider() {
if (!enabled) {
return new GoogleFirebaseAuthenticator(false);
}
return new GoogleFirebaseAuthenticator(credentials, database);
}
}

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