diff --git a/src/main/java/io/phasetwo/keycloak/magic/MagicLink.java b/src/main/java/io/phasetwo/keycloak/magic/MagicLink.java index a5f6a3e..f81096a 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/MagicLink.java +++ b/src/main/java/io/phasetwo/keycloak/magic/MagicLink.java @@ -51,6 +51,9 @@ @JBossLog public class MagicLink { + public static final String CREATE_NONEXISTENT_USER_CONFIG_PROPERTY = + "ext-magic-create-nonexistent-user"; + public static Consumer registerEvent(final EventBuilder event) { return new Consumer() { @Override diff --git a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticator.java b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticator.java index 5711f4b..54e7d87 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticator.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticator.java @@ -1,12 +1,13 @@ package io.phasetwo.keycloak.magic.auth; +import static io.phasetwo.keycloak.magic.MagicLink.CREATE_NONEXISTENT_USER_CONFIG_PROPERTY; +import static io.phasetwo.keycloak.magic.auth.util.Authenticators.is; import static org.keycloak.services.validation.Validation.FIELD_USERNAME; import io.phasetwo.keycloak.magic.MagicLink; import io.phasetwo.keycloak.magic.auth.token.MagicLinkActionToken; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; -import java.util.Map; import java.util.OptionalInt; import lombok.extern.jbosslog.JBossLog; import org.keycloak.authentication.AuthenticationFlowContext; @@ -17,7 +18,6 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.forms.login.LoginFormsProvider; -import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.UserModel; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.messages.Messages; @@ -25,7 +25,6 @@ @JBossLog public class MagicLinkAuthenticator extends UsernamePasswordForm { - static final String CREATE_NONEXISTENT_USER_CONFIG_PROPERTY = "ext-magic-create-nonexistent-user"; static final String UPDATE_PROFILE_ACTION_CONFIG_PROPERTY = "ext-magic-update-profile-action"; static final String UPDATE_PASSWORD_ACTION_CONFIG_PROPERTY = "ext-magic-update-password-action"; @@ -140,19 +139,6 @@ private boolean isActionTokenPersistent(AuthenticationFlowContext context, boole return is(context, ACTION_TOKEN_PERSISTENT_CONFIG_PROPERTY, defaultValue); } - private boolean is(AuthenticationFlowContext context, String propName, boolean defaultValue) { - AuthenticatorConfigModel authenticatorConfig = context.getAuthenticatorConfig(); - if (authenticatorConfig == null) return defaultValue; - - Map config = authenticatorConfig.getConfig(); - if (config == null) return defaultValue; - - String v = config.get(propName); - if (v == null || "".equals(v)) return defaultValue; - - return v.trim().toLowerCase().equals("true"); - } - @Override protected boolean validateForm( AuthenticationFlowContext context, MultivaluedMap formData) { diff --git a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticatorFactory.java b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticatorFactory.java index 2b844b8..685efb7 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticatorFactory.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticatorFactory.java @@ -1,8 +1,9 @@ package io.phasetwo.keycloak.magic.auth; +import static io.phasetwo.keycloak.magic.MagicLink.CREATE_NONEXISTENT_USER_CONFIG_PROPERTY; + import com.google.auto.service.AutoService; import io.phasetwo.keycloak.magic.MagicLink; -import java.util.Arrays; import java.util.List; import lombok.extern.jbosslog.JBossLog; import org.keycloak.Config; @@ -71,7 +72,7 @@ public String getHelpText() { public List getConfigProperties() { ProviderConfigProperty createUser = new ProviderConfigProperty(); createUser.setType(ProviderConfigProperty.BOOLEAN_TYPE); - createUser.setName(MagicLinkAuthenticator.CREATE_NONEXISTENT_USER_CONFIG_PROPERTY); + createUser.setName(CREATE_NONEXISTENT_USER_CONFIG_PROPERTY); createUser.setLabel("Force create user"); createUser.setHelpText( "Creates a new user when an email is provided that does not match an existing user."); @@ -99,7 +100,7 @@ public List getConfigProperties() { "Toggle whether magic link should be persistent until expired."); actionTokenPersistent.setDefaultValue(true); - return Arrays.asList(createUser, updateProfile, updatePassword, actionTokenPersistent); + return List.of(createUser, updateProfile, updatePassword, actionTokenPersistent); } @Override diff --git a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticator.java b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticator.java index db9764f..fa50df2 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticator.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticator.java @@ -1,5 +1,7 @@ package io.phasetwo.keycloak.magic.auth; +import static io.phasetwo.keycloak.magic.MagicLink.CREATE_NONEXISTENT_USER_CONFIG_PROPERTY; +import static io.phasetwo.keycloak.magic.auth.util.Authenticators.is; import static io.phasetwo.keycloak.magic.auth.util.MagicLinkConstants.SESSION_CONFIRMED; import static io.phasetwo.keycloak.magic.auth.util.MagicLinkConstants.SESSION_EXPIRATION; import static io.phasetwo.keycloak.magic.auth.util.MagicLinkConstants.SESSION_INITIATED; @@ -116,7 +118,7 @@ public void action(AuthenticationFlowContext context) { context.getSession(), context.getRealm(), email, - false, + isForceCreate(context, false), false, false, MagicLink.registerEvent(event)); @@ -164,6 +166,10 @@ public void action(AuthenticationFlowContext context) { context.challenge(context.form().createForm("view-email-continuation.ftl")); } + private boolean isForceCreate(AuthenticationFlowContext context, boolean defaultValue) { + return is(context, CREATE_NONEXISTENT_USER_CONFIG_PROPERTY, defaultValue); + } + @Override protected boolean validateForm( AuthenticationFlowContext context, MultivaluedMap formData) { diff --git a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticatorFactory.java b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticatorFactory.java index b7a00b5..f7d5e76 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticatorFactory.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkContinuationAuthenticatorFactory.java @@ -1,5 +1,7 @@ package io.phasetwo.keycloak.magic.auth; +import static io.phasetwo.keycloak.magic.MagicLink.CREATE_NONEXISTENT_USER_CONFIG_PROPERTY; + import com.google.auto.service.AutoService; import io.phasetwo.keycloak.magic.auth.util.MagicLinkConstants; import java.util.List; @@ -15,7 +17,9 @@ @JBossLog @AutoService(AuthenticatorFactory.class) public class MagicLinkContinuationAuthenticatorFactory implements AuthenticatorFactory { + public static final String PROVIDER_ID = "magic-link-continuation-form"; + private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.ALTERNATIVE, @@ -23,13 +27,13 @@ public class MagicLinkContinuationAuthenticatorFactory implements AuthenticatorF }; @Override - public String getDisplayType() { - return "Magic Link continuation"; + public Authenticator create(KeycloakSession session) { + return new MagicLinkContinuationAuthenticator(); } @Override - public String getHelpText() { - return "Sign in with a magic link that will be sent to your email."; + public String getId() { + return PROVIDER_ID; } @Override @@ -42,18 +46,38 @@ public boolean isConfigurable() { return true; } + @Override + public boolean isUserSetupAllowed() { + return true; + } + @Override public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { return REQUIREMENT_CHOICES; } @Override - public boolean isUserSetupAllowed() { - return true; + public String getDisplayType() { + return "Magic Link continuation"; + } + + @Override + public String getHelpText() { + return "Sign in with a magic link that will be sent to your email."; } @Override public List getConfigProperties() { + // Force create user property configuration + ProviderConfigProperty createUser = new ProviderConfigProperty(); + createUser.setType(ProviderConfigProperty.BOOLEAN_TYPE); + createUser.setName(CREATE_NONEXISTENT_USER_CONFIG_PROPERTY); + createUser.setLabel("Force create user"); + createUser.setHelpText( + "Creates a new user when an email is provided that does not match an existing user."); + createUser.setDefaultValue(true); + + // Expiration time property configuration ProviderConfigProperty timeout = new ProviderConfigProperty(); timeout.setType(ProviderConfigProperty.STRING_TYPE); timeout.setName(MagicLinkConstants.TIMEOUT); @@ -62,12 +86,7 @@ public List getConfigProperties() { "Magic link authenticator expiration time in minutes. Default expiration period 10 minutes."); timeout.setDefaultValue("10"); - return List.of(timeout); - } - - @Override - public Authenticator create(KeycloakSession session) { - return new MagicLinkContinuationAuthenticator(); + return List.of(createUser, timeout); } @Override @@ -78,9 +97,4 @@ public void postInit(KeycloakSessionFactory factory) {} @Override public void close() {} - - @Override - public String getId() { - return PROVIDER_ID; - } } diff --git a/src/main/java/io/phasetwo/keycloak/magic/auth/util/Authenticators.java b/src/main/java/io/phasetwo/keycloak/magic/auth/util/Authenticators.java new file mode 100644 index 0000000..aff9da8 --- /dev/null +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/util/Authenticators.java @@ -0,0 +1,22 @@ +package io.phasetwo.keycloak.magic.auth.util; + +import com.google.common.base.Strings; +import java.util.Map; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.models.AuthenticatorConfigModel; + +public final class Authenticators { + public static boolean is( + AuthenticationFlowContext context, String propName, boolean defaultValue) { + AuthenticatorConfigModel authenticatorConfig = context.getAuthenticatorConfig(); + if (authenticatorConfig == null) return defaultValue; + + Map config = authenticatorConfig.getConfig(); + if (config == null) return defaultValue; + + String v = config.get(propName); + if (Strings.isNullOrEmpty(v)) return defaultValue; + + return Boolean.parseBoolean(v.trim()); + } +}