Skip to content

Commit

Permalink
Merge pull request #38 from project-sunbird/release-1.12
Browse files Browse the repository at this point in the history
Release 1.12
  • Loading branch information
bvinayakumar authored Dec 19, 2018
2 parents 0bf271b + 7d561f4 commit 367065f
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 117 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<String, Object> 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
Expand All @@ -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<String, Object> 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<String, Object> response = new HashMap<>();
response.put(Constants.OTP, code);
response.put(Constants.TTL, (ttl / 60));
return response;
}

private void sendSMS(Map<String, Object> 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<String, Object> 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<String> 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<String, Object> 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);
Expand Down Expand Up @@ -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);
Expand All @@ -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<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String enteredCode = formData.getFirst(KeycloakSmsAuthenticatorConstants.ANSW_SMS_CODE);
KeycloakSession session = context.getSession();
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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;
}

}
2 changes: 1 addition & 1 deletion keycloak/sms-provider/templates/sms-validation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<div class="${properties.kcFormGroupClass!}">
<div class="field">
<div class="${properties.kcLabelWrapperClass!}">
<lable for="totp" class="${properties.kcLabelClass!}">Enter the code we sent to your device</label>
<lable for="totp" class="${properties.kcLabelClass!}">Please enter the OTP that has been sent to you</label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input id="totp" name="smsCode" type="text" class="${properties.kcInputClass!}" />
Expand Down

0 comments on commit 367065f

Please sign in to comment.