diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/AbstractPhoneFormAuthenticator.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/AbstractPhoneFormAuthenticator.java new file mode 100755 index 00000000..6b1e41ab --- /dev/null +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/AbstractPhoneFormAuthenticator.java @@ -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 Bill Burke + * @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 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 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 inputData) { + List 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; + } + } + +} diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordForm.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordForm.java new file mode 100755 index 00000000..4547a174 --- /dev/null +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordForm.java @@ -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 Bill Burke + * @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 formData = context.getHttpRequest().getDecodedFormParameters(); + if (formData.containsKey("cancel")) { + context.cancelLogin(); + return; + } + if (!validateForm(context, formData)) { + return; + } + context.success(); + } + + protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { + logger.error("PhonePasswordForm@validateForm - called"); + return validateUserAndPassword(context, formData); + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + MultivaluedMap 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 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() { + + } +} diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordFormFactory.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordFormFactory.java new file mode 100755 index 00000000..d3df0db5 --- /dev/null +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordFormFactory.java @@ -0,0 +1,108 @@ +/* + * 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.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.UsernamePasswordForm; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class PhonePasswordFormFactory implements AuthenticatorFactory { + + private static final Logger logger = Logger.getLogger(PhonePasswordFormFactory.class); + + public static final String PROVIDER_ID = "auth-phone-password-form"; + public static final PhonePasswordForm SINGLETON = new PhonePasswordForm(); + + @Override + public Authenticator create(KeycloakSession session) { + logger.error("PhonePasswordFormFactory@create - PhonePasswordFormFactory is created"); + return SINGLETON; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getReferenceCategory() { + return UserCredentialModel.PASSWORD; + } + + @Override + public boolean isConfigurable() { + return false; + } + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED + }; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public String getDisplayType() { + return "Phone Password Form"; + } + + @Override + public String getHelpText() { + return "Validates a phone and password from login form."; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + +} diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticator.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java similarity index 99% rename from keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticator.java rename to keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java index 947bf6de..cabfea70 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticator.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticator.java @@ -1,4 +1,4 @@ -package org.sunbird.keycloak; +package org.sunbird.keycloak.resetcredential.sms; import org.apache.http.util.TextUtils; import org.jboss.logging.Logger; diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorConstants.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorConstants.java similarity index 90% rename from keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorConstants.java rename to keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorConstants.java index a88cefee..ff6e7cf8 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorConstants.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorConstants.java @@ -1,11 +1,11 @@ -package org.sunbird.keycloak; +package org.sunbird.keycloak.resetcredential.sms; /** * Created by joris on 18/11/2016. */ // TODO(shriharshs): Move strings for mobile, smsCode, countryCode, defaultCountry code to configuration public class KeycloakSmsAuthenticatorConstants { - public static final String ATTR_MOBILE = "mobile"; + public static final String ATTR_MOBILE = "phone"; public static final String ANSW_SMS_CODE = "smsCode"; public static final String COUNTRY_CODE = "+91"; public static final String DEFAULT_COUNTRY_CODE = "0"; diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorCredentialProvider.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorCredentialProvider.java similarity index 99% rename from keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorCredentialProvider.java rename to keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorCredentialProvider.java index a0a15ec7..a414ddba 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorCredentialProvider.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorCredentialProvider.java @@ -1,4 +1,4 @@ -package org.sunbird.keycloak; +package org.sunbird.keycloak.resetcredential.sms; import org.jboss.logging.Logger; import org.keycloak.common.util.Time; diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorCredentialProviderFactory.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorCredentialProviderFactory.java similarity index 94% rename from keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorCredentialProviderFactory.java rename to keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorCredentialProviderFactory.java index 6288b0e7..67cc5d59 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorCredentialProviderFactory.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorCredentialProviderFactory.java @@ -1,4 +1,4 @@ -package org.sunbird.keycloak; +package org.sunbird.keycloak.resetcredential.sms; import org.jboss.logging.Logger; import org.keycloak.credential.CredentialProvider; diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorFactory.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorFactory.java similarity index 98% rename from keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorFactory.java rename to keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorFactory.java index 34d935fa..6868ae90 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorFactory.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorFactory.java @@ -1,4 +1,4 @@ -package org.sunbird.keycloak; +package org.sunbird.keycloak.resetcredential.sms; import org.jboss.logging.Logger; import org.keycloak.Config; diff --git a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorUtil.java b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorUtil.java similarity index 99% rename from keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorUtil.java rename to keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorUtil.java index 2480cc37..0db6c74f 100644 --- a/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/KeycloakSmsAuthenticatorUtil.java +++ b/keycloak/sms-provider/src/main/java/org/sunbird/keycloak/resetcredential/sms/KeycloakSmsAuthenticatorUtil.java @@ -1,4 +1,4 @@ -package org.sunbird.keycloak; +package org.sunbird.keycloak.resetcredential.sms; import com.amazonaws.util.StringUtils; import org.jboss.logging.Logger; diff --git a/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 500f32fa..f820d6e0 100755 --- a/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -1 +1,2 @@ -org.sunbird.keycloak.KeycloakSmsAuthenticatorFactory +org.sunbird.keycloak.resetcredential.sms.KeycloakSmsAuthenticatorFactory +org.sunbird.keycloak.login.phone.PhonePasswordFormFactory diff --git a/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory b/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory index edd6c136..fd4daa97 100644 --- a/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory +++ b/keycloak/sms-provider/src/main/resources/META-INF/services/org.keycloak.credential.CredentialProviderFactory @@ -1 +1 @@ -org.sunbird.keycloak.KeycloakSmsAuthenticatorCredentialProviderFactory +org.sunbird.keycloak.resetcredential.sms.KeycloakSmsAuthenticatorCredentialProviderFactory