This is an automated email from the ASF dual-hosted git repository. solomax pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/openmeetings.git
The following commit(s) were added to refs/heads/master by this push: new c64509a [OPENMEETINGS-2476] HttpURLConnection is replaced with HttpClient c64509a is described below commit c64509a16cd061dca8800db9bcdaf384bdbf7ee0 Author: Maxim Solodovnik <solomax...@gmail.com> AuthorDate: Tue Oct 13 09:20:06 2020 +0700 [OPENMEETINGS-2476] HttpURLConnection is replaced with HttpClient --- .../openmeetings/db/dao/user/IUserManager.java | 3 - .../openmeetings/db/entity/server/OAuthServer.java | 17 +- .../openmeetings/web/admin/oauth/OAuthForm.java | 2 +- .../apache/openmeetings/web/app/Application.java | 3 + .../apache/openmeetings/web/app/UserManager.java | 216 +++++++++++++++++++- .../openmeetings/web/pages/auth/SignInDialog.java | 2 +- .../openmeetings/web/pages/auth/SignInPage.java | 222 +-------------------- .../web/app/TestUserManagerMocked.java | 58 ++++-- 8 files changed, 279 insertions(+), 244 deletions(-) diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/IUserManager.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/IUserManager.java index 262b381..94d6aba 100644 --- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/IUserManager.java +++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/dao/user/IUserManager.java @@ -18,11 +18,9 @@ */ package org.apache.openmeetings.db.dao.user; -import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.Locale; -import org.apache.openmeetings.db.dto.user.OAuthUser; import org.apache.openmeetings.db.entity.user.User; import org.apache.openmeetings.util.OmException; @@ -35,7 +33,6 @@ public interface IUserManager { Object registerUser(User u, String password, String hash) throws OmException, NoSuchAlgorithmException; Long getLanguage(Locale loc); - User loginOAuth(OAuthUser user, long serverId) throws IOException, NoSuchAlgorithmException; boolean kickExternal(Long roomId, String externalType, String externalId); boolean kickUsersByRoomId(Long roomId); diff --git a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/server/OAuthServer.java b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/server/OAuthServer.java index 24285f8..d223706 100644 --- a/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/server/OAuthServer.java +++ b/openmeetings-db/src/main/java/org/apache/openmeetings/db/entity/server/OAuthServer.java @@ -58,6 +58,14 @@ import org.apache.openmeetings.db.entity.HistoricalEntity; public class OAuthServer extends HistoricalEntity { private static final long serialVersionUID = 1L; + public enum RequestTokenMethod { + POST, GET + } + + public enum RequestInfoMethod { + POST, GET, HEADER + } + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") @@ -116,7 +124,6 @@ public class OAuthServer extends HistoricalEntity { @MapKeyColumn(name = "name") @Column(name = "value") @CollectionTable(name = "oauth_mapping", joinColumns = @JoinColumn(name = "oauth_id")) - //FIXME TODO @XmlElement(name = "attrMapping", required = false) @XmlTransient private Map<String, String> mapping = new LinkedHashMap<>(); @@ -263,12 +270,4 @@ public class OAuthServer extends HistoricalEntity { .append(", isDeleted()=").append(isDeleted()) .append("]").toString(); } - - public enum RequestTokenMethod { - POST, GET - } - - public enum RequestInfoMethod { - POST, GET, HEADER - } } diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java index 7ad488a..61ac9bb 100644 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java +++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/admin/oauth/OAuthForm.java @@ -18,9 +18,9 @@ */ package org.apache.openmeetings.web.admin.oauth; +import static org.apache.openmeetings.web.app.UserManager.getRedirectUri; import static org.apache.openmeetings.web.app.WebSession.getUserId; import static org.apache.openmeetings.web.common.confirmation.ConfirmationBehavior.newOkCancelDangerConfirm; -import static org.apache.openmeetings.web.pages.auth.SignInPage.getRedirectUri; import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java index 460d840..87b2a85 100644 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java +++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/Application.java @@ -176,6 +176,8 @@ public class Application extends AuthenticatedWebApplication implements IApplica @Autowired private UserDao userDao; @Autowired + private UserManager userManager; + @Autowired private ClientManager cm; @Autowired private WhiteboardManager wbManager; @@ -340,6 +342,7 @@ public class Application extends AuthenticatedWebApplication implements IApplica setExtProcessTtl(cfgDao.getInt(CONFIG_EXT_PROCESS_TTL, getExtProcessTtl())); Version.logOMStarted(); recordingDao.resetProcessingStatus(); //we are starting so all processing recordings are now errors + userManager.initHttpClient(); setInitComplete(true); } catch (Exception err) { log.error("[appStart]", err); diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java index b2a5010..53265d5 100644 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java +++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/app/UserManager.java @@ -18,29 +18,52 @@ */ package org.apache.openmeetings.web.app; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.UUID.randomUUID; import static org.apache.openmeetings.db.dao.user.UserDao.getNewUserInstance; import static org.apache.openmeetings.db.util.TimezoneUtil.getTimeZone; import static org.apache.openmeetings.util.OmException.UNKNOWN; +import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_IGNORE_BAD_SSL; import static org.apache.openmeetings.util.OpenmeetingsVariables.getBaseUrl; import static org.apache.openmeetings.util.OpenmeetingsVariables.getDefaultGroup; import static org.apache.openmeetings.util.OpenmeetingsVariables.getDefaultLang; import static org.apache.openmeetings.util.OpenmeetingsVariables.getMinLoginLength; import static org.apache.openmeetings.util.OpenmeetingsVariables.isAllowRegisterFrontend; import static org.apache.openmeetings.util.OpenmeetingsVariables.isSendVerificationEmail; +import static org.apache.openmeetings.web.app.Application.urlForPage; import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse.BodyHandlers; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.Duration; import java.util.Date; +import java.util.HashMap; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.apache.openmeetings.db.dao.basic.ConfigurationDao; import org.apache.openmeetings.db.dao.label.LabelDao; import org.apache.openmeetings.db.dao.user.GroupDao; import org.apache.openmeetings.db.dao.user.IUserManager; import org.apache.openmeetings.db.dao.user.UserDao; import org.apache.openmeetings.db.dto.user.OAuthUser; import org.apache.openmeetings.db.entity.basic.Client; +import org.apache.openmeetings.db.entity.server.OAuthServer; +import org.apache.openmeetings.db.entity.server.OAuthServer.RequestInfoMethod; import org.apache.openmeetings.db.entity.user.User; import org.apache.openmeetings.db.entity.user.User.Right; import org.apache.openmeetings.db.entity.user.User.Type; @@ -49,15 +72,20 @@ import org.apache.openmeetings.service.mail.EmailManager; import org.apache.openmeetings.util.OmException; import org.apache.openmeetings.util.crypt.CryptProvider; import org.apache.openmeetings.util.crypt.ICrypt; +import org.apache.openmeetings.web.pages.auth.SignInPage; import org.apache.wicket.IConverterLocator; import org.apache.wicket.core.util.lang.PropertyResolver; import org.apache.wicket.core.util.lang.PropertyResolverConverter; +import org.apache.wicket.request.flow.RedirectToUrlException; +import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.util.string.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.github.openjson.JSONObject; + /** * * @author swagner @@ -68,6 +96,8 @@ public class UserManager implements IUserManager { private static final Logger log = LoggerFactory.getLogger(UserManager.class); @Autowired + private ConfigurationDao cfgDao; + @Autowired private GroupDao groupDao; @Autowired private UserDao userDao; @@ -75,6 +105,7 @@ public class UserManager implements IUserManager { private EmailManager emailManager; @Autowired private IClientManager cm; + private HttpClient httpClient; private static boolean sendConfirmation() { String baseURL = getBaseUrl(); @@ -229,14 +260,63 @@ public class UserManager implements IUserManager { return LabelDao.getLanguage(loc, getDefaultLang()); } - @Override - public User loginOAuth(OAuthUser user, long serverId) throws IOException, NoSuchAlgorithmException { + // ============= OAuth2 methods ============= + public void initHttpClient() { + HttpClient.Builder builder = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(10)); + final boolean ignoreBadSsl = cfgDao.getBool(CONFIG_IGNORE_BAD_SSL, false); + System.setProperty("jdk.internal.httpclient.disableHostnameVerification", String.valueOf(ignoreBadSsl)); + if (ignoreBadSsl) { + TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + //no-op + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + //no-op + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] {}; + } + }}; + try { + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + SSLParameters sslParams = new SSLParameters(); + sslParams.setEndpointIdentificationAlgorithm(""); + builder.sslContext(sslContext) + .sslParameters(sslParams); + } catch (Exception e) { + log.error("[initHttpClient]", e); + } + } + httpClient = builder.build(); + } + + public User loginOAuth(String code, OAuthServer server) throws IOException, NoSuchAlgorithmException, InterruptedException { + if (code == null) { + showAuth(server); + return null; + } + log.debug("OAuth response code={}", code); + AuthInfo authInfo = getToken(code, server); + if (authInfo == null) { + return null; + } + log.debug("OAuthInfo={}", authInfo); + OAuthUser user = getAuthParams(authInfo, code, server); + if (!userDao.validLogin(user.getLogin())) { log.error("Invalid login, please check parameters"); return null; } - User u = userDao.getByLogin(user.getLogin(), Type.OAUTH, serverId); - if (!userDao.checkEmail(user.getEmail(), Type.OAUTH, serverId, u == null ? null : u.getId())) { + User u = userDao.getByLogin(user.getLogin(), Type.OAUTH, server.getId()); + if (!userDao.checkEmail(user.getEmail(), Type.OAUTH, server.getId(), u == null ? null : u.getId())) { log.error("Another user with the same email exists"); return null; } @@ -246,7 +326,7 @@ public class UserManager implements IUserManager { final User fUser = getNewUserInstance(null); fUser.setType(Type.OAUTH); fUser.getRights().remove(Right.LOGIN); - fUser.setDomainId(serverId); + fUser.setDomainId(server.getId()); fUser.addGroup(groupDao.get(getDefaultGroup())); for (Map.Entry<String, String> entry : user.getUserData().entrySet()) { final String expression = entry.getKey(); @@ -262,6 +342,103 @@ public class UserManager implements IUserManager { return u; } + private static Map<String, String> getInitParams(final OAuthServer s) { + Map<String, String> params = new HashMap<>(); + params.put("{$client_id}", s.getClientId()); + params.put("{$redirect_uri}", getRedirectUri(s)); + return params; + } + + public static void showAuth(final OAuthServer s) { + String authUrl = prepareUrl(s.getRequestKeyUrl(), getInitParams(s)); + log.debug("redirectUrl={}", authUrl); + throw new RedirectToUrlException(authUrl); + } + + private static String prepareUrl(String urlTemplate, Map<String, String> params) { + String result = urlTemplate; + for (Entry<String, String> e : params.entrySet()) { + if (e.getValue() != null) { + result = result.replace(e.getKey(), URLEncoder.encode(e.getValue(), UTF_8)); + } + } + return result; + } + + public static String getRedirectUri(OAuthServer server) { + String result = ""; + if (server.getId() != null) { + String base = getBaseUrl(); + result = urlForPage(SignInPage.class, new PageParameters().add("oauthid", server.getId()), base); + } + return result; + } + + private static Map<String, String> getParams(final OAuthServer s, String code, AuthInfo authInfo) { + Map<String, String> params = getInitParams(s); + params.put("{$client_id}", s.getClientId()); + params.put("{$client_secret}", s.getClientSecret()); + if (authInfo != null) { + params.put("{$access_token}", authInfo.accessToken); + params.put("{$user_id}", authInfo.userId); + } + if (code != null) { + params.put("{$code}", code); + } + return params; + } + + private static HttpRequest.Builder setNoCache(HttpRequest.Builder builder) { + return builder + .header("Cache-Control", "no-cache, no-store, must-revalidate") + .header("Pragma", "no-cache") + .header("Expires", "0"); + } + + String doRequest(HttpRequest request) throws IOException, InterruptedException { // extracted as package private for testing + return httpClient.send(request, BodyHandlers.ofString()).body(); + } + + private AuthInfo getToken(String code, OAuthServer server) throws IOException, InterruptedException { + // build url params to request auth token + String requestTokenParams = prepareUrl(server.getRequestTokenAttributes(), getParams(server, code, null)); + // request auth token + HttpRequest request = setNoCache( + HttpRequest.newBuilder() + .uri(URI.create(server.getRequestTokenUrl())) + .header("Content-Type", "application/x-www-form-urlencoded") + .header("charset", UTF_8.name()) + .method(server.getRequestTokenMethod().name(), BodyPublishers.ofString(requestTokenParams)) + ).build(); + + String resp = doRequest(request); + // parse json result + AuthInfo result = new AuthInfo(resp); + // access token must be specified + if (result.accessToken == null) { + log.error("Response doesn't contain access_token field:\n {}", resp); + return null; + } + return result; + } + + private OAuthUser getAuthParams(AuthInfo authInfo, String code, OAuthServer server) throws IOException, InterruptedException { + // prepare url + String requestInfoUrl = prepareUrl(server.getRequestInfoUrl(), getParams(server, code, authInfo)); + HttpRequest.Builder builder = setNoCache(HttpRequest.newBuilder().uri(URI.create(requestInfoUrl))); + + if (server.getRequestInfoMethod() == RequestInfoMethod.HEADER) { + builder.header("Authorization", "Bearer " + authInfo.accessToken); + } else { + builder.method(server.getRequestInfoMethod().name(), BodyPublishers.noBody()); + } + + String json = doRequest(builder.build()); + log.debug("User info={}", json); + // parse json result + return new OAuthUser(json, server); + } + private class LanguageConverter extends PropertyResolverConverter { private static final long serialVersionUID = 1L; final String expression; @@ -297,4 +474,33 @@ public class UserManager implements IUserManager { return String.valueOf(object); } } + + private static class AuthInfo { + final String accessToken; + final String refreshToken; + final String tokenType; + final String userId; + final long expiresIn; + + AuthInfo(String jsonStr) { + log.debug("AuthInfo={}", jsonStr); + JSONObject json = new JSONObject(jsonStr); + accessToken = json.optString("access_token"); + refreshToken = json.optString("refresh_token"); + tokenType = json.optString("token_type"); + userId = json.optString("user_id"); + expiresIn = json.optLong("expires_in"); + } + + @Override + public String toString() { + return new StringBuilder() + .append("AuthInfo [accessToken=").append(accessToken) + .append(", refreshToken=").append(refreshToken) + .append(", tokenType=").append(tokenType) + .append(", userId=").append(userId) + .append(", expiresIn=").append(expiresIn) + .append("]").toString(); + } + } } diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java index 0bb98d2..fac217b 100644 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java +++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInDialog.java @@ -20,9 +20,9 @@ package org.apache.openmeetings.web.pages.auth; import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_DEFAULT_LDAP_ID; import static org.apache.openmeetings.web.app.Application.getAuthenticationStrategy; +import static org.apache.openmeetings.web.app.UserManager.showAuth; import static org.apache.openmeetings.web.pages.HashPage.APP; import static org.apache.openmeetings.web.pages.HashPage.APP_TYPE_NETWORK; -import static org.apache.openmeetings.web.pages.auth.SignInPage.showAuth; import java.util.List; diff --git a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java index 0e794e6..b26cd30 100644 --- a/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java +++ b/openmeetings-web/src/main/java/org/apache/openmeetings/web/pages/auth/SignInPage.java @@ -18,43 +18,19 @@ */ package org.apache.openmeetings.web.pages.auth; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.openmeetings.util.OpenmeetingsVariables.CONFIG_IGNORE_BAD_SSL; -import static org.apache.openmeetings.util.OpenmeetingsVariables.getBaseUrl; import static org.apache.openmeetings.util.OpenmeetingsVariables.isAllowRegisterFrontend; -import static org.apache.openmeetings.web.app.Application.urlForPage; -import java.io.DataOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.apache.commons.io.IOUtils; import org.apache.openmeetings.db.dao.basic.ConfigurationDao; import org.apache.openmeetings.db.dao.server.OAuth2Dao; -import org.apache.openmeetings.db.dao.user.IUserManager; -import org.apache.openmeetings.db.dto.user.OAuthUser; import org.apache.openmeetings.db.entity.server.OAuthServer; -import org.apache.openmeetings.db.entity.server.OAuthServer.RequestInfoMethod; import org.apache.openmeetings.db.entity.user.User; import org.apache.openmeetings.db.entity.user.User.Type; import org.apache.openmeetings.util.OmException; import org.apache.openmeetings.web.app.Application; +import org.apache.openmeetings.web.app.UserManager; import org.apache.openmeetings.web.app.WebSession; import org.apache.openmeetings.web.common.OmModalCloseButton; import org.apache.openmeetings.web.pages.BaseInitedPage; @@ -67,7 +43,6 @@ import org.apache.wicket.model.Model; import org.apache.wicket.model.ResourceModel; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.cycle.RequestCycle; -import org.apache.wicket.request.flow.RedirectToUrlException; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.spring.injection.annot.SpringBean; import org.apache.wicket.util.string.StringValue; @@ -75,7 +50,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.github.openjson.JSONException; -import com.github.openjson.JSONObject; import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.Modal; import de.agilecoders.wicket.core.markup.html.bootstrap.dialog.TextContentModal; @@ -137,7 +111,7 @@ public class SignInPage extends BaseInitedPage { @SpringBean private ConfigurationDao cfgDao; @SpringBean - private IUserManager userManager; + private UserManager userManager; @SpringBean private OAuth2Dao oauthDao; @@ -162,20 +136,14 @@ public class SignInPage extends BaseInitedPage { return; } - if (!p.get("code").isNull()) { // got code - String code = p.get("code").toString(); - log.debug("OAuth response code={}", code); - AuthInfo authInfo = getToken(code, server); - if (authInfo == null) { - return; - } - log.debug("OAuthInfo={}", authInfo); - OAuthUser user = getAuthParams(authInfo, code, server); - loginViaOAuth2(user, serverId); - } else { // redirect to get code - showAuth(server); + User u = userManager.loginOAuth(p.get("code").toOptionalString(), server); + + if (u != null && WebSession.get().signIn(u)) { + setResponsePage(Application.get().getHomePage()); + } else { + log.error("Failed to login via OAuth2!"); } - } catch (IOException|NoSuchAlgorithmException|JSONException e) { + } catch (IOException|NoSuchAlgorithmException|InterruptedException|JSONException e) { log.error("OAuth2 login error", e); } } @@ -240,176 +208,4 @@ public class SignInPage extends BaseInitedPage { protected void onParameterArrival(IRequestParameters params, AjaxRequestTarget arg1) { WebSession.get().setArea(getUrlFragment(params)); } - - // ============= OAuth2 methods ============= - private static Map<String, String> getInitParams(final OAuthServer s) { - Map<String, String> params = new HashMap<>(); - params.put("{$client_id}", s.getClientId()); - params.put("{$redirect_uri}", getRedirectUri(s)); - return params; - } - - public static void showAuth(final OAuthServer s) { - String authUrl = prepareUrl(s.getRequestKeyUrl(), getInitParams(s)); - log.debug("redirectUrl={}", authUrl); - throw new RedirectToUrlException(authUrl); - } - - private static String prepareUrl(String urlTemplate, Map<String, String> params) { - String result = urlTemplate; - for (Entry<String, String> e : params.entrySet()) { - if (e.getValue() != null) { - try { - result = result.replace(e.getKey(), URLEncoder.encode(e.getValue(), UTF_8.name())); - } catch (UnsupportedEncodingException err) { - log.error("Unexpected exception while encoding URI param {}", e, err); - } - } - } - return result; - } - - public static String getRedirectUri(OAuthServer server) { - String result = ""; - if (server.getId() != null) { - String base = getBaseUrl(); - result = urlForPage(SignInPage.class, new PageParameters().add("oauthid", server.getId()), base); - } - return result; - } - - private void prepareConnection(URLConnection inConnection) { - if (!(inConnection instanceof HttpsURLConnection)) { - return; - } - if (!cfgDao.getBool(CONFIG_IGNORE_BAD_SSL, false)) { - return; - } - TrustManager[] trustAllCerts = new TrustManager[] {new X509TrustManager() { - @Override - public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - //no-op - } - - @Override - public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - //no-op - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[] {}; - } - }}; - try { - HttpsURLConnection connection = (HttpsURLConnection)inConnection; - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); - SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); - connection.setSSLSocketFactory(sslSocketFactory); - connection.setHostnameVerifier((arg0, arg1) -> true); - } catch (Exception e) { - log.error("[prepareConnection]", e); - } - } - - private static Map<String, String> getParams(final OAuthServer s, String code, AuthInfo authInfo) { - Map<String, String> params = getInitParams(s); - params.put("{$client_id}", s.getClientId()); - params.put("{$client_secret}", s.getClientSecret()); - if (authInfo != null) { - params.put("{$access_token}", authInfo.accessToken); - params.put("{$user_id}", authInfo.userId); - } - if (code != null) { - params.put("{$code}", code); - } - return params; - } - - private AuthInfo getToken(String code, OAuthServer server) throws IOException { - String requestTokenBaseUrl = server.getRequestTokenUrl(); - // build url params to request auth token - String requestTokenParams = server.getRequestTokenAttributes(); - requestTokenParams = prepareUrl(requestTokenParams, getParams(server, code, null)); - // request auth token - HttpURLConnection connection = (HttpURLConnection) new URL(requestTokenBaseUrl).openConnection(); - prepareConnection(connection); - connection.setRequestMethod(server.getRequestTokenMethod().name()); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - connection.setRequestProperty("charset", UTF_8.name()); - connection.setRequestProperty("Content-Length", String.valueOf(requestTokenParams.length())); - connection.setDoInput(true); - connection.setDoOutput(true); - connection.setUseCaches(false); - DataOutputStream paramsOutputStream = new DataOutputStream(connection.getOutputStream()); - paramsOutputStream.writeBytes(requestTokenParams); - paramsOutputStream.flush(); - String sourceResponse = IOUtils.toString(connection.getInputStream(), UTF_8); - // parse json result - AuthInfo result = new AuthInfo(sourceResponse); - // access token must be specified - if (result.accessToken == null) { - log.error("Response doesn't contain access_token field:\n {}", sourceResponse); - return null; - } - return result; - } - - private OAuthUser getAuthParams(AuthInfo authInfo, String code, OAuthServer server) throws IOException { - // prepare url - String requestInfoUrl = server.getRequestInfoUrl(); - requestInfoUrl = prepareUrl(requestInfoUrl, getParams(server, code, authInfo)); - // send request - HttpURLConnection connection = (HttpURLConnection) new URL(requestInfoUrl).openConnection(); - if (server.getRequestInfoMethod() == RequestInfoMethod.HEADER) { - connection.setRequestProperty("Authorization", String.format("Bearer %s", authInfo.accessToken)); - } else { - connection.setRequestMethod(server.getRequestInfoMethod().name()); - } - prepareConnection(connection); - String json = IOUtils.toString(connection.getInputStream(), UTF_8); - log.debug("User info={}", json); - // parse json result - return new OAuthUser(json, server); - } - - private void loginViaOAuth2(OAuthUser user, long serverId) throws IOException, NoSuchAlgorithmException { - User u = userManager.loginOAuth(user, serverId); - - if (u != null && WebSession.get().signIn(u)) { - setResponsePage(Application.get().getHomePage()); - } else { - log.error("Failed to login via OAuth2!"); - } - } - - private static class AuthInfo { - final String accessToken; - final String refreshToken; - final String tokenType; - final String userId; - final long expiresIn; - - AuthInfo(String jsonStr) { - log.debug("AuthInfo={}", jsonStr); - JSONObject json = new JSONObject(jsonStr); - accessToken = json.optString("access_token"); - refreshToken = json.optString("refresh_token"); - tokenType = json.optString("token_type"); - userId = json.optString("user_id"); - expiresIn = json.optLong("expires_in"); - } - - @Override - public String toString() { - return new StringBuilder() - .append("AuthInfo [accessToken=").append(accessToken) - .append(", refreshToken=").append(refreshToken) - .append(", tokenType=").append(tokenType) - .append(", userId=").append(userId) - .append(", expiresIn=").append(expiresIn) - .append("]").toString(); - } - } } diff --git a/openmeetings-web/src/test/java/org/apache/openmeetings/web/app/TestUserManagerMocked.java b/openmeetings-web/src/test/java/org/apache/openmeetings/web/app/TestUserManagerMocked.java index ce19e5b..b57c21e 100644 --- a/openmeetings-web/src/test/java/org/apache/openmeetings/web/app/TestUserManagerMocked.java +++ b/openmeetings-web/src/test/java/org/apache/openmeetings/web/app/TestUserManagerMocked.java @@ -18,6 +18,10 @@ */ package org.apache.openmeetings.web.app; +import static org.apache.openmeetings.db.dto.user.OAuthUser.PARAM_EMAIL; +import static org.apache.openmeetings.db.dto.user.OAuthUser.PARAM_FNAME; +import static org.apache.openmeetings.db.dto.user.OAuthUser.PARAM_LNAME; +import static org.apache.openmeetings.db.dto.user.OAuthUser.PARAM_LOGIN; import static org.apache.openmeetings.util.OpenmeetingsVariables.setCryptClassName; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -25,16 +29,18 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.when; import java.io.IOException; +import java.net.http.HttpRequest; import java.security.NoSuchAlgorithmException; import org.apache.openmeetings.db.dao.user.GroupDao; import org.apache.openmeetings.db.dao.user.UserDao; -import org.apache.openmeetings.db.dto.user.OAuthUser; import org.apache.openmeetings.db.entity.server.OAuthServer; +import org.apache.openmeetings.db.entity.server.OAuthServer.RequestInfoMethod; +import org.apache.openmeetings.db.entity.server.OAuthServer.RequestTokenMethod; import org.apache.openmeetings.db.entity.user.User; import org.apache.openmeetings.db.entity.user.User.Type; import org.apache.openmeetings.db.manager.IClientManager; @@ -44,6 +50,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; @@ -59,25 +66,52 @@ class TestUserManagerMocked { @Mock private IClientManager cm; @InjectMocks - private UserManager userManager; + private UserManager userManager = Mockito.spy(new UserManager()); @Test - void oauthTest() throws NoSuchAlgorithmException, IOException { + void oauthTest() throws NoSuchAlgorithmException, IOException, InterruptedException { + OAuthServer server = new OAuthServer() + .setName("Google") + .setIconUrl("https://www.google.com/images/google_favicon_128.png") + .setEnabled(false) + .setClientId("secret-client-id") + .setClientSecret("secret-client-secret") + .setRequestKeyUrl("https://accounts.google.com/o/oauth2/auth?redirect_uri={$redirect_uri}&response_type=code&client_id={$client_id}" + + "&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile") + .setRequestTokenUrl("https://accounts.google.com/o/oauth2/token") + .setRequestTokenMethod(RequestTokenMethod.POST) + .setRequestTokenAttributes("code={$code}&client_id={$client_id}&client_secret={$client_secret}&redirect_uri={$redirect_uri}&grant_type=authorization_code") + .setRequestInfoUrl("https://www.googleapis.com/oauth2/v1/userinfo?access_token={$access_token}") + .setRequestInfoMethod(RequestInfoMethod.GET) + .addMapping(PARAM_LOGIN, "preferred_username") + .addMapping(PARAM_EMAIL, "email") + .addMapping(PARAM_FNAME, "given_name") + .addMapping(PARAM_LNAME, "family_name"); setCryptClassName(SCryptImplementation.class.getCanonicalName()); doReturn(true).when(userDao).validLogin(anyString()); - doReturn(true).when(userDao).checkEmail(anyString(), eq(Type.OAUTH), any(Long.class), nullable(Long.class)); - when(userDao.update(any(User.class), nullable(String.class), any(Long.class))).then(new Answer<User>() { + doReturn(true).when(userDao).checkEmail(eq("openmeeti...@pod.land"), eq(Type.OAUTH), nullable(Long.class), nullable(Long.class)); + doAnswer(new Answer<User>() { @Override public User answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); return (User)args[0]; } - }); - final String json = "{\"email\":\"openmeeti...@pod.land\",\"email_verified\":true,\"id\":78207,\"nationalcode_verified\":false,\"phone_number_verified\":false,\"preferred_username\":\"openmeetings\",\"sub\":\"78207\",\"user_metadata\":\"{\\\"mail.auto-forward\\\":true,\\\"email\\\":{\\\"auto_forward\\\":true}}\"}"; - OAuthUser user = new OAuthUser(json, new OAuthServer() - .addMapping(OAuthUser.PARAM_LOGIN, "preferred_username") - .addMapping(OAuthUser.PARAM_EMAIL, "email")); - User u = userManager.loginOAuth(user, 1); + }).when(userDao).update(any(User.class), nullable(String.class), any(Long.class)); + final String userJson = "{\"email\":\"openmeeti...@pod.land\",\"email_verified\":true,\"id\":78207,\"nationalcode_verified\":false,\"phone_number_verified\":false,\"preferred_username\":\"openmeetings\",\"sub\":\"78207\",\"user_metadata\":\"{\\\"mail.auto-forward\\\":true,\\\"email\\\":{\\\"auto_forward\\\":true}}\"}"; + final String tokenJson = "{\"accessToken\": \"aaa\", \"refreshToken\": \"bbb\", \"tokenType\": \"cccc\", \"userId\": \"ddddd\", \"expiresIn\": \"eeeeee\"}"; + doAnswer(new Answer<String>() { + @Override + public String answer(InvocationOnMock invocation) throws Throwable { + Object[] args = invocation.getArguments(); + HttpRequest req = (HttpRequest)args[0]; + if (req.uri().getHost().equals("accounts.google.com")) { + return tokenJson; + } + return userJson; + } + }).when(userManager).doRequest(any(HttpRequest.class)); + + User u = userManager.loginOAuth("bla-bla-bla", server); assertNotNull(u, "Valid user should be created"); assertEquals("openmeetings", u.getLogin(), "User should have valid login"); assertEquals("openmeeti...@pod.land", u.getAddress().getEmail(), "User should have valid email");