Java tutorial
/******************************************************************************* * Copyright (c) 2015-2016, WSO2.Telco Inc. (http://www.wso2telco.com) * <p> * All Rights Reserved. WSO2.Telco Inc. licences 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 * <p> * http://www.apache.org/licenses/LICENSE-2.0 * <p> * 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.wso2telco.gsma.authenticators.sms; import com.wso2telco.Util; import com.wso2telco.core.config.model.MobileConnectConfig; import com.wso2telco.core.config.service.ConfigurationService; import com.wso2telco.core.config.service.ConfigurationServiceImpl; import com.wso2telco.core.sp.config.utils.exception.DataAccessException; import com.wso2telco.core.sp.config.utils.service.SpConfigService; import com.wso2telco.core.sp.config.utils.service.impl.SpConfigServiceImpl; import com.wso2telco.gsma.authenticators.AuthenticatorException; import com.wso2telco.gsma.authenticators.BaseApplicationAuthenticator; import com.wso2telco.gsma.authenticators.Constants; import com.wso2telco.gsma.authenticators.DBUtils; import com.wso2telco.gsma.authenticators.cryptosystem.AESencrp; import com.wso2telco.gsma.authenticators.model.SMSMessage; import com.wso2telco.gsma.authenticators.util.*; import com.wso2telco.gsma.shorten.SelectShortUrl; import com.wso2telco.ids.datapublisher.model.UserStatus; import com.wso2telco.ids.datapublisher.util.DataPublisherUtil; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hashids.Hashids; import org.wso2.carbon.identity.application.authentication.framework.*; import org.wso2.carbon.identity.application.authentication.framework.config.ConfigurationFacade; import org.wso2.carbon.identity.application.authentication.framework.config.model.StepConfig; import org.wso2.carbon.identity.application.authentication.framework.context.AuthenticationContext; import org.wso2.carbon.identity.application.authentication.framework.exception.AuthenticationFailedException; import org.wso2.carbon.identity.application.authentication.framework.exception.LogoutFailedException; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.user.registration.stub.UserRegistrationAdminServiceIdentityException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.rmi.RemoteException; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; // TODO: Auto-generated Javadoc /** * The Class SMSAuthenticator. */ public class ServerInitiatedSMSAuthenticator extends AbstractApplicationAuthenticator implements LocalApplicationAuthenticator, BaseApplicationAuthenticator { /** * The Constant serialVersionUID. */ private static final long serialVersionUID = -1189332409518227376L; /** * The log. */ private static Log log = LogFactory.getLog(ServerInitiatedSMSAuthenticator.class); /** * The Configuration service */ protected static ConfigurationService configurationService = new ConfigurationServiceImpl(); protected SpConfigService spConfigService = new SpConfigServiceImpl(); private static final String AUTH_FAILED = "Authentication failed"; private static final String AUTH_FAILED_DETAILED = "SMS Authentication failed while trying to authenticate"; private static MobileConnectConfig mobileConnectConfigs = null; static { mobileConnectConfigs = configurationService.getDataHolder().getMobileConnectConfig(); } /** * The Enum UserResponse. */ protected enum UserResponse { /** * The pending. */ PENDING, /** * The approved. */ APPROVED, /** * The rejected. */ REJECTED, /** * The Expired. */ EXPIRED } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator#canHandle(javax * .servlet.http.HttpServletRequest) */ @Override public boolean canHandle(HttpServletRequest request) { if (log.isDebugEnabled()) { log.debug(this.getClass().getName() + " canHandle invoked"); } return true; } public AuthenticatorFlowStatus processRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException, LogoutFailedException { try { processAuthenticationResponse(request, response, context); if (this instanceof LocalApplicationAuthenticator && !context.getSequenceConfig().getApplicationConfig().isSaaSApp()) { String e = context.getSubject().getTenantDomain(); String stepMap1 = context.getTenantDomain(); if (!StringUtils.equals(e, stepMap1)) { context.setProperty("UserTenantDomainMismatch", Boolean.valueOf(true)); throw new AuthenticationFailedException("Service Provider tenant domain must be equal to user" + " tenant domain for non-SaaS applications"); } } request.setAttribute(FrameworkConstants.REQ_ATTR_HANDLED, Boolean.TRUE); publishAuthenticationStepAttempt(request, context, context.getSubject(), true); return AuthenticatorFlowStatus.SUCCESS_COMPLETED; } catch (AuthenticationFailedException e) { Object property = context.getProperty(Constants.IS_TERMINATED); boolean isTerminated = false; if (property != null) { isTerminated = (boolean) property; } Map stepMap = context.getSequenceConfig().getStepMap(); boolean stepHasMultiOption = false; publishAuthenticationStepAttempt(request, context, e.getUser(), false); if (stepMap != null && !stepMap.isEmpty()) { StepConfig stepConfig = (StepConfig) stepMap.get(Integer.valueOf(context.getCurrentStep())); if (stepConfig != null) { stepHasMultiOption = stepConfig.isMultiOption(); } } if (isTerminated) { throw new AuthenticationFailedException("Authenticator is terminated"); } if (retryAuthenticationEnabled() && !stepHasMultiOption) { context.setRetrying(true); context.setCurrentAuthenticator(getName()); initiateAuthenticationRequest(request, response, context); return AuthenticatorFlowStatus.INCOMPLETE; } else { throw e; } } } private void publishAuthenticationStepAttempt(HttpServletRequest request, AuthenticationContext context, User user, boolean success) { AuthenticationDataPublisher authnDataPublisherProxy = FrameworkServiceDataHolder.getInstance() .getAuthnDataPublisherProxy(); if (authnDataPublisherProxy != null && authnDataPublisherProxy.isEnabled(context)) { boolean isFederated = this instanceof FederatedApplicationAuthenticator; Map<String, Object> paramMap = new HashMap<>(); paramMap.put(FrameworkConstants.AnalyticsAttributes.USER, user); if (isFederated) { // Setting this value to authentication context in order to use in AuthenticationSuccess Event context.setProperty(FrameworkConstants.AnalyticsAttributes.HAS_FEDERATED_STEP, true); paramMap.put(FrameworkConstants.AnalyticsAttributes.IS_FEDERATED, true); } else { // Setting this value to authentication context in order to use in AuthenticationSuccess Event context.setProperty(FrameworkConstants.AnalyticsAttributes.HAS_LOCAL_STEP, true); paramMap.put(FrameworkConstants.AnalyticsAttributes.IS_FEDERATED, false); } Map<String, Object> unmodifiableParamMap = Collections.unmodifiableMap(paramMap); if (success) { authnDataPublisherProxy.publishAuthenticationStepSuccess(request, context, unmodifiableParamMap); } else { authnDataPublisherProxy.publishAuthenticationStepFailure(request, context, unmodifiableParamMap); } } } private void sendSMS(HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException, LogoutFailedException { UserStatus userStatus = (UserStatus) context.getParameter(Constants.USER_STATUS_DATA_PUBLISHING_PARAM); SMSMessage smsMessage = getRedirectInitAuthentication(response, context, userStatus); if (smsMessage != null) { try { BasicFutureCallback futureCallback = userStatus != null ? new SMSFutureCallback(userStatus.cloneUserStatus(), "SMS") : new SMSFutureCallback(); smsMessage.setFutureCallback(futureCallback); String smsResponse = new SendSMS().sendSMS(smsMessage.getMsisdn(), smsMessage.getMessageText(), smsMessage.getOperator(), smsMessage.getFutureCallback()); } catch (IOException e) { DataPublisherUtil.updateAndPublishUserStatus(userStatus, DataPublisherUtil.UserState.SMS_AUTH_PROCESSING_FAIL, e.getMessage()); log.error(AUTH_FAILED, e); throw new AuthenticationFailedException(e.getMessage(), e); } } else { log.error(AUTH_FAILED_DETAILED); throw new AuthenticationFailedException(AUTH_FAILED_DETAILED); } } @Override public AuthenticatorFlowStatus process(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException, LogoutFailedException { DataPublisherUtil.updateAndPublishUserStatus( (UserStatus) context.getParameter(Constants.USER_STATUS_DATA_PUBLISHING_PARAM), DataPublisherUtil.UserState.USSD_AUTH_PROCESSING, "ServerInitiatedSMSAuthenticator processing started"); if (context.isLogoutRequest()) { return AuthenticatorFlowStatus.SUCCESS_COMPLETED; } else { return processRequest(request, response, context); } } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework * .AbstractApplicationAuthenticator#initiateAuthenticationRequest(javax.servlet.http.HttpServletRequest, javax * .servlet.http.HttpServletResponse, org.wso2.carbon.identity.application.authentication.framework.context * .AuthenticationContext) */ @Override protected void initiateAuthenticationRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { log.info("Initiating authentication request"); } protected SMSMessage getRedirectInitAuthentication(HttpServletResponse response, AuthenticationContext context, UserStatus userStatus) throws AuthenticationFailedException { SMSMessage smsMessage = null; String queryParams = FrameworkUtils.getQueryStringWithFrameworkContextId(context.getQueryParams(), context.getCallerSessionKey(), context.getContextIdentifier()); if (log.isDebugEnabled()) { log.debug("Query parameters : " + queryParams); } try { String retryParam = ""; if (context.isRetrying()) { retryParam = "&authFailure=true&authFailureMsg=login.fail.message"; } else { // Insert entry to DB only if this is not a retry DBUtils.insertUserResponse(context.getContextIdentifier(), UserResponse.PENDING.name()); } //MSISDN will be saved in the context in the MSISDNAuthenticator String msisdn = (String) context.getProperty(Constants.MSISDN); Application application = new Application(); MobileConnectConfig connectConfig = configurationService.getDataHolder().getMobileConnectConfig(); MobileConnectConfig.SMSConfig smsConfig = connectConfig.getSmsConfig(); String encryptedContextIdentifier = AESencrp.encrypt(context.getContextIdentifier()); //String messageURL = connectConfig.getSmsConfig().getAuthUrl() + Constants.AUTH_URL_ID_PREFIX; String messageURL = mobileConnectConfigs.getBackChannelConfig().getSmsCallbackUrl() + Constants.AUTH_URL_ID_PREFIX; //todo: add to config Map<String, String> paramMap = Util.createQueryParamMap(queryParams); String client_id = paramMap.get(Constants.CLIENT_ID); String operator = (String) context.getProperty(Constants.OPERATOR); if (smsConfig.isShortUrl()) { // If a URL shortening service is enabled, then we need to encrypt the context identifier, create the // message URL and shorten it. log.info("URL shortening service is enabled"); SelectShortUrl selectShortUrl = new SelectShortUrl(); messageURL = selectShortUrl.getShortUrl(smsConfig.getShortUrlClass(), messageURL + response.encodeURL(encryptedContextIdentifier), smsConfig.getAccessToken(), smsConfig.getShortUrlService()); } else { // If a URL shortening service is not enabled, we need to created a hash key for the encrypted // context identifier and insert a database entry mapping ths hash key to the context identifier. // This is done to shorten the message URL as much as possible. log.info("Generating hash key for the SMS"); String hashForContextId = getHashForContextId(encryptedContextIdentifier); messageURL += hashForContextId; DBUtils.insertHashKeyContextIdentifierMapping(hashForContextId, context.getContextIdentifier()); } // prepare the USSD message from template HashMap<String, String> variableMap = new HashMap<String, String>(); variableMap.put("application", application.changeApplicationName( context.getSequenceConfig().getApplicationConfig().getApplicationName())); variableMap.put("link", messageURL); boolean isRegistering = (boolean) context.getProperty(Constants.IS_REGISTERING); OutboundMessage.MessageType messageType = OutboundMessage.MessageType.SMS_LOGIN; if (isRegistering) { messageType = OutboundMessage.MessageType.SMS_REGISTRATION; } String messageText = OutboundMessage.prepare(client_id, messageType, variableMap, operator); if (log.isDebugEnabled()) { log.debug("Message URL: " + messageURL); log.debug("Message: " + messageText); log.debug("Operator: " + operator); } DBUtils.insertAuthFlowStatus(msisdn, Constants.STATUS_PENDING, context.getContextIdentifier()); smsMessage = new SMSMessage(); smsMessage.setMsisdn(msisdn); smsMessage.setMessageText(messageText); smsMessage.setOperator(operator); smsMessage.setClient_id(client_id); } catch (Exception e) { DataPublisherUtil.updateAndPublishUserStatus(userStatus, DataPublisherUtil.UserState.SMS_AUTH_PROCESSING_FAIL, e.getMessage()); log.error(AUTH_FAILED, e); throw new AuthenticationFailedException(e.getMessage(), e); } return smsMessage; } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework * .AbstractApplicationAuthenticator#processAuthenticationResponse(javax.servlet.http.HttpServletRequest, javax * .servlet.http.HttpServletResponse, org.wso2.carbon.identity.application.authentication.framework.context * .AuthenticationContext) */ @Override protected void processAuthenticationResponse(HttpServletRequest request, HttpServletResponse response, AuthenticationContext context) throws AuthenticationFailedException { log.info("Processing authentication response"); UserStatus userStatus = (UserStatus) context.getParameter(Constants.USER_STATUS_DATA_PUBLISHING_PARAM); String sessionDataKey = request.getParameter("sessionDataKey"); String msisdn = (String) context.getProperty("msisdn"); /* String operator = (String) context.getProperty("operator"); String userAction = request.getParameter(Constants.ACTION);*/ if (log.isDebugEnabled()) { log.debug("SessionDataKey : " + sessionDataKey); } try { sendSMS(response, context); } catch (LogoutFailedException e) { throw new AuthenticationFailedException(e.getMessage(), e); } AuthenticationContextHelper.setSubject(context, msisdn); log.info("Authentication success"); DataPublisherUtil.updateAndPublishUserStatus(userStatus, DataPublisherUtil.UserState.SMS_AUTH_SUCCESS, "Server Initiated SMS Authentication success"); } /** * Terminates the authenticator due to user implicit action * * @param context Authentication Context * @param authFlowStatus Authflow status * @param sessionID sessionID * @param userResponse user response status * @throws AuthenticationFailedException */ private void terminateAuthentication(AuthenticationContext context, String sessionID, String userResponse, String authFlowStatus) throws AuthenticationFailedException { log.info("User has terminated the authentication flow"); context.setProperty(Constants.IS_TERMINATED, true); try { DBUtils.updateUserResponse(sessionID, userResponse); if (!DBUtils.getAuthFlowStatus(sessionID).equalsIgnoreCase(UserResponse.APPROVED.name())) DBUtils.updateAuthFlowStatus(sessionID, authFlowStatus); } catch (AuthenticatorException e) { log.error("Authentication Exception occurred in terminateAuthentication method in SMSAuthenticator", e); } throw new AuthenticationFailedException("Authenticator is terminated"); } protected String getHashForContextId(String contextIdentifier) { int hashLength = 7; Hashids hashids = new Hashids(contextIdentifier, hashLength); return hashids.encode(new Date().getTime()); } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework * .AbstractApplicationAuthenticator#retryAuthenticationEnabled() */ @Override protected boolean retryAuthenticationEnabled() { return false; } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework * .ApplicationAuthenticator#getContextIdentifier(javax.servlet.http.HttpServletRequest) */ @Override public String getContextIdentifier(HttpServletRequest request) { return request.getParameter("sessionDataKey"); } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator#getFriendlyName() */ @Override public String getFriendlyName() { return Constants.SERVER_INITIATED_SMS_AUTHENTICATOR_FRIENDLY_NAME; } /* (non-Javadoc) * @see org.wso2.carbon.identity.application.authentication.framework.ApplicationAuthenticator#getName() */ @Override public String getName() { return Constants.SERVER_INITIATED_SMS_AUTHENTICATOR_NAME; } @Override public String getAmrValue(int acr) { return "SMS_URL_OK"; } }