Custom SPI - Link for user to set their initial password

Just wanted to follow up on anyone looking to solve this same problem. Credit to @claudiunicolaa on his post here - Bypass "Perform the following action(s)" modal - #3 by gwin003

I’ll add my code, but most of it was lifted directly from the extended classes.

What you want to do is create these 3 files:

CustomExecuteActionsActionToken

public class CustomExecuteActionsActionToken extends DefaultActionToken {

  private static final Logger LOG = Logger.getLogger(CustomExecuteActionsActionToken.class);

  public static final String TOKEN_TYPE = "custom-token";
  private static final String JSON_FIELD_REQUIRED_ACTIONS = "rqac";
  private static final String JSON_FIELD_REDIRECT_URI = "reduri";

  public CustomExecuteActionsActionToken(String userId, int absoluteExpirationInSecs, List<String> requiredActions,
      String redirectUri, String clientId) {
    super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null);
    setRequiredActions(requiredActions == null ? new LinkedList<>() : new LinkedList<>(requiredActions));
    setRedirectUri(redirectUri);
    this.issuedFor = clientId;
  }

  private CustomExecuteActionsActionToken() {
    // Required to deserialize from JWT
    super();
  }

  @JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
  public List<String> getRequiredActions() {
    return (List<String>) getOtherClaims().get(JSON_FIELD_REQUIRED_ACTIONS);
  }

  @JsonProperty(value = JSON_FIELD_REQUIRED_ACTIONS)
  public final void setRequiredActions(List<String> requiredActions) {
    if (requiredActions == null) {
      getOtherClaims().remove(JSON_FIELD_REQUIRED_ACTIONS);
    } else {
      setOtherClaims(JSON_FIELD_REQUIRED_ACTIONS, requiredActions);
    }
  }

  @JsonProperty(value = JSON_FIELD_REDIRECT_URI)
  public String getRedirectUri() {
    return (String) getOtherClaims().get(JSON_FIELD_REDIRECT_URI);
  }

  @JsonProperty(value = JSON_FIELD_REDIRECT_URI)
  public final void setRedirectUri(String redirectUri) {
    if (redirectUri == null) {
      getOtherClaims().remove(JSON_FIELD_REDIRECT_URI);
    } else {
      setOtherClaims(JSON_FIELD_REDIRECT_URI, redirectUri);
    }
  }

}

CustomExecuteActionsActionTokenHandler


public class CustomExecuteActionsActionTokenHandler extends AbstractActionTokenHandler<CustomExecuteActionsActionToken> {

  public CustomExecuteActionsActionTokenHandler() {
    super(CustomExecuteActionsActionToken.TOKEN_TYPE,
        CustomExecuteActionsActionToken.class,
        Messages.INVALID_CODE,
        EventType.EXECUTE_ACTIONS,
        Errors.NOT_ALLOWED);
  }

  private static final Logger LOG = Logger.getLogger(CustomExecuteActionsActionTokenHandler.class);

  @Override
  public Response handleToken(CustomExecuteActionsActionToken token,
      ActionTokenContext<CustomExecuteActionsActionToken> tokenContext) {
    AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
    final UriInfo uriInfo = tokenContext.getUriInfo();
    final RealmModel realm = tokenContext.getRealm();
    final KeycloakSession session = tokenContext.getSession();

    LOG.debugf("CustomExecuteActionsActionTokenHandler: handleToken");

    if (tokenContext.isAuthenticationSessionFresh()) {
      // Update the authentication session in the token
      String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
      token.setCompoundAuthenticationSessionId(authSessionEncodedId);
      // THE BELOW CODE IS THE IMPORTANT PIECE TO COMMENT OUT
      /*
       * UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(),
       * token.serialize(session, realm, uriInfo),
       * authSession.getClient().getClientId(), authSession.getTabId());
       * String confirmUri = builder.build(realm.getName()).toString();
       * 
       * return session.getProvider(LoginFormsProvider.class)
       * .setAuthenticationSession(authSession)
       * .setSuccess(Messages.CONFIRM_EXECUTION_OF_ACTIONS)
       * .setAttribute(Constants.TEMPLATE_ATTR_ACTION_URI, confirmUri)
       * .setAttribute(Constants.TEMPLATE_ATTR_REQUIRED_ACTIONS,
       * token.getRequiredActions())
       * .createInfoPage();
       */
    }

    String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getSession(), token.getRedirectUri(),
        authSession.getClient());

    LOG.debugf("CustomExecuteActionsActionTokenHandler.redirectUri: %s", redirectUri);

    if (redirectUri != null) {
      authSession.setAuthNote(AuthenticationManager.SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS, "true");

      authSession.setRedirectUri(redirectUri);
      authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
    }

    token.getRequiredActions().stream().forEach(authSession::addRequiredAction);

    UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
    // verify user email as we know it is valid as this entry point would never have
    // gotten here.
    user.setEmailVerified(true);

    String nextAction = AuthenticationManager.nextRequiredAction(tokenContext.getSession(), authSession,
        tokenContext.getRequest(), tokenContext.getEvent());
    return AuthenticationManager.redirectToRequiredActions(tokenContext.getSession(), tokenContext.getRealm(),
        authSession, tokenContext.getUriInfo(), nextAction);

  }

  // everything below this is a copy pasta of ExecuteActionsActionTokenHandler
  @Override
  public Predicate<? super CustomExecuteActionsActionToken>[] getVerifiers(
      ActionTokenContext<CustomExecuteActionsActionToken> tokenContext) {
    return TokenUtils.predicates(
        TokenUtils.checkThat(
            // either redirect URI is not specified or must be valid for the client
            t -> t.getRedirectUri() == null
                || RedirectUtils.verifyRedirectUri(tokenContext.getSession(), t.getRedirectUri(),
                    tokenContext.getAuthenticationSession().getClient()) != null,
            Errors.INVALID_REDIRECT_URI,
            Messages.INVALID_REDIRECT_URI),

        verifyEmail(tokenContext));
  }

  @Override
  public boolean canUseTokenRepeatedly(CustomExecuteActionsActionToken token,
      ActionTokenContext<CustomExecuteActionsActionToken> tokenContext) {
    RealmModel realm = tokenContext.getRealm();
    KeycloakSessionFactory sessionFactory = tokenContext.getSession().getKeycloakSessionFactory();

    return token.getRequiredActions().stream()
        .map(actionName -> realm.getRequiredActionProviderByAlias(actionName)) // get realm-specific model from action
                                                                               // name and filter out irrelevant
        .filter(Objects::nonNull)
        .filter(RequiredActionProviderModel::isEnabled)

        .map(RequiredActionProviderModel::getProviderId) // get provider ID from model

        .map(providerId -> (RequiredActionFactory) sessionFactory.getProviderFactory(RequiredActionProvider.class,
            providerId))
        .filter(Objects::nonNull)

        .noneMatch(RequiredActionFactory::isOneTimeAction);
  }
}

CustomExecuteActionsActionTokenHandlerFactory

package customStuff;

import org.keycloak.Config.Scope;
import org.keycloak.authentication.actiontoken.ActionTokenHandler;
import org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

public class CustomExecuteActionsActionTokenHandlerFactory implements ActionTokenHandlerFactory<CustomExecuteActionsActionToken> {


  @Override
  public String getId() {
    return CustomExecuteActionsActionToken.TOKEN_TYPE;
  }

  @Override
  public ActionTokenHandler<CustomExecuteActionsActionToken> create(KeycloakSession session) {
    return new CustomExecuteActionsActionTokenHandler();
  }

  @Override
  public void init(Scope config) {
    
  }

  @Override
  public void postInit(KeycloakSessionFactory factory) {
  }

  @Override
  public void close() {
  }
  
}

Remember to register your CustomExecuteActionsActionTokenHandlerFactory as you would other SPIs. (org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory is the file name)

After adding these three files and registering the handler factory, you can use your CustomExecuteActionsActionToken in your other custom SPIs.

1 Like