-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue #14 feature: Login using phone number
- Loading branch information
1 parent
559e61f
commit c268785
Showing
11 changed files
with
441 additions
and
9 deletions.
There are no files selected for viewing
211 changes: 211 additions & 0 deletions
211
...ovider/src/main/java/org/sunbird/keycloak/login/phone/AbstractPhoneFormAuthenticator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
|
||
} |
112 changes: 112 additions & 0 deletions
112
keycloak/sms-provider/src/main/java/org/sunbird/keycloak/login/phone/PhonePasswordForm.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | ||
|
||
} | ||
} |
Oops, something went wrong.