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 all 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
20 changes: 20 additions & 0 deletions authenticator-required-action/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Authenticator-Required-Action
Imagine a user authenticates via an Identity Provider (IdP) and requires a specific action, such as resetting their password or agreeing to new terms of service.
This extension ensures that the configured required action is automatically set for the user once they authenticate through the Identity Provider.
```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 available Required-Actions, go to Realm 'master' -> Provider info -> Search for 'req' or scroll down until you see 'required-action' in the column for SPI
![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,95 @@
package de.conciso.keycloak.authentication.required_action;

import jakarta.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.sessions.AuthenticationSessionModel;

import java.util.Optional;

public class RequiredActionAuthenticator implements Authenticator {
private static final Logger LOGGER = Logger.getLogger(RequiredActionAuthenticator.class);
private static final String LOG_ERROR_MESSAGE_MISSING_AUTH_CONFIG = "AuthenticatorConfig is missing on RequiredActionAuthenticator";
private static final String HTML_ERROR_MESSAGE = LOG_ERROR_MESSAGE_MISSING_AUTH_CONFIG + "!\nPlease contact your administrator";
private static final String LOG_ERROR_MESSAGE_NON_EXISTENT_REQUIRED_ACTION = "AuthenticatorConfig references an unknown RequiredAction, please double check if it really exists: 'NON_EXISTENT_REQUIRED_ACTION'";


@Override
public void authenticate(AuthenticationFlowContext context) {
var authenticatorConfig = Optional.ofNullable(
context.getAuthenticatorConfig()
.getConfig()
.get(RequiredActionConstants.CONFIG_REQUIRED_ACTION_KEY));
if (authenticatorConfig.isEmpty()) {
LOGGER.error(LOG_ERROR_MESSAGE_MISSING_AUTH_CONFIG);
context.getEvent()
.realm(context.getRealm())
.client(context.getSession().getContext().getClient())
.user(context.getUser())
.error(Errors.INVALID_CONFIG);
context.failure(AuthenticationFlowError.INTERNAL_ERROR,
htmlErrorResponse(context,
LOG_ERROR_MESSAGE_MISSING_AUTH_CONFIG + HTML_ERROR_MESSAGE));
} else if (!doesRequiredActionExists(context, authenticatorConfig.get())) {
LOGGER.error(LOG_ERROR_MESSAGE_NON_EXISTENT_REQUIRED_ACTION);
context.getEvent()
.realm(context.getRealm())
.client(context.getSession().getContext().getClient())
.user(context.getUser())
.error(Errors.INVALID_CONFIG);
context.failure(AuthenticationFlowError.INTERNAL_ERROR,
htmlErrorResponse(context,
LOG_ERROR_MESSAGE_NON_EXISTENT_REQUIRED_ACTION + HTML_ERROR_MESSAGE));
} else {
context.getUser().addRequiredAction(authenticatorConfig.get());
context.success();
}
}

private boolean doesRequiredActionExists(AuthenticationFlowContext context, String providerId) {
var requiredAction = context.getRealm().getRequiredActionProvidersStream()
.map(RequiredActionProviderModel::getProviderId)
.filter(id -> id.equals(providerId))
.findFirst();
return requiredAction.isPresent();
}

private Response htmlErrorResponse(AuthenticationFlowContext context, String errorMessage) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
return context.form()
.setError(errorMessage, authSession.getAuthenticatedUser().getUsername(),
authSession.getClient().getClientId())
.createErrorPage(Response.Status.INTERNAL_SERVER_ERROR);
}

@Override
public void action(AuthenticationFlowContext context) {
}

@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
Loading