I already created a registration authenticator by implementing the authenticator interface. I want to verify the user email, and then I want to store the user in Keycloak.
I don't know where I can store my OTP. Currently, I am storing my opt in AuthenticationSession. It works fine, but if my keycloak server restarts, my session is cleared, and I'm getting a session timeout. I don't want that. Can someone shine a light on this?
package fileuploader.authenticators;
import fileuploader.authenticators.messages.Messages;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@JBossLog
public class RegistrationAuthenticator implements Authenticator {
public static final String PROVIDED_ID = "file-uploader-registration-form";
public static final String VERIFY_USERNAME_FORM = "verify-username.ftl";
@Override
public void authenticate(AuthenticationFlowContext context) {
Response challenge = context.form()
.createForm(VERIFY_USERNAME_FORM);
context.challenge(challenge);
}
public Response createForm(AuthenticationFlowContext context,String message){
LoginFormsProvider formsProvider = context.form().addError(new FormMessage(message));
return formsProvider.createForm(VERIFY_USERNAME_FORM);
}
public Response validateForm(AuthenticationFlowContext context,String email){
UserModel user = context.getSession().users().getUserByEmail(context.getRealm(),email);
if(user != null ){
return createForm(context, Messages.Error.USER_ALREADY_EXISTS);
}
return null;
}
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String,String> formData = context.getHttpRequest().getDecodedFormParameters();
String email = formData.get("email").get(0);
String password =formData.get("password").get(0);
Response formError = validateForm(context,email);
log.info("this is error : "+formError);
if(formError!=null){
context.challenge(formError);
return;
}
context.getAuthenticationSession().setAuthNote("email", email);
context.getAuthenticationSession().setAuthNote("password", password);
context.success();
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
log.info("Set Required is called");
}
@Override
public void close() {
log.info("Close Called...");
}
}
package fileuploader.authenticators;
import fileuploader.authenticators.messages.Messages;
import lombok.extern.jbosslog.JBossLog;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.email.EmailSenderProvider;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.email.EmailTemplateSpi;
import org.keycloak.forms.login.LoginFormsProvider;
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 javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.Properties;
import java.util.Random;
import java.util.ResourceBundle;
@JBossLog
public class OtpAuthenticator implements Authenticator {
public static final String PROVIDED_ID = "file-uploader-otp-form";
private static final String OTP_FORM = "otp-form.ftl";
ResourceBundle bundle = ResourceBundle.getBundle("application");
public String generateRandomOTP()
{
Random random = new Random();
int min = 100000;
int max = 999999;
int randomNumber = random.nextInt(max - min + 1) + min;
return String.valueOf(randomNumber);
}
public void sendOtpToEmail(AuthenticationFlowContext context)
{
UserModel user = context.getUser();
String smtpHost = bundle.getString("email.host");
String smtpPort = bundle.getString("email.port");
String smtpUsername = bundle.getString("email.username");
String smtpPassword = bundle.getString("email.password");
String recipientEmail =context.getAuthenticationSession().getAuthNote("email");
String subject = "OTP conformation";
String otp = generateRandomOTP();
user.setAttribute("otp", Collections.singletonList(otp));
context.getAuthenticationSession().setAuthNote("otp",otp);
long otpGeneratedTimestamp = System.currentTimeMillis();
context.getAuthenticationSession().setAuthNote("otpGeneratedTimestamp", String.valueOf(otpGeneratedTimestamp));
Properties props = new Properties();
props.put("mail.smtp.host", smtpHost);
props.put("mail.smtp.port", smtpPort);
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
Session session = Session.getInstance(props, new javax.mail.Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(smtpUsername, smtpPassword);
}
});
try
{
Message emailMessage = new MimeMessage(session);
emailMessage.setFrom(new InternetAddress(bundle.getString("email.username")));
emailMessage.setRecipients(Message.RecipientType.TO, InternetAddress.parse(recipientEmail));
emailMessage.setSubject(subject);
emailMessage.setText("We're excited to have you on board as a valued member of our platform. To ensure the security of your account, we have generated a one-time password (OTP) for your registration ." +
"\nYour OTP Code: "+otp+
"\nPlease use this OTP to complete your registration on our website. After entering your username and password, you'll be prompted to enter this OTP to verify your identity. Remember, this OTP is unique and should be kept confidential. Do not share it with anyone, including our support team." +
"\nIf you did not request this OTP or have any concerns, please contact our support team immediately at [email protected].");
Transport.send(emailMessage);
context.success();
}
catch (MessagingException e)
{
e.printStackTrace();
}
}
@Override
public void authenticate(AuthenticationFlowContext context) {
sendOtpToEmail(context);
Response challenge = context.form()
.createForm(OTP_FORM);
context.challenge(challenge);
}
private boolean validateOTP(AuthenticationFlowContext context,String otpFromUser, long timestamp){
long currentTime = System.currentTimeMillis();
log.info("time : "+ (currentTime - timestamp));
String otp= context.getAuthenticationSession().getAuthNote("otp");
// String test = user.getFirstAttribute("otp");
// log.info("otp from attribute : "+ test);
log.info("original opt : " +otp);
log.info("from user opt : " +otpFromUser);
return otpFromUser.equals(otp) && (currentTime - timestamp) <= 60000;
}
@Override
public void action(AuthenticationFlowContext context) {
MultivaluedMap<String,String> formData = context.getHttpRequest().getDecodedFormParameters();
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
String otpFromUser = formData.get("otp").get(0);
if (formData.containsKey("resend")) {
sendOtpToEmail(context);
LoginFormsProvider formsProvider = context.form().addSuccess(new FormMessage(Messages.Success.RESENT_OTP_SUCCESS));
context.challenge(formsProvider.createForm(OTP_FORM));
return;
}
long otpGeneratedTime = Long.parseLong(context.getAuthenticationSession().getAuthNote("otpGeneratedTimestamp"));
boolean isValid = validateOTP(context,otpFromUser , otpGeneratedTime);
log.info("valid state : "+isValid);
if(!isValid) {
LoginFormsProvider formsProvider = context
.form()
.addError(new FormMessage(Messages.Error.INVALID_OTP));
context.challenge(formsProvider.createForm(OTP_FORM));
return ;
}
String email = context.getAuthenticationSession().getAuthNote("email");
String password = context.getAuthenticationSession().getAuthNote("password");
UserModel userModel = session.users().addUser(realm,email);
userModel.setEnabled(true);
userModel.setEmail(email);
userModel.setEmailVerified(true);
session.userCredentialManager()
.updateCredential(realm, userModel, UserCredentialModel.password(password));
context.setUser(userModel);
context.success();
}
@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) {
}
@Override
public void close() {
log.info("Otp Authenticator closed ");
}
}