diff --git a/code/authentication-spi/telegram-authentication-spi/.gitignore b/code/authentication-spi/telegram-authentication-spi/.gitignore new file mode 100644 index 0000000..693002a --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/code/authentication-spi/telegram-authentication-spi/README.md b/code/authentication-spi/telegram-authentication-spi/README.md new file mode 100644 index 0000000..dd03b2b --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/README.md @@ -0,0 +1,10 @@ +rh-sso-custom-authentication-spi +--- + +Expected variable as host:[port] default value: localhost:8081 => USER_STORAGE_CUSTOM_SPI_TARGET_HOST + +Must be setted (no default value): +Telegram bot token variable: TELEGRAM_TOKEN +Telegram bot name: TELEGRAM_BOT_USERNAME + +The Authenticator has been developed with a rolling action for obtaining the telegram ID instead of the @username for simplification; this avoids the use of the telegram api_id, phone_registered and api_hash. If desired, the authenticator can be modified for doing a lookup of the Telegram ID from the @username calling https://core.telegram.org/method/users.getFullUser diff --git a/code/authentication-spi/telegram-authentication-spi/pom.xml b/code/authentication-spi/telegram-authentication-spi/pom.xml new file mode 100644 index 0000000..f11b19f --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + com.olleb + telegram-authentication-spi + 1.0-SNAPSHOT + jar + + + UTF-8 + 11 + 11 + + 15.0.1.Final + + 18.0.2 + + + + + + + org.wildfly.bom + wildfly-javaee8-with-tools + ${version.server.bom} + pom + import + + + + + + + org.jboss.resteasy + resteasy-client + + + org.jboss.resteasy + resteasy-jackson2-provider + + + org.jboss.logging + jboss-logging + provided + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-services + ${keycloak.version} + provided + + + + org.junit.jupiter + junit-jupiter + 5.9.1 + test + + + org.hamcrest + hamcrest-core + 2.2 + test + + + diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredAction.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredAction.java new file mode 100644 index 0000000..2620d26 --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredAction.java @@ -0,0 +1,113 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.action; + +import java.lang.invoke.MethodHandles; +import java.util.Locale; +import java.util.function.Predicate; + +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.common.util.SecretGenerator; +import org.keycloak.theme.Theme; + +import com.olleb.service.TelegramMessage; +import com.olleb.service.TelegramService; + +/** + * @author Àngel Ollé Blázquez + */ +public class TelegramRequiredAction implements RequiredActionProvider { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private static final TelegramService TELEGRAM_SERVICE = TelegramService.getInstance(); + + private static final Predicate BLANK = s -> s == null || s.isBlank(); + + @Override + public void evaluateTriggers(RequiredActionContext context) { + String telegramId = context.getUser().getFirstAttribute(TelegramRequiredActionConstants.TELEGRAM_ID_ATTRIBUTE); + if (BLANK.test(telegramId)) { + context.getUser().addRequiredAction(TelegramRequiredActionConstants.PROVIDER_ID); + } + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + try { + Theme theme = context.getSession().theme().getTheme(Theme.Type.LOGIN); + String msg = theme.getMessages(Locale.ENGLISH) + .getProperty(TelegramRequiredActionConstants.TELEGRAM_ID_PROPERTY); + String code = SecretGenerator.getInstance().randomString( + TelegramRequiredActionConstants.TELEGRAM_ROLLING_CODE_LENGTH, + SecretGenerator.DIGITS); + + context.getAuthenticationSession() + .setAuthNote(TelegramRequiredActionConstants.TELEGRAM_SECURITY_CODE_ATTRIBUTE, code); + + msg = String.format(msg, code, TelegramService.getTelegramBotUsername()); + + Response response = context.form() + .setAttribute(TelegramRequiredActionConstants.TELEGRAM_MESSAGE_SECURITY_CODE_ATTRIBUTE, msg) + .createForm(TelegramRequiredActionConstants.TEMPLATE); + + context.challenge(response); + } catch (Exception e) { + LOGGER.error(e); + } + } + + @Override + public void processAction(RequiredActionContext context) { + String subscriptionCode = context.getHttpRequest().getDecodedFormParameters() + .getFirst(TelegramRequiredActionConstants.TELEGRAM_ROLLING_CODE_ATTRIBUTE); + + if (BLANK.test(subscriptionCode)) { + requiredActionChallenge(context); + return; + } + + String code = context.getAuthenticationSession() + .getAuthNote(TelegramRequiredActionConstants.TELEGRAM_SECURITY_CODE_ATTRIBUTE); + TelegramMessage message = TELEGRAM_SERVICE.getMessageFromSecurityCode(code); + + if (message != null && message.getSubscriptionCode().equals(subscriptionCode)) { + String userId = message.getId(); + String username = message.getUsername(); + + context.getUser().setSingleAttribute(TelegramRequiredActionConstants.TELEGRAM_ID_ATTRIBUTE, userId); + context.getUser().setSingleAttribute(TelegramRequiredActionConstants.TELEGRAM_USERNAME_ATTRIBUTE, username); + context.getUser().removeRequiredAction(TelegramRequiredActionConstants.PROVIDER_ID); + + context.success(); + return; + } + + requiredActionChallenge(context); + } + + @Override + public void close() { + // no-op + } + +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredActionConstants.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredActionConstants.java new file mode 100644 index 0000000..2503eec --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredActionConstants.java @@ -0,0 +1,37 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.action; + +public final class TelegramRequiredActionConstants { + + static final String PROVIDER_ID = "telegram-otp-required-action"; + static final String DISPLAY_TEXT = "Telegram ID"; + + static final String TEMPLATE = "telegram-id-config.ftl"; + static final String TELEGRAM_ID_ATTRIBUTE = "telegram_id"; + static final String TELEGRAM_ID_PROPERTY = "telegram-id-label"; + static final String TELEGRAM_USERNAME_ATTRIBUTE = "telegram_username"; + static final String TELEGRAM_ROLLING_CODE_ATTRIBUTE = "telegram_code"; + static final String TELEGRAM_SECURITY_CODE_ATTRIBUTE = "scode"; + static final String TELEGRAM_MESSAGE_SECURITY_CODE_ATTRIBUTE = "msgcode"; + static final int TELEGRAM_ROLLING_CODE_LENGTH = 6; + + private TelegramRequiredActionConstants() { + // no-op constructor + } +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredActionFactory.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredActionFactory.java new file mode 100644 index 0000000..c733cee --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/action/TelegramRequiredActionFactory.java @@ -0,0 +1,63 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.action; + +import org.keycloak.Config.Scope; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Àngel Ollé Blázquez + */ +public class TelegramRequiredActionFactory implements RequiredActionFactory { + + private static final TelegramRequiredAction TELEGRAM_REQUIRED_ACTION = new TelegramRequiredAction(); + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return TELEGRAM_REQUIRED_ACTION; + } + + @Override + public String getId() { + return TelegramRequiredActionConstants.PROVIDER_ID; + } + + @Override + public String getDisplayText() { + return TelegramRequiredActionConstants.DISPLAY_TEXT; + } + + @Override + public void init(Scope config) { + // no-op + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticator.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticator.java new file mode 100644 index 0000000..c95179b --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticator.java @@ -0,0 +1,107 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.authenticator; + +import java.lang.invoke.MethodHandles; +import java.util.Locale; +import java.util.Map; + +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.Authenticator; +import org.keycloak.common.util.SecretGenerator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.theme.Theme; + +import com.olleb.service.TelegramService; + +/** + * @author Àngel Ollé Blázquez + */ +public class TelegramAuthenticator implements Authenticator { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + @Override + public void authenticate(AuthenticationFlowContext context) { + Map config = context.getAuthenticatorConfig().getConfig(); + KeycloakSession keycloakSession = context.getSession(); + + String id = context.getUser().getFirstAttribute(TelegramAuthenticatorConstants.TELEGRAM_ID_ATTRIBUTE); + + try { + int length = Integer.parseInt(config.get(TelegramAuthenticatorConstants.LC_NAME)); + String code = SecretGenerator.getInstance().randomString(length, SecretGenerator.DIGITS); + + // EN only for this demo + Theme theme = keycloakSession.theme().getTheme(Theme.Type.LOGIN); + String msg = theme.getMessages(Locale.ENGLISH) + .getProperty(TelegramAuthenticatorConstants.TELEGRAM_MSG_PROPERTY_DESC); + + msg = String.format(msg, code); + + TelegramService.getInstance().send(id, msg); + context.getAuthenticationSession().setAuthNote("code", code); + + Response response = context.form().createForm(TelegramAuthenticatorConstants.TEMPLATE); + context.challenge(response); + + } catch (Exception e) { + LOGGER.error(e); + } + } + + @Override + public void action(AuthenticationFlowContext context) { + String formCode = context.getHttpRequest().getDecodedFormParameters().getFirst("telegram_code"); + String code = context.getAuthenticationSession().getAuthNote("code"); + + if (code == null) { + return; + } + + if (formCode.equals(code)) { + context.success(); + } + } + + @Override + public boolean requiresUser() { + return true; + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + String id = user.getFirstAttribute(TelegramAuthenticatorConstants.TELEGRAM_ID_ATTRIBUTE); + return !(id == null || id.isBlank()); + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + user.addRequiredAction(TelegramAuthenticatorConstants.REQUIRED_ACTION); + } + + @Override + public void close() { + // no-op + } +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticatorConstants.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticatorConstants.java new file mode 100644 index 0000000..1186532 --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticatorConstants.java @@ -0,0 +1,42 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.authenticator; + +public final class TelegramAuthenticatorConstants { + + static final String PROVIDER_ID = "telegram-authenticator"; + + static final String REQUIRED_ACTION = "telegram-otp-required-action"; + static final String TEMPLATE = "telegram-otp.ftl"; + static final String TELEGRAM_ID_ATTRIBUTE = "telegram_id"; + static final String TELEGRAM_MSG_PROPERTY_DESC = "telegram-otp-desc"; + + static final String DISPLAY_TYPE = "Telegram Authentication"; + static final String REFERENCE_CATEGORY = "otp"; + static final String HELP_TEXT = "OTP Telegram Authentication"; + + // length config properties + static final String LC_NAME = "length"; + static final String LC_LABEL = "Code length"; + static final String LC_HELP_TEXT = "Number of digits of the security code"; + static final int LC_DEFAULT_VALUE = 6; + + private TelegramAuthenticatorConstants() { + } + +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticatorFactory.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticatorFactory.java new file mode 100644 index 0000000..952fcfc --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/authenticator/TelegramAuthenticatorFactory.java @@ -0,0 +1,109 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.authenticator; + +import java.util.List; + +import org.keycloak.Config.Scope; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +/** + * @author Àngel Ollé Blázquez + */ +public class TelegramAuthenticatorFactory implements AuthenticatorFactory { + + private static final TelegramAuthenticator TELEGRAM_AUTHENTICATOR = new TelegramAuthenticator(); + + private static final List configProperties; + + static { + configProperties = List.of( + new ProviderConfigProperty( + TelegramAuthenticatorConstants.LC_NAME, + TelegramAuthenticatorConstants.LC_LABEL, + TelegramAuthenticatorConstants.LC_HELP_TEXT, + ProviderConfigProperty.STRING_TYPE, + TelegramAuthenticatorConstants.LC_DEFAULT_VALUE)); + } + + @Override + public Authenticator create(KeycloakSession session) { + return TELEGRAM_AUTHENTICATOR; + } + + @Override + public String getId() { + return TelegramAuthenticatorConstants.PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return TelegramAuthenticatorConstants.DISPLAY_TYPE; + } + + @Override + public String getReferenceCategory() { + return TelegramAuthenticatorConstants.REFERENCE_CATEGORY; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public boolean isUserSetupAllowed() { + return true; + } + + @Override + public String getHelpText() { + return TelegramAuthenticatorConstants.HELP_TEXT; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public void init(Scope config) { + // no-op + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramMessage.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramMessage.java new file mode 100644 index 0000000..16421af --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramMessage.java @@ -0,0 +1,79 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.service; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * @author Àngel Ollé Blázquez + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class TelegramMessage { + + private String id; + private String username; + private String text; + private String subscriptionCode; + + public String getUsername() { + return username; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getSubscriptionCode() { + return subscriptionCode; + } + + public void setSubscriptionCode(String subscriptionCode) { + this.subscriptionCode = subscriptionCode; + } + + @JsonProperty("message") + private void unpackNameFromNestedObject(JsonNode json) { + id = unpackChatValueFromMessageChat(json, "id"); + username = unpackChatValueFromMessageChat(json, "username"); + text = json.get("text").asText(); + } + + private String unpackChatValueFromMessageChat(JsonNode json, String property) { + return json.get("chat").get(property).asText(); + } + + @Override + public String toString() { + return "TelegramMessage [id=" + id + ", username=" + username + ", text=" + text + ", subscriptionCode=" + + subscriptionCode + "]"; + } +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramService.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramService.java new file mode 100644 index 0000000..045c78a --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramService.java @@ -0,0 +1,135 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.service; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; + +import org.jboss.logging.Logger; +import org.keycloak.common.util.SecretGenerator; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Àngel Ollé Blázquez + */ +public class TelegramService { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private static TelegramService telegramService; + + private static final ObjectMapper mapper = new ObjectMapper(); + + // only for demo purposes, can cause memory leak + private static List messages = new ArrayList<>(); + + private static int lastUpdateId = 0; + + // SimpleHttp or JsonSimpleHttp from keycloak can be used instead + private static final WebTarget target = ClientBuilder + .newClient() + .target(TelegramServiceConstants.TELEGRAM_BASE_URL) + .resolveTemplate("apitoken", TelegramServiceConstants.TELEGRAM_TOKEN); + + private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + static { + Runnable updateTask = () -> { + String offset = lastUpdateId > 0 ? Integer.toString(lastUpdateId) : ""; + Response response = target.path("getUpdates") + .queryParam("offset", offset) + .request() + .get(); + if (response != null) { + JsonNode json; + try { + json = mapper.readTree(response.readEntity(String.class)).get("result"); + if (json.size() > 0) { + if (lastUpdateId == 0) { + lastUpdateId = json.get(json.size() - 1).get("update_id").intValue(); + } + lastUpdateId++; + } + + messages.addAll(mapper.convertValue(json, + new TypeReference>() { + })); + } catch (IOException e) { + LOGGER.error(e); + } + } + messages.stream().forEach(m -> { + if (m.getSubscriptionCode() == null) { + String subscriptionCode = "S" + SecretGenerator.getInstance().randomString( + TelegramServiceConstants.SUBSCRIPTIONCODE_LENGTH, + SecretGenerator.ALPHANUM); + m.setSubscriptionCode(subscriptionCode); + target.path("sendMessage") + .queryParam("chat_id", m.getId()) + .queryParam("text", "SSO: Your enrollment code is: " + subscriptionCode) + .request() + .get(); + } + }); + + }; + + executorService.scheduleAtFixedRate(updateTask, 5, 15, TimeUnit.SECONDS); + + // demo purposes, no graceful needed + Runtime.getRuntime().addShutdownHook(new Thread(executorService::shutdownNow)); + } + + private TelegramService() { + } + + public void send(String id, String message) { + target.path("sendMessage") + .queryParam("chat_id", id) + .queryParam("text", message) + .request() + .get(); + } + + public TelegramMessage getMessageFromSecurityCode(String code) { + return messages.stream().filter(m -> m.getText().equals(code)).findAny().orElse(null); + } + + public static TelegramService getInstance() { + if (telegramService == null) { + telegramService = new TelegramService(); + } + return telegramService; + } + + public static String getTelegramBotUsername() { + return TelegramServiceConstants.TELEGRAM_BOT_USERNAME; + } + +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramServiceConstants.java b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramServiceConstants.java new file mode 100644 index 0000000..cf93db4 --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/java/com/olleb/service/TelegramServiceConstants.java @@ -0,0 +1,35 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.service; + +import java.util.Optional; + +public final class TelegramServiceConstants { + + static final String TELEGRAM_BOT_USERNAME = Optional.ofNullable(System.getenv("TELEGRAM_BOT_USERNAME")) + .orElseThrow(IllegalArgumentException::new); + + static final String TELEGRAM_TOKEN = Optional.ofNullable(System.getenv("TELEGRAM_TOKEN")) + .orElseThrow(IllegalArgumentException::new); + + static final String TELEGRAM_BASE_URL = "https://api.telegram.org/bot{apitoken}"; + static final int SUBSCRIPTIONCODE_LENGTH = 10; + + private TelegramServiceConstants() { + } +} diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/code/authentication-spi/telegram-authentication-spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100644 index 0000000..3364419 --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1 @@ +com.olleb.authenticator.TelegramAuthenticatorFactory diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/code/authentication-spi/telegram-authentication-spi/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory new file mode 100644 index 0000000..f152803 --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory @@ -0,0 +1 @@ +com.olleb.action.TelegramRequiredActionFactory diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/messages/messages_en.properties b/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/messages/messages_en.properties new file mode 100644 index 0000000..ce31896 --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/messages/messages_en.properties @@ -0,0 +1,6 @@ +telegram-otp-desc=SSO: Your security code is %1$s. +telegram-otp-field=Insert your Telegram 2FA code +telegram-update-id=Telegram 2FA Subscription +telegram-id-label=Please, follow the instructions below for updating your Telegram ID.\ +Open the telegram application and send /start and after the following enrollment code %1$s to %2$s.\ +Introduce the bot response subscription code below. diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/templates/telegram-id-config.ftl b/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/templates/telegram-id-config.ftl new file mode 100644 index 0000000..8f629ed --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/templates/telegram-id-config.ftl @@ -0,0 +1,23 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "header"> + ${msg("telegram-update-id")} + <#elseif section = "form"> +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ + diff --git a/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/templates/telegram-otp.ftl b/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/templates/telegram-otp.ftl new file mode 100644 index 0000000..f0450cc --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/main/resources/theme-resources/templates/telegram-otp.ftl @@ -0,0 +1,21 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "form"> +
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+ + diff --git a/code/authentication-spi/telegram-authentication-spi/src/test/java/com/olleb/SerializationTest.java b/code/authentication-spi/telegram-authentication-spi/src/test/java/com/olleb/SerializationTest.java new file mode 100644 index 0000000..215421d --- /dev/null +++ b/code/authentication-spi/telegram-authentication-spi/src/test/java/com/olleb/SerializationTest.java @@ -0,0 +1,49 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.olleb.service.TelegramMessage; + +public class SerializationTest { + + private static final String json = "[{\"update_id\":981138569,\"message\":{\"message_id\":9,\"from\":{\"id\":1111111,\"is_bot\":false,\"first_name\":\"Àngel\",\"last_name\":\"O.\",\"username\":\"anyusername\",\"language_code\":\"en\"},\"chat\":{\"id\":1111111,\"first_name\":\"Àngel\",\"last_name\":\"O.\",\"username\":\"anyusername\",\"type\":\"private\"},\"date\":1671026112,\"text\":\"834529\"}},{\"update_id\":981138570,\"message\":{\"message_id\":10,\"from\":{\"id\":1111111,\"is_bot\":false,\"first_name\":\"Àngel\",\"last_name\":\"O.\",\"username\":\"anyusername\",\"language_code\":\"en\"},\"chat\":{\"id\":1111111,\"first_name\":\"Àngel\",\"last_name\":\"O.\",\"username\":\"anyusername\",\"type\":\"private\"},\"date\":1671026420,\"text\":\"421120\"}}]"; + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testJsonNodeToTelegramMessageList() throws JsonParseException, JsonProcessingException, IOException { + JsonNode jsonNode = mapper.readTree(mapper.getFactory().createParser(json)); + List list = mapper.convertValue(jsonNode, + new TypeReference>() { + }); + assertThat(list.isEmpty(), is(false)); + } + +} diff --git a/code/exports/demo-realm.json b/code/exports/demo-realm.json new file mode 100644 index 0000000..9d32ddb --- /dev/null +++ b/code/exports/demo-realm.json @@ -0,0 +1,2379 @@ +{ + "id": "demo", + "realm": "demo", + "notBefore": 1672412231, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "cf384c61-12f6-45ae-9259-e6ca640658ee", + "name": "default-roles-demo", + "description": "${role_default-roles}", + "composite": true, + "composites": { + "realm": [ + "offline_access", + "uma_authorization" + ], + "client": { + "account": [ + "manage-account", + "view-profile" + ] + } + }, + "clientRole": false, + "containerId": "demo", + "attributes": {} + }, + { + "id": "5b024183-5465-4c79-8f73-f4a468c8520b", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "demo", + "attributes": {} + }, + { + "id": "98b6763d-1652-4f41-aa0c-ce415ba45129", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "demo", + "attributes": {} + }, + { + "id": "01887db1-195a-4cf3-84ca-9c7e2573c788", + "name": "assistant", + "composite": false, + "clientRole": false, + "containerId": "demo", + "attributes": {} + }, + { + "id": "7e94126e-4fce-43d3-96dd-57fa79154b64", + "name": "vet", + "description": "Veterinary role", + "composite": false, + "clientRole": false, + "containerId": "demo", + "attributes": {} + } + ], + "client": { + "realm-management": [ + { + "id": "3f6c20cf-d4fd-4444-a380-23b662fb8e81", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "e9927fa4-3d48-4e4f-87ed-b7ad54b4ec1a", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "c95c155a-ac12-4fd2-95f5-dd1ec90bffc2", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "b819c1fd-38c4-447c-9d30-6be9df5e2cb1", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "7b8a16c4-3691-4e82-9b70-4550176c281a", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "9012b77a-2cae-4705-bc0f-671e6b0acf36", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "b768fb62-0dd4-4421-ad29-e13dc04e5829", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "c0d00673-2dd0-477f-b856-f1209b022b8b", + "name": "realm-admin", + "description": "${role_realm-admin}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "view-realm", + "view-authorization", + "query-users", + "create-client", + "view-clients", + "view-identity-providers", + "query-groups", + "manage-realm", + "view-events", + "manage-events", + "manage-users", + "view-users", + "query-clients", + "manage-authorization", + "query-realms", + "manage-identity-providers", + "impersonation", + "manage-clients" + ] + } + }, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "3847d846-7317-495e-8659-4ed285f24813", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "78574170-6403-462b-a555-f35f798aa13c", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "1d2d71e9-c7e9-4e5e-ba03-02863f254135", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "a4e80140-950f-4aa4-9e3a-2bb39a40a36b", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "9cc647a2-f547-42d0-a6e5-12e969982640", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "0ad5aa0b-06c6-43b4-89f6-1a209e583139", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "realm-management": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "7239f3a9-afe8-4a43-9c44-4867fbf6dab1", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "73c6d6a4-5d1c-4371-9664-4a82a05e6678", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "65d4eacd-e2cf-4807-9595-c363391f1c51", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "20a77e61-aa07-403e-8cc0-cf34873ce864", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + }, + { + "id": "7299fb84-9546-4c23-9c11-20fe1dcc4443", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "attributes": {} + } + ], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "e7f11882-5d9b-4999-bac5-4bc263478c9d", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "ee8ecc7d-3cc9-43af-909d-4d227c102a14", + "attributes": {} + } + ], + "quarkus-petclinic": [ + { + "id": "719c8848-82a2-4fe3-8a6b-5c390ca4299d", + "name": "uma_protection", + "composite": false, + "clientRole": true, + "containerId": "6898ce4b-12c3-4d3e-b2cb-fbcf7c9aaac4", + "attributes": {} + } + ], + "account": [ + { + "id": "e342814f-ba91-4180-ba91-05b9200c6e1e", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + }, + { + "id": "bbca3e08-0a24-4686-8da9-49e23416d6ad", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + }, + { + "id": "559d5ea2-b441-46b5-92ee-255ca40aac6b", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + }, + { + "id": "d6c28df5-61b0-4818-bad4-a65db8a96e8a", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + }, + { + "id": "d9cf60c4-dba0-45a4-88aa-f39021843f51", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + }, + { + "id": "9a455a38-5c43-40c2-ab42-9a31059ddebd", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + }, + { + "id": "f421aa3c-e482-42b0-82ea-631269a434e2", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "3e452e38-4afa-4088-b10f-0257aa26314b", + "attributes": {} + } + ] + } + }, + "groups": [ + { + "id": "a7fee573-d6fc-4445-9b3e-50ae86aef1b5", + "name": "staff", + "path": "/staff", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [ + { + "id": "e0478cfd-b74e-405e-8859-399256822572", + "name": "Personal staff", + "path": "/staff/Personal staff", + "attributes": {}, + "realmRoles": [], + "clientRoles": {}, + "subGroups": [] + } + ] + } + ], + "defaultRole": { + "id": "cf384c61-12f6-45ae-9259-e6ca640658ee", + "name": "default-roles-demo", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "demo" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "11a4d9d8-6b2f-4275-8559-00960ae3080c", + "createdTimestamp": 1672410647535, + "username": "service-account-quarkus-petclinic", + "enabled": true, + "totp": false, + "emailVerified": false, + "serviceAccountClientId": "quarkus-petclinic", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "default-roles-demo" + ], + "clientRoles": { + "quarkus-petclinic": [ + "uma_protection" + ] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "3e452e38-4afa-4088-b10f-0257aa26314b", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/demo/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/demo/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "e89fdbbe-e035-4d62-a0e5-54c1f4340d4b", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/demo/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/demo/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "b6cfc758-98b1-4498-a256-0d3a6837c1db", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "dc019342-66aa-407b-807e-bd76439a8ee6", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "ee8ecc7d-3cc9-43af-909d-4d227c102a14", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "6898ce4b-12c3-4d3e-b2cb-fbcf7c9aaac4", + "clientId": "quarkus-petclinic", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "**********", + "redirectUris": [ + "http://localhost:8081/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "frontchannel.logout.session.required": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "saml.allow.ecp.flow": "false", + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "client.secret.creation.time": "1672410647", + "saml.encrypt": "false", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "saml.artifact.binding": "false", + "saml_force_name_id_format": "false", + "acr.loa.map": "{}", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "d5a21778-1c82-43a2-8a1d-e9ae7412da4c", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "dc070144-d39b-4949-813a-f571a99ce7c3", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "af1db684-8561-4ece-8f5a-8716cfdfb0f2", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "authorizationSettings": { + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "resources": [ + { + "name": "Default Resource", + "type": "urn:quarkus-petclinic:resources:default", + "ownerManagedAccess": false, + "attributes": {}, + "_id": "200dcb1b-b2f3-49de-915c-73f9137b7855", + "uris": [ + "/*" + ] + }, + { + "name": "Vets Resource", + "ownerManagedAccess": false, + "displayName": "Vets Resource", + "attributes": {}, + "_id": "6cba842b-e868-49b3-8a82-f7dede316f21", + "uris": [ + "/vets.html" + ] + } + ], + "policies": [ + { + "id": "c7500782-9404-47c5-9047-25a159412c97", + "name": "Vet Role Policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"vet\",\"required\":true}]" + } + }, + { + "id": "03d2feb7-dac1-442d-aa4f-803bec9573aa", + "name": "Default Group Policy", + "type": "group", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "groups": "[{\"path\":\"/staff\",\"extendChildren\":true}]" + } + }, + { + "id": "b35887da-4b63-4cae-8b2e-5afb930099dc", + "name": "Default Resource Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Default Resource\"]", + "applyPolicies": "[\"Default Group Policy\"]" + } + }, + { + "id": "9381cb25-87d0-44af-b3cd-f37f3f7a7175", + "name": "Vets Resource Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "resources": "[\"Vets Resource\"]", + "applyPolicies": "[\"Vet Role Policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" + } + }, + { + "id": "ab818510-b061-414c-b9ec-62e5b842ffe3", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "32f6f1f3-873b-4f28-858b-1b70caacf9b1", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/demo/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/demo/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "99800e91-eeee-46cc-8cf3-2ff225cfb423", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "89dabfdc-61f4-48fa-ad54-07f9e3e8e3d8", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "f7e4e5f2-d4a8-48b4-b629-55b94b726595", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "37aff539-edd9-4263-ae4d-df6252dda3bf", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "7e5b7f17-8149-4435-8e11-2ea357451697", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "44d5423b-3307-41cc-bb97-8bf7af67f3e5", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "fcbec64f-fbcf-4c8b-b52b-9a4b0f8a1745", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "6ef50a4b-de01-41b2-ba05-662065073dab", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "f53dd8ad-3988-451f-821f-58b0fd4124dc", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "dee982db-9a3d-4b93-b952-64ec269c4056", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "9b5bc916-41af-4c1d-ac40-4806446a8324", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "45e86893-1aae-458c-8f82-1f4c9c698043", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "534abf9a-e54c-480f-aeb7-0d4048ff9316", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "c9c284e4-aa20-4dd2-a646-3b09664d913f", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "3e78b141-a362-4444-ae77-75ab2bde13e8", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "6432e2de-dede-49c7-95f0-e0447dece389", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "149140aa-fed1-41e3-a2d5-04bafe788c16", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "42ed0456-5a34-46c0-9707-06a0f89f91d1", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "2dc9a1ee-4a59-4b2b-9d9e-7f596b9c1830", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "f1e2741b-76ec-4872-843e-3ebab478e7fe", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "1667da3b-941e-4121-8c6c-cc6832f89e4f", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "c0d12b44-906d-4ca5-b4e4-38d4d977d6c2", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "6bbf8637-3255-416d-bbf2-be5cddd3f7af", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + }, + { + "id": "ac0f9b69-01c1-4ddf-a40d-d42340135279", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "416ed72f-b5cc-4071-851a-ca74db5dd421", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "id": "fe89f327-0ac3-4036-8d52-4aba68fd9096", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "ad7e94e8-3083-48b7-92d0-e002e2aba727", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "6f4ebcc7-6bd9-4e4b-94a7-993935224313", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "a36ea744-94d1-4821-b23d-af12b8fe4146", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "6f4da556-f027-4c94-a9e8-c8b5ec3be122", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "1ac3b2c5-6bfe-4d24-8a3e-a3c458a5bcad", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "ec9b8c64-ee7c-4ff0-8627-eb131b166d9d", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "4849edc1-ac16-4a4a-9efe-8d3337b51e6d", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "37ae89c3-4606-4ca3-9636-eb77d8ce43f6", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "aae26fc9-7374-4ab8-accb-a6419b32f6e6", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "65781f1e-bcd1-4040-a38a-0cf752d7b149", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "b3f8858e-023d-4321-be77-a52564be1c75", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "6beb1272-3463-4dfe-a8dc-e7005aac7a29", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "acr", + "email", + "role_list", + "roles", + "web-origins", + "profile" + ], + "defaultOptionalClientScopes": [ + "microprofile-jwt", + "offline_access", + "address", + "phone" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "94e81b61-29c6-46bf-b988-169c7283f3d8", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-full-name-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper" + ] + } + }, + { + "id": "15244826-c7de-419e-9f51-7fff7656d695", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "saml-role-list-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "965250ee-7d7d-4a85-9fce-e66a72b04bca", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "779fafbb-179e-40fe-ba1a-b6ca8c09c8f2", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "ba711770-f489-4d57-9717-0cb2b4022e93", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "111ac067-67fe-4134-9772-ae656b4fa17d", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "b5648a1b-6d47-4293-94b8-ce915dcb97f0", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "0208c11e-1c3a-4302-80ff-6c633eccc9c8", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "c8333e4c-7b77-4178-841c-c3153b79b827", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "74f7e193-7f1e-4e60-a64f-1a020d2fde18", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "827fa2e8-b1e4-41ff-a7cc-dbee63bd1560", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "6be122f5-9caf-4602-a877-eaf54435dedb", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "85f83c50-04d6-42e9-8d64-8ed1d1b457da", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "a64ba96c-2122-4036-b15f-9ce081b6c84e", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "basic-auth-otp", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1e7f2fed-1b4b-4a75-b14b-6602e14c5356", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "4206bf21-787c-4262-9044-52f28132d036", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9810a193-020d-4f78-96cd-bc614ee9cdfc", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "f643a9fe-df1f-4287-84df-26146505bb67", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "22b52681-1020-4701-a7ff-08060339af11", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d6b36d4a-e829-41a6-9a23-470142567d71", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "f630405c-e60e-43d0-8e4c-997c052e7958", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "3d52a778-cef1-4009-a83b-a0951de60f70", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "2bba63ea-80d9-4b57-aeac-dc7d656b816b", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "1b15e80b-023f-4d87-9045-d2203183e3ed", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "f037b84d-4c87-42c2-b2fd-26fcc6d2a24a", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "47833c6e-d3a6-4070-8228-db9868463818", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "8cdba8c4-d8e2-4b85-b3ee-c6e165cc84e7", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "42b7455d-7951-484f-9f5a-83d4205c1428", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Authentication Options", + "userSetupAllowed": false + } + ] + }, + { + "id": "552228f6-bc94-494e-8113-831f04ce7a9e", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "eeae67c4-dd34-4289-bdbe-4bf7a07a2efe", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-profile-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "a1e9111a-e1de-44c1-91ad-959906b7b56c", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "bc9006d2-f6ba-4bef-abfb-342628bc68c8", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "96a85a89-637f-4773-996d-d85a7c724f9d", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "3dba44e9-522d-4085-8d42-8efd243602ad", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "oauth2DevicePollingInterval": "5", + "parRequestUriLifespan": "60", + "cibaInterval": "5" + }, + "keycloakVersion": "18.0.0.redhat-00001", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file diff --git a/code/exports/quarkus-petclinic-client.json b/code/exports/quarkus-petclinic-client.json new file mode 100644 index 0000000..6895c20 --- /dev/null +++ b/code/exports/quarkus-petclinic-client.json @@ -0,0 +1,113 @@ +{ + "clientId": "quarkus-petclinic", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "http://localhost:8081/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": true, + "authorizationServicesEnabled": true, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "frontchannel.logout.session.required": "false", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature.keyinfo.ext": "false", + "use.refresh.tokens": "true", + "oidc.ciba.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "require.pushed.authorization.requests": "false", + "saml.client.signature": "false", + "saml.allow.ecp.flow": "false", + "id.token.as.detached.signature": "false", + "saml.assertion.signature": "false", + "client.secret.creation.time": "1672410647", + "saml.encrypt": "false", + "saml.server.signature": "false", + "exclude.session.state.from.auth.response": "false", + "saml.artifact.binding": "false", + "saml_force_name_id_format": "false", + "acr.loa.map": "{}", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "token.response.type.bearer.lower-case": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientId", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientId", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } +} \ No newline at end of file diff --git a/code/exports/standalone.xml b/code/exports/standalone.xml new file mode 100644 index 0000000..c50bd33 --- /dev/null +++ b/code/exports/standalone.xml @@ -0,0 +1,619 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + h2 + + sa + sa + + + + jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE + h2 + + sa + sa + + + + + org.h2.jdbcx.JdbcDataSource + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + auth + + + classpath:${jboss.home.dir}/providers/* + + + master + 900 + + 2592000 + true + true + ${jboss.home.dir}/themes + + + + + + + + + + + + + jpa + + + basic + + + + + + + + + + + + + + + + + + + default + + + + + + + + ${keycloak.jta.lookup.provider:jboss} + + + + + + + + + + + ${keycloak.x509cert.lookup.provider:default} + + + + default + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/code/scripts/cleanup.sh b/code/scripts/cleanup.sh new file mode 100755 index 0000000..a139196 --- /dev/null +++ b/code/scripts/cleanup.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# helper script, place in $JBOSS_HOME/bin + +rm -rf ../standalone/deployments/* +rm -rf ../standalone/log/* +rm -rf ../standalone/data +cp ../standalone/configuration/standalone.xml.orig.bk ../standalone/configuration/standalone.xml -v + diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/.gitignore b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/.gitignore new file mode 100644 index 0000000..693002a --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/README.md b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/README.md new file mode 100644 index 0000000..919596b --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/README.md @@ -0,0 +1,6 @@ +rh-sso-custom-spi +--- + +Expected variable as host:[port] => USER_STORAGE_CUSTOM_SPI_TARGET_HOST + +default value: localhost:8081 diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/pom.xml b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/pom.xml new file mode 100644 index 0000000..9d491c4 --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + com.olleb + custom-bdd-user-storage-provider + 1.0-SNAPSHOT + jar + + + UTF-8 + 11 + 11 + + 7.6.0.GA + 7.4.4.GA-redhat-00011 + 18.0.0.redhat-00001 + + 3.4.1.Final-redhat-00001 + 5.3.25.Final-redhat-00002 + + + + + org.jboss.logging + jboss-logging + ${jboss.logging.version} + provided + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-model-jpa + ${keycloak.version} + provided + + + org.hibernate + hibernate-core + ${hibernate.core.version} + provided + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + + diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java new file mode 100644 index 0000000..9dd7d24 --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java @@ -0,0 +1,176 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.EntityManager; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; + +/** + * see the internal org.keycloak.models.jpa.UserAdapter + * see the internal org.keycloak.models.jpa.entities.UserEntity + */ +public class DemoUserStorageProvider + implements UserStorageProvider, UserLookupProvider, UserQueryProvider, CredentialInputValidator { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private final KeycloakSession keycloakSession; + private final ComponentModel componentModel; + + EntityManager entityManager; + + public DemoUserStorageProvider(KeycloakSession keycloakSession, ComponentModel componentModel) { + this.keycloakSession = keycloakSession; + this.componentModel = componentModel; + entityManager = keycloakSession.getProvider(JpaConnectionProvider.class, "jpa-user-store").getEntityManager(); + } + + @Override + public void close() { + // ok + } + + // for demo simplification, the search will return all the users + + @Override + public List getUsers(RealmModel realm) { + List users= entityManager.createNamedQuery("findAll", ExternalUser.class) + .getResultList(); + return users.stream().map(u -> new ExternalUserAdapter(keycloakSession, realm, componentModel, u)).collect(Collectors.toList()); + } + + @Override + public List getUsers(RealmModel realm, int firstResult, int maxResults) { + return getUsers(realm); + } + + @Override + public List searchForUser(String search, RealmModel realm) { + return getUsers(realm); + } + + @Override + public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { + return searchForUser(search, realm); + } + + @Override + public List searchForUser(Map params, RealmModel realm) { + // ignore params + return searchForUser(params, realm, 0, Integer.MAX_VALUE); + } + + @Override + public List searchForUser(Map params, RealmModel realm, int firstResult, + int maxResults) { + return getUsers(realm, firstResult, maxResults); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return Collections.emptyList(); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return Collections.emptyList(); + } + + @Override + public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { + return Collections.emptyList(); + } + + @Override + public UserModel getUserById(String id, RealmModel realmModel) { + // StorageId is used for referencing users that are stored outside of Keycloak + String externalId = StorageId.externalId(id); + ExternalUser externalUser = entityManager.find(ExternalUser.class, externalId); + return new ExternalUserAdapter(keycloakSession, realmModel, componentModel, externalUser); + } + + @Override + public UserModel getUserByUsername(String username, RealmModel realmModel) { + ExternalUser externalUser = entityManager.createNamedQuery("findUserByUsername", ExternalUser.class) + .setParameter("username", username).getResultList().get(0); + return new ExternalUserAdapter(keycloakSession, realmModel, componentModel, externalUser); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realmModel) { + ExternalUser externalUser = entityManager.createNamedQuery("findUserByEmail", ExternalUser.class) + .setParameter("email", email).getResultList().get(0); + return new ExternalUserAdapter(keycloakSession, realmModel, componentModel, externalUser); + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(PasswordCredentialModel.TYPE); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return supportsCredentialType(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { + if (!supportsCredentialType(credentialInput.getType())) { + return false; + } + StorageId storageId = new StorageId(user.getId()); + final ExternalUser externalUser = entityManager.createNamedQuery("findUserById", ExternalUser.class) + .setParameter("id", storageId.getExternalId()).getResultList().get(0); + if (externalUser != null) { + String password = externalUser.getPassword(); + return password != null && password.equals(credentialInput.getChallengeResponse()); + } + return false; + } + + private Optional findByUsername(Set users, String username) { + return users.stream().filter(u -> u.getUsername().equals(username)).findFirst(); + } + + private Optional findByEmail(Set users, String email) { + return users.stream().filter(u -> u.getEmail().equals(email)).findFirst(); + } + +} diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java new file mode 100644 index 0000000..d2e04e9 --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java @@ -0,0 +1,38 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.UserStorageProviderFactory; + +public class DemoUserStorageProviderFactory implements UserStorageProviderFactory { + + private static final String PROVIDER_ID = "demo-user-jpa-provider"; + + @Override + public DemoUserStorageProvider create(KeycloakSession keycloakSession, ComponentModel componentModel) { + return new DemoUserStorageProvider(keycloakSession, componentModel); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + +} diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/ExternalUser.java b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/ExternalUser.java new file mode 100644 index 0000000..6580e0c --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/ExternalUser.java @@ -0,0 +1,64 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import javax.persistence.Id; +import javax.persistence.NamedQuery; + +@NamedQuery(name = "findAll", query = "select u from User u") +@NamedQuery(name = "findUserByEmail", query = "select u from User u where u.email = :email") +@NamedQuery(name = "findUserByUsername", query = "select u from User u where u.username = :username") +@NamedQuery(name = "findUserById", query = "select u from User u where u.id = :id") +public class ExternalUser { + + @Id + private String id; + + private String username; + private String email; + private String password; + + public String getEmail() { + return email; + } + + public String getUsername() { + return username; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getId() { + return id; + } + +} diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/ExternalUserAdapter.java b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/ExternalUserAdapter.java new file mode 100644 index 0000000..7944d42 --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/java/com/olleb/ExternalUserAdapter.java @@ -0,0 +1,60 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.adapter.AbstractUserAdapterFederatedStorage; + +public class ExternalUserAdapter extends AbstractUserAdapterFederatedStorage { + + private ExternalUser user; + + public ExternalUserAdapter(KeycloakSession keycloakSession, RealmModel realmModel, ComponentModel componentModel, + ExternalUser user) { + super(keycloakSession, realmModel, componentModel); + + this.user = user; + + if (storageId == null) { + storageId = new StorageId(componentModel.getId(), user.getId()); + } + } + + @Override + public String getUsername() { + return user.getUsername(); + } + + @Override + public String getEmail() { + return user.getEmail(); + } + + public String getPassword() { + return user.getPassword(); + } + + @Override + public void setUsername(String username) { + user.setUsername(username); + } + +} diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/META-INF/persistence.xml b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/META-INF/persistence.xml new file mode 100644 index 0000000..eed071a --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,17 @@ + + + + com.olleb.ExternalUser + java:jboss/datasources/ExampleDS + + + + + + + + \ No newline at end of file diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 0000000..eabde7e --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1 @@ +com.olleb.DemoUserStorageProviderFactory diff --git a/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/import.sql b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/import.sql new file mode 100644 index 0000000..35826a3 --- /dev/null +++ b/code/user-storage-spi/bdd/custom-bdduser-storage-spi/src/main/resources/import.sql @@ -0,0 +1,10 @@ +CREATE TABLE "user" ( + id VARCHAR(36) PRIMARY KEY, + username VARCHAR(100) NOT NULL, + email VARCHAR(100) NOT NULL, + passw VARCHAR(100) NOT NULL +); + +INSERT INTO "user" (id, username, email, passw) VALUES ('1', 'user1', 'user1@example.com', '$2a$10$ybcHrYSSCKnRbtTeTDeTQOTwZSCD1ZQHxZvJiPkgzwbPpIFyiYX9e'); +INSERT INTO "user" (id, username, email, passw) VALUES ('2', 'user2', 'user2@example.com', '$2a$10$ybcHrYSSCKnRbtTeTDeTQOTwZSCD1ZQHxZvJiPkgzwbPpIFyiYX9e'); +INSERT INTO "user" (id, username, email, passw) VALUES ('3', 'user3', 'user3@example.com', '$2a$10$ybcHrYSSCKnRbtTeTDeTQOTwZSCD1ZQHxZvJiPkgzwbPpIFyiYX9e'); diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/.gitignore b/code/user-storage-spi/rest/custom-user-storage-spi/.gitignore new file mode 100644 index 0000000..693002a --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/README.md b/code/user-storage-spi/rest/custom-user-storage-spi/README.md new file mode 100644 index 0000000..919596b --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/README.md @@ -0,0 +1,6 @@ +rh-sso-custom-spi +--- + +Expected variable as host:[port] => USER_STORAGE_CUSTOM_SPI_TARGET_HOST + +default value: localhost:8081 diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/pom.xml b/code/user-storage-spi/rest/custom-user-storage-spi/pom.xml new file mode 100644 index 0000000..12e46f4 --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + com.olleb + custom-user-storage-spi + 1.0-SNAPSHOT + jar + + + UTF-8 + 11 + 11 + + 15.0.1.Final + + 18.0.2 + + + + + + + org.wildfly.bom + wildfly-javaee8-with-tools + ${version.server.bom} + pom + import + + + + + + + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + provided + + + + org.jboss.logging + jboss-logging + provided + + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + + + + + + org.wildfly.plugins + wildfly-jar-maven-plugin + ${version.wildfly-jar.maven.plugin} + + + + + diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java new file mode 100644 index 0000000..7736049 --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java @@ -0,0 +1,188 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; + +import com.olleb.User.UserBuilder; + +/** + * @author Àngel Ollé Blázquez + */ + +// https://www.keycloak.org/docs/18.0/server_development/index.html#_provider_capability_interfaces +public class DemoUserStorageProvider + implements UserStorageProvider, UserLookupProvider, UserQueryProvider, CredentialInputValidator { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private final KeycloakSession keycloakSession; + private final ComponentModel componentModel; + private final ExternalUsersService service; + + public DemoUserStorageProvider(KeycloakSession keycloakSession, ComponentModel componentModel, ExternalUsersService service) { + this.keycloakSession = keycloakSession; + this.componentModel = componentModel; + this.service = service; + } + + @Override + public void close() { + // ok + } + + // TODO catch the results avoid multiple calls + + // for demo simplification, the search will return all the users + + @Override + public List getUsers(RealmModel realm) { + Set externalUsers = service.getUsers(); + return externalUsers.stream().map(e -> this.userModelMapper(e, realm)).collect(Collectors.toList()); + } + + @Override + public List getUsers(RealmModel realm, int firstResult, int maxResults) { + return getUsers(realm); + } + + @Override + public List searchForUser(String search, RealmModel realm) { + return getUsers(realm); + } + + @Override + public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { + return searchForUser(search, realm); + } + + @Override + public List searchForUser(Map params, RealmModel realm) { + // ignore params + return searchForUser(params, realm, 0, Integer.MAX_VALUE); + } + + @Override + public List searchForUser(Map params, RealmModel realm, int firstResult, + int maxResults) { + return getUsers(realm, firstResult, maxResults); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + return Collections.emptyList(); + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + return Collections.emptyList(); + } + + @Override + public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { + return Collections.emptyList(); + } + + @Override + public UserModel getUserById(String id, RealmModel realmModel) { + // StorageId is used for referencing users that are stored outside of Keycloak + String externalId = StorageId.externalId(id); + ExternalUser externalUser = service.getUserById(externalId); + return userModelMapper(externalUser, realmModel); + } + + @Override + public UserModel getUserByUsername(String username, RealmModel realm) { + Set users = service.getUsers(); + ExternalUser externalUser = findByUsername(users, username).orElseThrow(); + return userModelMapper(externalUser, realm); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realm) { + Set users = service.getUsers(); + ExternalUser externalUser = findByEmail(users, email).orElseThrow(); + return userModelMapper(externalUser, realm); + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(PasswordCredentialModel.TYPE); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return supportsCredentialType(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { + if (!supportsCredentialType(credentialInput.getType())) { + return false; + } + StorageId storageId = new StorageId(user.getId()); + final ExternalUser externalUser = service.getUserById(storageId.getExternalId()); + if (externalUser != null) { + String password = externalUser.getPassword(); + return password != null && password.equals(credentialInput.getChallengeResponse()); + } + return false; + } + + // TODO for now, the search stuff will be done here instead of at rest api + + private Optional findByUsername(Set users, String username) { + return users.stream().filter(u -> u.getUsername().equals(username)).findFirst(); + } + + private Optional findByEmail(Set users, String email) { + return users.stream().filter(u -> u.getEmail().equals(email)).findFirst(); + } + + // + + private UserModel userModelMapper(ExternalUser externalUser, RealmModel realmModel) { + return new UserBuilder(keycloakSession, realmModel, componentModel, externalUser.getUsername(), + externalUser.getExternalId()) + .setEmail(externalUser.getEmail()) + .setFirstName(externalUser.getFirstName()) + .setLastName(externalUser.getLastName()) + .build(); + } + +} diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java new file mode 100644 index 0000000..bb6e6f0 --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java @@ -0,0 +1,53 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.util.Optional; + +import org.keycloak.Config.Scope; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.UserStorageProviderFactory; + +/** + * @author Àngel Ollé Blázquez + */ +public class DemoUserStorageProviderFactory implements UserStorageProviderFactory { + + private ExternalUsersService service; + + @Override + public DemoUserStorageProvider create(KeycloakSession keycloakSession, ComponentModel componentModel) { + return new DemoUserStorageProvider(keycloakSession, componentModel, service); + } + + @Override + public String getId() { + return "demo-user-provider"; + } + + @Override + public void init(Scope config) { + String host = Optional + .ofNullable(System.getenv("USER_STORAGE_CUSTOM_SPI_TARGET_HOST")) + .orElse(config.get("target-host")); + + service = new ExternalUsersService(host); + } + +} diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/ExternalUser.java b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/ExternalUser.java new file mode 100644 index 0000000..cbbc656 --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/ExternalUser.java @@ -0,0 +1,86 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Àngel Ollé Blázquez + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ExternalUser { + + @JsonProperty("userId") + private String externalId; + + private String username; + private String email; + private String firstName; + private String lastName; + private String password; + + public String getExternalId() { + return externalId; + } + + public String getEmail() { + return email; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/ExternalUsersService.java b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/ExternalUsersService.java new file mode 100644 index 0000000..6c414b6 --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/ExternalUsersService.java @@ -0,0 +1,48 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.util.Optional; +import java.util.Set; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.GenericType; + +/** + * @author Àngel Ollé Blázquez + */ +public class ExternalUsersService { + + private String endpoint; + + public ExternalUsersService(String host) { + this.endpoint = "http://" + Optional.ofNullable(host).orElse("localhost:8081") + "/api/v1/users"; + } + + public Set getUsers() { + GenericType> externalUsersListType = new GenericType<>() { + }; + return ClientBuilder.newClient().target(endpoint).request().get(externalUsersListType); + } + + public ExternalUser getUserById(String id) { + return ClientBuilder.newClient().target(endpoint).path("{id}").resolveTemplate("id", id).request() + .get(ExternalUser.class); + } + +} diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/User.java b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/User.java new file mode 100644 index 0000000..e2f87c0 --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/java/com/olleb/User.java @@ -0,0 +1,107 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.adapter.AbstractUserAdapter; + +/** + * @author Àngel Ollé Blázquez + */ +public class User extends AbstractUserAdapter { + + private final String username; + private final String email; + private final String firstName; + private final String lastName; + + private User(UserBuilder userBuilder) { + super(userBuilder.keycloakSession, userBuilder.realmModel, userBuilder.componentModel); + this.username = userBuilder.username; + this.email = userBuilder.email; + this.firstName = userBuilder.firstName; + this.lastName = userBuilder.lastName; + + if (storageId == null) { + storageId = new StorageId(userBuilder.componentModel.getId(), userBuilder.externalId); + } + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getEmail() { + return email; + } + + @Override + public String getFirstName() { + return firstName; + } + + @Override + public String getLastName() { + return lastName; + } + + public static class UserBuilder { + private final KeycloakSession keycloakSession; + private final RealmModel realmModel; + private final ComponentModel componentModel; + private final String username; + private final String externalId; + + private String email; + private String firstName; + private String lastName; + + public User build() { + return new User(this); + } + + public UserBuilder(KeycloakSession keycloakSession, RealmModel realmModel, ComponentModel componentModel, + String username, String externalId) { + this.keycloakSession = keycloakSession; + this.realmModel = realmModel; + this.componentModel = componentModel; + this.username = username; + this.externalId = externalId; + } + + public UserBuilder setEmail(String email) { + this.email = email; + return this; + } + + public UserBuilder setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public UserBuilder setLastName(String lastName) { + this.lastName = lastName; + return this; + } + } +} diff --git a/code/user-storage-spi/rest/custom-user-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 0000000..eabde7e --- /dev/null +++ b/code/user-storage-spi/rest/custom-user-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1 @@ +com.olleb.DemoUserStorageProviderFactory diff --git a/code/user-storage-spi/rest/jaxrs-user-store/.dockerignore b/code/user-storage-spi/rest/jaxrs-user-store/.dockerignore new file mode 100644 index 0000000..94810d0 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/code/user-storage-spi/rest/jaxrs-user-store/.gitignore b/code/user-storage-spi/rest/jaxrs-user-store/.gitignore new file mode 100644 index 0000000..693002a --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/.gitignore b/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/.gitignore new file mode 100644 index 0000000..e72f5e8 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/MavenWrapperDownloader.java b/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..1708393 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.1.1"; + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION + + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to use instead of the + * default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main( String args[] ) + { + System.out.println( "- Downloader started" ); + File baseDirectory = new File( args[0] ); + System.out.println( "- Using base directory: " + baseDirectory.getAbsolutePath() ); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File( baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH ); + String url = DEFAULT_DOWNLOAD_URL; + if ( mavenWrapperPropertyFile.exists() ) + { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try + { + mavenWrapperPropertyFileInputStream = new FileInputStream( mavenWrapperPropertyFile ); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load( mavenWrapperPropertyFileInputStream ); + url = mavenWrapperProperties.getProperty( PROPERTY_NAME_WRAPPER_URL, url ); + } + catch ( IOException e ) + { + System.out.println( "- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'" ); + } + finally + { + try + { + if ( mavenWrapperPropertyFileInputStream != null ) + { + mavenWrapperPropertyFileInputStream.close(); + } + } + catch ( IOException e ) + { + // Ignore ... + } + } + } + System.out.println( "- Downloading from: " + url ); + + File outputFile = new File( baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH ); + if ( !outputFile.getParentFile().exists() ) + { + if ( !outputFile.getParentFile().mkdirs() ) + { + System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + + "'" ); + } + } + System.out.println( "- Downloading to: " + outputFile.getAbsolutePath() ); + try + { + downloadFileFromURL( url, outputFile ); + System.out.println( "Done" ); + System.exit( 0 ); + } + catch ( Throwable e ) + { + System.out.println( "- Error downloading" ); + e.printStackTrace(); + System.exit( 1 ); + } + } + + private static void downloadFileFromURL( String urlString, File destination ) + throws Exception + { + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + String username = System.getenv( "MVNW_USERNAME" ); + char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + URL website = new URL( urlString ); + ReadableByteChannel rbc; + rbc = Channels.newChannel( website.openStream() ); + FileOutputStream fos = new FileOutputStream( destination ); + fos.getChannel().transferFrom( rbc, 0, Long.MAX_VALUE ); + fos.close(); + rbc.close(); + } + +} diff --git a/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/maven-wrapper.properties b/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..61a2ef1 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/code/user-storage-spi/rest/jaxrs-user-store/README.md b/code/user-storage-spi/rest/jaxrs-user-store/README.md new file mode 100644 index 0000000..5d755d6 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/README.md @@ -0,0 +1,60 @@ +# user-store Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./mvnw compile quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./mvnw package +``` +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./mvnw package -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./mvnw package -Pnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./mvnw package -Pnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/user-store-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. + +## Related Guides + +- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/code/user-storage-spi/rest/jaxrs-user-store/mvnw b/code/user-storage-spi/rest/jaxrs-user-store/mvnw new file mode 100755 index 0000000..eaa3d30 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/code/user-storage-spi/rest/jaxrs-user-store/mvnw.cmd b/code/user-storage-spi/rest/jaxrs-user-store/mvnw.cmd new file mode 100644 index 0000000..abb7c32 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/code/user-storage-spi/rest/jaxrs-user-store/ocp-deploy.sh b/code/user-storage-spi/rest/jaxrs-user-store/ocp-deploy.sh new file mode 100755 index 0000000..7b95832 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/ocp-deploy.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +./mvnw install -Dquarkus.kubernetes.deploy=true -DskipTests=true -Dquarkus.kubernetes-client.trust-certs=true + diff --git a/code/user-storage-spi/rest/jaxrs-user-store/pom.xml b/code/user-storage-spi/rest/jaxrs-user-store/pom.xml new file mode 100644 index 0000000..663d499 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/pom.xml @@ -0,0 +1,129 @@ + + + 4.0.0 + com.olleb + jaxrs-user-store + 1.0.0-SNAPSHOT + + 3.8.1 + 11 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 2.14.2.Final + true + 3.0.0-M7 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-openshift + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native + + + native + + + + false + native + + + + \ No newline at end of file diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.jvm b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..fe7ab75 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/user-store-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/user-store-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/user-store-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.legacy-jar b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..c81e8ba --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/user-store-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/user-store-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/user-store-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.native b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..6d9022e --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/user-store . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/user-store +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.native-micro b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..e794cea --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/user-store . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/user-store +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/model/User.java b/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/model/User.java new file mode 100644 index 0000000..7ae6b7d --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/model/User.java @@ -0,0 +1,105 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.model; + +/** + * @author Àngel Ollé Blázquez + */ +public class User { + + private final String userId; + + private final String username; + private final String email; + private final String firstName; + private final String lastName; + private final String password; + + private User(UserBuilder userBuilder) { + this.userId = userBuilder.userId; + this.username = userBuilder.username; + this.email = userBuilder.email; + this.firstName = userBuilder.firstName; + this.lastName = userBuilder.lastName; + this.password = userBuilder.password; + } + + public String getUserId() { + return userId; + } + + public String getEmail() { + return email; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public static class UserBuilder { + private final String userId; + private final String username; + + private String email; + private String firstName; + private String lastName; + private String password; + + public User build() { + return new User(this); + } + + public UserBuilder(String userId, String username) { + this.userId = userId; + this.username = username; + } + + public UserBuilder setEmail(String email) { + this.email = email; + return this; + } + + public UserBuilder setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public UserBuilder setLastName(String lastName) { + this.lastName = lastName; + return this; + } + + public UserBuilder setPassword(String password){ + this.password = password; + return this; + } + } + +} diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/rest/UserResource.java b/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/rest/UserResource.java new file mode 100644 index 0000000..fa98983 --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/rest/UserResource.java @@ -0,0 +1,63 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.rest; + +import java.lang.invoke.MethodHandles; +import java.util.Set; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.logging.Logger; + +import com.olleb.model.User; +import com.olleb.service.UserService; + +import io.smallrye.common.constraint.NotNull; + +/** + * @author Àngel Ollé Blázquez + */ +@Path("api/v1/users") +@Produces(MediaType.APPLICATION_JSON) +public class UserResource { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + @Inject + UserService userService; + + @GET + public Set getUsers() { + LOGGER.info("getUsers"); + return userService.getUsers(); + } + + @GET + @Path("{id}") + public User getUserById(@NotNull @PathParam("id") final String id) { + LOGGER.info("getUserById: " + id); + return userService.getUserById(id).orElseThrow(NotFoundException::new); + } + +} diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/service/UserService.java b/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/service/UserService.java new file mode 100644 index 0000000..2cb0a1c --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/java/com/olleb/service/UserService.java @@ -0,0 +1,84 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.service; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.enterprise.context.ApplicationScoped; + +import com.olleb.model.User; +import com.olleb.model.User.UserBuilder; + +/** + * @author Àngel Ollé Blázquez + */ +@ApplicationScoped +public class UserService { + + private static final Map userMap = Map.of( + "1", new UserBuilder("1", "nata") + .setFirstName("Nata") + .setLastName("Natilla") + .setEmail("nata@example.com") + .setPassword("natapassword") + .build(), + "2", new UserBuilder("2", "freud") + .setFirstName("Freud") + .setLastName("Redhead") + .setEmail("freud@example.com") + .setPassword("freudpassword") + .build(), + "3", new UserBuilder("3", "mao") + .setFirstName("Mao") + .setLastName("Meow") + .setEmail("mao@example.com") + .setPassword("maopassword") + .build(), + "4", new UserBuilder("4", "dark") + .setFirstName("Dark") + .setLastName("Butcher") + .setEmail("dark@example.com") + .setPassword("darkpassword") + .build(), + "5", new UserBuilder("5", "abel") + .setFirstName("Abel") + .setLastName("Miiii") + .setEmail("abel@example.com") + .setPassword("abelpassword") + .build(), + "6", new UserBuilder("6", "flash") + .setFirstName("Flash") + .setLastName("Jr") + .setEmail("flash@example.com") + .setPassword("flashpassword") + .build() + + ); + + public Set getUsers() { + return userMap.values().stream().collect(Collectors.toSet()); + } + + public Optional getUserById(final String id) { + return Optional.of(userMap.get(id)); + } + +} diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/resources/META-INF/resources/index.html b/code/user-storage-spi/rest/jaxrs-user-store/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..efe0c0d --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,283 @@ + + + + + user-store - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/code/user-storage-spi/rest/jaxrs-user-store/src/main/resources/application.properties b/code/user-storage-spi/rest/jaxrs-user-store/src/main/resources/application.properties new file mode 100644 index 0000000..f97918e --- /dev/null +++ b/code/user-storage-spi/rest/jaxrs-user-store/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.http.port=8081 + +quarkus.openshift.route.expose=true diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.dockerignore b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.dockerignore new file mode 100644 index 0000000..94810d0 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.gitignore b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.gitignore new file mode 100644 index 0000000..693002a --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/.gitignore b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/.gitignore new file mode 100644 index 0000000..e72f5e8 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/MavenWrapperDownloader.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..1708393 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.1.1"; + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION + + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to use instead of the + * default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main( String args[] ) + { + System.out.println( "- Downloader started" ); + File baseDirectory = new File( args[0] ); + System.out.println( "- Using base directory: " + baseDirectory.getAbsolutePath() ); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File( baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH ); + String url = DEFAULT_DOWNLOAD_URL; + if ( mavenWrapperPropertyFile.exists() ) + { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try + { + mavenWrapperPropertyFileInputStream = new FileInputStream( mavenWrapperPropertyFile ); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load( mavenWrapperPropertyFileInputStream ); + url = mavenWrapperProperties.getProperty( PROPERTY_NAME_WRAPPER_URL, url ); + } + catch ( IOException e ) + { + System.out.println( "- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'" ); + } + finally + { + try + { + if ( mavenWrapperPropertyFileInputStream != null ) + { + mavenWrapperPropertyFileInputStream.close(); + } + } + catch ( IOException e ) + { + // Ignore ... + } + } + } + System.out.println( "- Downloading from: " + url ); + + File outputFile = new File( baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH ); + if ( !outputFile.getParentFile().exists() ) + { + if ( !outputFile.getParentFile().mkdirs() ) + { + System.out.println( "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + + "'" ); + } + } + System.out.println( "- Downloading to: " + outputFile.getAbsolutePath() ); + try + { + downloadFileFromURL( url, outputFile ); + System.out.println( "Done" ); + System.exit( 0 ); + } + catch ( Throwable e ) + { + System.out.println( "- Error downloading" ); + e.printStackTrace(); + System.exit( 1 ); + } + } + + private static void downloadFileFromURL( String urlString, File destination ) + throws Exception + { + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + String username = System.getenv( "MVNW_USERNAME" ); + char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + URL website = new URL( urlString ); + ReadableByteChannel rbc; + rbc = Channels.newChannel( website.openStream() ); + FileOutputStream fos = new FileOutputStream( destination ); + fos.getChannel().transferFrom( rbc, 0, Long.MAX_VALUE ); + fos.close(); + rbc.close(); + } + +} diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/maven-wrapper.properties b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..61a2ef1 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/README.md b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/README.md new file mode 100644 index 0000000..6297da3 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/README.md @@ -0,0 +1,60 @@ +# rh-sso-custom-spi Project + +This project uses Quarkus, the Supersonic Subatomic Java Framework. + +If you want to learn more about Quarkus, please visit its website: https://quarkus.io/ . + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./mvnw compile quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. + +## Packaging and running the application + +The application can be packaged using: +```shell script +./mvnw package +``` +It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory. +Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory. + +The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`. + +If you want to build an _über-jar_, execute the following command: +```shell script +./mvnw package -Dquarkus.package.type=uber-jar +``` + +The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`. + +## Creating a native executable + +You can create a native executable using: +```shell script +./mvnw package -Pnative +``` + +Or, if you don't have GraalVM installed, you can run the native executable build in a container using: +```shell script +./mvnw package -Pnative -Dquarkus.native.container-build=true +``` + +You can then execute your native executable with: `./target/rh-sso-custom-spi-1.0.0-SNAPSHOT-runner` + +If you want to learn more about building native executables, please consult https://quarkus.io/guides/maven-tooling. + +## Related Guides + +- RESTEasy Reactive ([guide](https://quarkus.io/guides/resteasy-reactive)): A JAX-RS implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it. + +## Provided Code + +### RESTEasy Reactive + +Easily start your Reactive RESTful Web Services + +[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources) diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/mvnw b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/mvnw new file mode 100755 index 0000000..eaa3d30 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/mvnw.cmd b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/mvnw.cmd new file mode 100644 index 0000000..abb7c32 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/pom.xml b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/pom.xml new file mode 100644 index 0000000..64e5229 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/pom.xml @@ -0,0 +1,155 @@ + + + 4.0.0 + com.olleb + quarkus-custom-user-storage-spi + 1.0.0-SNAPSHOT + + 3.8.1 + 11 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 2.14.2.Final + true + 3.0.0-M7 + + 18.0.2 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkus + quarkus-rest-client + + + io.quarkus + quarkus-openshift + + + org.keycloak + keycloak-core + ${keycloak.version} + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + + + org.keycloak + keycloak-adapter-core + ${keycloak.version} + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native + + + native + + + + false + native + + + + diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.jvm b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..66e34d4 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/rh-sso-custom-spi-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/rh-sso-custom-spi-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/rh-sso-custom-spi-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.legacy-jar b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..9070dc9 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/rh-sso-custom-spi-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/rh-sso-custom-spi-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/rh-sso-custom-spi-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.14 + +ENV LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.native b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..5c4c43a --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/rh-sso-custom-spi . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/rh-sso-custom-spi +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.6 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.native-micro b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..66c5dd5 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/rh-sso-custom-spi . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/rh-sso-custom-spi +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java new file mode 100644 index 0000000..577b50b --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProvider.java @@ -0,0 +1,198 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.lang.invoke.MethodHandles; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.credential.CredentialInput; +import org.keycloak.credential.CredentialInputValidator; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.credential.PasswordCredentialModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.user.UserLookupProvider; +import org.keycloak.storage.user.UserQueryProvider; + +import com.olleb.User.UserBuilder; + +/** + * @author Àngel Ollé Blázquez + */ +// https://www.keycloak.org/docs/18.0/server_development/index.html#_provider_capability_interfaces +public class DemoUserStorageProvider + implements UserStorageProvider, UserLookupProvider, UserQueryProvider, CredentialInputValidator { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private final KeycloakSession keycloakSession; + private final ComponentModel componentModel; + + @Inject + @RestClient + ExtensionsService extensionsService; + + public DemoUserStorageProvider(KeycloakSession keycloakSession, ComponentModel componentModel) { + this.keycloakSession = keycloakSession; + this.componentModel = componentModel; + } + + @Override + public void close() { + // ok + } + + // TODO catch the results avoid multiple calls + + @Override + public List getUsers(RealmModel realm) { + Set externalUsers = extensionsService.getUsers(); + return externalUsers.stream().map(e -> this.userModelMapper(e, realm)).collect(Collectors.toList()); + } + + @Override + public List getUsers(RealmModel realm, int firstResult, int maxResults) { + Set externalUsers = extensionsService.getUsers(); + return externalUsers.stream().collect(Collectors.toList()).subList(firstResult, maxResults).stream() + .map(e -> this.userModelMapper(e, realm)).collect(Collectors.toList()); + } + + @Override + public List searchForUser(String search, RealmModel realm) { + // TODO Auto-generated method stub + return Collections.emptyList(); + } + + @Override + public List searchForUser(String search, RealmModel realm, int firstResult, int maxResults) { + // TODO Auto-generated method stub + return Collections.emptyList(); + } + + @Override + public List searchForUser(Map params, RealmModel realm) { + // TODO Auto-generated method stub + return Collections.emptyList(); + } + + @Override + public List searchForUser(Map params, RealmModel realm, int firstResult, + int maxResults) { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group) { + // TODO Auto-generated method stub + return null; + } + + @Override + public List getGroupMembers(RealmModel realm, GroupModel group, int firstResult, int maxResults) { + // TODO Auto-generated method stub + return null; + } + + @Override + public List searchForUserByUserAttribute(String attrName, String attrValue, RealmModel realm) { + // TODO Auto-generated method stub + return null; + } + + @Override + public UserModel getUserById(String id, RealmModel realmModel) { + // StorageId is used for referencing users that are stored outside of Keycloak + StorageId storageId = new StorageId(id); + ExternalUser externalUser = extensionsService.getUserById(storageId.getExternalId()); + return userModelMapper(externalUser, realmModel); + } + + @Override + public UserModel getUserByUsername(String username, RealmModel realm) { + Set users = extensionsService.getUsers(); + if(users.isEmpty()) throw new RuntimeException(); + ExternalUser externalUser = findByUsername(users, username).orElseThrow(); + return userModelMapper(externalUser, realm); + } + + @Override + public UserModel getUserByEmail(String email, RealmModel realm) { + Set users = extensionsService.getUsers(); + ExternalUser externalUser = findByEmail(users, email).orElseThrow(); + return userModelMapper(externalUser, realm); + } + + @Override + public boolean supportsCredentialType(String credentialType) { + return credentialType.equals(PasswordCredentialModel.TYPE); + } + + @Override + public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) { + return supportsCredentialType(credentialType); + } + + @Override + public boolean isValid(RealmModel realm, UserModel user, CredentialInput credentialInput) { + if (!supportsCredentialType(credentialInput.getType())) { + return false; + } + StorageId storageId = new StorageId(user.getId()); + ExternalUser externalUser = extensionsService.getUserById(storageId.getExternalId()); + if (externalUser != null) { + String password = externalUser.getPassword(); + return password != null && password.equals(credentialInput.getChallengeResponse()); + } + return false; + } + + // TODO for now, the search stuff will be done here instead of at rest api + + private Optional findByUsername(Set users, String username) { + return users.stream().filter(u -> u.getUsername().equals(username)).findFirst(); + } + + private Optional findByEmail(Set users, String email) { + return users.stream().filter(u -> u.getEmail().equals(email)).findFirst(); + } + + // + + private UserModel userModelMapper(ExternalUser externalUser, RealmModel realmModel) { + return new UserBuilder(keycloakSession, realmModel, componentModel, externalUser.getUsername()) + .setEmail(externalUser.getEmail()) + .setFirstName(externalUser.getFirstName()) + .setLastName(externalUser.getLastName()) + .build(); + } + +} \ No newline at end of file diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java new file mode 100644 index 0000000..09af87b --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/DemoUserStorageProviderFactory.java @@ -0,0 +1,39 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.storage.UserStorageProviderFactory; + +/** + * @author Àngel Ollé Blázquez + */ +public class DemoUserStorageProviderFactory implements UserStorageProviderFactory { + + @Override + public DemoUserStorageProvider create(KeycloakSession keycloakSession, ComponentModel componentModel) { + return new DemoUserStorageProvider(keycloakSession, componentModel); + } + + @Override + public String getId() { + return "demo-user-provider"; + } + +} \ No newline at end of file diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/ExtensionsService.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/ExtensionsService.java new file mode 100644 index 0000000..309d388 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/ExtensionsService.java @@ -0,0 +1,50 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.annotations.jaxrs.PathParam; + +/** + * @author Àngel Ollé Blázquez + */ +@ApplicationScoped +@RegisterRestClient(configKey = "com.olleb.extensions.api") +@ClientHeaderParam(name = "Authorization", value = "{token}") +public interface ExtensionsService { + + @GET + Set getUsers(); + + @GET + @Path("{id}") + ExternalUser getUserById(@PathParam("id") String userId); + + default String token() { + return ConfigProvider.getConfig().getOptionalValue("extensions.token", String.class).orElseThrow(); + } + +} diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/ExternalUser.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/ExternalUser.java new file mode 100644 index 0000000..d43e762 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/ExternalUser.java @@ -0,0 +1,81 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +/** + * @author Àngel Ollé Blázquez + */ +public class ExternalUser { + + private String externalId; + + private String username; + private String email; + private String firstName; + private String lastName; + private String password; + + public String getExternalId() { + return externalId; + } + + public String getEmail() { + return email; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public void setExternalId(String externalId) { + this.externalId = externalId; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + +} diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/StartupListener.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/StartupListener.java new file mode 100644 index 0000000..57bc55b --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/StartupListener.java @@ -0,0 +1,51 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import java.lang.invoke.MethodHandles; +import java.util.Set; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.logging.Logger; + +import io.quarkus.runtime.StartupEvent; + +/** + * @author Àngel Ollé Blázquez + */ +@ApplicationScoped +public class StartupListener { + + private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + @Inject + @RestClient + ExtensionsService extensionsService; + + void onStart(@Observes StartupEvent ev) { + Set users = extensionsService.getUsers(); + LOGGER.info(users); + ExternalUser user = extensionsService.getUserById("1"); + LOGGER.info(user); + } + +} diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/User.java b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/User.java new file mode 100644 index 0000000..c213f27 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/java/com/olleb/User.java @@ -0,0 +1,100 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb; + +import org.keycloak.component.ComponentModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.storage.adapter.AbstractUserAdapter; + +/** + * @author Àngel Ollé Blázquez + */ +public class User extends AbstractUserAdapter { + + private final String username; + private final String email; + private final String firstName; + private final String lastName; + + private User(UserBuilder userBuilder) { + super(userBuilder.keycloakSession, userBuilder.realmModel, userBuilder.componentModel); + this.username = userBuilder.username; + this.email = userBuilder.email; + this.firstName = userBuilder.firstName; + this.lastName = userBuilder.lastName; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public String getEmail() { + return email; + } + + @Override + public String getFirstName() { + return firstName; + } + + @Override + public String getLastName() { + return lastName; + } + + public static class UserBuilder { + private final KeycloakSession keycloakSession; + private final RealmModel realmModel; + private final ComponentModel componentModel; + private final String username; + + private String email; + private String firstName; + private String lastName; + + public User build() { + return new User(this); + } + + public UserBuilder(KeycloakSession keycloakSession, RealmModel realmModel, ComponentModel componentModel, + String username) { + this.keycloakSession = keycloakSession; + this.realmModel = realmModel; + this.componentModel = componentModel; + this.username = username; + } + + public UserBuilder setEmail(String email) { + this.email = email; + return this; + } + + public UserBuilder setFirstName(String firstName) { + this.firstName = firstName; + return this; + } + + public UserBuilder setLastName(String lastName) { + this.lastName = lastName; + return this; + } + } +} diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/META-INF/resources/index.html b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..9576e5e --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,283 @@ + + + + + rh-sso-custom-spi - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive (guide)
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 0000000..eabde7e --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1 @@ +com.olleb.DemoUserStorageProviderFactory diff --git a/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/application.properties b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/application.properties new file mode 100644 index 0000000..a57f4a3 --- /dev/null +++ b/code/user-storage-spi/rest/quarkus-custom-user-storage-spi/src/main/resources/application.properties @@ -0,0 +1,9 @@ +quarkus.openshift.env.secrets=custom-spi-secret +quarkus.openshift.env.configmaps=custom-spi-config + +quarkus.rest-client.com.olleb.extensions.api.url=http://${HOST:localhost:8081}/api/v1/users + +quarkus.rest-client.com.olleb.extensions.api.hostname-verifier=io.quarkus.restclient.NoopHostnameVerifier +quarkus.tls.trust-all=true + +extensions.token=abc diff --git a/code/vault-spi/389ds/start.sh b/code/vault-spi/389ds/start.sh new file mode 100755 index 0000000..c99c08d --- /dev/null +++ b/code/vault-spi/389ds/start.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +podman run \ + -p 3389:3389 \ + -e DS_DM_PASSWORD="password" \ + docker.io/389ds/dirsrv diff --git a/code/vault-spi/conjur-spi/.gitignore b/code/vault-spi/conjur-spi/.gitignore new file mode 100644 index 0000000..693002a --- /dev/null +++ b/code/vault-spi/conjur-spi/.gitignore @@ -0,0 +1,40 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties +.flattened-pom.xml + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/code/vault-spi/conjur-spi/README.md b/code/vault-spi/conjur-spi/README.md new file mode 100644 index 0000000..4eb44e1 --- /dev/null +++ b/code/vault-spi/conjur-spi/README.md @@ -0,0 +1,2 @@ +conjur-spi vault +--- diff --git a/code/vault-spi/conjur-spi/pom.xml b/code/vault-spi/conjur-spi/pom.xml new file mode 100644 index 0000000..e9b6214 --- /dev/null +++ b/code/vault-spi/conjur-spi/pom.xml @@ -0,0 +1,142 @@ + + + + 4.0.0 + + com.olleb + conjur-spi + 1.0-SNAPSHOT + jar + + + UTF-8 + 11 + 11 + + 15.0.1.Final + + 18.0.2 + + + + + + + org.wildfly.bom + wildfly-javaee8-with-tools + ${version.server.bom} + pom + import + + + + + + + org.jboss.logging + jboss-logging + provided + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-services + ${keycloak.version} + provided + + + + + com.cyberark.conjur.api + conjur-api + 3.0.3 + + + + commons-codec + commons-codec + 1.15 + + + + com.google.code.gson + gson + 2.9.0 + + + + joda-time + joda-time + 2.3 + + + + org.junit.jupiter + junit-jupiter + 5.9.1 + test + + + org.hamcrest + hamcrest-core + 2.2 + test + + + + + + + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + + + + + + diff --git a/code/vault-spi/conjur-spi/src/main/java/com/olleb/vault/ConjurVaultProvider.java b/code/vault-spi/conjur-spi/src/main/java/com/olleb/vault/ConjurVaultProvider.java new file mode 100644 index 0000000..1a43b84 --- /dev/null +++ b/code/vault-spi/conjur-spi/src/main/java/com/olleb/vault/ConjurVaultProvider.java @@ -0,0 +1,53 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.vault; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import org.keycloak.vault.DefaultVaultRawSecret; +import org.keycloak.vault.VaultProvider; +import org.keycloak.vault.VaultRawSecret; + +import com.cyberark.conjur.api.Conjur; + +/** + * @author Àngel Ollé Blázquez + */ +public class ConjurVaultProvider implements VaultProvider { + + Conjur conjur; + + public ConjurVaultProvider(Conjur conjur) { + this.conjur = conjur; + } + + @Override + public VaultRawSecret obtainSecret(String vaultSecretId) { + String secret = conjur.variables().retrieveSecret(vaultSecretId); + ByteBuffer byteBuffer = ByteBuffer.wrap(secret.getBytes(StandardCharsets.UTF_8)); + return DefaultVaultRawSecret.forBuffer(Optional.of(byteBuffer)); + } + + @Override + public void close() { + // no-op + } + +} diff --git a/code/vault-spi/conjur-spi/src/main/java/com/olleb/vault/ConjurVaultProviderFactory.java b/code/vault-spi/conjur-spi/src/main/java/com/olleb/vault/ConjurVaultProviderFactory.java new file mode 100644 index 0000000..9c8596d --- /dev/null +++ b/code/vault-spi/conjur-spi/src/main/java/com/olleb/vault/ConjurVaultProviderFactory.java @@ -0,0 +1,66 @@ +/* + * This file is part of Red Hat Single Sign-On workshop + * Copyright (C) 2022 Àngel Ollé Blázquez + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.olleb.vault; + +import org.keycloak.Config.Scope; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.vault.VaultProvider; +import org.keycloak.vault.VaultProviderFactory; + +import com.cyberark.conjur.api.Conjur; +import com.cyberark.conjur.api.Credentials; + +/** + * @author Àngel Ollé Blázquez + */ +public class ConjurVaultProviderFactory implements VaultProviderFactory { + + // private static final Logger LOGGER = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + private static final String PROVIDER_ID = "conjur-vault"; + + private Credentials credentials; + + @Override + public VaultProvider create(KeycloakSession session) { + Conjur conjur = new Conjur(credentials); + return new ConjurVaultProvider(conjur); + } + + @Override + public void init(Scope config) { + credentials = Credentials.fromSystemProperties(); + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + // no-op + } + + @Override + public void close() { + // no-op + } + + @Override + public String getId() { + return PROVIDER_ID; + } + +} diff --git a/code/vault-spi/conjur-spi/src/main/resources/META-INF/jboss-deployment-structure.xml b/code/vault-spi/conjur-spi/src/main/resources/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000..673bee8 --- /dev/null +++ b/code/vault-spi/conjur-spi/src/main/resources/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/code/vault-spi/conjur-spi/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory b/code/vault-spi/conjur-spi/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory new file mode 100644 index 0000000..46ec2ba --- /dev/null +++ b/code/vault-spi/conjur-spi/src/main/resources/META-INF/services/org.keycloak.vault.VaultProviderFactory @@ -0,0 +1 @@ +com.olleb.vault.ConjurVaultProviderFactory diff --git a/code/vault-spi/conjur/Makefile b/code/vault-spi/conjur/Makefile new file mode 100644 index 0000000..8bec6ae --- /dev/null +++ b/code/vault-spi/conjur/Makefile @@ -0,0 +1,34 @@ +CONJUR-CLI_IMAGE_NAME=conjur-cli +CONJUR-CLI_GIT_URL=https://github.com/cyberark/cyberark-conjur-cli.git +PROXY_IMAGE_NAME=httpd-conjur-proxy + +CONJUR_POD_NAME=conjur-pod +# it's a demo, it's ok +CONJUR_DATA_KEY=tnwICwIa2kOVH2KkWvpPw+VA5AagyJPtu4nEim1UwCc= + +.PHONY: build clean all start + +all: build run + +build: + podman build -t $(CONJUR-CLI_IMAGE_NAME) $(CONJUR-CLI_GIT_URL) + podman build -t $(PROXY_IMAGE_NAME) proxy + +run: + podman run -dt --pod new:$(CONJUR_POD_NAME) -e POSTGRES_HOST_AUTH_METHOD=trust -p 8432:5432 -p 18443:443 -p 18080:80 --name postgres docker.io/postgres:10.16 + + podman run -dt --pod $(CONJUR_POD_NAME) -e DATABASE_URL=postgresql://postgres:secret@postgres/postgres -e CONJUR_DATA_KEY="$(CONJUR_DATA_KEY)" --name conjur docker.io/cyberark/conjur server + + podman run -dt --pod $(CONJUR_POD_NAME) --name $(PROXY_IMAGE_NAME) $(PROXY_IMAGE_NAME) + + podman run -dt --pod $(CONJUR_POD_NAME) --name $(CONJUR-CLI_IMAGE_NAME) --entrypoint="sleep" conjur-cli infinity + +stop: + podman pod stop $(CONJUR_POD_NAME) + +rm: + podman pod rm -f $(CONJUR_POD_NAME) + +clean: + podman rmi $$(podman images -f 'dangling=true' -q) -f 2>/dev/null || true + podman rmi $(CONJUR-CLI_IMAGE_NAME) $(PROXY_IMAGE_NAME) docker.io/cyberark/conjur docker.io/postgres:10.16 2>/dev/null || true diff --git a/code/vault-spi/conjur/cli.sh b/code/vault-spi/conjur/cli.sh new file mode 100755 index 0000000..4daa448 --- /dev/null +++ b/code/vault-spi/conjur/cli.sh @@ -0,0 +1,2 @@ +JBOSS_HOME=~/opt/rh-sso-7.6/ +$JBOSS_HOME/bin/jboss-cli.sh --file=spi.cli --connect diff --git a/code/vault-spi/conjur/conjur-pod.yml b/code/vault-spi/conjur/conjur-pod.yml new file mode 100644 index 0000000..878439e --- /dev/null +++ b/code/vault-spi/conjur/conjur-pod.yml @@ -0,0 +1,154 @@ +# Generation of Kubernetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-3.0.1 +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2023-01-09T14:08:10Z" + labels: + app: conjur-pod + name: conjur-pod +spec: + containers: + - args: + - server + command: + - conjurctl + env: + - name: PATH + value: /usr/local/pgsql/bin:/var/lib/ruby/bin:/usr/local/ssl/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: container + value: podman + - name: LD_LIBRARY_PATH + value: /usr/local/ssl/lib + - name: OPENSSL_FIPS + value: "1" + - name: RAILS_ENV + value: production + - name: DATABASE_URL + value: postgresql://postgres:secret@postgres/postgres + - name: CONJUR_DATA_KEY + value: tnwICwIa2kOVH2KkWvpPw+VA5AagyJPtu4nEim1UwCc= + image: docker.io/cyberark/conjur + name: conjur + ports: + - containerPort: 5432 + hostPort: 8432 + protocol: TCP + - containerPort: 443 + hostPort: 18443 + protocol: TCP + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + tty: true + workingDir: /opt/conjur-server + - args: + - postgres + command: + - docker-entrypoint.sh + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/postgresql/10/bin + - name: TERM + value: xterm + - name: container + value: podman + - name: LANG + value: en_US.utf8 + - name: PG_MAJOR + value: "10" + - name: PGDATA + value: /var/lib/postgresql/data + - name: GOSU_VERSION + value: "1.12" + - name: PG_VERSION + value: 10.16-1.pgdg90+1 + - name: POSTGRES_HOST_AUTH_METHOD + value: trust + image: docker.io/postgres:10.16 + name: postgres + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + tty: true + workingDir: / + - command: + - /usr/sbin/apache2ctl + - -D + - FOREGROUND + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: container + value: podman + - name: DEBIAN_FRONTEND + value: noninteractive + image: localhost/httpd-conjur-proxy:latest + name: httpd-conjur-proxy + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + tty: true + workingDir: / + - args: + - infinity + command: + - sleep + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: container + value: podman + image: localhost/conjur-cli:latest + name: conjur-cli + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + tty: true + workingDir: / + dnsConfig: {} + restartPolicy: Never +status: {} + diff --git a/code/vault-spi/conjur/keycloak_env b/code/vault-spi/conjur/keycloak_env new file mode 100644 index 0000000..9aca89b --- /dev/null +++ b/code/vault-spi/conjur/keycloak_env @@ -0,0 +1,4 @@ +export CONJUR_ACCOUNT=demoAccount +export CONJUR_AUTHN_API_KEY=$APIKEY +export CONJUR_APPLIANCE_URL=http://localhost:18080 +export CONJUR_AUTHN_LOGIN=admin diff --git a/code/vault-spi/conjur/policy.yml b/code/vault-spi/conjur/policy.yml new file mode 100644 index 0000000..2b6fcd3 --- /dev/null +++ b/code/vault-spi/conjur/policy.yml @@ -0,0 +1,5 @@ +- !policy + id: ldap + body: + - !host ldap + - !variable password diff --git a/code/vault-spi/conjur/proxy/Containerfile b/code/vault-spi/conjur/proxy/Containerfile new file mode 100644 index 0000000..093c0c9 --- /dev/null +++ b/code/vault-spi/conjur/proxy/Containerfile @@ -0,0 +1,25 @@ +FROM debian:latest + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update \ +&& apt-get -y install \ +apache2 \ +ssl-cert + +RUN a2enmod proxy \ +&& a2enmod proxy_http \ +&& a2enmod ssl + +COPY conjur-ssl.conf /etc/apache2/sites-available/ +RUN sed -i '0,/Listen [0-9]*/s///' /etc/apache2/ports.conf +#RUN sed -i 's/\(Listen 80\)/\180/' /etc/apache2/ports.conf + +RUN a2ensite conjur-ssl \ +&& a2dissite 000-default + +#WORKDIR /var/www/html + +EXPOSE 443 + +ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"] diff --git a/code/vault-spi/conjur/proxy/conjur-ssl.conf b/code/vault-spi/conjur/proxy/conjur-ssl.conf new file mode 100644 index 0000000..a00b7bd --- /dev/null +++ b/code/vault-spi/conjur/proxy/conjur-ssl.conf @@ -0,0 +1,21 @@ + + + ServerAdmin webmaster@localhost + + ErrorLog ${APACHE_LOG_DIR}/conjur-ssl-error.log + CustomLog ${APACHE_LOG_DIR}/conjur-ssl-access.log combined + + SSLEngine on + + SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem + SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key + + AllowEncodedSlashes On + + ProxyPass / http://conjur/ nocanon + ProxyPassReverse / http://conjur/ nocanon + ProxyRequests Off + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/code/vault-spi/conjur/proxy/conjur.conf b/code/vault-spi/conjur/proxy/conjur.conf new file mode 100644 index 0000000..ab37348 --- /dev/null +++ b/code/vault-spi/conjur/proxy/conjur.conf @@ -0,0 +1,12 @@ + + ServerAdmin webmaster@localhost + + ErrorLog ${APACHE_LOG_DIR}/conjur-error.log + CustomLog ${APACHE_LOG_DIR}/conjur-access.log combined + + ProxyPass / http://conjur/ + ProxyPassReverse / http://conjur/ + ProxyRequests Off + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet diff --git a/code/vault-spi/conjur/spi.cli b/code/vault-spi/conjur/spi.cli new file mode 100644 index 0000000..d27701e --- /dev/null +++ b/code/vault-spi/conjur/spi.cli @@ -0,0 +1,4 @@ +/subsystem=keycloak-server/spi=vault:add +/subsystem=keycloak-server/spi=vault/provider=conjur-vault:add(enabled=true) +/subsystem=keycloak-server/spi=vault:write-attribute(name=default-provider,value=conjur-vault) +:reload diff --git a/code/vault-spi/conjur/start-ssl.sh b/code/vault-spi/conjur/start-ssl.sh new file mode 100755 index 0000000..da40663 --- /dev/null +++ b/code/vault-spi/conjur/start-ssl.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#set -x + +FILE=$(mktemp) +trap 'rm -rf -- "$FILE"' EXIT + +#make all + +podman exec -ti conjur conjurctl account create demoAccount > $FILE + +export APIKEY=$(sed -n '/API/ s/.*: *//p' $FILE | tr -d '\r') +echo "APIKEY: $APIKEY\n" + +# TODO refactor +echo "creating the conjur stuff..." + +TOKEN=$(curl -k --header "Accept-Encoding: base64" -d "$APIKEY" https://localhost:18443/authn/demoAccount/admin/authenticate) +echo "token: $TOKEN" + +curl -k -H "Authorization: Token token=\"${TOKEN}\"" -d "$(< policy.yml)" https://localhost:18443/policies/demoAccount/policy/root +curl -k -H "Authorization: Token token=\"${TOKEN}\"" -d "password" https://localhost:18443/secrets/demoAccount/variable/ldap/password +#curl -k -H "Authorization: Token token=\"${TOKEN}\"" https://localhost:18443/secrets/demoAccount/variable/ldap/password diff --git a/code/vault-spi/conjur/start.sh b/code/vault-spi/conjur/start.sh new file mode 100755 index 0000000..bea4c54 --- /dev/null +++ b/code/vault-spi/conjur/start.sh @@ -0,0 +1,22 @@ +#!/bin/bash +#set -x + +FILE=$(mktemp) +trap 'rm -rf -- "$FILE"' EXIT + +#make all + +podman exec -ti conjur conjurctl account create demoAccount > $FILE + +export APIKEY=$(sed -n '/API/ s/.*: *//p' $FILE | tr -d '\r') +echo "APIKEY: $APIKEY\n" + +# TODO refactor +echo "creating the conjur stuff..." + +TOKEN=$(curl --header "Accept-Encoding: base64" -d "${APIKEY}" http://localhost:18080/authn/demoAccount/admin/authenticate) +#echo "token: $TOKEN" + +curl -H "Authorization: Token token=\"${TOKEN}\"" -d "$(< policy.yml)" http://localhost:18080/policies/demoAccount/policy/root +curl -H "Authorization: Token token=\"${TOKEN}\"" -d "password" http://localhost:18080/secrets/demoAccount/variable/ldap/password +#curl -k -H "Authorization: Token token=\"${TOKEN}\"" http://localhost:18080/secrets/demoAccount/variable/ldap/password