diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java index df31ba8d..0b7297ea 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java @@ -1,37 +1,27 @@ package org.sunbird.keycloak.resetcredential.sms; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.HashMap; import java.util.List; -import java.util.Objects; -import java.util.concurrent.TimeUnit; +import java.util.Map; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.actiontoken.DefaultActionTokenKey; -import org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionToken; -import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; -import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialModel; -import org.keycloak.email.EmailException; -import org.keycloak.email.EmailTemplateProvider; -import org.keycloak.events.Details; -import org.keycloak.events.Errors; -import org.keycloak.events.EventBuilder; -import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; -import org.keycloak.models.utils.FormMessage; -import org.keycloak.services.ServicesLogger; -import org.keycloak.services.messages.Messages; -import org.keycloak.sessions.AuthenticationSessionModel; +import org.sunbird.keycloak.utils.Constants; +import org.sunbird.keycloak.utils.HttpClient; /** * Created by joris on 11/11/2016. @@ -66,13 +56,13 @@ public void authenticate(AuthenticationFlowContext context) { } if (StringUtils.isNotBlank(mobileNumber) || StringUtils.isNotBlank(userEmail)) { - if (StringUtils.isNotBlank(userEmail)) { - logger.debug("KeycloakSmsAuthenticator@authenticate - Sending Email - " + userEmail); - sendEmail(context); - } + Map otpResponse = generateOTP(context); + if (StringUtils.isNotBlank(mobileNumber)) { - logger.debug("KeycloakSmsAuthenticator@authenticate - Sending SMS - " + mobileNumber); - sendSMS(context, mobileNumber); + sendSMS(otpResponse, context, mobileNumber); + } + if (StringUtils.isNotBlank(userEmail)) { + sendEmailViaSunbird(otpResponse, context, userEmail); } } else { // The mobile number is NOT configured --> complain @@ -83,104 +73,76 @@ public void authenticate(AuthenticationFlowContext context) { } } - private void sendSMS(AuthenticationFlowContext context, String mobileNumber) { - // The mobile number is configured --> send an SMS - long nrOfDigits = KeycloakSmsAuthenticatorUtil.getConfigLong(context.getAuthenticatorConfig(), KeycloakSmsAuthenticatorConstants.CONF_PRP_SMS_CODE_LENGTH, 8L); - logger.debug("Using nrOfDigits " + nrOfDigits); + private Map generateOTP(AuthenticationFlowContext context) { + // The mobile number is configured --> send an SMS + long nrOfDigits = KeycloakSmsAuthenticatorUtil.getConfigLong(context.getAuthenticatorConfig(), + KeycloakSmsAuthenticatorConstants.CONF_PRP_SMS_CODE_LENGTH, 8L); + logger.debug("Using nrOfDigits " + nrOfDigits); - logger.debug("KeycloakSmsAuthenticator@sendSMS"); + logger.debug("KeycloakSmsAuthenticator@sendSMS"); - long ttl = KeycloakSmsAuthenticatorUtil.getConfigLong(context.getAuthenticatorConfig(), KeycloakSmsAuthenticatorConstants.CONF_PRP_SMS_CODE_TTL, 10 * 60L); // 10 minutes in s + long ttl = KeycloakSmsAuthenticatorUtil.getConfigLong(context.getAuthenticatorConfig(), + KeycloakSmsAuthenticatorConstants.CONF_PRP_SMS_CODE_TTL, 10 * 60L); // 10 minutes in s - logger.debug("Using ttl " + ttl + " (s)"); - - String code = KeycloakSmsAuthenticatorUtil.getSmsCode(nrOfDigits); - - storeSMSCode(context, code, new Date().getTime() + (ttl * 1000)); // s --> ms - if (KeycloakSmsAuthenticatorUtil.sendSmsCode(mobileNumber, code, context.getAuthenticatorConfig())) { - Response challenge = context.form().createForm("sms-validation.ftl"); - context.challenge(challenge); - } else { - Response challenge = context.form() - .setError("SMS could not be sent.") - .createForm("sms-validation-error.ftl"); - context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, challenge); - } + logger.debug("Using ttl " + ttl + " (s)"); + String code = KeycloakSmsAuthenticatorUtil.getSmsCode(nrOfDigits); + storeSMSCode(context, code, new Date().getTime() + (ttl * 1000)); // s --> ms + Map response = new HashMap<>(); + response.put(Constants.OTP, code); + response.put(Constants.TTL, (ttl / 60)); + return response; + } + + private void sendSMS(Map otpResponse, AuthenticationFlowContext context, + String mobileNumber) { + logger.debug("KeycloakSmsAuthenticator@sendSMS - Sending SMS"); + + if (KeycloakSmsAuthenticatorUtil.sendSmsCode(mobileNumber, + (String) otpResponse.get(Constants.OTP), context.getAuthenticatorConfig())) { + navigateToEnterOTPPage(context, true); + } else { + navigateToEnterOTPPage(context, false); + } } - private void sendEmail(AuthenticationFlowContext context) { - logger.debug("KeycloakSmsAuthenticator@sendEmail"); - - UserModel user = context.getUser(); - AuthenticationSessionModel authenticationSession = context.getAuthenticationSession(); - String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME); - - // we don't want people guessing usernames, so if there was a problem obtaining the user, the user will be null. - // just reset login for with a success message - if (user == null) { - context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); - return; - } + private void sendEmailViaSunbird(Map otpResponse, + AuthenticationFlowContext context, String userEmail) { + logger.debug("KeycloakSmsAuthenticator@sendEmailViaSunbird - Sending Email via Sunbird API"); - String actionTokenUserId = authenticationSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID); - if (actionTokenUserId != null && Objects.equals(user.getId(), actionTokenUserId)) { - logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping " + CREDENTIAL_TYPE + " screen and using user '%s' ", user.getUsername()); - context.success(); - return; - } + List emails = new ArrayList<>(Arrays.asList(userEmail)); + otpResponse.put(Constants.RECIPIENT_EMAILS, emails); + otpResponse.put(Constants.SUBJECT, Constants.MAIL_SUBJECT); + otpResponse.put(Constants.REALM_NAME, context.getRealm().getDisplayName()); + otpResponse.put(Constants.EMAIL_TEMPLATE_TYPE, Constants.FORGOT_PASSWORD_EMAIL_TEMPLATE); + otpResponse.put(Constants.BODY, Constants.BODY); - EventBuilder event = context.getEvent(); - // we don't want people guessing usernames, so if there is a problem, just continuously challenge - if (user.getEmail() == null || user.getEmail().trim().length() == 0) { - event.user(user) - .detail(Details.USERNAME, username) - .error(Errors.INVALID_EMAIL); + Map request = new HashMap<>(); + request.put(Constants.REQUEST, otpResponse); - context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); - return; - } + HttpResponse response = HttpClient.post(request, + (System.getenv(Constants.SUNBIRD_LMS_BASE_URL) + Constants.SEND_NOTIFICATION_URI), + System.getenv(Constants.SUNBIRD_LMS_AUTHORIZATION)); - int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan(); - int absoluteExpirationInSecs = Time.currentTime() + validityInSecs; - - // We send the secret in the email in a link as a query param. - ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId()); - String link = UriBuilder - .fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo()))) - .build() - .toString(); - long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs); - try { - logger.debug("sendEmail - Reset Link : " + link); - - context.getSession().getProvider(EmailTemplateProvider.class).setRealm(context.getRealm()).setUser(user).sendPasswordReset(link, expirationInMinutes); - - event.clone().event(EventType.SEND_RESET_PASSWORD) - .user(user) - .detail(Details.USERNAME, username) - .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getId()).success(); - context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT)); - - - Response challenge = context.form() - .createForm("password-reset-email.ftl"); - context.failureChallenge(AuthenticationFlowError.UNKNOWN_USER, challenge); - - } catch (EmailException e) { - event.clone().event(EventType.SEND_RESET_PASSWORD) - .detail(Details.USERNAME, username) - .user(user) - .error(Errors.EMAIL_SEND_FAILED); - ServicesLogger.LOGGER.failedToSendPwdResetEmail(e); - Response challenge = context.form() - .setError(Messages.EMAIL_SENT_ERROR) - .createErrorPage(); - context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge); - } + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200) { + navigateToEnterOTPPage(context, true); + } else { + navigateToEnterOTPPage(context, false); + } } - + private void navigateToEnterOTPPage(AuthenticationFlowContext context, Boolean flag) { + if (flag) { + Response challenge = context.form().createForm("sms-validation.ftl"); + context.challenge(challenge); + } else { + Response challenge = + context.form().setError("OTP could not be sent.").createForm("sms-validation-error.ftl"); + context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR, challenge); + } + } + @Override public void action(AuthenticationFlowContext context) { logger.debug("action called ... context = " + context); @@ -236,7 +198,7 @@ public void action(AuthenticationFlowContext context) { // Store the code + expiration time in a UserCredential. Keycloak will persist these in the DB. // When the code is validated on another node (in a clustered environment) the other nodes have access to it's values too. private void storeSMSCode(AuthenticationFlowContext context, String code, Long expiringAt) { - logger.debug("KeycloakSmsAuthenticator@storeSMSCode" + "User name = " + context.getUser().getUsername()); + logger.debug("KeycloakSmsAuthenticator@storeSMSCode called"); UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(KeycloakSmsAuthenticatorConstants.USR_CRED_MDL_SMS_CODE); @@ -249,12 +211,10 @@ private void storeSMSCode(AuthenticationFlowContext context, String code, Long e context.getSession().userCredentialManager().updateCredential(context.getRealm(), context.getUser(), credentials); } - protected CODE_STATUS validateCode(AuthenticationFlowContext context) { - logger.debug("KeycloakSmsAuthenticator@validateCode"); + logger.debug("KeycloakSmsAuthenticator@validateCode called"); CODE_STATUS result = CODE_STATUS.INVALID; - logger.debug("validateCode called ... "); MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); String enteredCode = formData.getFirst(KeycloakSmsAuthenticatorConstants.ANSW_SMS_CODE); KeycloakSession session = context.getSession(); @@ -266,14 +226,14 @@ protected CODE_STATUS validateCode(AuthenticationFlowContext context) { /*CredentialModel expTimeString = (CredentialModel) timeCreds.get(0);*/ logger.debug("KeycloakSmsAuthenticator@validateCode " + "User name = " + context.getUser().getUsername()); - logger.debug("KeycloakSmsAuthenticator@validateCode " + "Expected code = " + expectedCode.getValue() + " entered code = " + enteredCode); + logger.debug("KeycloakSmsAuthenticator@validateCode " + "Expected code = " + expectedCode.getValue() + " entered code = " + enteredCode); if (expectedCode != null) { result = enteredCode.equals(expectedCode.getValue()) ? CODE_STATUS.VALID : CODE_STATUS.INVALID; } logger.debug("result : " + result); - logger.debug("KeycloakSmsAuthenticator@validateCode- Result -" + result); + logger.debug("KeycloakSmsAuthenticator@validateCode - Result -" + result); return result; } diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/Constants.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/Constants.java index 0840ad4c..7c7b959f 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/Constants.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/Constants.java @@ -28,5 +28,19 @@ private Constants(){} public static final String ERROR_REALM_ADMIN_ROLE_ACCESS = "Does not have realm admin role."; public static final String ERROR_INVALID_PARAMETER_VALUE = "Invalid value {0} for parameter {1}."; public static final String ERROR_MANDATORY_PARAM_MISSING = "Mandatory parameter {0} is missing."; - + public static final String OTP = "otp"; + public static final String EMAIL = "email"; + public static final String TTL = "ttl"; + public static final String SUNBIRD_LMS_AUTHORIZATION = "sunbird_authorization"; + + public static final String MAIL_SUBJECT = "Reset password"; + public static final String SUBJECT = "subject"; + public static final String EMAIL_TEMPLATE_TYPE = "emailTemplateType"; + public static final String REALM_NAME = "realmName"; + public static final String SEND_NOTIFICATION_URI = "/user/v1/notification/email"; + public static final String SUNBIRD_LMS_BASE_URL = "sunbird_lms_base_url"; + public static final String BODY = "body"; + public static final String RECIPIENT_EMAILS = "recipientEmails"; + public static final String FORGOT_PASSWORD_EMAIL_TEMPLATE = "forgotPasswordWithOTP"; + public static final String REQUEST = "request"; } diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/HttpClient.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/HttpClient.java new file mode 100644 index 00000000..f5d30faa --- /dev/null +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/utils/HttpClient.java @@ -0,0 +1,47 @@ +package org.sunbird.keycloak.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.jboss.logging.Logger; + +public class HttpClient { + + private static Logger logger = Logger.getLogger(HttpClient.class); + + private HttpClient() {} + + public static HttpResponse post(Map requestBody, String uri, + String authorizationKey) { + logger.debug("HttpClient: post called"); + try (CloseableHttpClient client = HttpClients.createDefault()) { + ObjectMapper mapper = new ObjectMapper(); + HttpPost httpPost = new HttpPost(uri); + logger.debug("HttpClient:post: uri = " + uri); + String authKey = Constants.BEARER + " " + authorizationKey; + StringEntity entity = new StringEntity(mapper.writeValueAsString(requestBody)); + logger.debug("HttpClient:post: request entity = " + entity); + httpPost.setEntity(entity); + httpPost.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON); + httpPost.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON); + if (StringUtils.isNotBlank(authKey)) { + httpPost.setHeader(HttpHeaders.AUTHORIZATION, authKey); + } + CloseableHttpResponse response = client.execute(httpPost); + logger.debug("HttpClient:post: statusCode = " + response.getStatusLine().getStatusCode()); + return response; + } catch (Exception e) { + logger.error("HttpClient:post: Exception occurred = " + e); + } + return null; + } + +} diff --git a/keycloak/sms-provider/templates/sms-validation.ftl b/keycloak/sms-provider/templates/sms-validation.ftl index e7d1124e..5783031d 100755 --- a/keycloak/sms-provider/templates/sms-validation.ftl +++ b/keycloak/sms-provider/templates/sms-validation.ftl @@ -18,7 +18,7 @@
- Enter the code we sent to your device + Please enter the OTP that has been sent to you