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:
@@ -32,6 +32,7 @@ buildscript {
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url "https://kamax.io/maven/releases/" }
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -45,6 +46,9 @@ dependencies {
|
||||
// Spring Boot - standalone app
|
||||
compile 'org.springframework.boot:spring-boot-starter-web:1.5.3.RELEASE'
|
||||
|
||||
// Matrix Java SDK
|
||||
compile 'io.kamax:matrix-java-sdk:0.0.1'
|
||||
|
||||
// ed25519 handling
|
||||
compile 'net.i2p.crypto:eddsa:0.1.0'
|
||||
|
||||
@@ -63,6 +67,9 @@ dependencies {
|
||||
// Phone numbers validation
|
||||
compile 'com.googlecode.libphonenumber:libphonenumber:8.7.1'
|
||||
|
||||
// Google Firebase Authentication backend
|
||||
compile 'com.google.firebase:firebase-admin:5.3.0'
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
|
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();
|
||||
}
|
||||
|
||||
}
|
47
src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java
Normal file
47
src/main/groovy/io/kamax/mxisd/auth/UserAuthResult.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
@@ -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,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;
|
||||
}
|
||||
|
||||
}
|
65
src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java
Normal file
65
src/main/groovy/io/kamax/mxisd/config/FirebaseConfig.java
Normal 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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user