This is an automated email from the ASF dual-hosted git repository. harikrishna pushed a commit to branch 2FA in repository https://gitbox.apache.org/repos/asf/cloudstack.git
commit 507db9ef1a5207bdd7bf5da1231b0a427eb7bea8 Author: Harikrishna Patnala <[email protected]> AuthorDate: Thu Oct 6 05:56:44 2022 +0530 Recent partial changes --- .../org/apache/cloudstack/api/ApiConstants.java | 5 ++ .../cloudstack/api/response/LoginCmdResponse.java | 12 +++ client/pom.xml | 10 +++ .../auth/GoogleUserTwoFactorAuthenticator.java | 28 +++---- server/src/main/java/com/cloud/api/ApiServer.java | 5 ++ server/src/main/java/com/cloud/api/ApiServlet.java | 25 +++++- .../cloud/api/auth/TwoFactorAuthenticationCmd.java | 88 ++++++++++++++++++++++ 7 files changed, 158 insertions(+), 15 deletions(-) diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 55002f70b1b..f4ae1a93a1d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -227,6 +227,8 @@ public class ApiConstants { public static final String IP_ADDRESSES = "ipaddresses"; public static final String IP6_ADDRESS = "ip6address"; public static final String IP_ADDRESS_ID = "ipaddressid"; + public static final String IS_2FA_ENABLED = "is2faenabled"; + public static final String IS_ASYNC = "isasync"; public static final String IP_AVAILABLE = "ipavailable"; public static final String IP_LIMIT = "iplimit"; @@ -905,6 +907,9 @@ public class ApiConstants { public static final String ADMINS_ONLY = "adminsonly"; public static final String ANNOTATION_FILTER = "annotationfilter"; + public static final String TWOFACTORAUTHENTICATION = "twofactorauthentication"; + public static final String SETUPTWOFACTORAUTHENTICATION = "setup2fa"; + public static final String TWOFACTORAUTHENTICATIONCODE = "2facode"; public static final String LOGIN = "login"; public static final String LOGOUT = "logout"; public static final String LIST_IDPS = "listIdps"; diff --git a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java index d2d122efb66..baba7ba805f 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/LoginCmdResponse.java @@ -70,6 +70,10 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { @Param(description = "Session key that can be passed in subsequent Query command calls", isSensitive = true) private String sessionKey; + @SerializedName(value = ApiConstants.IS_2FA_ENABLED) + @Param(description = "Is two factor authentication enabled") + private String is2FAenabled; + public String getUsername() { return username; } @@ -163,4 +167,12 @@ public class LoginCmdResponse extends AuthenticationCmdResponse { public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; } + + public String Is2FAenabled() { + return is2FAenabled; + } + + public void set2FAenabled(String is2FAenabled) { + this.is2FAenabled = is2FAenabled; + } } diff --git a/client/pom.xml b/client/pom.xml index 9394180848b..c0e7fc6116d 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -172,6 +172,16 @@ <artifactId>cloud-plugin-user-authenticator-sha256salted</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-plugin-user-two-factor-authenticator-google</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.cloudstack</groupId> + <artifactId>cloud-plugin-user-two-factor-authenticator-static-pin</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>org.apache.cloudstack</groupId> <artifactId>cloud-plugin-metrics</artifactId> diff --git a/plugins/user-two-factor-authenticators/google/src/main/java/org/apache/cloudstack/auth/GoogleUserTwoFactorAuthenticator.java b/plugins/user-two-factor-authenticators/google/src/main/java/org/apache/cloudstack/auth/GoogleUserTwoFactorAuthenticator.java index 21aa63fa933..8dec2a862ea 100644 --- a/plugins/user-two-factor-authenticators/google/src/main/java/org/apache/cloudstack/auth/GoogleUserTwoFactorAuthenticator.java +++ b/plugins/user-two-factor-authenticators/google/src/main/java/org/apache/cloudstack/auth/GoogleUserTwoFactorAuthenticator.java @@ -18,17 +18,21 @@ package org.apache.cloudstack.auth; import javax.inject.Inject; +import com.cloud.utils.exception.CloudRuntimeException; import de.taimos.totp.TOTP; import com.cloud.exception.CloudAuthenticationException; import com.cloud.user.UserAccount; import org.apache.commons.codec.binary.Base32; import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import com.cloud.user.dao.UserAccountDao; import com.cloud.utils.component.AdapterBase; +import java.security.SecureRandom; + public class GoogleUserTwoFactorAuthenticator extends AdapterBase implements UserTwoFactorAuthenticator { public static final Logger s_logger = Logger.getLogger(GoogleUserTwoFactorAuthenticator.class); @@ -37,7 +41,6 @@ public class GoogleUserTwoFactorAuthenticator extends AdapterBase implements Use @Override public void check2FA(String code, UserAccount userAccount) throws CloudAuthenticationException { - // TODO: in future get userAccount specific 2FA key String expectedCode = get2FACode(get2FAKey(userAccount)); if (expectedCode.equals(code)) { s_logger.info("2FA matches user's input"); @@ -48,18 +51,6 @@ public class GoogleUserTwoFactorAuthenticator extends AdapterBase implements Use public static String get2FAKey(UserAccount userAccount) { return userAccount.getKeyFor2fa(); - //return "7t4gabg72liipmq7n43lt3cw66fel4iz"; - /* - This logic can be replaced on per-user-account basis - where the key is generated to show the user one-time QR code, - and then stored in DB. - For #CCC21 hackathon, we'll take shortcuts ;) - SecureRandom random = new SecureRandom(); - byte[] bytes = new byte[20]; - random.nextBytes(bytes); - Base32 base32 = new Base32(); - return base32.encodeToString(bytes); - */ } public static String get2FACode(String secretKey) { @@ -69,4 +60,15 @@ public class GoogleUserTwoFactorAuthenticator extends AdapterBase implements Use return TOTP.getOTP(hexKey); } + public static void setup2FAKey(UserAccount userAccount) { + if (StringUtils.isNotEmpty(userAccount.getKeyFor2fa())) { + throw new CloudRuntimeException(String.format("2FA key is already setup for the user account %s", userAccount.getAccountName())); + } + SecureRandom random = new SecureRandom(); + byte[] bytes = new byte[20]; + random.nextBytes(bytes); + Base32 base32 = new Base32(); + String key = base32.encodeToString(bytes); + userAccount.setKeyFor2fa(key); + } } diff --git a/server/src/main/java/com/cloud/api/ApiServer.java b/server/src/main/java/com/cloud/api/ApiServer.java index 76b592a9d90..30c5ba0218a 100644 --- a/server/src/main/java/com/cloud/api/ApiServer.java +++ b/server/src/main/java/com/cloud/api/ApiServer.java @@ -1069,6 +1069,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer if (ApiConstants.SESSIONKEY.equalsIgnoreCase(attrName)) { response.setSessionKey(attrObj.toString()); } + if (ApiConstants.IS_2FA_ENABLED.equalsIgnoreCase(attrName)) { + response.set2FAenabled(attrObj.toString()); + } } } response.setResponseName("loginresponse"); @@ -1132,6 +1135,8 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer session.setAttribute("timezoneoffset", Float.valueOf(offsetInHrs).toString()); } + session.setAttribute("2FAenabled", Boolean.toString(userAcct.is2faEnabled())); + // (bug 5483) generate a session key that the user must submit on every request to prevent CSRF, add that // to the login response so that session-based authenticators know to send the key back final SecureRandom sesssionKeyRandom = new SecureRandom(); diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 1ab12d326af..2062e220c7a 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -35,6 +35,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import com.cloud.user.UserAccount; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.ApiServerService; import org.apache.cloudstack.api.ServerApiException; @@ -296,13 +297,33 @@ public class ApiServlet extends HttpServlet { if (isNew && s_logger.isTraceEnabled()) { s_logger.trace(String.format("new session: %s", session)); + // 1. 2fa enabled + // 2. except login command with 2fa code + // 3. login command and 2fa is succeeded + s_logger.trace("Checking if two factor authentication is enabled, if enabled it will be verified"); + UserAccount userAccount = accountMgr.getUserAccountById(userId); + boolean is2FAenabled = userAccount.is2faEnabled(); + if (is2FAenabled) { + if (command != null && !command.equals(ApiConstants.TWOFACTORAUTHENTICATION) ) { + if (session != null) { + invalidateHttpSession(session, String.format("request verification failed for %s from %s", userId, remoteAddress.getHostAddress())); + } + + auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "two factor authentication is not done"); + final String serializedResponse = + apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "two factor authentication is not done", params, + responseType); + HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value()); + + } + } } if (!isNew) { userId = (Long)session.getAttribute("userid"); final String account = (String) session.getAttribute("account"); final Object accountObj = session.getAttribute("accountobj"); if (account != null) { - if (invalidateHttpSesseionIfNeeded(req, resp, auditTrailSb, responseType, params, session, account)) return; + if (invalidateHttpSessionIfNeeded(req, resp, auditTrailSb, responseType, params, session, account)) return; } else { if (s_logger.isDebugEnabled()) { s_logger.debug("no account, this request will be validated through apikey(%s)/signature"); @@ -399,7 +420,7 @@ public class ApiServlet extends HttpServlet { return true; } - private boolean invalidateHttpSesseionIfNeeded(HttpServletRequest req, HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String account) { + private boolean invalidateHttpSessionIfNeeded(HttpServletRequest req, HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String account) { if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) { String msg = String.format("invalidating session %s for account %s", session.getId(), account); invalidateHttpSession(session, msg); diff --git a/server/src/main/java/com/cloud/api/auth/TwoFactorAuthenticationCmd.java b/server/src/main/java/com/cloud/api/auth/TwoFactorAuthenticationCmd.java new file mode 100644 index 00000000000..66282f0e823 --- /dev/null +++ b/server/src/main/java/com/cloud/api/auth/TwoFactorAuthenticationCmd.java @@ -0,0 +1,88 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.api.auth; + +import com.cloud.user.Account; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.auth.APIAuthenticationType; +import org.apache.cloudstack.api.auth.APIAuthenticator; +import org.apache.cloudstack.api.auth.PluggableAPIAuthenticator; +import org.apache.cloudstack.api.response.SuccessResponse; +import org.apache.log4j.Logger; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.net.InetAddress; +import java.util.List; +import java.util.Map; + +@APICommand(name = ApiConstants.TWOFACTORAUTHENTICATION, description = "Checks the 2fa code for the user.", requestHasSensitiveInfo = false, responseObject = SuccessResponse.class, entityType = {}) +public class TwoFactorAuthenticationCmd extends BaseCmd implements APIAuthenticator { + + public static final Logger s_logger = Logger.getLogger(TwoFactorAuthenticationCmd.class.getName()); + private static final String s_name = "twofactorauthenticationresponse"; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.TWOFACTORAUTHENTICATIONCODE, type = CommandType.STRING, description = "two factor authentication code", required = true) + private String twoFactorAuthenticationCode; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public String getTwoFactorAuthenticationCode() { + return twoFactorAuthenticationCode; + } + + @Override + public void execute() throws ServerApiException { + throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "This is an authentication api, cannot be used directly"); + } + + @Override + public String getCommandName() { + return s_name; + } + + @Override + public long getEntityOwnerId() { + return Account.Type.NORMAL.ordinal(); + } + + @Override + public String authenticate(String command, Map<String, Object[]> params, HttpSession session, InetAddress remoteAddress, String responseType, StringBuilder auditTrailSb, HttpServletRequest req, HttpServletResponse resp) throws ServerApiException { + return null; + } + + @Override + public APIAuthenticationType getAPIType() { + return APIAuthenticationType.LOGIN_API; + } + + @Override + public void setAuthenticators(List<PluggableAPIAuthenticator> authenticators) { + } +}
