-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from jjaferson/master
Delay authentication plugin for keycloak
- Loading branch information
Showing
8 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<classpath> | ||
<classpathentry kind="src" output="target/classes" path="src/main/java"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"> | ||
<attributes> | ||
<attribute name="maven.pomderived" value="true"/> | ||
<attribute name="org.eclipse.jst.component.dependency" value="/WEB-INF/lib"/> | ||
</attributes> | ||
</classpathentry> | ||
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.web.container"/> | ||
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.module.container"/> | ||
<classpathentry kind="output" path="target/classes"/> | ||
</classpath> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# KeyCloak Delay Authentication Flow | ||
|
||
Plugin to delay authentication of users until they get created in Keycloak and added in 3scale by RHMI operator. | ||
|
||
# System Requirements | ||
|
||
You need to have Keycloak 9.0.2 running. | ||
|
||
All you need to build this project is Java 8.0 (Java SDK 1.8) or later and Maven 3.1.1 or later. | ||
|
||
# Build and Deploy | ||
|
||
### Build the plugin | ||
|
||
To build the plugin you need to first intall the dependencies: | ||
|
||
`$ mvn install` | ||
|
||
and then gerenate the `.jar` file: | ||
|
||
`$ mvn package` | ||
|
||
Once the plugin is built you should be able to find the .jar file in `./plugins/<projectname>.jar`. | ||
|
||
### Install Keycloak locally using Docker Compose | ||
|
||
There is a `docker-compose.yaml` file available in the root of the project that can be used to install Keycloak locally with Maria DB and deploy the plugin. | ||
|
||
Steps to install Keycloak locally: | ||
|
||
1) Open the terminal and navigate to the root of the project. | ||
2) Run ```docker-compose up```. | ||
3) When keycloak finishes the install open http://0.0.0.0:8080/ in a browser and it you should be able to see keycloak login screen. | ||
4) Admin credentials for Keycloak are set in the docker-compose.yaml file `KEYCLOAK_USER=admin-test` and `KEYCLOAK_PASSWORD=admin-test`. | ||
|
||
### Deploy the plugin | ||
|
||
To deploy the plugin you need to copy the `.jar` file to `/opt/jboss/keycloak/providers` in Keycloak. | ||
|
||
PS.: this directory is mapped in the docker-compose.yaml file available in the repo. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
version: '3.7' | ||
services: | ||
keycloak: | ||
image: 'jboss/keycloak' | ||
ports: | ||
- "8080:8080" | ||
environment: | ||
- KEYCLOAK_USER=admin-test | ||
- KEYCLOAK_PASSWORD=admin-test | ||
- JDBC_PARAMS='connectTimeout=30' | ||
- DB_VENDOR=mariadb | ||
- DB_ADDR=mariadb | ||
- DB_DATABASE=keycloak | ||
- DB_USER=keycloak | ||
- DB_PASSWORD=password | ||
- JGROUPS_DISCOVERY_PROTOCOL=JDBC_PING | ||
- JGROUPS_DISCOVERY_PROPERTIES=datasource_jndi_name=java:jboss/datasources/KeycloakDS,info_writer_sleep_time=500 | ||
volumes: | ||
- ./plugins:/opt/jboss/keycloak/providers | ||
depends_on: | ||
- mariadb | ||
mariadb: | ||
image: mariadb | ||
volumes: | ||
- data:/var/lib/mysql | ||
environment: | ||
MYSQL_ROOT_PASSWORD: root | ||
MYSQL_DATABASE: keycloak | ||
MYSQL_USER: keycloak | ||
MYSQL_PASSWORD: password | ||
# Copy-pasted from https://github.com/docker-library/mariadb/issues/94 | ||
healthcheck: | ||
test: ["CMD", "mysqladmin", "ping", "--silent"] | ||
volumes: | ||
data: | ||
driver: local |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
<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/maven-v4_0_0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
<groupId>org.keycloak.plugin.rhmi</groupId> | ||
<artifactId>authdelay</artifactId> | ||
<packaging>jar</packaging> | ||
<version>0.0.1-SNAPSHOT</version> | ||
<name>authdelay Maven Webapp</name> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>junit</groupId> | ||
<artifactId>junit</artifactId> | ||
<version>3.8.1</version> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-server-spi</artifactId> | ||
<scope>provided</scope> | ||
<version>9.0.2</version> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-server-spi-private</artifactId> | ||
<version>9.0.2</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.keycloak</groupId> | ||
<artifactId>keycloak-services</artifactId> | ||
<version>9.0.2</version> | ||
<exclusions> | ||
<exclusion> | ||
<groupId>com.google.guava</groupId> | ||
<artifactId>guava</artifactId> | ||
</exclusion> | ||
</exclusions> | ||
</dependency> | ||
</dependencies> | ||
<build> | ||
<finalName>authdelay</finalName> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-jar-plugin</artifactId> | ||
<version>2.3.1</version> | ||
<configuration> | ||
<outputDirectory>./plugins/</outputDirectory> | ||
</configuration> | ||
</plugin> | ||
</plugins> | ||
|
||
|
||
</build> | ||
|
||
</project> |
165 changes: 165 additions & 0 deletions
165
src/main/java/org/keycloak/plugin/rhmi/DelayAuthentication.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package org.keycloak.plugin.rhmi; | ||
|
||
import org.keycloak.authentication.AuthenticationFlowContext; | ||
import org.keycloak.authentication.AuthenticationFlowError; | ||
import org.keycloak.authentication.Authenticator; | ||
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator; | ||
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants; | ||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; | ||
import org.keycloak.broker.provider.BrokeredIdentityContext; | ||
import org.keycloak.models.ClientModel; | ||
import org.keycloak.models.FederatedIdentityModel; | ||
import org.keycloak.models.KeycloakSession; | ||
import org.keycloak.models.RealmModel; | ||
import org.keycloak.models.UserModel; | ||
import org.keycloak.services.Urls; | ||
import org.keycloak.sessions.AuthenticationSessionModel; | ||
import org.jboss.logging.Logger; | ||
|
||
|
||
import java.net.URI; | ||
import java.util.List; | ||
import java.util.Locale; | ||
|
||
import javax.ws.rs.core.Response; | ||
import org.keycloak.utils.MediaType; | ||
|
||
public class DelayAuthentication extends AbstractIdpAuthenticator implements Authenticator { | ||
|
||
private static final String USER_CREATED_ATTRIBUTE = "3scale_user_created"; | ||
private static final String USER_CREATED_VALUE = "true"; | ||
private static final Logger logger = Logger.getLogger(DelayAuthentication.class); | ||
|
||
|
||
public void close() { | ||
} | ||
|
||
/** | ||
* checks whether user has been created in keycloak and received creation attribute | ||
* and received the created attribute set to true | ||
* @param user | ||
* @return | ||
*/ | ||
private boolean isUserCreated(UserModel user) { | ||
|
||
if (user == null) { | ||
logger.debug("User is null"); | ||
return false; | ||
} | ||
|
||
String userCreatedAtt = user.getFirstAttribute(DelayAuthentication.USER_CREATED_ATTRIBUTE); | ||
if (userCreatedAtt == null) { | ||
logger.debugf("User is created but %s attribute in keycloak is null", DelayAuthentication.USER_CREATED_ATTRIBUTE); | ||
return false; | ||
} | ||
|
||
if (!userCreatedAtt.equals(DelayAuthentication.USER_CREATED_VALUE)) { | ||
logger.debugf("%s attribute value in keycloak is not equals to %s", | ||
DelayAuthentication.USER_CREATED_ATTRIBUTE, | ||
DelayAuthentication.USER_CREATED_VALUE | ||
); | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
|
||
|
||
public boolean requiresUser() { | ||
return false; | ||
} | ||
|
||
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { | ||
return false; | ||
} | ||
|
||
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { | ||
} | ||
|
||
|
||
@Override | ||
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, | ||
BrokeredIdentityContext brokerContext) { | ||
|
||
if (isUserCreated(context.getUser())) { | ||
redirectToAfterFirstBrokerLoginSuccess(context, brokerContext); | ||
} else { | ||
showAccountProvisioningPage(context); | ||
} | ||
} | ||
|
||
@Override | ||
protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, | ||
BrokeredIdentityContext brokerContext) { | ||
|
||
// get user by federated identity - IdentityBrokerService method authenticated | ||
KeycloakSession session = context.getSession(); | ||
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(brokerContext.getIdpConfig().getAlias(), brokerContext.getId(), | ||
brokerContext.getUsername(), brokerContext.getToken()); | ||
|
||
RealmModel realm = context.getRealm(); | ||
|
||
UserModel federatedUser = session.users().getUserByFederatedIdentity(federatedIdentityModel, realm); | ||
context.setUser(federatedUser); | ||
|
||
if (isUserCreated(context.getUser())) { | ||
redirectToAfterFirstBrokerLoginSuccess(context, brokerContext); | ||
} else { | ||
showAccountProvisioningPage(context); | ||
} | ||
} | ||
|
||
private void showAccountProvisioningPage(AuthenticationFlowContext context) { | ||
String accessCode = context.generateAccessCode(); | ||
URI action = context.getActionUrl(accessCode); | ||
String templatedHTML = getTemplateHTML(action); | ||
|
||
Response challengeResponse = Response | ||
.status(Response.Status.OK) | ||
.type(MediaType.TEXT_HTML_UTF_8) | ||
.language(Locale.ENGLISH) | ||
.entity(templatedHTML) | ||
.build(); | ||
context.challenge(challengeResponse); | ||
} | ||
|
||
/** | ||
* redirects user | ||
* @param context | ||
* @param brokerContext | ||
*/ | ||
private void redirectToAfterFirstBrokerLoginSuccess(AuthenticationFlowContext context, BrokeredIdentityContext brokerContext) { | ||
|
||
AuthenticationSessionModel authSession = context.getAuthenticationSession(); | ||
|
||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.serialize(brokerContext); | ||
serializedCtx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); | ||
|
||
// sets AFTER_FIRST_BROKER_LOGIN to true | ||
authSession.setAuthNote(PostBrokerLoginConstants.PBL_AFTER_FIRST_BROKER_LOGIN, String.valueOf(true)); | ||
|
||
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + brokerContext.getIdpConfig().getAlias(); | ||
authSession.setAuthNote(authStateNoteKey, "true"); | ||
|
||
RealmModel realm = context.getRealm(); | ||
ClientModel authSessionClientModel = authSession.getClient(); | ||
URI baseUri = context.getUriInfo().getBaseUri(); | ||
|
||
URI redirect = Urls.identityProviderAfterPostBrokerLogin(baseUri, realm.getName(), context.generateAccessCode(), authSessionClientModel.getClientId(), authSession.getTabId()); | ||
Response challengeResponse = Response.status(302) | ||
.location(redirect) | ||
.build(); | ||
context.challenge(challengeResponse); | ||
} | ||
|
||
private String getTemplateHTML(URI actionURI) { | ||
return "<!DOCTYPE html><html><head>" | ||
+ "<meta charset=\"UTF-8\"><meta http-equiv=\"refresh\" content=\"5;" + actionURI + "\" />" | ||
+ "<title>Red Hat Managed Integrations</title></head>" | ||
+ "<body>" | ||
+ "<h1>Your account is being provisioned</h1>" | ||
+ "<h3>Please wait for a moment...</h3></body></html>"; | ||
} | ||
|
||
} |
Oops, something went wrong.