Skip to content

Commit

Permalink
Merge pull request #759 from SujanSanjula96/sms-otp-first-factor
Browse files Browse the repository at this point in the history
Add SMS OTP captcha connector
  • Loading branch information
SujanSanjula96 authored Oct 27, 2023
2 parents c18ee6d + f952c00 commit a75c4da
Show file tree
Hide file tree
Showing 2 changed files with 292 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
/*
* Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. 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 org.wso2.carbon.identity.captcha.connector.recaptcha;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.identity.application.authentication.framework.AuthenticationFlowHandler;
import org.wso2.carbon.identity.application.authentication.framework.config.model.AuthenticatorConfig;
import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext;
import org.wso2.carbon.identity.application.authentication.framework.model.AuthenticatedIdPData;
import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils;
import org.wso2.carbon.identity.application.common.model.Property;
import org.wso2.carbon.identity.captcha.connector.CaptchaPostValidationResponse;
import org.wso2.carbon.identity.captcha.connector.CaptchaPreValidationResponse;
import org.wso2.carbon.identity.captcha.exception.CaptchaException;
import org.wso2.carbon.identity.captcha.internal.CaptchaDataHolder;
import org.wso2.carbon.identity.captcha.util.CaptchaConstants;
import org.wso2.carbon.identity.captcha.util.CaptchaConstants.ReCaptchaConnectorPropertySuffixes;
import org.wso2.carbon.identity.captcha.util.CaptchaUtil;
import org.wso2.carbon.identity.governance.IdentityGovernanceException;
import org.wso2.carbon.identity.governance.IdentityGovernanceService;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.wso2.carbon.identity.captcha.util.CaptchaConstants.SSO_LOGIN_RECAPTCHA_CONNECTOR_NAME;

/**
* Recaptcha Connector for SMS OTP.
*/
public class SMSOTPCaptchaConnector extends AbstractReCaptchaConnector {

private static final Log log = LogFactory.getLog(SMSOTPCaptchaConnector.class);
private static final String SECURED_DESTINATIONS = "/commonauth";
public static final String SMS_OTP_AUTHENTICATOR_NAME = "SMSOTP";
public static final String IS_REDIRECT_TO_SMS_OTP = "isRedirectToSmsOTP";
public static final String RESEND_CODE = "resendCode";
private static final String ON_FAIL_REDIRECT_URL = "/authenticationendpoint/login.do";
private static final String SMS_OTP_LOGIN_ATTEMPT_FAIL_CLAIM = "http://wso2.org/claims/identity/failedLoginAttempts";
private static final String RECAPTCHA_PARAM = "reCaptcha";
private static final String AUTH_FAILURE = "authFailure";
private static final String AUTH_FAILURE_MSG = "authFailureMsg";
private static final String RECAPTCHA_FAIL_MSG = "recaptcha.fail.message";
private static final String OTP_CODE_PARAM = "OTPcode";
private IdentityGovernanceService identityGovernanceService;

@Override
public void init(IdentityGovernanceService identityGovernanceService) {

this.identityGovernanceService = identityGovernanceService;
}

@Override
public int getPriority() {
return 30;
}

@Override
public boolean canHandle(ServletRequest servletRequest, ServletResponse servletResponse) throws CaptchaException {

String path = ((HttpServletRequest) servletRequest).getRequestURI();
if (StringUtils.isBlank(path) || !(CaptchaUtil.isPathAvailable(path, SECURED_DESTINATIONS))) {
return false;
}

String sessionDataKey = servletRequest.getParameter(FrameworkUtils.SESSION_DATA_KEY);
if (sessionDataKey == null) {
return false;
}
AuthenticationContext context = FrameworkUtils.getAuthenticationContextFromCache(sessionDataKey);
if (context == null
|| !SMS_OTP_AUTHENTICATOR_NAME.equals(context.getCurrentAuthenticator())
|| (SMS_OTP_AUTHENTICATOR_NAME.equals(context.getCurrentAuthenticator())
&& !Boolean.parseBoolean((String) context.getProperty(IS_REDIRECT_TO_SMS_OTP)))) {
return false;
}

if (servletRequest.getParameter(OTP_CODE_PARAM) == null) {
return false;
}
if (context.getCurrentStep() != 1 || isPreviousIdPAuthenticationFlowHandler(context)) {
return false;
}
if (Boolean.parseBoolean(servletRequest.getParameter(RESEND_CODE))) {
return false;
}
return isSmsRecaptchaEnabled(servletRequest);
}

@Override
public CaptchaPreValidationResponse preValidate(ServletRequest servletRequest, ServletResponse servletResponse)
throws CaptchaException {

CaptchaPreValidationResponse preValidationResponse = new CaptchaPreValidationResponse();

String sessionDataKey = servletRequest.getParameter(FrameworkUtils.SESSION_DATA_KEY);
AuthenticationContext context = FrameworkUtils.getAuthenticationContextFromCache(sessionDataKey);
String username = context.getLastAuthenticatedUser().getUserName();
String tenantDomain = context.getLastAuthenticatedUser().getTenantDomain();

Property[] connectorConfigs = null;
try {
connectorConfigs = identityGovernanceService.getConfiguration(new String[]{
SSO_LOGIN_RECAPTCHA_CONNECTOR_NAME +
CaptchaConstants.ReCaptchaConnectorPropertySuffixes.ENABLE_ALWAYS}, tenantDomain);
} catch (IdentityGovernanceException e) {
// Can happen due to invalid user/invalid tenant/invalid configuration.
log.error("Unable to load connector configuration.", e);
}

if (CaptchaDataHolder.getInstance().isForcefullyEnabledRecaptchaForAllTenants() || (connectorConfigs != null &&
connectorConfigs.length != 0 && (Boolean.parseBoolean(connectorConfigs[0].getValue())))) {

Map<String, String> params = new HashMap<>();
params.put(AUTH_FAILURE, Boolean.TRUE.toString());
params.put(AUTH_FAILURE_MSG, RECAPTCHA_FAIL_MSG);
preValidationResponse.setCaptchaAttributes(params);
preValidationResponse.setOnCaptchaFailRedirectUrls(getFailedUrlList());
preValidationResponse.setCaptchaValidationRequired(true);

} else if (CaptchaUtil.isMaximumFailedLoginAttemptsReached(MultitenantUtils.getTenantAwareUsername(username),
tenantDomain, SMS_OTP_LOGIN_ATTEMPT_FAIL_CLAIM)) {
preValidationResponse.setCaptchaValidationRequired(true);
preValidationResponse.setMaxFailedLimitReached(true);

preValidationResponse.setOnCaptchaFailRedirectUrls(getFailedUrlList());
Map<String, String> params = new HashMap<>();
params.put(RECAPTCHA_PARAM, Boolean.TRUE.toString());
params.put(AUTH_FAILURE, Boolean.TRUE.toString());
params.put(AUTH_FAILURE_MSG, RECAPTCHA_FAIL_MSG);
preValidationResponse.setCaptchaAttributes(params);
}
// Post validate all requests
preValidationResponse.setMaxFailedLimitReached(true);
preValidationResponse.setPostValidationRequired(true);

return preValidationResponse;
}

/**
* Get the URLs which need to send back in case of failure.
*
* @return list of failed urls
*/
private List<String> getFailedUrlList() {

List<String> failedRedirectUrls = new ArrayList<>();
String failedRedirectUrlStr = CaptchaDataHolder.getInstance().getReCaptchaErrorRedirectUrls();
if (StringUtils.isNotBlank(failedRedirectUrlStr)) {
failedRedirectUrls = new ArrayList<>(Arrays.asList(failedRedirectUrlStr.split(",")));
}
failedRedirectUrls.add(ON_FAIL_REDIRECT_URL);
return failedRedirectUrls;
}

@Override
public CaptchaPostValidationResponse postValidate(ServletRequest servletRequest, ServletResponse servletResponse)
throws CaptchaException {

if (!StringUtils.isBlank(CaptchaConstants.getEnableSecurityMechanism())) {
CaptchaConstants.removeEnabledSecurityMechanism();
CaptchaPostValidationResponse validationResponse = new CaptchaPostValidationResponse();
validationResponse.setSuccessfulAttempt(false);
validationResponse.setEnableCaptchaResponsePath(true);
Map<String, String> params = new HashMap<>();
params.put(RECAPTCHA_PARAM, Boolean.TRUE.toString());
validationResponse.setCaptchaAttributes(params);
return validationResponse;
}
return null;
}

/**
* Check whether the SMS OTP reCaptcha is enabled.
*
* @param servletRequest Servlet request.
* @return True if SMS OTP reCaptcha is enabled.
* @throws CaptchaException If error occurred while checking the SMS OTP reCaptcha enablement.
*/
public boolean isSmsRecaptchaEnabled(ServletRequest servletRequest) throws CaptchaException {

if (CaptchaDataHolder.getInstance().isForcefullyEnabledRecaptchaForAllTenants()) {
return true;
}

String sessionDataKey = servletRequest.getParameter(FrameworkUtils.SESSION_DATA_KEY);
if (sessionDataKey == null) {
return false;
}
AuthenticationContext context = FrameworkUtils.getAuthenticationContextFromCache(sessionDataKey);
if (context == null
|| context.getLastAuthenticatedUser() == null
|| StringUtils.isBlank(context.getLastAuthenticatedUser().getUserName())) {
return false;
}

String username = context.getLastAuthenticatedUser().getUserName();
String tenantDomain = context.getLastAuthenticatedUser().getTenantDomain();
Property[] connectorConfigs;
try {
connectorConfigs = identityGovernanceService.getConfiguration(new String[]{
SSO_LOGIN_RECAPTCHA_CONNECTOR_NAME + ReCaptchaConnectorPropertySuffixes.ENABLE_ALWAYS,
SSO_LOGIN_RECAPTCHA_CONNECTOR_NAME + ReCaptchaConnectorPropertySuffixes.ENABLE,
SSO_LOGIN_RECAPTCHA_CONNECTOR_NAME + ReCaptchaConnectorPropertySuffixes.MAX_ATTEMPTS},
tenantDomain);
} catch (IdentityGovernanceException e) {
// Can happen due to invalid user/invalid tenant/invalid configuration.
if (log.isDebugEnabled()) {
log.debug("Unable to load connector configuration.", e);
}
return false;
}
if (ArrayUtils.isEmpty(connectorConfigs) || connectorConfigs.length != 3 ||
!(Boolean.parseBoolean(connectorConfigs[0].getValue()) ||
Boolean.parseBoolean(connectorConfigs[1].getValue()))) {
return false;
}
if (!Boolean.parseBoolean(connectorConfigs[0].getValue())
&& Boolean.parseBoolean(connectorConfigs[1].getValue())
&& !CaptchaUtil.isMaximumFailedLoginAttemptsReached(username, tenantDomain,
SMS_OTP_LOGIN_ATTEMPT_FAIL_CLAIM)) {
return false;
}
return CaptchaDataHolder.getInstance().isReCaptchaEnabled();
}

/**
* This method checks if all the authentication steps up to now have been performed by authenticators that
* implements AuthenticationFlowHandler interface. If so, it returns true.
* AuthenticationFlowHandlers may not perform actual authentication though the authenticated user is set in the
* context. Hence, this method can be used to determine if the user has been authenticated by a previous step.
*
* @param context AuthenticationContext
* @return true if all the authentication steps up to now have been performed by AuthenticationFlowHandlers.
*/
private boolean isPreviousIdPAuthenticationFlowHandler(AuthenticationContext context) {

boolean hasPreviousAuthenticators = false;
Map<String, AuthenticatedIdPData> currentAuthenticatedIdPs = context.getCurrentAuthenticatedIdPs();
if (currentAuthenticatedIdPs != null && !currentAuthenticatedIdPs.isEmpty()) {
for (Map.Entry<String, AuthenticatedIdPData> entry : currentAuthenticatedIdPs.entrySet()) {
AuthenticatedIdPData authenticatedIdPData = entry.getValue();
if (authenticatedIdPData != null) {
List<AuthenticatorConfig> authenticators = authenticatedIdPData.getAuthenticators();
if (authenticators != null) {
for (AuthenticatorConfig authenticator : authenticators) {
hasPreviousAuthenticators = true;
if (!(authenticator.getApplicationAuthenticator() instanceof AuthenticationFlowHandler)) {
return false;
}
}
}
}
}
}
if (hasPreviousAuthenticators) {
return true;
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.wso2.carbon.identity.captcha.connector.recaptcha.LiteUserSelfSignUpReCaptchaConnector;
import org.wso2.carbon.identity.captcha.connector.recaptcha.PasswordRecoveryReCaptchaConnector;
import org.wso2.carbon.identity.captcha.connector.recaptcha.ResendConfirmationReCaptchaConnector;
import org.wso2.carbon.identity.captcha.connector.recaptcha.SMSOTPCaptchaConnector;
import org.wso2.carbon.identity.captcha.connector.recaptcha.SSOLoginReCaptchaConfig;
import org.wso2.carbon.identity.captcha.connector.recaptcha.SelfSignUpReCaptchaConnector;
import org.wso2.carbon.identity.captcha.connector.recaptcha.UsernameRecoveryReCaptchaConnector;
Expand Down Expand Up @@ -91,6 +92,10 @@ protected void activate(ComponentContext context) {
captchaConnector = new EmailOTPCaptchaConnector();
captchaConnector.init(CaptchaDataHolder.getInstance().getIdentityGovernanceService());
CaptchaDataHolder.getInstance().addCaptchaConnector(captchaConnector);
// Initialize and register SMSOTPRecaptchaConnector.
captchaConnector = new SMSOTPCaptchaConnector();
captchaConnector.init(CaptchaDataHolder.getInstance().getIdentityGovernanceService());
CaptchaDataHolder.getInstance().addCaptchaConnector(captchaConnector);
AuthenticationDataPublisher failedLoginAttemptValidator = new FailLoginAttemptValidator();
context.getBundleContext().registerService(AuthenticationDataPublisher.class,
failedLoginAttemptValidator, null);
Expand Down

0 comments on commit a75c4da

Please sign in to comment.