/* * mxisd - Matrix Identity Server Daemon * Copyright (C) 2017 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 . */ 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.ThreePid; import io.kamax.matrix._MatrixID; import io.kamax.matrix._ThreePid; import io.kamax.matrix.json.GsonUtil; import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.BackendAuthResult; import io.kamax.mxisd.config.AuthenticationConfig; import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MxisdConfig; import io.kamax.mxisd.dns.ClientDnsOverwrite; import io.kamax.mxisd.exception.RemoteLoginException; import io.kamax.mxisd.invitation.InvitationManager; 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.lang3.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.LoggerFactory; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Objects; public class AuthManager { private static final 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"; private final Gson gson = GsonUtil.get(); // FIXME replace private List providers; private MatrixConfig mxCfg; private AuthenticationConfig cfg; private InvitationManager invMgr; private ClientDnsOverwrite dns; private LookupStrategy strategy; private CloseableHttpClient client; public AuthManager( MxisdConfig cfg, List providers, LookupStrategy strategy, InvitationManager invMgr, ClientDnsOverwrite dns, CloseableHttpClient client ) { this.cfg = cfg.getAuth(); this.mxCfg = cfg.getMatrix(); 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) { _MatrixID mxid = MatrixID.asAcceptable(id); for (AuthenticatorProvider provider : providers) { if (!provider.isEnabled()) { continue; } log.info("Attempting authentication with store {}", provider.getClass().getSimpleName()); BackendAuthResult result = provider.authenticate(mxid, password); if (result.isSuccess()) { String mxId; if (UserIdType.Localpart.is(result.getId().getType())) { mxId = MatrixID.from(result.getId().getValue(), mxCfg.getDomain()).acceptable().getId(); } else if (UserIdType.MatrixID.is(result.getId().getType())) { mxId = MatrixID.asAcceptable(result.getId().getValue()).getId(); } else { log.warn("Unsupported User ID type {} for backend {}", result.getId().getType(), provider.getClass().getSimpleName()); continue; } UserAuthResult authResult = new UserAuthResult().success(result.getProfile().getDisplayName()); for (_ThreePid pid : result.getProfile().getThreePids()) { authResult.withThreePid(pid.getMedium(), pid.getAddress()); } log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName()); for (ThreePid pid : authResult.getThreePids()) { log.info("Processing {} for {}", pid, id); invMgr.publishMappingIfInvited(new ThreePidMapping(pid, mxId)); } try { MatrixID.asAcceptable(mxId); } catch (IllegalArgumentException e) { log.warn("The returned User ID {} is not a valid Matrix ID. Login might fail at the Homeserver level", mxId); } invMgr.lookupMappingsForInvites(); return authResult; } } 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(), StandardCharsets.UTF_8); } } catch (IOException e) { throw new RuntimeException(e); } } }