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