Skip to content

Commit

Permalink
Merge pull request #1 from jjaferson/master
Browse files Browse the repository at this point in the history
Delay authentication plugin for keycloak
  • Loading branch information
matskiv authored Apr 9, 2020
2 parents fa7c62f + 42d1211 commit 84a9cff
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 0 deletions.
27 changes: 27 additions & 0 deletions .classpath
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>
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,11 @@

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

# MacOS files
.DS_Store

/target/*
/plugin/*
.settings/*
.project
40 changes: 40 additions & 0 deletions README.md
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.
36 changes: 36 additions & 0 deletions docker-compose.yaml
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
57 changes: 57 additions & 0 deletions pom.xml
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 src/main/java/org/keycloak/plugin/rhmi/DelayAuthentication.java
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>";
}

}
Loading

0 comments on commit 84a9cff

Please sign in to comment.