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 9694a2b..8137b82 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticator.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticator.java @@ -1,6 +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.get; import static io.phasetwo.keycloak.magic.auth.util.Authenticators.is; import static org.keycloak.services.validation.Validation.FIELD_USERNAME; @@ -29,6 +30,7 @@ public class MagicLinkAuthenticator extends UsernamePasswordForm { static final String UPDATE_PASSWORD_ACTION_CONFIG_PROPERTY = "ext-magic-update-password-action"; static final String ACTION_TOKEN_PERSISTENT_CONFIG_PROPERTY = "ext-magic-allow-token-reuse"; + static final String ACTION_TOKEN_LIFE_SPAN = "ext-magic-token-life-span"; @Override public void authenticate(AuthenticationFlowContext context) { @@ -83,12 +85,14 @@ public void action(AuthenticationFlowContext context) { if (user == null || MagicLink.trimToNull(user.getEmail()) == null || !MagicLink.isValidEmail(user.getEmail())) { - context.getEvent() - .detail(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, email) - .event(EventType.LOGIN_ERROR).error(Errors.INVALID_EMAIL); context - .getAuthenticationSession() - .setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, email); + .getEvent() + .detail(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, email) + .event(EventType.LOGIN_ERROR) + .error(Errors.INVALID_EMAIL); + context + .getAuthenticationSession() + .setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, email); log.debugf("user attempted to login with username/email: %s", email); context.forceChallenge(context.form().createForm("view-email.ftl")); return; @@ -101,11 +105,13 @@ public void action(AuthenticationFlowContext context) { return; // the enabledUser method sets the challenge } + OptionalInt lifespan = getActionTokenLifeSpan(context, ""); + MagicLinkActionToken token = MagicLink.createActionToken( user, clientId, - OptionalInt.empty(), + lifespan, rememberMe(context), context.getAuthenticationSession(), isActionTokenPersistent(context, true)); @@ -143,6 +149,22 @@ private boolean isActionTokenPersistent(AuthenticationFlowContext context, boole return is(context, ACTION_TOKEN_PERSISTENT_CONFIG_PROPERTY, defaultValue); } + private OptionalInt getActionTokenLifeSpan( + AuthenticationFlowContext context, String defaultValue) { + String lifespan = get(context, ACTION_TOKEN_LIFE_SPAN, defaultValue); + + if ("".equals(lifespan)) { + return OptionalInt.empty(); + } + + try { + return OptionalInt.of(Integer.parseInt(lifespan)); + } catch (NumberFormatException e) { + log.error("Failed to parse lifespan", e); + return OptionalInt.empty(); + } + } + @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 685efb7..d8453f6 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticatorFactory.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/MagicLinkAuthenticatorFactory.java @@ -100,7 +100,15 @@ public List getConfigProperties() { "Toggle whether magic link should be persistent until expired."); actionTokenPersistent.setDefaultValue(true); - return List.of(createUser, updateProfile, updatePassword, actionTokenPersistent); + ProviderConfigProperty actionTokenLifeSpan = new ProviderConfigProperty(); + actionTokenLifeSpan.setType(ProviderConfigProperty.STRING_TYPE); + actionTokenLifeSpan.setName(MagicLinkAuthenticator.ACTION_TOKEN_LIFE_SPAN); + actionTokenLifeSpan.setLabel("Token lifespan"); + actionTokenLifeSpan.setHelpText( + "Amount of time the magic link is valid, in seconds. If this value is not specific, it will use the default 86400s (1 day)"); + + return List.of( + createUser, updateProfile, updatePassword, actionTokenPersistent, actionTokenLifeSpan); } @Override 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 index aff9da8..732e759 100644 --- a/src/main/java/io/phasetwo/keycloak/magic/auth/util/Authenticators.java +++ b/src/main/java/io/phasetwo/keycloak/magic/auth/util/Authenticators.java @@ -19,4 +19,18 @@ public static boolean is( return Boolean.parseBoolean(v.trim()); } + + public static String get( + AuthenticationFlowContext context, String propName, String 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 v.trim(); + } }