Add support for username rewrite (Fix #103)
This commit is contained in:
@@ -74,7 +74,15 @@ See your Identity store [documentation](../stores/README.md) on how to enable th
|
|||||||
|
|
||||||
|
|
||||||
## Advanced
|
## Advanced
|
||||||
The Authentication feature allows users to login to their Homeserver by using their 3PIDs in a configured Identity store.
|
The Authentication feature allows users to:
|
||||||
|
- Rewrite usernames matching a pattern to be mapped to another username via a 3PID.
|
||||||
|
- login to their Homeserver by using their 3PIDs in a configured Identity store.
|
||||||
|
|
||||||
|
This feature also allows to work around the following issues:
|
||||||
|
- Lowercase all usernames for synapse, allowing case-insensitive login
|
||||||
|
- Unable to login on synapse if username is numerical
|
||||||
|
- Any generic transformation of username prior to sending to synapse, bypassing the restriction that password providers
|
||||||
|
cannot change the localpart being authenticated.
|
||||||
|
|
||||||
### Overview
|
### Overview
|
||||||
This is performed by intercepting the Homeserver endpoint `/_matrix/client/r0/login` as depicted below:
|
This is performed by intercepting the Homeserver endpoint `/_matrix/client/r0/login` as depicted below:
|
||||||
@@ -109,10 +117,10 @@ Steps of user authentication using a 3PID:
|
|||||||
4. The response from the Homeserver is sent back to the client, believing it was the HS which directly answered.
|
4. The response from the Homeserver is sent back to the client, believing it was the HS which directly answered.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
- [Basic Authentication configured and working](#basic)
|
|
||||||
- Reverse proxy setup
|
|
||||||
- Homeserver
|
|
||||||
- Compatible [Identity store](../stores/README.md)
|
- Compatible [Identity store](../stores/README.md)
|
||||||
|
- [Basic Authentication configured and working](#basic)
|
||||||
|
- Client and Homeserver using the [C2S API r0.4.x](https://matrix.org/docs/spec/client_server/r0.4.0.html) or later
|
||||||
|
- Reverse proxy setup
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
#### Reverse Proxy
|
#### Reverse Proxy
|
||||||
@@ -153,3 +161,40 @@ In case the hostname is the same as your Matrix domain and `server.name` is not
|
|||||||
`matrix.domain` and will still probably have the correct value.
|
`matrix.domain` and will still probably have the correct value.
|
||||||
|
|
||||||
`value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`.
|
`value` is the base internal URL of the Homeserver, without any `/_matrix/..` or trailing `/`.
|
||||||
|
|
||||||
|
#### Username rewrite
|
||||||
|
In mxisd config:
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
rewrite:
|
||||||
|
user:
|
||||||
|
rules:
|
||||||
|
- regex: <your regexp>
|
||||||
|
medium: 'your.custom.medium.type'
|
||||||
|
```
|
||||||
|
`rules` takes a list of rules. Rules have two properties:
|
||||||
|
- `regexp`: The regex pattern to match. This **MUST** match the full string. See [Java regex](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html) for syntax.
|
||||||
|
- `medium`: Custom 3PID type that will be used in the 3PID lookup. This can be anything you want and needs to be supported
|
||||||
|
by your Identity store config and/or code.
|
||||||
|
|
||||||
|
Rules are matched in listed order.
|
||||||
|
|
||||||
|
Common regexp patterns:
|
||||||
|
- Numerical usernames: `[0-9]+`
|
||||||
|
|
||||||
|
##### LDAP Example
|
||||||
|
If your users use their numerical employee IDs, which cannot be used with synapse, you can make it work with (relevant config only):
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
rewrite:
|
||||||
|
user:
|
||||||
|
rules:
|
||||||
|
- regex: '[0-9]+'
|
||||||
|
medium: 'kmx.employee.id'
|
||||||
|
|
||||||
|
ldap:
|
||||||
|
attribute:
|
||||||
|
threepid:
|
||||||
|
kmx.employee.id:
|
||||||
|
- 'ldapAttributeForEmployeeId'
|
||||||
|
```
|
||||||
|
@@ -20,37 +20,95 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.auth;
|
package io.kamax.mxisd.auth;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
import com.google.i18n.phonenumbers.NumberParseException;
|
||||||
|
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
||||||
|
import com.google.i18n.phonenumbers.Phonenumber;
|
||||||
import io.kamax.matrix.MatrixID;
|
import io.kamax.matrix.MatrixID;
|
||||||
import io.kamax.matrix.ThreePid;
|
import io.kamax.matrix.ThreePid;
|
||||||
import io.kamax.matrix._MatrixID;
|
import io.kamax.matrix._MatrixID;
|
||||||
import io.kamax.matrix._ThreePid;
|
import io.kamax.matrix._ThreePid;
|
||||||
|
import io.kamax.matrix.json.GsonUtil;
|
||||||
import io.kamax.mxisd.UserIdType;
|
import io.kamax.mxisd.UserIdType;
|
||||||
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
|
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
|
||||||
import io.kamax.mxisd.auth.provider.BackendAuthResult;
|
import io.kamax.mxisd.auth.provider.BackendAuthResult;
|
||||||
|
import io.kamax.mxisd.config.AuthenticationConfig;
|
||||||
import io.kamax.mxisd.config.MatrixConfig;
|
import io.kamax.mxisd.config.MatrixConfig;
|
||||||
|
import io.kamax.mxisd.dns.ClientDnsOverwrite;
|
||||||
|
import io.kamax.mxisd.exception.RemoteLoginException;
|
||||||
import io.kamax.mxisd.invitation.InvitationManager;
|
import io.kamax.mxisd.invitation.InvitationManager;
|
||||||
import io.kamax.mxisd.lookup.ThreePidMapping;
|
import io.kamax.mxisd.lookup.ThreePidMapping;
|
||||||
|
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
|
||||||
|
import io.kamax.mxisd.util.RestClientUtils;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang.StringUtils;
|
||||||
|
import org.apache.http.HttpEntity;
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.client.methods.HttpPost;
|
||||||
|
import org.apache.http.client.utils.URIBuilder;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AuthManager {
|
public class AuthManager {
|
||||||
|
|
||||||
private Logger log = LoggerFactory.getLogger(AuthManager.class);
|
private static final String TypeKey = "type";
|
||||||
|
private static final String UserKey = "user";
|
||||||
|
private static final String IdentifierKey = "identifier";
|
||||||
|
private static final String ThreepidMediumKey = "medium";
|
||||||
|
private static final String ThreepidAddressKey = "address";
|
||||||
|
private static final String UserIdTypeValue = "m.id.user";
|
||||||
|
private static final String ThreepidTypeValue = "m.id.thirdparty";
|
||||||
|
|
||||||
@Autowired
|
private final Logger log = LoggerFactory.getLogger(AuthManager.class);
|
||||||
private List<AuthenticatorProvider> providers = new ArrayList<>();
|
private final Gson gson = GsonUtil.get();
|
||||||
|
|
||||||
@Autowired
|
private List<AuthenticatorProvider> providers;
|
||||||
private MatrixConfig mxCfg;
|
private MatrixConfig mxCfg;
|
||||||
|
private AuthenticationConfig cfg;
|
||||||
|
private InvitationManager invMgr;
|
||||||
|
private ClientDnsOverwrite dns;
|
||||||
|
private LookupStrategy strategy;
|
||||||
|
private CloseableHttpClient client;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private InvitationManager invMgr;
|
public AuthManager(
|
||||||
|
AuthenticationConfig cfg,
|
||||||
|
MatrixConfig mxCfg,
|
||||||
|
List<AuthenticatorProvider> providers,
|
||||||
|
LookupStrategy strategy,
|
||||||
|
InvitationManager invMgr,
|
||||||
|
ClientDnsOverwrite dns,
|
||||||
|
CloseableHttpClient client
|
||||||
|
) {
|
||||||
|
this.cfg = cfg;
|
||||||
|
this.mxCfg = mxCfg;
|
||||||
|
this.providers = new ArrayList<>(providers);
|
||||||
|
this.strategy = strategy;
|
||||||
|
this.invMgr = invMgr;
|
||||||
|
this.dns = dns;
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String resolveProxyUrl(URI target) {
|
||||||
|
URIBuilder builder = dns.transform(target);
|
||||||
|
String urlToLogin = builder.toString();
|
||||||
|
log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin);
|
||||||
|
return urlToLogin;
|
||||||
|
}
|
||||||
|
|
||||||
public UserAuthResult authenticate(String id, String password) {
|
public UserAuthResult authenticate(String id, String password) {
|
||||||
_MatrixID mxid = MatrixID.asAcceptable(id);
|
_MatrixID mxid = MatrixID.asAcceptable(id);
|
||||||
@@ -92,4 +150,128 @@ public class AuthManager {
|
|||||||
return new UserAuthResult().failure();
|
return new UserAuthResult().failure();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String proxyLogin(URI target, String body) {
|
||||||
|
JsonObject reqJsonObject = io.kamax.matrix.json.GsonUtil.parseObj(body);
|
||||||
|
|
||||||
|
GsonUtil.findObj(reqJsonObject, IdentifierKey).ifPresent(obj -> {
|
||||||
|
GsonUtil.findString(obj, TypeKey).ifPresent(type -> {
|
||||||
|
if (StringUtils.equals(type, UserIdTypeValue)) {
|
||||||
|
log.info("Login request is User ID type");
|
||||||
|
|
||||||
|
if (cfg.getRewrite().getUser().getRules().isEmpty()) {
|
||||||
|
log.info("No User ID rewrite rules to apply");
|
||||||
|
} else {
|
||||||
|
log.info("User ID rewrite rules: checking for a match");
|
||||||
|
|
||||||
|
String userId = GsonUtil.getStringOrThrow(obj, UserKey);
|
||||||
|
for (AuthenticationConfig.Rule m : cfg.getRewrite().getUser().getRules()) {
|
||||||
|
if (m.getPattern().matcher(userId).matches()) {
|
||||||
|
log.info("Found matching pattern, resolving to 3PID with medium {}", m.getMedium());
|
||||||
|
|
||||||
|
// Remove deprecated login info on the top object if exists to avoid duplication
|
||||||
|
reqJsonObject.remove(UserKey);
|
||||||
|
obj.addProperty(TypeKey, ThreepidTypeValue);
|
||||||
|
obj.addProperty(ThreepidMediumKey, m.getMedium());
|
||||||
|
obj.addProperty(ThreepidAddressKey, userId);
|
||||||
|
|
||||||
|
log.info("Rewrite to 3PID done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("User ID rewrite rules: done checking rules");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
GsonUtil.findObj(reqJsonObject, IdentifierKey).ifPresent(obj -> {
|
||||||
|
GsonUtil.findString(obj, TypeKey).ifPresent(type -> {
|
||||||
|
if (StringUtils.equals(type, ThreepidTypeValue)) {
|
||||||
|
// Remove deprecated login info if exists to avoid duplication
|
||||||
|
reqJsonObject.remove(ThreepidMediumKey);
|
||||||
|
reqJsonObject.remove(ThreepidAddressKey);
|
||||||
|
|
||||||
|
GsonUtil.findPrimitive(obj, ThreepidMediumKey).ifPresent(medium -> {
|
||||||
|
GsonUtil.findPrimitive(obj, ThreepidAddressKey).ifPresent(address -> {
|
||||||
|
log.info("Login request with medium '{}' and address '{}'", medium.getAsString(), address.getAsString());
|
||||||
|
strategy.findLocal(medium.getAsString(), address.getAsString()).ifPresent(lookupDataOpt -> {
|
||||||
|
obj.remove(ThreepidMediumKey);
|
||||||
|
obj.remove(ThreepidAddressKey);
|
||||||
|
obj.addProperty(TypeKey, UserIdTypeValue);
|
||||||
|
obj.addProperty(UserKey, lookupDataOpt.getMxid().getLocalPart());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.equals(type, "m.id.phone")) {
|
||||||
|
// Remove deprecated login info if exists to avoid duplication
|
||||||
|
reqJsonObject.remove(ThreepidMediumKey);
|
||||||
|
reqJsonObject.remove(ThreepidAddressKey);
|
||||||
|
|
||||||
|
GsonUtil.findPrimitive(obj, "number").ifPresent(number -> {
|
||||||
|
GsonUtil.findPrimitive(obj, "country").ifPresent(country -> {
|
||||||
|
log.info("Login request with phone '{}'-'{}'", country.getAsString(), number.getAsString());
|
||||||
|
try {
|
||||||
|
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
|
||||||
|
Phonenumber.PhoneNumber phoneNumber = phoneUtil.parse(number.getAsString(), country.getAsString());
|
||||||
|
String msisdn = phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164).replace("+", "");
|
||||||
|
String medium = "msisdn";
|
||||||
|
strategy.findLocal(medium, msisdn).ifPresent(lookupDataOpt -> {
|
||||||
|
obj.remove("country");
|
||||||
|
obj.remove("number");
|
||||||
|
obj.addProperty(TypeKey, UserIdTypeValue);
|
||||||
|
obj.addProperty(UserKey, lookupDataOpt.getMxid().getLocalPart());
|
||||||
|
});
|
||||||
|
} catch (NumberParseException e) {
|
||||||
|
log.error("Not a valid phone number");
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// invoke 'login' on homeserver
|
||||||
|
HttpPost httpPost = RestClientUtils.post(resolveProxyUrl(target), gson, reqJsonObject);
|
||||||
|
try (CloseableHttpResponse httpResponse = client.execute(httpPost)) {
|
||||||
|
// check http status
|
||||||
|
int status = httpResponse.getStatusLine().getStatusCode();
|
||||||
|
log.info("http status = {}", status);
|
||||||
|
if (status != 200) {
|
||||||
|
// try to get possible json error message from response
|
||||||
|
// otherwise just get returned plain error message
|
||||||
|
String errcode = String.valueOf(httpResponse.getStatusLine().getStatusCode());
|
||||||
|
String error = EntityUtils.toString(httpResponse.getEntity());
|
||||||
|
if (httpResponse.getEntity() != null) {
|
||||||
|
try {
|
||||||
|
JsonObject bodyJson = new JsonParser().parse(error).getAsJsonObject();
|
||||||
|
if (bodyJson.has("errcode")) {
|
||||||
|
errcode = bodyJson.get("errcode").getAsString();
|
||||||
|
}
|
||||||
|
if (bodyJson.has("error")) {
|
||||||
|
error = bodyJson.get("error").getAsString();
|
||||||
|
}
|
||||||
|
throw new RemoteLoginException(status, errcode, error, bodyJson);
|
||||||
|
} catch (JsonSyntaxException e) {
|
||||||
|
log.warn("Response body is not JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new RemoteLoginException(status, errcode, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return response
|
||||||
|
HttpEntity entity = httpResponse.getEntity();
|
||||||
|
if (Objects.isNull(entity)) {
|
||||||
|
log.warn("Expected HS to return data but got nothing");
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return IOUtils.toString(httpResponse.getEntity().getContent(), httpResponse.getEntity().getContentType().getValue());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
110
src/main/java/io/kamax/mxisd/config/AuthenticationConfig.java
Normal file
110
src/main/java/io/kamax/mxisd/config/AuthenticationConfig.java
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* mxisd - Matrix Identity Server Daemon
|
||||||
|
* Copyright (C) 2018 Kamax Sarl
|
||||||
|
*
|
||||||
|
* https://www.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.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "auth")
|
||||||
|
public class AuthenticationConfig {
|
||||||
|
|
||||||
|
public static class Rule {
|
||||||
|
|
||||||
|
private String regex;
|
||||||
|
private transient Pattern pattern;
|
||||||
|
private String medium;
|
||||||
|
|
||||||
|
public String getRegex() {
|
||||||
|
return regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRegex(String regex) {
|
||||||
|
this.regex = regex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pattern getPattern() {
|
||||||
|
return pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPattern(Pattern pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMedium() {
|
||||||
|
return medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMedium(String medium) {
|
||||||
|
this.medium = medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class User {
|
||||||
|
|
||||||
|
private List<Rule> rules = new ArrayList<>();
|
||||||
|
|
||||||
|
public List<Rule> getRules() {
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRules(List<Rule> mappings) {
|
||||||
|
this.rules = mappings;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Rewrite {
|
||||||
|
|
||||||
|
private User user = new User();
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(User user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rewrite rewrite = new Rewrite();
|
||||||
|
|
||||||
|
public Rewrite getRewrite() {
|
||||||
|
return rewrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRewrite(Rewrite rewrite) {
|
||||||
|
this.rewrite = rewrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void build() {
|
||||||
|
getRewrite().getUser().getRules().forEach(mapping -> mapping.setPattern(Pattern.compile(mapping.getRegex())));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -20,25 +20,18 @@
|
|||||||
|
|
||||||
package io.kamax.mxisd.controller.auth.v1;
|
package io.kamax.mxisd.controller.auth.v1;
|
||||||
|
|
||||||
import com.google.gson.*;
|
import com.google.gson.Gson;
|
||||||
import com.google.i18n.phonenumbers.NumberParseException;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.i18n.phonenumbers.Phonenumber;
|
|
||||||
import io.kamax.mxisd.auth.AuthManager;
|
import io.kamax.mxisd.auth.AuthManager;
|
||||||
import io.kamax.mxisd.auth.UserAuthResult;
|
import io.kamax.mxisd.auth.UserAuthResult;
|
||||||
import io.kamax.mxisd.controller.auth.v1.io.CredentialsValidationResponse;
|
import io.kamax.mxisd.controller.auth.v1.io.CredentialsValidationResponse;
|
||||||
import io.kamax.mxisd.dns.ClientDnsOverwrite;
|
|
||||||
import io.kamax.mxisd.exception.JsonMemberNotFoundException;
|
import io.kamax.mxisd.exception.JsonMemberNotFoundException;
|
||||||
import io.kamax.mxisd.exception.RemoteLoginException;
|
|
||||||
import io.kamax.mxisd.lookup.strategy.LookupStrategy;
|
|
||||||
import io.kamax.mxisd.util.GsonParser;
|
import io.kamax.mxisd.util.GsonParser;
|
||||||
import io.kamax.mxisd.util.GsonUtil;
|
import io.kamax.mxisd.util.GsonUtil;
|
||||||
import io.kamax.mxisd.util.RestClientUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang.StringUtils;
|
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpGet;
|
import org.apache.http.client.methods.HttpGet;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
|
||||||
import org.apache.http.client.utils.URIBuilder;
|
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.util.EntityUtils;
|
import org.apache.http.util.EntityUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -54,10 +47,11 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@CrossOrigin
|
@CrossOrigin
|
||||||
@RequestMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
// TODO export into SDK
|
// TODO export into SDK
|
||||||
@@ -71,23 +65,9 @@ public class AuthController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AuthManager mgr;
|
private AuthManager mgr;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private LookupStrategy strategy;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ClientDnsOverwrite dns;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CloseableHttpClient client;
|
private CloseableHttpClient client;
|
||||||
|
|
||||||
private String resolveProxyUrl(HttpServletRequest req) {
|
|
||||||
URI target = URI.create(req.getRequestURL().toString());
|
|
||||||
URIBuilder builder = dns.transform(target);
|
|
||||||
String urlToLogin = builder.toString();
|
|
||||||
log.info("Proxy resolution: {} to {}", target.toString(), urlToLogin);
|
|
||||||
return urlToLogin;
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST)
|
@RequestMapping(value = "/_matrix-internal/identity/v1/check_credentials", method = RequestMethod.POST)
|
||||||
public String checkCredentials(HttpServletRequest req) {
|
public String checkCredentials(HttpServletRequest req) {
|
||||||
try {
|
try {
|
||||||
@@ -120,7 +100,9 @@ public class AuthController {
|
|||||||
|
|
||||||
@RequestMapping(value = logV1Url, method = RequestMethod.GET)
|
@RequestMapping(value = logV1Url, method = RequestMethod.GET)
|
||||||
public String getLogin(HttpServletRequest req, HttpServletResponse res) {
|
public String getLogin(HttpServletRequest req, HttpServletResponse res) {
|
||||||
try (CloseableHttpResponse hsResponse = client.execute(new HttpGet(resolveProxyUrl(req)))) {
|
URI target = URI.create(req.getRequestURL().toString());
|
||||||
|
|
||||||
|
try (CloseableHttpResponse hsResponse = client.execute(new HttpGet(mgr.resolveProxyUrl(target)))) {
|
||||||
res.setStatus(hsResponse.getStatusLine().getStatusCode());
|
res.setStatus(hsResponse.getStatusLine().getStatusCode());
|
||||||
return EntityUtils.toString(hsResponse.getEntity());
|
return EntityUtils.toString(hsResponse.getEntity());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -130,98 +112,11 @@ public class AuthController {
|
|||||||
|
|
||||||
@RequestMapping(value = logV1Url, method = RequestMethod.POST)
|
@RequestMapping(value = logV1Url, method = RequestMethod.POST)
|
||||||
public String login(HttpServletRequest req) {
|
public String login(HttpServletRequest req) {
|
||||||
|
URI target = URI.create(req.getRequestURL().toString());
|
||||||
try {
|
try {
|
||||||
JsonObject reqJsonObject = parser.parse(req.getInputStream());
|
return mgr.proxyLogin(target, IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8));
|
||||||
|
|
||||||
// find 3PID in main object
|
|
||||||
GsonUtil.findPrimitive(reqJsonObject, "medium").ifPresent(medium -> {
|
|
||||||
GsonUtil.findPrimitive(reqJsonObject, "address").ifPresent(address -> {
|
|
||||||
log.info("Login request with medium '{}' and address '{}'", medium.getAsString(), address.getAsString());
|
|
||||||
strategy.findLocal(medium.getAsString(), address.getAsString()).ifPresent(lookupDataOpt -> {
|
|
||||||
reqJsonObject.addProperty("user", lookupDataOpt.getMxid().getLocalPart());
|
|
||||||
reqJsonObject.remove("medium");
|
|
||||||
reqJsonObject.remove("address");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// find 3PID in 'identifier' object
|
|
||||||
GsonUtil.findObj(reqJsonObject, "identifier").ifPresent(identifier -> {
|
|
||||||
GsonUtil.findPrimitive(identifier, "type").ifPresent(type -> {
|
|
||||||
|
|
||||||
if (StringUtils.equals(type.getAsString(), "m.id.thirdparty")) {
|
|
||||||
GsonUtil.findPrimitive(identifier, "medium").ifPresent(medium -> {
|
|
||||||
GsonUtil.findPrimitive(identifier, "address").ifPresent(address -> {
|
|
||||||
log.info("Login request with medium '{}' and address '{}'", medium.getAsString(), address.getAsString());
|
|
||||||
strategy.findLocal(medium.getAsString(), address.getAsString()).ifPresent(lookupDataOpt -> {
|
|
||||||
identifier.addProperty("type", "m.id.user");
|
|
||||||
identifier.addProperty("user", lookupDataOpt.getMxid().getLocalPart());
|
|
||||||
identifier.remove("medium");
|
|
||||||
identifier.remove("address");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.equals(type.getAsString(), "m.id.phone")) {
|
|
||||||
GsonUtil.findPrimitive(identifier, "number").ifPresent(number -> {
|
|
||||||
GsonUtil.findPrimitive(identifier, "country").ifPresent(country -> {
|
|
||||||
log.info("Login request with phone '{}'-'{}'", country.getAsString(), number.getAsString());
|
|
||||||
try {
|
|
||||||
PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
|
|
||||||
Phonenumber.PhoneNumber phoneNumber = phoneUtil.parse(number.getAsString(), country.getAsString());
|
|
||||||
String canon_phoneNumber = phoneUtil.format(phoneNumber, PhoneNumberUtil.PhoneNumberFormat.E164).replace("+", "");
|
|
||||||
String medium = "msisdn";
|
|
||||||
strategy.findLocal(medium, canon_phoneNumber).ifPresent(lookupDataOpt -> {
|
|
||||||
identifier.addProperty("type", "m.id.user");
|
|
||||||
identifier.addProperty("user", lookupDataOpt.getMxid().getLocalPart());
|
|
||||||
identifier.remove("country");
|
|
||||||
identifier.remove("number");
|
|
||||||
});
|
|
||||||
} catch (NumberParseException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// invoke 'login' on homeserver
|
|
||||||
HttpPost httpPost = RestClientUtils.post(resolveProxyUrl(req), gson, reqJsonObject);
|
|
||||||
try (CloseableHttpResponse httpResponse = client.execute(httpPost)) {
|
|
||||||
// check http status
|
|
||||||
int status = httpResponse.getStatusLine().getStatusCode();
|
|
||||||
log.info("http status = {}", status);
|
|
||||||
if (status != 200) {
|
|
||||||
// try to get possible json error message from response
|
|
||||||
// otherwise just get returned plain error message
|
|
||||||
String errcode = String.valueOf(httpResponse.getStatusLine().getStatusCode());
|
|
||||||
String error = EntityUtils.toString(httpResponse.getEntity());
|
|
||||||
if (httpResponse.getEntity() != null) {
|
|
||||||
try {
|
|
||||||
JsonObject bodyJson = new JsonParser().parse(error).getAsJsonObject();
|
|
||||||
if (bodyJson.has("errcode")) {
|
|
||||||
errcode = bodyJson.get("errcode").getAsString();
|
|
||||||
}
|
|
||||||
if (bodyJson.has("error")) {
|
|
||||||
error = bodyJson.get("error").getAsString();
|
|
||||||
}
|
|
||||||
throw new RemoteLoginException(status, errcode, error, bodyJson);
|
|
||||||
} catch (JsonSyntaxException e) {
|
|
||||||
log.warn("Response body is not JSON");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new RemoteLoginException(status, errcode, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// return response
|
|
||||||
JsonObject respJsonObject = parser.parseOptional(httpResponse).get();
|
|
||||||
return gson.toJson(respJsonObject);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
log.error("Unable to read input data from client");
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user