Skip to content

Commit

Permalink
Issue #14 feature: Login using phone number
Browse files Browse the repository at this point in the history
  • Loading branch information
shriharshs committed Nov 15, 2017
1 parent 559e61f commit c268785
Show file tree
Hide file tree
Showing 11 changed files with 441 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* 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
*
* 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.sunbird.keycloak.login.phone;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AbstractFormAuthenticator;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.credential.CredentialInput;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.CredentialRepresentation;
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.LinkedList;
import java.util.List;

/**
* @author <a href="mailto:[email protected]">Bill Burke</a>
* @version $Revision: 1 $
*/
public abstract class AbstractPhoneFormAuthenticator extends AbstractFormAuthenticator {

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

public static final String REGISTRATION_FORM_ACTION = "registration_form";
public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME";

@Override
public void action(AuthenticationFlowContext context) {

}

protected Response invalidUser(AuthenticationFlowContext context) {
return context.form()
.setError(Messages.INVALID_USER)
.createLogin();
}

protected Response disabledUser(AuthenticationFlowContext context) {
return context.form()
.setError(Messages.ACCOUNT_DISABLED).createLogin();
}

protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
return context.form()
.setError(Messages.INVALID_USER).createLogin();
}

protected Response invalidCredentials(AuthenticationFlowContext context) {
return context.form()
.setError(Messages.INVALID_USER).createLogin();
}

protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
context.getEvent().error(eventError);
Response challengeResponse = context.form()
.setError(loginFormError).createLogin();
context.failureChallenge(authenticatorError, challengeResponse);
return challengeResponse;
}

public boolean invalidUser(AuthenticationFlowContext context, UserModel user) {
if (user == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = invalidUser(context);
context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse);
return true;
}
return false;
}

public boolean enabledUser(AuthenticationFlowContext context, UserModel user) {
if (!user.isEnabled()) {
context.getEvent().user(user);
context.getEvent().error(Errors.USER_DISABLED);
Response challengeResponse = disabledUser(context);
context.failureChallenge(AuthenticationFlowError.USER_DISABLED, challengeResponse);
return false;
}
if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) {
context.getEvent().user(user);
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
Response challengeResponse = temporarilyDisabledUser(context);
context.failureChallenge(AuthenticationFlowError.USER_TEMPORARILY_DISABLED, challengeResponse);
return false;
}
}
return true;
}

public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
logger.error("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 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) {
logger.error("AbstractPhoneFormAuthenticator@getUser : Users List - " + userModels.size());
logger.error("AbstractPhoneFormAuthenticator@getUser : First User - " + userModels.get(0).getUsername());
return userModels.get(0);
}
} else {
return KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
}

logger.error("AbstractPhoneFormAuthenticator@getUser : No user found!");
return null;
}

public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData) {
List<CredentialInput> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
credentials.add(UserCredentialModel.password(password));
if (password != null && !password.isEmpty() && context.getSession().userCredentialManager().isValid(context.getRealm(), user, credentials)) {
return true;
} else {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = invalidCredentials(context);
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
context.clearUser();
return false;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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
*
* 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.sunbird.keycloak.login.phone;

import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;

import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;

/**
* @author <a href="mailto:[email protected]">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PhonePasswordForm extends AbstractPhoneFormAuthenticator implements Authenticator {
protected static ServicesLogger log = ServicesLogger.LOGGER;
private static final Logger logger = Logger.getLogger(PhonePasswordForm.class);

@Override
public void action(AuthenticationFlowContext context) {
logger.error("PhonePasswordForm@action - called");
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.cancelLogin();
return;
}
if (!validateForm(context, formData)) {
return;
}
context.success();
}

protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
logger.error("PhonePasswordForm@validateForm - called");
return validateUserAndPassword(context, formData);
}

@Override
public void authenticate(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);

String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());

if (loginHint != null || rememberMeUsername != null) {
if (loginHint != null) {
formData.add(AuthenticationManager.FORM_USERNAME, loginHint);
} else {
formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
formData.add("rememberMe", "on");
}
}
Response challengeResponse = challenge(context, formData);
context.challenge(challengeResponse);
}

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

protected Response challenge(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
LoginFormsProvider forms = context.form();

if (formData.size() > 0) forms.setFormData(formData);

return forms.createLogin();
}


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

@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// never called
}

@Override
public void close() {

}
}
Loading

0 comments on commit c268785

Please sign in to comment.