Skip to content

Commit

Permalink
Add configuration to invalidate action token after one time use (By d…
Browse files Browse the repository at this point in the history
…efault reusable) (#47)

* Action token should only be valid for one time user

* Add action token persistent config

* Add json properties and compact api object

* add backwards compatibility

---------

Co-authored-by: nadeemm <[email protected]>
  • Loading branch information
tfcornerstone and nadeemm authored Aug 23, 2023
1 parent 25670d0 commit cef3509
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 9 deletions.
35 changes: 30 additions & 5 deletions src/main/java/io/phasetwo/keycloak/magic/MagicLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,23 @@ public static UserModel getOrCreate(
return user;
}

public static MagicLinkActionToken createActionToken(
UserModel user,
String clientId,
OptionalInt validity,
Boolean rememberMe,
AuthenticationSessionModel authSession) {
return createActionToken(
user, clientId, validity, rememberMe, authSession, true);
}

public static MagicLinkActionToken createActionToken(
UserModel user,
String clientId,
OptionalInt validity,
Boolean rememberMe,
AuthenticationSessionModel authSession) {
AuthenticationSessionModel authSession,
Boolean isActionTokenPersistent) {
String redirectUri = authSession.getRedirectUri();
String scope = authSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
Expand All @@ -108,7 +119,19 @@ public static MagicLinkActionToken createActionToken(
"Attempting MagicLinkAuthenticator for %s, %s, %s", user.getEmail(), clientId, redirectUri);
log.infof("MagicLinkAuthenticator extra vars %s %s %s %b", scope, state, nonce, rememberMe);
return createActionToken(
user, clientId, redirectUri, validity, scope, nonce, state, rememberMe);
user, clientId, redirectUri, validity, scope, nonce, state, rememberMe, isActionTokenPersistent);
}

public static MagicLinkActionToken createActionToken(
UserModel user,
String clientId,
String redirectUri,
OptionalInt validity,
String scope,
String nonce,
String state,
Boolean rememberMe) {
return createActionToken(user, clientId, redirectUri, validity, scope, nonce, state, rememberMe, true);
}

public static MagicLinkActionToken createActionToken(
Expand All @@ -119,7 +142,8 @@ public static MagicLinkActionToken createActionToken(
String scope,
String nonce,
String state,
Boolean rememberMe) {
Boolean rememberMe,
Boolean isActionTokenPersistent) {
// build the action token
int validityInSecs = validity.orElse(60 * 60 * 24); // 1 day
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
Expand All @@ -132,13 +156,14 @@ public static MagicLinkActionToken createActionToken(
scope,
nonce,
state,
rememberMe);
rememberMe,
isActionTokenPersistent);
return token;
}

public static MagicLinkActionToken createActionToken(
UserModel user, String clientId, String redirectUri, OptionalInt validity) {
return createActionToken(user, clientId, redirectUri, validity, null, null, null, false);
return createActionToken(user, clientId, redirectUri, validity, null, null, null, false, true);
}

public static String linkFromActionToken(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public class MagicLinkAuthenticator extends UsernamePasswordForm {
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";

static final String ACTION_TOKEN_PERSISTENT_CONFIG_PROPERTY = "ext-magic-allow-token-reuse";

@Override
public void authenticate(AuthenticationFlowContext context) {
log.info("MagicLinkAuthenticator.authenticate");
Expand Down Expand Up @@ -101,7 +103,8 @@ public void action(AuthenticationFlowContext context) {
clientId,
OptionalInt.empty(),
rememberMe(context),
context.getAuthenticationSession());
context.getAuthenticationSession(),
isActionTokenPersistent(context, true));
String link = MagicLink.linkFromActionToken(context.getSession(), context.getRealm(), token);
boolean sent = MagicLink.sendMagicLinkEmail(context.getSession(), user, link);
log.infof("sent email to %s? %b. Link? %s", user.getEmail(), sent, link);
Expand Down Expand Up @@ -132,6 +135,10 @@ private boolean isUpdatePassword(AuthenticationFlowContext context, boolean defa
return is(context, UPDATE_PASSWORD_ACTION_CONFIG_PROPERTY, defaultValue);
}

private boolean isActionTokenPersistent(AuthenticationFlowContext context, boolean defaultValue) {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,14 @@ public List<ProviderConfigProperty> getConfigProperties() {
updatePassword.setHelpText("Add an UPDATE_PASSWORD required action if the user was created.");
updatePassword.setDefaultValue(false);

return Arrays.asList(createUser, updateProfile, updatePassword);
ProviderConfigProperty actionTokenPersistent = new ProviderConfigProperty();
actionTokenPersistent.setType(ProviderConfigProperty.BOOLEAN_TYPE);
actionTokenPersistent.setName(MagicLinkAuthenticator.ACTION_TOKEN_PERSISTENT_CONFIG_PROPERTY);
actionTokenPersistent.setLabel("Allow magic link to be reusable");
actionTokenPersistent.setHelpText("Toggle whether magic link should be persistent until expired.");
actionTokenPersistent.setDefaultValue(true);

return Arrays.asList(createUser, updateProfile, updatePassword, actionTokenPersistent);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public class MagicLinkActionToken extends DefaultActionToken {
private static final String JSON_FIELD_REMEMBER_ME = "rme";
private static final String JSON_FIELD_STRING_NONCE = "nce";

private static final String JSON_FIELD_REUSABLE = "ru";

@JsonProperty(value = JSON_FIELD_REDIRECT_URI)
private String redirectUri;

Expand All @@ -26,6 +28,9 @@ public class MagicLinkActionToken extends DefaultActionToken {
@JsonProperty(value = JSON_FIELD_REMEMBER_ME)
private Boolean rememberMe = false;

@JsonProperty(value = JSON_FIELD_REUSABLE)
private Boolean actionTokenPersistent = true;

@JsonProperty(value = JSON_FIELD_STRING_NONCE)
private String nonce;

Expand Down Expand Up @@ -59,13 +64,15 @@ public MagicLinkActionToken(
String scope,
String nonce,
String state,
Boolean rememberMe) {
Boolean rememberMe,
Boolean isActionTokenPersistent) {
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, nonce(nonce));
this.redirectUri = redirectUri;
this.issuedFor = clientId;
this.scopes = scope;
this.state = state;
this.rememberMe = rememberMe;
this.actionTokenPersistent = isActionTokenPersistent;
this.nonce = nonce;
}

Expand Down Expand Up @@ -114,6 +121,14 @@ public void setRememberMe(Boolean value) {
this.rememberMe = value;
}

public Boolean getActionTokenPersistent() {
return this.actionTokenPersistent;
}

public void setActionTokenPersistent(Boolean value) {
this.actionTokenPersistent = value;
}

public String getNonce() {
return this.nonce;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public AuthenticationSessionModel startFreshAuthenticationSession(
return tokenContext.createAuthenticationSessionForClient(token.getIssuedFor());
}

@Override
public boolean canUseTokenRepeatedly(MagicLinkActionToken token, ActionTokenContext<MagicLinkActionToken> tokenContext) {
return token.getActionTokenPersistent(); //Invalidate action token after one use if configured to do so
}

@Override
public Response handleToken(
MagicLinkActionToken token, ActionTokenContext<MagicLinkActionToken> tokenContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ public class MagicLinkRequest {

@JsonProperty("remember_me")
private Boolean rememberMe = false;

@JsonProperty("reusable")
private Boolean actionTokenPersistent = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ public MagicLinkResponse createMagicLink(final MagicLinkRequest rep) {
rep.getScope(),
rep.getNonce(),
rep.getState(),
rep.getRememberMe());
rep.getRememberMe(),
rep.getActionTokenPersistent());
String link = MagicLink.linkFromActionToken(session, realm, token);
boolean sent = false;
if (sendEmail) {
Expand Down

0 comments on commit cef3509

Please sign in to comment.