Skip to content

Commit

Permalink
Add new authenticator required action (#16)
Browse files Browse the repository at this point in the history
* Fix 'NoClassDefFoundError: org/apache/logging/log4j/util/Lazy' error

* Fix 'HTTP 405 Method Not Allowed' error when calling the REST endpoint

* Add JUnit test for event listener

* Add resources

* Edit JUnit-test to extract expected successfull login time from access-token instead of logs

* Remove unnecessary environment variable configuration

* Refactor JUnit test: Extract  from access-token without adding new external dependencies.

* Remove unnecessary dependency

* Refactor: Change access modifiers of methods to private

* Refactor JUnit test: Remove duplicate method

* Rename test files to *IT.java and configure Maven Failsafe plugin

* Fixing open conversations on PR, refactored MAVEN-Plugins

* WiP added a functional Authenticator without any testing. Tests will be added next

* WiP added UI testing for Terms and condition

* Added UI_Tests to check functionality for required action authenticator

* change package to verify

* add steps for playwright tests

* add steps for playwright tests #2

* added documentation

* fix missing dash

* add one more header

* fixing comments

* WiP added check for empty AuthenticatorConfig

* WiP added check for empty AuthenticatorConfig #2 added TODO to test

* WiP added check for empty AuthenticatorConfig #2 added TODO to test

* Resolved all comments

---------

Co-authored-by: Hassan El Mailoudi <[email protected]>
  • Loading branch information
robson90 and Hassan El Mailoudi authored Oct 22, 2024
1 parent 6ce9045 commit 1db5ec7
Show file tree
Hide file tree
Showing 16 changed files with 2,416 additions and 12 deletions.
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

0 comments on commit 1db5ec7

Please sign in to comment.