diff --git a/.classpath b/.classpath
new file mode 100644
index 0000000..644a6fe
--- /dev/null
+++ b/.classpath
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.gitignore b/.gitignore
index a1c2a23..2d79ada 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..da18f8e
--- /dev/null
+++ b/README.md
@@ -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/.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.
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..e7f50f9
--- /dev/null
+++ b/docker-compose.yaml
@@ -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
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..40798d3
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,57 @@
+
+ 4.0.0
+ org.keycloak.plugin.rhmi
+ authdelay
+ jar
+ 0.0.1-SNAPSHOT
+ authdelay Maven Webapp
+
+
+
+ junit
+ junit
+ 3.8.1
+ test
+
+
+ org.keycloak
+ keycloak-server-spi
+ provided
+ 9.0.2
+
+
+ org.keycloak
+ keycloak-server-spi-private
+ 9.0.2
+ provided
+
+
+ org.keycloak
+ keycloak-services
+ 9.0.2
+
+
+ com.google.guava
+ guava
+
+
+
+
+
+ authdelay
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 2.3.1
+
+ ./plugins/
+
+
+
+
+
+
+
+
diff --git a/src/main/java/org/keycloak/plugin/rhmi/DelayAuthentication.java b/src/main/java/org/keycloak/plugin/rhmi/DelayAuthentication.java
new file mode 100644
index 0000000..c95157c
--- /dev/null
+++ b/src/main/java/org/keycloak/plugin/rhmi/DelayAuthentication.java
@@ -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 ""
+ + ""
+ + "Red Hat Managed Integrations"
+ + ""
+ + "Your account is being provisioned
"
+ + "Please wait for a moment...
";
+ }
+
+}
diff --git a/src/main/java/org/keycloak/plugin/rhmi/DelayAuthenticationAuthenticatorFactory.java b/src/main/java/org/keycloak/plugin/rhmi/DelayAuthenticationAuthenticatorFactory.java
new file mode 100644
index 0000000..b4db2f0
--- /dev/null
+++ b/src/main/java/org/keycloak/plugin/rhmi/DelayAuthenticationAuthenticatorFactory.java
@@ -0,0 +1,70 @@
+package org.keycloak.plugin.rhmi;
+
+import java.util.List;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.provider.ProviderConfigProperty;
+
+public class DelayAuthenticationAuthenticatorFactory implements AuthenticatorFactory {
+
+
+ private static final DelayAuthentication SINGLETON = new DelayAuthentication();
+
+ public Authenticator create(KeycloakSession session) {
+ return SINGLETON;
+ }
+
+ public void init(Scope config) {
+ }
+
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ public void close() {
+ }
+
+ public String getId() {
+ return "delay-authentication";
+ }
+
+ public String getDisplayType() {
+ return "Delay Authentication";
+ }
+
+ public String getReferenceCategory() {
+ return "Delay Authentication";
+ }
+
+ /**
+ * Delay needs to be mandatory
+ */
+ public boolean isConfigurable() {
+ return false;
+ }
+
+ public Requirement[] getRequirementChoices() {
+ Requirement[] req = {
+ AuthenticationExecutionModel.Requirement.REQUIRED
+ };
+ return req;
+ }
+
+ public boolean isUserSetupAllowed() {
+ return false;
+ }
+
+ public String getHelpText() {
+ return "Delay authentication until user is created on 3scale by the rhmi operator";
+ }
+
+ public List getConfigProperties() {
+ return null;
+ }
+
+}
diff --git a/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
new file mode 100644
index 0000000..5b9e96f
--- /dev/null
+++ b/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2016 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.plugin.rhmi.DelayAuthenticationAuthenticatorFactory
\ No newline at end of file