Skip to content

Commit

Permalink
Merge pull request #27 from project-sunbird/reset-credential-with-phone
Browse files Browse the repository at this point in the history
Issue #SB-1603 feat:Use cellphone number to trigger password reset OT…
  • Loading branch information
kochhar authored Mar 1, 2018
2 parents e21c972 + 19f2303 commit 5ace28e
Show file tree
Hide file tree
Showing 6 changed files with 401 additions and 88 deletions.
Original file line number Diff line number Diff line change
@@ -1,117 +1,109 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
* Copyright 2016 Red Hat, Inc. and/or its affiliates and other contributors as indicated by
* the @author tags.
*
* Licensed 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
* Licensed 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.
* 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.sunbird.keycloak.login.phone;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.sunbird.keycloak.resetcredential.sms.KeycloakSmsAuthenticatorConstants;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.List;
import org.sunbird.keycloak.utils.Constants;
import org.sunbird.keycloak.utils.SunbirdModelUtils;

public abstract class AbstractPhoneFormAuthenticator extends AbstractUsernameFormAuthenticator {

private static final Logger logger = Logger.getLogger(AbstractPhoneFormAuthenticator.class);

public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
logger.debug("AbstractPhoneFormAuthenticator@validateUserAndPassword - Username -" + username);

if (username == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = invalidUser(context);
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
return false;
}

// remove leading and trailing whitespace
username = username.trim();

context.getEvent().detail(Details.USERNAME, username);
context.getAuthenticationSession().setAuthNote(AbstractPhoneFormAuthenticator.ATTEMPTED_USERNAME, username);

UserModel user = null;
try {
user = getUser(context, username);
} catch (ModelDuplicateException mde) {
ServicesLogger.LOGGER.modelDuplicateException(mde);

// Could happen during federation import
if (mde.getDuplicateFieldName() != null && mde.getDuplicateFieldName().equals(UserModel.EMAIL)) {
setDuplicateUserChallenge(context, Errors.EMAIL_IN_USE, Messages.EMAIL_EXISTS, AuthenticationFlowError.INVALID_USER);
} else {
setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS, AuthenticationFlowError.INVALID_USER);
}

return false;
}

if (invalidUser(context, user)) {
return false;
}

if (!validatePassword(context, user, inputData)) {
return false;
}

if (!enabledUser(context, user)) {
return false;
}

String rememberMe = inputData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
if (remember) {
context.getAuthenticationSession().setAuthNote(Details.REMEMBER_ME, "true");
context.getEvent().detail(Details.REMEMBER_ME, "true");
} else {
context.getAuthenticationSession().removeAuthNote(Details.REMEMBER_ME);
}
context.setUser(user);
return true;
private static final Logger logger = Logger.getLogger(AbstractPhoneFormAuthenticator.class);

@Override
public boolean validateUserAndPassword(AuthenticationFlowContext context,
MultivaluedMap<String, String> inputData) {
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
logger.debug("AbstractPhoneFormAuthenticator@validateUserAndPassword - Username -" + username);

if (username == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = invalidUser(context);
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
return false;
}

// remove leading and trailing whitespace
username = username.trim();

context.getEvent().detail(Details.USERNAME, username);
context.getAuthenticationSession()
.setAuthNote(AbstractPhoneFormAuthenticator.ATTEMPTED_USERNAME, username);

UserModel user = null;
try {

user = SunbirdModelUtils.getUserByNameEmailOrPhone(context, username);

} catch (ModelDuplicateException mde) {
ServicesLogger.LOGGER.modelDuplicateException(mde);

// Could happen during federation import
if (mde.getDuplicateFieldName() != null
&& mde.getDuplicateFieldName().equals(UserModel.EMAIL)) {
setDuplicateUserChallenge(context, Errors.EMAIL_IN_USE, Messages.EMAIL_EXISTS,
AuthenticationFlowError.USER_CONFLICT);
} else if (mde.getDuplicateFieldName() != null
&& mde.getDuplicateFieldName().equals(UserModel.USERNAME)) {
setDuplicateUserChallenge(context, Errors.USERNAME_IN_USE, Messages.USERNAME_EXISTS,
AuthenticationFlowError.USER_CONFLICT);
} else if (mde.getDuplicateFieldName() != null
&& mde.getDuplicateFieldName().equals(KeycloakSmsAuthenticatorConstants.ATTR_MOBILE)) {
setDuplicateUserChallenge(context, Constants.MULTIPLE_USER_ASSOCIATED_WITH_PHONE,
Constants.MULTIPLE_USER_ASSOCIATED_WITH_PHONE, AuthenticationFlowError.USER_CONFLICT);
}

return false;
}

if (invalidUser(context, user)) {
return false;
}

if (!validatePassword(context, user, inputData)) {
return false;
}

if (!enabledUser(context, user)) {
return false;
}

private UserModel getUser(AuthenticationFlowContext context, String username) {
String numberRegex = "\\d+";
KeycloakSession session = context.getSession();

if (username.matches(numberRegex)) {
List<UserModel> userModels = session.users().searchForUserByUserAttribute(KeycloakSmsAuthenticatorConstants.ATTR_MOBILE, username, context.getRealm());

if (userModels != null && userModels.size() > 0) {
return userModels.get(0);
} else {
return KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
}
} else {
return KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
}
String rememberMe = inputData.getFirst("rememberMe");
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on");
if (remember) {
context.getAuthenticationSession().setAuthNote(Details.REMEMBER_ME, "true");
context.getEvent().detail(Details.REMEMBER_ME, "true");
} else {
context.getAuthenticationSession().removeAuthNote(Details.REMEMBER_ME);
}
context.setUser(user);
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package org.sunbird.keycloak.resetcredential.chooseuser;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
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.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import org.sunbird.keycloak.resetcredential.sms.KeycloakSmsAuthenticator;
import org.sunbird.keycloak.resetcredential.sms.KeycloakSmsAuthenticatorConstants;
import org.sunbird.keycloak.utils.Constants;
import org.sunbird.keycloak.utils.SunbirdModelUtils;


/**
*
* @author Amit Kumar
*
* This class will override the choose user action for reset credential flow under
* Authentication. Here we are overriding action method for getting user details by
* attributes (i.e phone)
*/
public class ResetCredentialChooseUserAuthenticator implements Authenticator {


private static Logger logger = Logger.getLogger(KeycloakSmsAuthenticator.class);
public static final String PROVIDER_ID = "reset-credentials-choose-user";


@Override
public void authenticate(AuthenticationFlowContext context) {

String existingUserId =
context.getAuthenticationSession().getAuthNote(AbstractIdpAuthenticator.EXISTING_USER_INFO);
if (existingUserId != null) {
UserModel existingUser = AbstractIdpAuthenticator.getExistingUser(context.getSession(),
context.getRealm(), context.getAuthenticationSession());

logger.debugf(
"Forget-password triggered when reauthenticating user after first broker login. Skipping reset-credential-choose-user screen and using user '%s' ",
existingUser.getUsername());
context.setUser(existingUser);
context.success();
return;
}

String actionTokenUserId =
context.getAuthenticationSession().getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
if (actionTokenUserId != null) {
UserModel existingUser =
context.getSession().users().getUserById(actionTokenUserId, context.getRealm());

// Action token logics handles checks for user ID validity and user being enabled

logger.debugf(
"Forget-password triggered when reauthenticating user after authentication via action token. Skipping reset-credential-choose-user screen and using user '%s' ",
existingUser.getUsername());
context.setUser(existingUser);
context.success();
return;
}

Response challenge = context.form().createPasswordReset();
context.challenge(challenge);

}

@Override
public void action(AuthenticationFlowContext context) {
EventBuilder event = context.getEvent();
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
String username = formData.getFirst("username");
if (username == null || username.isEmpty()) {
event.error(Errors.USERNAME_MISSING);
Response challenge = context.form().setError(Messages.MISSING_USERNAME).createPasswordReset();
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
return;
}
UserModel user = null;
try {

user = SunbirdModelUtils.getUserByNameEmailOrPhone(context, username);
//user not found for provided username
if(user == null){
event.error(Messages.INVALID_USER);
Response challenge = context.form().setError(Errors.USER_NOT_FOUND).createPasswordReset();
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challenge);
return;
}
} catch (ModelDuplicateException mde) {
ServicesLogger.LOGGER.modelDuplicateException(mde);

// Could happen during federation import
String errMsg = "";
if (mde.getDuplicateFieldName() != null
&& mde.getDuplicateFieldName().equals(UserModel.EMAIL)) {
errMsg = Constants.MULTIPLE_USER_ASSOCIATED_WITH_EMAIL;
} else if (mde.getDuplicateFieldName() != null
&& mde.getDuplicateFieldName().equals(UserModel.USERNAME)) {
errMsg = Constants.MULTIPLE_USER_ASSOCIATED_WITH_USERNAME;
} else if (mde.getDuplicateFieldName() != null
&& mde.getDuplicateFieldName().equals(KeycloakSmsAuthenticatorConstants.ATTR_MOBILE)) {
errMsg = Constants.MULTIPLE_USER_ASSOCIATED_WITH_PHONE;
}
event.error(Messages.INVALID_USER);
Response challenge = context.form().setError(errMsg).createPasswordReset();
context.failureChallenge(AuthenticationFlowError.USER_CONFLICT, challenge);
return;
}
context.getAuthenticationSession()
.setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, username);

// we don't want people guessing usernames, so if there is a problem, just continue, but don't
// set the user
// a null user will notify further executions, that this was a failure.
if (user == null) {
event.clone().detail(Details.USERNAME, username).error(Errors.USER_NOT_FOUND);
} else if (!user.isEnabled()) {
event.clone().detail(Details.USERNAME, username).user(user).error(Errors.USER_DISABLED);
} else {
context.setUser(user);
}

context.success();

}

@Override
public void close() {
logger.debug("ResetCredentialChooseUserAuthenticator close called ... ");
}

@Override
public boolean requiresUser() {
return false;
}

@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}

@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
logger.debug("ResetCredentialChooseUserAuthenticator setRequiredActions called ... ");
}

}
Loading

0 comments on commit 5ace28e

Please sign in to comment.