-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add new authenticator required action #16
Changes from 23 commits
f042072
935c713
a1a2983
e271890
432a66f
27ceab5
8d456ea
7c5a968
32d9434
7bed523
ff35d01
9370474
2277073
09b665e
a086ebb
94c5e5c
e0f0d25
e0209b6
94a6b52
c5b3930
1fedfe7
9d8a853
7af7283
15e4379
f5351ad
763d632
5f0e412
080cd03
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
# Authenticator-Required-Action | ||
|
||
```mermaid | ||
flowchart LR | ||
CompanyA(RealmCompanyA)-->UsersA | ||
CompanyB(RealmCompanyB)-->UsersB | ||
UsersA-- Connected via Idp alias: CompanyA ---RealmConciso | ||
UsersB-- Connected via Idp alias: CompanyB ---RealmConciso | ||
``` | ||
|
||
The First Broker login of Idp CompanyA is configured to set the 'TERMS_AND_CONDITIONS' for all users from CompanyA. Users from CompanyB dont have to accept the terms, so they dont get that action. | ||
|
||
## How to use | ||
![required-action-flow.png](../docs/pics/required-action-flow.png) | ||
|
||
![required-action-authenticator-config.png](../docs/pics/required-action-authenticator-config.png) | ||
|
||
To see your availabaddle Required-Actions, go to Realm 'master' -> Provider infor -> Search for 'req' or scroll down until you see 'required-action' in the column for SPI | ||
robson90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
![required-action-available-required-actions.png](../docs/pics/required-action-available-required-actions.png) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<parent> | ||
<groupId>de.conciso.keycloak-extensions</groupId> | ||
<artifactId>parent</artifactId> | ||
<version>1.0-SNAPSHOT</version> | ||
</parent> | ||
|
||
<artifactId>authenticator-required-action</artifactId> | ||
|
||
<properties> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
</properties> | ||
|
||
<dependencies> | ||
<!-- Keycloak Extension --> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-server-spi</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-server-spi-private</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-services</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>com.microsoft.playwright</groupId> | ||
<artifactId>playwright</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-surefire-plugin</artifactId> | ||
</plugin> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-failsafe-plugin</artifactId> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
|
||
</project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package de.conciso.keycloak.authentication.required_action; | ||
|
||
import org.keycloak.authentication.AuthenticationFlowContext; | ||
import org.keycloak.authentication.Authenticator; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.RealmModel; | ||
import org.keycloak.models.UserModel; | ||
|
||
public class RequiredActionAuthenticator implements Authenticator { | ||
|
||
@Override | ||
public void authenticate(AuthenticationFlowContext context) { | ||
context.getUser().addRequiredAction( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be good if we could make this more robust to configuration errors, e.g. what if no authenticator config exists, or what if the key is not present in the config, or the configured required action does not exist. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will look into it. |
||
context.getAuthenticatorConfig() | ||
.getConfig().get(RequiredActionConstants.CONFIG_REQUIRED_ACTION_KEY)); | ||
context.success(); | ||
} | ||
|
||
@Override | ||
public void action(AuthenticationFlowContext context) { | ||
//intentionally empty | ||
context.success(); | ||
} | ||
|
||
@Override | ||
public boolean requiresUser() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { | ||
return true; | ||
} | ||
|
||
@Override | ||
public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { | ||
//intentionally empty | ||
} | ||
|
||
@Override | ||
public void close() { | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package de.conciso.keycloak.authentication.required_action; | ||
|
||
import org.keycloak.Config; | ||
import org.keycloak.authentication.Authenticator; | ||
import org.keycloak.authentication.AuthenticatorFactory; | ||
import org.keycloak.models.AuthenticationExecutionModel; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.KeycloakSessionFactory; | ||
import org.keycloak.provider.ProviderConfigProperty; | ||
|
||
import java.util.List; | ||
|
||
public class RequiredActionAuthenticatorFactory implements AuthenticatorFactory { | ||
|
||
public static final String PROVIDER_ID = "required-action-authenticator"; | ||
|
||
@Override | ||
public String getId() { | ||
return PROVIDER_ID; | ||
} | ||
|
||
@Override | ||
public String getDisplayType() { | ||
return "Set Required-Action Authentication"; | ||
} | ||
|
||
@Override | ||
public String getReferenceCategory() { | ||
return "required-action"; | ||
} | ||
|
||
@Override | ||
public boolean isConfigurable() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { | ||
return new AuthenticationExecutionModel.Requirement[]{ | ||
AuthenticationExecutionModel.Requirement.REQUIRED, | ||
AuthenticationExecutionModel.Requirement.DISABLED}; | ||
} | ||
|
||
@Override | ||
public boolean isUserSetupAllowed() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public String getHelpText() { | ||
return "Sets the configured RequiredAction for the authenticating User"; | ||
} | ||
|
||
@Override | ||
public List<ProviderConfigProperty> getConfigProperties() { | ||
//TODO @Robin Maybe multivalued in the future ? | ||
return List.of( | ||
new ProviderConfigProperty( | ||
RequiredActionConstants.CONFIG_REQUIRED_ACTION_KEY, | ||
"Required Action", | ||
"Specifies the Required Action, that will be assigned to the authenticating User", | ||
ProviderConfigProperty.STRING_TYPE, | ||
"", | ||
false, | ||
true) | ||
); | ||
} | ||
|
||
@Override | ||
public Authenticator create(KeycloakSession keycloakSession) { | ||
return new RequiredActionAuthenticator(); | ||
} | ||
|
||
@Override | ||
public void init(Config.Scope scope) { | ||
|
||
} | ||
|
||
@Override | ||
public void postInit(KeycloakSessionFactory keycloakSessionFactory) { | ||
|
||
} | ||
|
||
@Override | ||
public void close() { | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package de.conciso.keycloak.authentication.required_action; | ||
|
||
public final class RequiredActionConstants { | ||
public static final String CONFIG_REQUIRED_ACTION_KEY = "REQUIRED_ACTION"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
de.conciso.keycloak.authentication.required_action.RequiredActionAuthenticatorFactory |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
package de.conciso.keycloak.authentication.required_action; | ||
|
||
import com.microsoft.playwright.Browser; | ||
import com.microsoft.playwright.BrowserType; | ||
import com.microsoft.playwright.Page; | ||
import com.microsoft.playwright.Playwright; | ||
import com.microsoft.playwright.options.AriaRole; | ||
import dasniko.testcontainers.keycloak.KeycloakContainer; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.testcontainers.containers.output.Slf4jLogConsumer; | ||
import org.testcontainers.junit.jupiter.Container; | ||
import org.testcontainers.junit.jupiter.Testcontainers; | ||
|
||
import java.util.HashMap; | ||
import java.util.stream.Stream; | ||
|
||
import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
@Testcontainers | ||
class RequiredActionAuthenticatorIT { | ||
private static final Logger LOGGER = LoggerFactory.getLogger(RequiredActionAuthenticatorIT.class); | ||
private static final String KEYCLOAK_VERSION = System.getProperty("keycloak.version", "latest"); | ||
private static final String REALM_NAME = "required-action"; | ||
private static final String ADMIN_USER = "admin"; | ||
private static final String ADMIN_PASS = "admin"; | ||
private static final String AUTHENTICATOR_CONFIG_ID = "9b46ecb5-2b16-40f0-beae-4fc4ba905292"; | ||
|
||
@Container | ||
private static final KeycloakContainer keycloak = | ||
new KeycloakContainer("quay.io/keycloak/keycloak:" + KEYCLOAK_VERSION) | ||
.withEnv("KEYCLOAK_ADMIN", ADMIN_USER) | ||
.withEnv("KEYCLOAK_ADMIN_PASSWORD", ADMIN_PASS) | ||
.withLogConsumer(new Slf4jLogConsumer(LOGGER).withSeparateOutputStreams()) | ||
.withProviderClassesFrom("target/classes") | ||
.withRealmImportFile("required-action-realm.json"); | ||
|
||
private static Stream<Arguments> provideUserLogins() { | ||
return Stream.of( | ||
Arguments.of("dieterbohlen", "dietersPassword", "TERMS_AND_CONDITIONS"), | ||
Arguments.of("mannimammut", "mannimammutsPassword", "UPDATE_PASSWORD"), | ||
Arguments.of("peterpommes", "peterpommesPassword", "VERIFY_PROFILE") | ||
); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("provideUserLogins") | ||
void testThatUsersHaveCorrectRequiredActionAfterLogin(String userName, String password, String requiredAction) { | ||
// Setting the specific required action to be added to the Authentication Config | ||
var authenticatorConfig = new AuthenticatorConfigRepresentation(); | ||
authenticatorConfig.setId(AUTHENTICATOR_CONFIG_ID); | ||
authenticatorConfig.setAlias("RequiredAction"); | ||
var configMap = new HashMap<String, String>(); | ||
configMap.put("REQUIRED_ACTION", requiredAction); | ||
authenticatorConfig.setConfig(configMap); | ||
|
||
var keycloakAdminClient = keycloak.getKeycloakAdminClient().realm(REALM_NAME); | ||
keycloakAdminClient.flows().updateAuthenticatorConfig(AUTHENTICATOR_CONFIG_ID, authenticatorConfig); | ||
assertThat(keycloakAdminClient.users().searchByUsername(userName, true).get(0).getRequiredActions()) | ||
.isEmpty(); | ||
|
||
executeBrowserFlow(userName, password, requiredAction); | ||
} | ||
|
||
void executeBrowserFlow(String userName, String password, String requiredAction) { | ||
try (Playwright playwright = Playwright.create()) { | ||
BrowserType chromium = playwright.chromium(); | ||
// comment me in if you want to run in headless mode ! | ||
//Browser browser = chromium.launch(new BrowserType.LaunchOptions().setHeadless(false)); | ||
Browser browser = chromium.launch(); | ||
Page page = browser.newPage(); | ||
page.navigate(String.format("http://localhost:%s/realms/required-action/account/", keycloak.getHttpPort())); | ||
|
||
page.getByText("Signing in").click(); | ||
page.waitForURL("**/realms/required-action/protocol/openid-connect/auth**"); | ||
|
||
// Login Page | ||
page.getByLabel("Username or email").click(); | ||
page.getByLabel("Username or email").fill(userName); | ||
|
||
page.getByLabel("Password").first().fill(password); | ||
page.getByLabel("Password").first().press("Enter"); | ||
|
||
// split here, for the specific required action | ||
switch (requiredAction) { | ||
case "TERMS_AND_CONDITIONS": | ||
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("Accept")).click(); | ||
break; | ||
case "UPDATE_PASSWORD": | ||
page.getByLabel("New Password").fill("Test123!"); | ||
page.getByLabel("Confirm Password").fill("Test123!"); | ||
page.getByLabel("Confirm Password").press("Enter"); | ||
break; | ||
case "VERIFY_PROFILE": | ||
assertThat(keycloak.getKeycloakAdminClient().realm(REALM_NAME).users().searchByUsername(userName, true).get(0).getRequiredActions()).containsExactly(requiredAction); | ||
break; | ||
default: | ||
Assertions.fail(); | ||
} | ||
|
||
page.waitForURL(String.format("http://localhost:%s/realms/required-action/account/#/security/signingin", keycloak.getHttpPort())); | ||
robson90 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
assertThat(page.getByRole(AriaRole.HEADING, new Page.GetByRoleOptions().setName("Signing in"))).isVisible(); | ||
|
||
browser.close(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you should add a general description of what problem or use case this authenticator is good for. Just starting with an example out of context does not really help to understand what it is doing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check if that is sufficent.