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