# Custom ResetCredentialEmail does not work after upgrade to Keycloak 21

**URL:** <https://forum.keycloak.org/t/custom-resetcredentialemail-does-not-work-after-upgrade-to-keycloak-21/21214>\
**Category:** Extending the server\
**Tags:** authentication\
**Created:** [March 31, 2023, 12:07pm UTC](https://forum.keycloak.org/t/custom-resetcredentialemail-does-not-work-after-upgrade-to-keycloak-21/21214 "2023-03-31T12:07:22Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![frankVit88](https://avatars.discourse-cdn.com/v4/letter/f/aeb1de/32.png) [@frankVit88](https://forum.keycloak.org/u/frankVit88)\
**Post date:** [March 31, 2023, 12:07pm UTC](https://forum.keycloak.org/t/custom-resetcredentialemail-does-not-work-after-upgrade-to-keycloak-21/21214/1 "2023-03-31T12:07:22Z")

</div>

Hello, our organisation’s keycloak forgot password flow has been customised so that when returning to the login page after having correctly sent the email, a message containing the operation performed and the address to which the email was sent is inserted in the context, in order to show a resume page rather than the classic red message below the username field of the login.

To do this, a jar was created containing a subclass of ResetCredentialEmail called **CustomResetCredentialEmail** and the related Factory class, **CustomResetCredentialEmailFactory** which implements AuthenticatorFactory, ConfigurableAuthenticatorFactory.

```auto
public class CustomResetCredentialEmail extends ResetCredentialEmail {

    private static final Logger logger = Logger.getLogger(CustomResetCredentialEmail.class);
    private static final String FORGOT_PASSWORD_EMAIL_SENT = "forgot-password-email-sent";
    private static final String FORGOT_PASSWORD_EMAIL_SEPARATOR = "|";

    @Override
    public void authenticate(AuthenticationFlowContext context) {
        logger.info("Custom Reset Credential Email");
        UserModel user = context.getUser();
        AuthenticationSessionModel authenticationSession = context.getAuthenticationSession();
        String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);

        String forgotPasswordEmailMessage = String.format("%s%s%s",FORGOT_PASSWORD_EMAIL_SENT,FORGOT_PASSWORD_EMAIL_SEPARATOR,username);

        // we don't want people guessing usernames, so if there was a problem obtaining the user, the user will be null.
        // just reset login for with a success message
        if (user == null) {
            context.forkWithSuccessMessage(new FormMessage(forgotPasswordEmailMessage));
            return;
        }

        String actionTokenUserId = authenticationSession.getAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID);
        if (actionTokenUserId != null && Objects.equals(user.getId(), actionTokenUserId)) {
            logger.debugf("Forget-password triggered when reauthenticating user after authentication via action token. Skipping " + PROVIDER_ID + " screen and using user '%s' ", user.getUsername());
            context.success();
            return;
        }

        EventBuilder event = context.getEvent();
        // we don't want people guessing usernames, so if there is a problem, just continuously challenge
        if (user.getEmail() == null || user.getEmail().trim().length() == 0) {
            event.user(user)
                .detail(Details.USERNAME, username)
                .error(Errors.INVALID_EMAIL);

            context.forkWithSuccessMessage(new FormMessage(forgotPasswordEmailMessage));
            return;
        }

        int validityInSecs = context.getRealm().getActionTokenGeneratedByUserLifespan(ResetCredentialsActionToken.TOKEN_TYPE);
        int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;

        // We send the secret in the email in a link as a query param.
        String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authenticationSession).getEncodedId();
        ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), user.getEmail(), absoluteExpirationInSecs, authSessionEncodedId, authenticationSession.getClient().getClientId());
        String link = UriBuilder
            .fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
            .build()
            .toString();
        long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
        try {
            context.getSession()
                .getProvider(EmailTemplateProvider.class)
                .setRealm(context.getRealm())
                .setUser(user)
                .setAuthenticationSession(authenticationSession)
                .sendPasswordReset(link, expirationInMinutes);

            event.clone().event(EventType.SEND_RESET_PASSWORD)
                .user(user)
                .detail(Details.USERNAME, username)
                .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getParentSession().getId()).success();
            context.forkWithSuccessMessage(new FormMessage(forgotPasswordEmailMessage));
        } catch (EmailException e) {
            event.clone().event(EventType.SEND_RESET_PASSWORD)
                .detail(Details.USERNAME, username)
                .user(user)
                .error(Errors.EMAIL_SEND_FAILED);
            ServicesLogger.LOGGER.failedToSendPwdResetEmail(e);
            Response challenge = context.form()
                .setError(Messages.EMAIL_SENT_ERROR)
                .createErrorPage(Response.Status.INTERNAL_SERVER_ERROR);
            context.failure(AuthenticationFlowError.INTERNAL_ERROR, challenge);
        }
    }

}

```

```auto
public class CustomResetCredentialEmailFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {

    public static final String PROVIDER_ID = "reset-credential-email";
    private static final CustomResetCredentialEmail SINGLETON = new CustomResetCredentialEmail();

    @Override
    public String getDisplayType() {
        return "Send Reset Email";
    }

    @Override
    public String getReferenceCategory() {
        return null;
    }

    @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 boolean isUserSetupAllowed() {
        return false;
    }

    @Override
    public String getHelpText() {
        return "Send email to user and wait for response.";
    }

    @Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return null;
    }

    @Override
    public void close() {

    }

    @Override
    public Authenticator create(KeycloakSession session) {
        return SINGLETON;
    }

    @Override
    public void init(Config.Scope config) {

    }

    @Override
    public void postInit(KeycloakSessionFactory factory) {

    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }
}

```

Inside the **META-INF/services** directory, the file **org.keycloak.authentication.AuthenticatorFactory** was added with the full name of the factory class inside.

At keycloak startup the new authenticator is recognised and loaded,

> **2023-03-31 07:41:10,994 WARN [org.keycloak.services] (build-10) KC-SERVICES0047: reset-credential-email (com.keycloakspi.authenticator.resetpassword.CustomResetCredentialEmailFactory) is implementing the internal SPI authenticator. This SPI is internal and may change without notice**

but **after upgrading to version 21** the authenticate method invoked still seems to be that of the default class and not the extended one ( **it worked with version 20 of keycloak** ).

Can anyone tell me whether anything has changed in the configuration/extension of authenticators in version 21?
