Skip to content

Commit ce48f56

Browse files
committed
add ChoiceAuthenticator and Enhanced Conditional Role Authenticator, some refactoring
1 parent 3bb4027 commit ce48f56

9 files changed

+340
-6
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.instipod.keycloakauthenticators;
2+
3+
import org.jboss.logging.Logger;
4+
import org.keycloak.authentication.AuthenticationFlowContext;
5+
import org.keycloak.authentication.AuthenticationFlowError;
6+
import org.keycloak.forms.login.LoginFormsProvider;
7+
import org.keycloak.models.AuthenticatorConfigModel;
8+
import org.keycloak.models.KeycloakSession;
9+
import org.keycloak.models.RealmModel;
10+
import org.keycloak.models.UserModel;
11+
12+
import javax.ws.rs.core.MultivaluedMap;
13+
import javax.ws.rs.core.Response;
14+
15+
public class ChoiceAuthenticator implements org.keycloak.authentication.Authenticator {
16+
public static final ChoiceAuthenticator SINGLETON = new ChoiceAuthenticator();
17+
18+
@Override
19+
public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
20+
LoginFormsProvider form = authenticationFlowContext.form();
21+
Response response = form.createForm(ChoiceAuthenticatorFactory.CHOICE_FILE);
22+
authenticationFlowContext.challenge(response);
23+
}
24+
25+
@Override
26+
public void action(AuthenticationFlowContext authenticationFlowContext) {
27+
AuthenticatorConfigModel authConfig = authenticationFlowContext.getAuthenticatorConfig();
28+
MultivaluedMap<String, String> formData = authenticationFlowContext.getHttpRequest().getDecodedFormParameters();
29+
30+
if (authConfig==null || authConfig.getConfig()==null) {
31+
authenticationFlowContext.failure(AuthenticationFlowError.INTERNAL_ERROR);
32+
return;
33+
}
34+
35+
String noteName = authConfig.getConfig().get(ChoiceAuthenticatorFactory.NOTE_NAME);
36+
37+
if (noteName.length() == 0) {
38+
authenticationFlowContext.failure(AuthenticationFlowError.INTERNAL_ERROR);
39+
return;
40+
}
41+
42+
if (!formData.containsKey("choice")) {
43+
//no choice returned
44+
LoginFormsProvider form = authenticationFlowContext.form();
45+
form.setError("You must make a selection to continue.");
46+
Response response = form.createForm(ChoiceAuthenticatorFactory.CHOICE_FILE);
47+
authenticationFlowContext.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, response);
48+
} else {
49+
//choice returned
50+
String choice = formData.getFirst("choice");
51+
//remove any nonalphanumeric characters
52+
choice = choice.replaceAll("[^a-zA-Z0-9]", "");
53+
54+
authenticationFlowContext.getAuthenticationSession().setAuthNote(noteName, choice);
55+
authenticationFlowContext.success();
56+
}
57+
}
58+
59+
@Override
60+
public boolean requiresUser() {
61+
return false;
62+
}
63+
64+
@Override
65+
public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
66+
return true;
67+
}
68+
69+
@Override
70+
public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) {
71+
72+
}
73+
74+
@Override
75+
public void close() {
76+
77+
}
78+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.instipod.keycloakauthenticators;
2+
3+
import org.keycloak.Config;
4+
import org.keycloak.authentication.Authenticator;
5+
import org.keycloak.models.AuthenticationExecutionModel;
6+
import org.keycloak.models.KeycloakSession;
7+
import org.keycloak.models.KeycloakSessionFactory;
8+
import org.keycloak.provider.ProviderConfigProperty;
9+
import org.keycloak.provider.ProviderConfigurationBuilder;
10+
11+
import java.util.Collections;
12+
import java.util.List;
13+
14+
public class ChoiceAuthenticatorFactory implements org.keycloak.authentication.AuthenticatorFactory {
15+
public static final String PROVIDER_ID = "user-choice";
16+
protected static final String NOTE_NAME = "choiceNoteName";
17+
protected static final String CHOICE_FILE = "user-choice.ftl";
18+
private static List<ProviderConfigProperty> commonConfig;
19+
20+
static {
21+
commonConfig = Collections.unmodifiableList(ProviderConfigurationBuilder.create()
22+
.property().name(NOTE_NAME).label("Note Name").helpText("Note to store the users choice in").type(ProviderConfigProperty.STRING_TYPE).add()
23+
.build()
24+
);
25+
}
26+
27+
@Override
28+
public String getDisplayType() {
29+
return "User Choice";
30+
}
31+
32+
@Override
33+
public String getReferenceCategory() {
34+
return "generic";
35+
}
36+
37+
@Override
38+
public boolean isConfigurable() {
39+
return true;
40+
}
41+
42+
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
43+
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED
44+
};
45+
46+
@Override
47+
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
48+
return REQUIREMENT_CHOICES;
49+
}
50+
51+
@Override
52+
public boolean isUserSetupAllowed() {
53+
return false;
54+
}
55+
56+
@Override
57+
public String getHelpText() {
58+
return "Shows a template to the user to make a choice, stores choice in selected note";
59+
}
60+
61+
@Override
62+
public List<ProviderConfigProperty> getConfigProperties() {
63+
return commonConfig;
64+
}
65+
66+
@Override
67+
public Authenticator create(KeycloakSession keycloakSession) {
68+
return ChoiceAuthenticator.SINGLETON;
69+
}
70+
71+
@Override
72+
public void init(Config.Scope scope) {
73+
//noop
74+
}
75+
76+
@Override
77+
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {
78+
//noop
79+
}
80+
81+
@Override
82+
public void close() {
83+
//noop
84+
}
85+
86+
@Override
87+
public String getId() {
88+
return PROVIDER_ID;
89+
}
90+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.instipod.keycloakauthenticators;
2+
3+
import com.instipod.keycloakauthenticators.utils.IPAddressMatcher;
4+
import com.instipod.keycloakauthenticators.utils.AuthenticatorUtils;
5+
import org.keycloak.authentication.AuthenticationFlowContext;
6+
import org.keycloak.models.UserModel;
7+
import org.keycloak.models.AuthenticatorConfigModel;
8+
import org.keycloak.models.KeycloakSession;
9+
import org.keycloak.models.RealmModel;
10+
11+
public class ConditionalRoleEnhancedAuthenticator implements org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator {
12+
public static final ConditionalRoleEnhancedAuthenticator SINGLETON = new ConditionalRoleEnhancedAuthenticator();
13+
14+
@Override
15+
public boolean matchCondition(AuthenticationFlowContext context) {
16+
AuthenticatorConfigModel authConfig = context.getAuthenticatorConfig();
17+
18+
if (authConfig!=null && authConfig.getConfig()!=null) {
19+
//evaluate
20+
String not = authConfig.getConfig().get(ConditionalRoleEnhancedAuthenticatorFactory.CONDITIONAL_NOT);
21+
String role = authConfig.getConfig().get(ConditionalRoleEnhancedAuthenticatorFactory.CONDITIONAL_ROLE);
22+
role = AuthenticatorUtils.variableReplace(context, role);
23+
24+
if (not.equalsIgnoreCase("true")) {
25+
//does not have role
26+
return !(AuthenticatorUtils.hasRole(context.getUser(), role));
27+
} else {
28+
//has role
29+
return (AuthenticatorUtils.hasRole(context.getUser(), role));
30+
}
31+
}
32+
33+
return false;
34+
}
35+
36+
@Override
37+
public void action(AuthenticationFlowContext context) {
38+
// Not used
39+
}
40+
41+
@Override
42+
public boolean requiresUser() {
43+
return true;
44+
}
45+
46+
@Override
47+
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
48+
// Not used
49+
}
50+
51+
@Override
52+
public void close() {
53+
// Does nothing
54+
}
55+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.instipod.keycloakauthenticators;
2+
3+
import org.keycloak.Config.Scope;
4+
import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator;
5+
import org.keycloak.models.AuthenticationExecutionModel;
6+
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
7+
import org.keycloak.models.KeycloakSessionFactory;
8+
import org.keycloak.provider.ProviderConfigProperty;
9+
import org.keycloak.provider.ProviderConfigurationBuilder;
10+
11+
import java.util.Collections;
12+
import java.util.List;
13+
14+
public class ConditionalRoleEnhancedAuthenticatorFactory implements org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticatorFactory {
15+
public static final String PROVIDER_ID = "conditional-role-ehnd";
16+
protected static final String CONDITIONAL_ROLE = "condRole";
17+
protected static final String CONDITIONAL_NOT = "condNot";
18+
19+
private static List<ProviderConfigProperty> commonConfig;
20+
21+
static {
22+
commonConfig = Collections.unmodifiableList(ProviderConfigurationBuilder.create()
23+
.property().name(CONDITIONAL_ROLE).label("Role").helpText("Role to check for (supports variables)").type(ProviderConfigProperty.STRING_TYPE).add()
24+
.property().name(CONDITIONAL_NOT).label("Not").helpText("If we should match on NOT having this role").type(ProviderConfigProperty.BOOLEAN_TYPE).add()
25+
.build()
26+
);
27+
}
28+
29+
@Override
30+
public void init(Scope config) {
31+
// no-op
32+
}
33+
34+
@Override
35+
public void postInit(KeycloakSessionFactory factory) {
36+
// no-op
37+
}
38+
39+
@Override
40+
public void close() {
41+
// no-op
42+
}
43+
44+
@Override
45+
public String getId() {
46+
return PROVIDER_ID;
47+
}
48+
49+
@Override
50+
public String getDisplayType() {
51+
return "Condition - Role (Enhanced)";
52+
}
53+
54+
@Override
55+
public String getReferenceCategory() {
56+
return "condition";
57+
}
58+
59+
@Override
60+
public boolean isConfigurable() {
61+
return true;
62+
}
63+
64+
private static final Requirement[] REQUIREMENT_CHOICES = {
65+
AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED
66+
};
67+
68+
@Override
69+
public Requirement[] getRequirementChoices() {
70+
return REQUIREMENT_CHOICES;
71+
}
72+
73+
@Override
74+
public boolean isUserSetupAllowed() {
75+
return false;
76+
}
77+
78+
@Override
79+
public String getHelpText() {
80+
return "Flow is executed only if user has specified role.";
81+
}
82+
83+
@Override
84+
public List<ProviderConfigProperty> getConfigProperties() {
85+
return commonConfig;
86+
}
87+
88+
@Override
89+
public ConditionalAuthenticator getSingleton() {
90+
return ConditionalRoleEnhancedAuthenticator.SINGLETON;
91+
}
92+
}

src/main/java/com/instipod/keycloakauthenticators/QRCodeAuthenticator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ public void action(AuthenticationFlowContext authenticationFlowContext) {
6464

6565
String attributeName = authConfig.getConfig().get(QRCodeAuthenticatorFactory.QR_KEY_ATTRIBUTE);
6666
String qrCodeData = formData.getFirst("qrCodeData");
67+
//remove all nonalphanumeric characters from data
68+
qrCodeData = qrCodeData.replaceAll("[^a-zA-Z0-9]", "");
6769

6870
if (attributeName.length() == 0) {
6971
logger.error("No attribute name to search was specified in QR Code Authenticator!");

src/main/java/com/instipod/keycloakauthenticators/SetNoteAuthenticator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.instipod.keycloakauthenticators;
22

3-
import com.instipod.keycloakauthenticators.utils.ValueUtils;
3+
import com.instipod.keycloakauthenticators.utils.AuthenticatorUtils;
44
import org.keycloak.authentication.AuthenticationFlowContext;
55
import org.keycloak.authentication.AuthenticationFlowError;
66
import org.keycloak.models.AuthenticatorConfigModel;
@@ -20,7 +20,7 @@ public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
2020
String noteName = authConfig.getConfig().get(SetNoteAuthenticatorFactory.NOTE_NAME);
2121
String noteValue = authConfig.getConfig().get(SetNoteAuthenticatorFactory.NOTE_VALUE);
2222

23-
noteValue = ValueUtils.variableReplace(authenticationFlowContext, noteValue);
23+
noteValue = AuthenticatorUtils.variableReplace(authenticationFlowContext, noteValue);
2424

2525
authenticationFlowContext.getAuthenticationSession().setAuthNote(noteName, noteValue);
2626
authenticationFlowContext.success();

src/main/java/com/instipod/keycloakauthenticators/SlackMessageAuthenticator.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.instipod.keycloakauthenticators;
22

3-
import com.instipod.keycloakauthenticators.utils.ValueUtils;
3+
import com.instipod.keycloakauthenticators.utils.AuthenticatorUtils;
44
import in.ashwanthkumar.slack.webhook.Slack;
55
import in.ashwanthkumar.slack.webhook.SlackMessage;
66
import org.keycloak.authentication.AuthenticationFlowContext;
@@ -26,7 +26,7 @@ public void authenticate(AuthenticationFlowContext authenticationFlowContext) {
2626
String isCritical = authConfig.getConfig().get(SlackMessageAuthenticatorFactory.SLACK_IS_CRITICAL);
2727
String webhook = authConfig.getConfig().get(SlackMessageAuthenticatorFactory.SLACK_WEBHOOK_URL);
2828

29-
message = ValueUtils.variableReplace(authenticationFlowContext, message);
29+
message = AuthenticatorUtils.variableReplace(authenticationFlowContext, message);
3030

3131
SlackMessage slackMessage = new SlackMessage(message);
3232

src/main/java/com/instipod/keycloakauthenticators/utils/ValueUtils.java renamed to src/main/java/com/instipod/keycloakauthenticators/utils/AuthenticatorUtils.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.instipod.keycloakauthenticators.utils;
22

33
import org.keycloak.authentication.AuthenticationFlowContext;
4+
import org.keycloak.models.RoleModel;
45
import org.keycloak.models.UserModel;
56

6-
public class ValueUtils {
7+
import java.util.Set;
8+
9+
public class AuthenticatorUtils {
710
public static String variableReplace(AuthenticationFlowContext context, String message) {
811
UserModel user = null;
912
try {
@@ -34,4 +37,16 @@ public static String variableReplace(AuthenticationFlowContext context, String m
3437

3538
return message;
3639
}
40+
41+
public static boolean hasRole(UserModel user, String roleName) {
42+
Set<RoleModel> roles = user.getRoleMappings();
43+
44+
for (RoleModel role : roles) {
45+
if (role.getName().equalsIgnoreCase(roleName)) {
46+
return true;
47+
}
48+
}
49+
50+
return false;
51+
}
3752
}

0 commit comments

Comments
 (0)