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.