Skip to content
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

Merged
merged 28 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f042072
Fix 'NoClassDefFoundError: org/apache/logging/log4j/util/Lazy' error
Jun 12, 2024
935c713
Fix 'HTTP 405 Method Not Allowed' error when calling the REST endpoint
Jun 12, 2024
a1a2983
Add JUnit test for event listener
Jun 12, 2024
e271890
Add resources
Jun 12, 2024
432a66f
Edit JUnit-test to extract expected successfull login time from acces…
Jun 13, 2024
27ceab5
Remove unnecessary environment variable configuration
Jun 13, 2024
8d456ea
Refactor JUnit test: Extract from access-token without adding new ex…
Jun 13, 2024
7c5a968
Remove unnecessary dependency
Jun 13, 2024
32d9434
Refactor: Change access modifiers of methods to private
Jun 13, 2024
7bed523
Refactor JUnit test: Remove duplicate method
Jun 13, 2024
ff35d01
Rename test files to *IT.java and configure Maven Failsafe plugin
Jun 17, 2024
9370474
Fixing open conversations on PR, refactored MAVEN-Plugins
robson90 Sep 30, 2024
2277073
WiP added a functional Authenticator without any testing. Tests will …
robson90 Sep 30, 2024
09b665e
Merge branch 'main' of github.com:conciso/keycloak-extensions into ad…
robson90 Sep 30, 2024
a086ebb
WiP added UI testing for Terms and condition
robson90 Oct 2, 2024
94c5e5c
Merge branch 'main' of github.com:conciso/keycloak-extensions into ad…
robson90 Oct 2, 2024
e0f0d25
Added UI_Tests to check functionality for required action authenticator
robson90 Oct 2, 2024
e0209b6
change package to verify
robson90 Oct 2, 2024
94a6b52
add steps for playwright tests
robson90 Oct 2, 2024
c5b3930
add steps for playwright tests #2
robson90 Oct 2, 2024
1fedfe7
added documentation
robson90 Oct 7, 2024
9d8a853
fix missing dash
robson90 Oct 7, 2024
7af7283
add one more header
robson90 Oct 7, 2024
15e4379
fixing comments
robson90 Oct 8, 2024
f5351ad
WiP added check for empty AuthenticatorConfig
robson90 Oct 8, 2024
763d632
WiP added check for empty AuthenticatorConfig #2 added TODO to test
robson90 Oct 8, 2024
5f0e412
WiP added check for empty AuthenticatorConfig #2 added TODO to test
robson90 Oct 8, 2024
080cd03
Resolved all comments
robson90 Oct 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '17'
cache: maven
- name: Build project with Maven
run: mvn -B clean package
run: mvn -B clean verify
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

This Repository contains free to use Extensions for the OpenSource Project [Keycloak](https://github.com/keycloak/keycloak)

Contained Extensions:

* Rest-Endpoints
* GetUsersByIdResource -> <root_url>/admins/realms/<realm_name>/users-by-id
* QueryParams:
* briefRepresentation true | false
* listWithIds List containing Ids of Users
* Returns List of Users
This repo contains the following extensions:

## Authenticator-Required-Action

You can add this Authenticator to your flow, so a User gets the defined Required-Action set on signing in.
[README.md - Authenticator-Required Action](./authenticator-required-action/README.md)

## Rest-Endpoints

* GetUsersByIdResource -> <root_url>/admins/realms/<realm_name>/users-by-id
* QueryParams:
* briefRepresentation true | false
* listWithIds List containing Ids of Users
* Returns List of Users
19 changes: 19 additions & 0 deletions authenticator-required-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Authenticator-Required-Action

Copy link
Member

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.

Copy link
Contributor Author

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.

```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)
56 changes: 56 additions & 0 deletions authenticator-required-action/pom.xml
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(
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
}
}
}
Loading