Skip to content

Commit

Permalink
feat | Added configuration options for "force create user" inside Mag…
Browse files Browse the repository at this point in the history
…icLinkContinuation flow (#85)

* feat: make force create user configurable via ui for magic link continuation flow

* refactor: added AuthenticatorSharedUtils with shared is method for Authenticators

* fix: typo in getConfigProperties (usage of double setType for createUser ProviderConfigProperty)

* fix: reset default param for isForceCreate inside MagicLinkContinuationAuthenticator back to false

* chore: address PR review comments
  • Loading branch information
alainkaiser authored Jul 18, 2024
1 parent a953526 commit 54530a7
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 37 deletions.
3 changes: 3 additions & 0 deletions src/main/java/io/phasetwo/keycloak/magic/MagicLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserModel> registerEvent(final EventBuilder event) {
return new Consumer<UserModel>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,15 +18,13 @@
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;

@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";

Expand Down Expand Up @@ -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<String, String> 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<String, String> formData) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -71,7 +72,7 @@ public String getHelpText() {
public List<ProviderConfigProperty> 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.");
Expand Down Expand Up @@ -99,7 +100,7 @@ public List<ProviderConfigProperty> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -116,7 +118,7 @@ public void action(AuthenticationFlowContext context) {
context.getSession(),
context.getRealm(),
email,
false,
isForceCreate(context, false),
false,
false,
MagicLink.registerEvent(event));
Expand Down Expand Up @@ -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<String, String> formData) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,21 +17,23 @@
@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,
AuthenticationExecutionModel.Requirement.DISABLED
};

@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
Expand All @@ -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<ProviderConfigProperty> 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);
Expand All @@ -62,12 +86,7 @@ public List<ProviderConfigProperty> 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
Expand All @@ -78,9 +97,4 @@ public void postInit(KeycloakSessionFactory factory) {}

@Override
public void close() {}

@Override
public String getId() {
return PROVIDER_ID;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> config = authenticatorConfig.getConfig();
if (config == null) return defaultValue;

String v = config.get(propName);
if (Strings.isNullOrEmpty(v)) return defaultValue;

return Boolean.parseBoolean(v.trim());
}
}

0 comments on commit 54530a7

Please sign in to comment.