From 710ad14d230d3856171c6803fea1cf78e6ed9ca2 Mon Sep 17 00:00:00 2001
From: vsirik
Date: Sun, 1 Jun 2025 16:03:05 +0300
Subject: [PATCH] work-variable
---
Dockerfile | 3 +-
docker-compose.yml | 12 +-
.../keycloak-eventlistner-1.0-SNAPSHOT.jar | Bin 0 -> 4777 bytes
keycloak/themes/mytheme/login/cli_splash.ftl | 7 +
keycloak/themes/mytheme/login/code.ftl | 19 ++
.../mytheme/login/delete-account-confirm.ftl | 33 +++
.../mytheme/login/delete-credential.ftl | 15 ++
keycloak/themes/mytheme/login/error.ftl | 16 ++
keycloak/themes/mytheme/login/footer.ftl | 3 +
.../mytheme/login/frontchannel-logout.ftl | 30 +++
.../mytheme/login/idp-review-user-profile.ftl | 23 ++
keycloak/themes/mytheme/login/info.ftl | 24 ++
.../themes/mytheme/login/link-idp-action.ftl | 15 ++
.../mytheme/login/login-config-totp.ftl | 114 ++++++++++
.../login/login-idp-link-confirm-override.ftl | 12 +
.../mytheme/login/login-idp-link-confirm.ftl | 13 ++
.../mytheme/login/login-idp-link-email.ftl | 16 ++
.../mytheme/login/login-oauth-grant.ftl | 68 ++++++
.../login-oauth2-device-verify-user-code.ftl | 31 +++
keycloak/themes/mytheme/login/login-otp.ftl | 59 +++++
.../mytheme/login/login-page-expired.ftl | 11 +
...ogin-passkeys-conditional-authenticate.ftl | 139 ++++++++++++
.../themes/mytheme/login/login-password.ftl | 52 +++++
.../login-recovery-authn-code-config.ftl | 186 ++++++++++++++++
.../login/login-recovery-authn-code-input.ftl | 45 ++++
.../themes/mytheme/login/login-reset-otp.ftl | 33 +++
.../mytheme/login/login-reset-password.ftl | 39 ++++
.../mytheme/login/login-update-password.ftl | 77 +++++++
.../mytheme/login/login-update-profile.ftl | 28 +++
.../themes/mytheme/login/login-username.ftl | 89 ++++++++
.../mytheme/login/login-verify-email.ftl | 36 +++
.../themes/mytheme/login/login-x509-info.ftl | 55 +++++
keycloak/themes/mytheme/login/login.ftl | 117 ++++++++++
.../themes/mytheme/login/logout-confirm.ftl | 38 ++++
.../themes/mytheme/login/password-commons.ftl | 12 +
.../themes/mytheme/login/register-commons.ftl | 27 +++
keycloak/themes/mytheme/login/register.ftl | 61 +++++
.../themes/mytheme/login/saml-post-form.ftl | 25 +++
.../mytheme/login/select-authenticator.ftl | 36 +++
.../mytheme/login/select-organization.ftl | 23 ++
keycloak/themes/mytheme/login/template.ftl | 205 +++++++++++++++++
keycloak/themes/mytheme/login/terms.ftl | 15 ++
.../themes/mytheme/login/update-email.ftl | 30 +++
.../mytheme/login/user-profile-commons.ftl | 210 ++++++++++++++++++
.../mytheme/login/webauthn-authenticate.ftl | 102 +++++++++
.../themes/mytheme/login/webauthn-error.ftl | 36 +++
.../mytheme/login/webauthn-register.ftl | 64 ++++++
keycloak/themes/mytheme/theme.properties | 3 +
pom.xml | 26 +++
.../example/keycloakdemo/DemoController.java | 21 --
.../components/KeycloakRoleConverter.java | 50 +++++
.../MultiTenantJwtAuthManagerResolver.java | 68 ++++++
.../config/SecurityConfiguration.java | 41 ++++
.../controller/DemoController.java | 26 +++
.../keycloakdemo/others/ConstantsClass.java | 14 ++
.../keycloakdemo/others/UserRoles.java | 18 ++
src/main/resources/application.properties | 2 +
57 files changed, 2546 insertions(+), 27 deletions(-)
create mode 100644 keycloak/providers/keycloak-eventlistner-1.0-SNAPSHOT.jar
create mode 100644 keycloak/themes/mytheme/login/cli_splash.ftl
create mode 100644 keycloak/themes/mytheme/login/code.ftl
create mode 100644 keycloak/themes/mytheme/login/delete-account-confirm.ftl
create mode 100644 keycloak/themes/mytheme/login/delete-credential.ftl
create mode 100644 keycloak/themes/mytheme/login/error.ftl
create mode 100644 keycloak/themes/mytheme/login/footer.ftl
create mode 100644 keycloak/themes/mytheme/login/frontchannel-logout.ftl
create mode 100644 keycloak/themes/mytheme/login/idp-review-user-profile.ftl
create mode 100644 keycloak/themes/mytheme/login/info.ftl
create mode 100644 keycloak/themes/mytheme/login/link-idp-action.ftl
create mode 100644 keycloak/themes/mytheme/login/login-config-totp.ftl
create mode 100644 keycloak/themes/mytheme/login/login-idp-link-confirm-override.ftl
create mode 100644 keycloak/themes/mytheme/login/login-idp-link-confirm.ftl
create mode 100644 keycloak/themes/mytheme/login/login-idp-link-email.ftl
create mode 100644 keycloak/themes/mytheme/login/login-oauth-grant.ftl
create mode 100644 keycloak/themes/mytheme/login/login-oauth2-device-verify-user-code.ftl
create mode 100644 keycloak/themes/mytheme/login/login-otp.ftl
create mode 100644 keycloak/themes/mytheme/login/login-page-expired.ftl
create mode 100644 keycloak/themes/mytheme/login/login-passkeys-conditional-authenticate.ftl
create mode 100644 keycloak/themes/mytheme/login/login-password.ftl
create mode 100644 keycloak/themes/mytheme/login/login-recovery-authn-code-config.ftl
create mode 100644 keycloak/themes/mytheme/login/login-recovery-authn-code-input.ftl
create mode 100644 keycloak/themes/mytheme/login/login-reset-otp.ftl
create mode 100644 keycloak/themes/mytheme/login/login-reset-password.ftl
create mode 100644 keycloak/themes/mytheme/login/login-update-password.ftl
create mode 100644 keycloak/themes/mytheme/login/login-update-profile.ftl
create mode 100644 keycloak/themes/mytheme/login/login-username.ftl
create mode 100644 keycloak/themes/mytheme/login/login-verify-email.ftl
create mode 100644 keycloak/themes/mytheme/login/login-x509-info.ftl
create mode 100644 keycloak/themes/mytheme/login/login.ftl
create mode 100644 keycloak/themes/mytheme/login/logout-confirm.ftl
create mode 100644 keycloak/themes/mytheme/login/password-commons.ftl
create mode 100644 keycloak/themes/mytheme/login/register-commons.ftl
create mode 100644 keycloak/themes/mytheme/login/register.ftl
create mode 100644 keycloak/themes/mytheme/login/saml-post-form.ftl
create mode 100644 keycloak/themes/mytheme/login/select-authenticator.ftl
create mode 100644 keycloak/themes/mytheme/login/select-organization.ftl
create mode 100644 keycloak/themes/mytheme/login/template.ftl
create mode 100644 keycloak/themes/mytheme/login/terms.ftl
create mode 100644 keycloak/themes/mytheme/login/update-email.ftl
create mode 100644 keycloak/themes/mytheme/login/user-profile-commons.ftl
create mode 100644 keycloak/themes/mytheme/login/webauthn-authenticate.ftl
create mode 100644 keycloak/themes/mytheme/login/webauthn-error.ftl
create mode 100644 keycloak/themes/mytheme/login/webauthn-register.ftl
create mode 100644 keycloak/themes/mytheme/theme.properties
delete mode 100644 src/main/java/com/example/keycloakdemo/DemoController.java
create mode 100644 src/main/java/com/example/keycloakdemo/components/KeycloakRoleConverter.java
create mode 100644 src/main/java/com/example/keycloakdemo/components/MultiTenantJwtAuthManagerResolver.java
create mode 100644 src/main/java/com/example/keycloakdemo/config/SecurityConfiguration.java
create mode 100644 src/main/java/com/example/keycloakdemo/controller/DemoController.java
create mode 100644 src/main/java/com/example/keycloakdemo/others/ConstantsClass.java
create mode 100644 src/main/java/com/example/keycloakdemo/others/UserRoles.java
diff --git a/Dockerfile b/Dockerfile
index 6eca022..5acb35d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,4 @@
-
FROM openjdk:17-jdk-slim
VOLUME /tmp
COPY target/spring-keycloak-demo-1.0.0.jar app.jar
-ENTRYPOINT ["java", "-jar", "/app.jar"]
+ENTRYPOINT ["java", "-jar", "/app.jar"]
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index bec2026..5515bd6 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,16 +1,20 @@
version: '3.8'
-
services:
keycloak:
- image: keycloak/keycloak:21.1.0
+ image: keycloak/keycloak:latest
container_name: keycloak
+ volumes:
+ - ./keycloak/providers:/opt/keycloak/providers
+ - ./keycloak/logs:/opt/keycloak/logs
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
+ - QUARKUS_LOG_FILE_ENABLED="true"
+ - QUARKUS_LOG_FILE_PATH=/opt/keycloak/logs/spi.log
+ - QUARKUS_LOG_FILE_LEVEL=INFO
command: start-dev
ports:
- "8080:8080"
-
spring-app:
build: .
container_name: spring-keycloak-demo
@@ -19,4 +23,4 @@ services:
environment:
SPRING_PROFILES_ACTIVE: dev
depends_on:
- - keycloak
+ - keycloak
\ No newline at end of file
diff --git a/keycloak/providers/keycloak-eventlistner-1.0-SNAPSHOT.jar b/keycloak/providers/keycloak-eventlistner-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000000000000000000000000000000000000..b05c7a0163af3f7675f3097a2897fa242c2a55a7
GIT binary patch
literal 4777
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
e?CyW@wp)LUp!-ND8314;eU6fDs}
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ <#if code.success>
+ ${msg("codeSuccessTitle")}
+ <#else>
+ ${kcSanitize(msg("codeErrorTitle", code.error))}
+ #if>
+ <#elseif section = "form">
+
+ <#if code.success>
+
${msg("copyCodeInstruction")}
+
+ <#else>
+
${kcSanitize(code.error)}
+ #if>
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/delete-account-confirm.ftl b/keycloak/themes/mytheme/login/delete-account-confirm.ftl
new file mode 100644
index 0000000..6aa93f0
--- /dev/null
+++ b/keycloak/themes/mytheme/login/delete-account-confirm.ftl
@@ -0,0 +1,33 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+
+ <#if section = "header">
+ ${msg("deleteAccountConfirm")}
+
+ <#elseif section = "form">
+
+
+ #if>
+
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/delete-credential.ftl b/keycloak/themes/mytheme/login/delete-credential.ftl
new file mode 100644
index 0000000..57f30e7
--- /dev/null
+++ b/keycloak/themes/mytheme/login/delete-credential.ftl
@@ -0,0 +1,15 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=false; section>
+ <#if section = "header">
+ ${msg("deleteCredentialTitle", credentialLabel)}
+ <#elseif section = "form">
+
+ ${msg("deleteCredentialMessage", credentialLabel)}
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/error.ftl b/keycloak/themes/mytheme/login/error.ftl
new file mode 100644
index 0000000..112ded3
--- /dev/null
+++ b/keycloak/themes/mytheme/login/error.ftl
@@ -0,0 +1,16 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=false; section>
+ <#if section = "header">
+ ${kcSanitize(msg("errorTitle"))?no_esc}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/footer.ftl b/keycloak/themes/mytheme/login/footer.ftl
new file mode 100644
index 0000000..0a440a3
--- /dev/null
+++ b/keycloak/themes/mytheme/login/footer.ftl
@@ -0,0 +1,3 @@
+<#macro content>
+<#-- The footer at the bottom of the "login-box". You can override this file in your custom theme to declare a custom login footer element -->
+#macro>
diff --git a/keycloak/themes/mytheme/login/frontchannel-logout.ftl b/keycloak/themes/mytheme/login/frontchannel-logout.ftl
new file mode 100644
index 0000000..3de7d25
--- /dev/null
+++ b/keycloak/themes/mytheme/login/frontchannel-logout.ftl
@@ -0,0 +1,30 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+
+ ${msg("frontchannel-logout.title")}
+ <#elseif section = "form">
+ ${msg("frontchannel-logout.message")}
+
+ <#list logout.clients as client>
+
+ ${client.name}
+
+
+ #list>
+
+ <#if logout.logoutRedirectUri?has_content>
+
+ ${msg("doContinue")}
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/idp-review-user-profile.ftl b/keycloak/themes/mytheme/login/idp-review-user-profile.ftl
new file mode 100644
index 0000000..1b70aec
--- /dev/null
+++ b/keycloak/themes/mytheme/login/idp-review-user-profile.ftl
@@ -0,0 +1,23 @@
+<#import "template.ftl" as layout>
+<#import "user-profile-commons.ftl" as userProfileCommons>
+<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
+ <#if section = "header">
+ ${msg("loginIdpReviewProfileTitle")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/info.ftl b/keycloak/themes/mytheme/login/info.ftl
new file mode 100644
index 0000000..d8cb648
--- /dev/null
+++ b/keycloak/themes/mytheme/login/info.ftl
@@ -0,0 +1,24 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=false; section>
+ <#if section = "header">
+ <#if messageHeader??>
+ ${kcSanitize(msg("${messageHeader}"))?no_esc}
+ <#else>
+ ${message.summary}
+ #if>
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/link-idp-action.ftl b/keycloak/themes/mytheme/login/link-idp-action.ftl
new file mode 100644
index 0000000..9b03e57
--- /dev/null
+++ b/keycloak/themes/mytheme/login/link-idp-action.ftl
@@ -0,0 +1,15 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=false; section>
+ <#if section = "header">
+ ${msg("linkIdpActionTitle", idpDisplayName)}
+ <#elseif section = "form">
+
+ ${msg("linkIdpActionMessage", idpDisplayName)}
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-config-totp.ftl b/keycloak/themes/mytheme/login/login-config-totp.ftl
new file mode 100644
index 0000000..d531eba
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-config-totp.ftl
@@ -0,0 +1,114 @@
+<#import "template.ftl" as layout>
+<#import "password-commons.ftl" as passwordCommons>
+<@layout.registrationLayout displayRequiredFields=false displayMessage=!messagesPerField.existsError('totp','userLabel'); section>
+
+ <#if section = "header">
+ ${msg("loginTotpTitle")}
+ <#elseif section = "form">
+
+
+ ${msg("loginTotpStep1")}
+
+
+ <#list totp.supportedApplications as app>
+ ${msg(app)}
+ #list>
+
+
+
+ <#if mode?? && mode = "manual">
+
+ ${msg("loginTotpManualStep2")}
+ ${totp.totpSecretEncoded}
+ ${msg("loginTotpScanBarcode")}
+
+
+ ${msg("loginTotpManualStep3")}
+
+
+ ${msg("loginTotpType")}: ${msg("loginTotp." + totp.policy.type)}
+ ${msg("loginTotpAlgorithm")}: ${totp.policy.getAlgorithmKey()}
+ ${msg("loginTotpDigits")}: ${totp.policy.digits}
+ <#if totp.policy.type = "totp">
+ ${msg("loginTotpInterval")}: ${totp.policy.period}
+ <#elseif totp.policy.type = "hotp">
+ ${msg("loginTotpCounter")}: ${totp.policy.initialCounter}
+ #if>
+
+
+
+ <#else>
+
+ ${msg("loginTotpStep2")}
+
+ ${msg("loginTotpUnableToScan")}
+
+ #if>
+
+ ${msg("loginTotpStep3")}
+ ${msg("loginTotpStep3DeviceName")}
+
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-idp-link-confirm-override.ftl b/keycloak/themes/mytheme/login/login-idp-link-confirm-override.ftl
new file mode 100644
index 0000000..a5b3630
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-idp-link-confirm-override.ftl
@@ -0,0 +1,12 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("confirmOverrideIdpTitle")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-idp-link-confirm.ftl b/keycloak/themes/mytheme/login/login-idp-link-confirm.ftl
new file mode 100644
index 0000000..c3537c5
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-idp-link-confirm.ftl
@@ -0,0 +1,13 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("confirmLinkIdpTitle")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-idp-link-email.ftl b/keycloak/themes/mytheme/login/login-idp-link-email.ftl
new file mode 100644
index 0000000..0020178
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-idp-link-email.ftl
@@ -0,0 +1,16 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("emailLinkIdpTitle", idpDisplayName)}
+ <#elseif section = "form">
+
+ ${msg("emailLinkIdp1", idpDisplayName, brokerContext.username, realm.displayName)}
+
+
+ ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")}
+
+
+ ${msg("emailLinkIdp4")} ${msg("doClickHere")} ${msg("emailLinkIdp5")}
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/login-oauth-grant.ftl b/keycloak/themes/mytheme/login/login-oauth-grant.ftl
new file mode 100644
index 0000000..d5cfc4a
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-oauth-grant.ftl
@@ -0,0 +1,68 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout bodyClass="oauth"; section>
+ <#if section = "header">
+ <#if client.attributes.logoUri??>
+
+ #if>
+
+ <#if client.name?has_content>
+ ${msg("oauthGrantTitle",advancedMsg(client.name))}
+ <#else>
+ ${msg("oauthGrantTitle",client.clientId)}
+ #if>
+
+ <#elseif section = "form">
+
+
${msg("oauthGrantRequest")}
+
+ <#if oauth.clientScopesRequested??>
+ <#list oauth.clientScopesRequested as clientScope>
+
+ <#if !clientScope.dynamicScopeParameter??>
+ ${advancedMsg(clientScope.consentScreenText)}
+ <#else>
+ ${advancedMsg(clientScope.consentScreenText)}: ${clientScope.dynamicScopeParameter}
+ #if>
+
+
+ #list>
+ #if>
+
+ <#if client.attributes.policyUri?? || client.attributes.tosUri??>
+
+ <#if client.name?has_content>
+ ${msg("oauthGrantInformation",advancedMsg(client.name))}
+ <#else>
+ ${msg("oauthGrantInformation",client.clientId)}
+ #if>
+ <#if client.attributes.tosUri??>
+ ${msg("oauthGrantReview")}
+ ${msg("oauthGrantTos")}
+ #if>
+ <#if client.attributes.policyUri??>
+ ${msg("oauthGrantReview")}
+ ${msg("oauthGrantPolicy")}
+ #if>
+
+ #if>
+
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-oauth2-device-verify-user-code.ftl b/keycloak/themes/mytheme/login/login-oauth2-device-verify-user-code.ftl
new file mode 100644
index 0000000..f254254
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-oauth2-device-verify-user-code.ftl
@@ -0,0 +1,31 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("oauth2DeviceVerificationTitle")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/login-otp.ftl b/keycloak/themes/mytheme/login/login-otp.ftl
new file mode 100644
index 0000000..7273007
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-otp.ftl
@@ -0,0 +1,59 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
+ <#if section="header">
+ ${msg("doLogIn")}
+ <#elseif section="form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/login-page-expired.ftl b/keycloak/themes/mytheme/login/login-page-expired.ftl
new file mode 100644
index 0000000..2b470e0
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-page-expired.ftl
@@ -0,0 +1,11 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("pageExpiredTitle")}
+ <#elseif section = "form">
+
+ ${msg("pageExpiredMsg1")} ${msg("doClickHere")} .
+ ${msg("pageExpiredMsg2")} ${msg("doClickHere")} .
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-passkeys-conditional-authenticate.ftl b/keycloak/themes/mytheme/login/login-passkeys-conditional-authenticate.ftl
new file mode 100644
index 0000000..315f762
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-passkeys-conditional-authenticate.ftl
@@ -0,0 +1,139 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayInfo=(realm.registrationAllowed && !registrationDisabled??); section>
+ <#if section = "title">
+ title
+ <#elseif section = "header">
+ ${kcSanitize(msg("passkey-login-title"))?no_esc}
+ <#elseif section = "form">
+
+
+
+
+
+
+ <#elseif section = "info">
+ <#if realm.registrationAllowed && !registrationDisabled??>
+
+ #if>
+ #if>
+
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-password.ftl b/keycloak/themes/mytheme/login/login-password.ftl
new file mode 100644
index 0000000..fd5d9f7
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-password.ftl
@@ -0,0 +1,52 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password'); section>
+ <#if section = "header">
+ ${msg("doLogIn")}
+ <#elseif section = "form">
+
+
+ #if>
+
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-recovery-authn-code-config.ftl b/keycloak/themes/mytheme/login/login-recovery-authn-code-config.ftl
new file mode 100644
index 0000000..c664fc7
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-recovery-authn-code-config.ftl
@@ -0,0 +1,186 @@
+<#import "template.ftl" as layout>
+<#import "password-commons.ftl" as passwordCommons>
+<@layout.registrationLayout; section>
+
+<#if section = "header">
+ ${msg("recovery-code-config-header")}
+<#elseif section = "form">
+
+
+
+
+
+
+ Warning alert:
+ ${msg("recovery-code-config-warning-title")}
+
+
+
${msg("recovery-code-config-warning-message")}
+
+
+
+
+ <#list recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList as code>
+ ${code?counter}: ${code[0..3]}-${code[4..7]}-${code[8..]}
+ #list>
+
+
+
+
+
+ ${msg("recovery-codes-print")}
+
+
+ ${msg("recovery-codes-download")}
+
+
+ ${msg("recovery-codes-copy")}
+
+
+
+
+
+
+ ${msg("recovery-codes-confirmation-message")}
+
+
+
+
+
+#if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-recovery-authn-code-input.ftl b/keycloak/themes/mytheme/login/login-recovery-authn-code-input.ftl
new file mode 100644
index 0000000..d32b22e
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-recovery-authn-code-input.ftl
@@ -0,0 +1,45 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('recoveryCodeInput'); section>
+
+ <#if section = "header">
+ ${msg("auth-recovery-code-header")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/login-reset-otp.ftl b/keycloak/themes/mytheme/login/login-reset-otp.ftl
new file mode 100644
index 0000000..d8fc580
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-reset-otp.ftl
@@ -0,0 +1,33 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
+ <#if section="header">
+ ${msg("doLogIn")}
+ <#elseif section="form">
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-reset-password.ftl b/keycloak/themes/mytheme/login/login-reset-password.ftl
new file mode 100644
index 0000000..8e96fb4
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-reset-password.ftl
@@ -0,0 +1,39 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section>
+ <#if section = "header">
+ ${msg("emailForgotTitle")}
+ <#elseif section = "form">
+
+ <#elseif section = "info" >
+ <#if realm.duplicateEmailsAllowed>
+ ${msg("emailInstructionUsername")}
+ <#else>
+ ${msg("emailInstruction")}
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-update-password.ftl b/keycloak/themes/mytheme/login/login-update-password.ftl
new file mode 100644
index 0000000..4861f44
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-update-password.ftl
@@ -0,0 +1,77 @@
+<#import "template.ftl" as layout>
+<#import "password-commons.ftl" as passwordCommons>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('password','password-confirm'); section>
+ <#if section = "header">
+ ${msg("updatePasswordTitle")}
+ <#elseif section = "form">
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-update-profile.ftl b/keycloak/themes/mytheme/login/login-update-profile.ftl
new file mode 100644
index 0000000..e09f5c3
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-update-profile.ftl
@@ -0,0 +1,28 @@
+<#import "template.ftl" as layout>
+<#import "user-profile-commons.ftl" as userProfileCommons>
+<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
+ <#if section = "header">
+ ${msg("loginProfileTitle")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/login-username.ftl b/keycloak/themes/mytheme/login/login-username.ftl
new file mode 100644
index 0000000..169d128
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-username.ftl
@@ -0,0 +1,89 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username') displayInfo=(realm.password && realm.registrationAllowed && !registrationDisabled??); section>
+ <#if section = "header">
+ ${msg("loginAccountTitle")}
+ <#elseif section = "form">
+
+
+ <#elseif section = "info" >
+ <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
+
+ #if>
+ <#elseif section = "socialProviders" >
+ <#if realm.password && social?? && social.providers?has_content>
+
+ #if>
+ #if>
+
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-verify-email.ftl b/keycloak/themes/mytheme/login/login-verify-email.ftl
new file mode 100644
index 0000000..347d606
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-verify-email.ftl
@@ -0,0 +1,36 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayInfo=true; section>
+ <#if section = "header">
+ ${msg("emailVerifyTitle")}
+ <#elseif section = "form">
+
+ <#if verifyEmail??>
+ ${msg("emailVerifyInstruction1",verifyEmail)}
+ <#else>
+ ${msg("emailVerifyInstruction4",user.email)}
+ #if>
+
+ <#if isAppInitiatedAction??>
+
+ #if>
+ <#elseif section = "info">
+ <#if !isAppInitiatedAction??>
+
+ ${msg("emailVerifyInstruction2")}
+
+ ${msg("doClickHere")} ${msg("emailVerifyInstruction3")}
+
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login-x509-info.ftl b/keycloak/themes/mytheme/login/login-x509-info.ftl
new file mode 100644
index 0000000..0228b06
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login-x509-info.ftl
@@ -0,0 +1,55 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("doLogIn")}
+ <#elseif section = "form">
+
+
+ #if>
+
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/login.ftl b/keycloak/themes/mytheme/login/login.ftl
new file mode 100644
index 0000000..f547c49
--- /dev/null
+++ b/keycloak/themes/mytheme/login/login.ftl
@@ -0,0 +1,117 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','password') displayInfo=realm.password && realm.registrationAllowed && !registrationDisabled??; section>
+ <#if section = "header">
+ ${msg("loginAccountTitle")}
+ <#elseif section = "form">
+
+
+ <#elseif section = "info" >
+ <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
+
+ #if>
+ <#elseif section = "socialProviders" >
+ <#if realm.password && social?? && social.providers?has_content>
+
+ #if>
+ #if>
+
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/logout-confirm.ftl b/keycloak/themes/mytheme/login/logout-confirm.ftl
new file mode 100644
index 0000000..2ac4c77
--- /dev/null
+++ b/keycloak/themes/mytheme/login/logout-confirm.ftl
@@ -0,0 +1,38 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("logoutConfirmTitle")}
+ <#elseif section = "form">
+
+
${msg("logoutConfirmHeader")}
+
+
+
+
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/password-commons.ftl b/keycloak/themes/mytheme/login/password-commons.ftl
new file mode 100644
index 0000000..233c781
--- /dev/null
+++ b/keycloak/themes/mytheme/login/password-commons.ftl
@@ -0,0 +1,12 @@
+<#macro logoutOtherSessions>
+
+#macro>
diff --git a/keycloak/themes/mytheme/login/register-commons.ftl b/keycloak/themes/mytheme/login/register-commons.ftl
new file mode 100644
index 0000000..7007797
--- /dev/null
+++ b/keycloak/themes/mytheme/login/register-commons.ftl
@@ -0,0 +1,27 @@
+<#macro termsAcceptance>
+ <#if termsAcceptanceRequired??>
+
+
+ #if>
+#macro>
diff --git a/keycloak/themes/mytheme/login/register.ftl b/keycloak/themes/mytheme/login/register.ftl
new file mode 100644
index 0000000..142daca
--- /dev/null
+++ b/keycloak/themes/mytheme/login/register.ftl
@@ -0,0 +1,61 @@
+<#import "template.ftl" as layout>
+<#import "user-profile-commons.ftl" as userProfileCommons>
+<#import "register-commons.ftl" as registerCommons>
+
+<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
+ <#if section = "header">
+ <#if messageHeader??>
+ ${kcSanitize(msg("${messageHeader}"))?no_esc}
+ <#else>
+ ${msg("registerTitle")}
+ #if>
+ <#elseif section = "form">
+
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/saml-post-form.ftl b/keycloak/themes/mytheme/login/saml-post-form.ftl
new file mode 100644
index 0000000..94b0c30
--- /dev/null
+++ b/keycloak/themes/mytheme/login/saml-post-form.ftl
@@ -0,0 +1,25 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "header">
+ ${msg("saml.post-form.title")}
+ <#elseif section = "form">
+
+ ${msg("saml.post-form.message")}
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/select-authenticator.ftl b/keycloak/themes/mytheme/login/select-authenticator.ftl
new file mode 100644
index 0000000..4fbe2d3
--- /dev/null
+++ b/keycloak/themes/mytheme/login/select-authenticator.ftl
@@ -0,0 +1,36 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayInfo=false; section>
+ <#if section = "header" || section = "show-username">
+ <#if section = "header">
+ ${msg("loginChooseAuthenticator")}
+ #if>
+ <#elseif section = "form">
+
+
+
+ #if>
+@layout.registrationLayout>
+
diff --git a/keycloak/themes/mytheme/login/select-organization.ftl b/keycloak/themes/mytheme/login/select-organization.ftl
new file mode 100644
index 0000000..6206afd
--- /dev/null
+++ b/keycloak/themes/mytheme/login/select-organization.ftl
@@ -0,0 +1,23 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout; section>
+ <#if section = "form">
+
+ #if>
+
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/template.ftl b/keycloak/themes/mytheme/login/template.ftl
new file mode 100644
index 0000000..01d802c
--- /dev/null
+++ b/keycloak/themes/mytheme/login/template.ftl
@@ -0,0 +1,205 @@
+<#import "footer.ftl" as loginFooter>
+<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
+
+ dir="${(locale.rtl)?then('rtl','ltr')}"#if>>
+
+
+
+
+
+
+ <#if properties.meta?has_content>
+ <#list properties.meta?split(' ') as meta>
+
+ #list>
+ #if>
+ ${msg("loginTitle",(realm.displayName!''))}
+
+ <#if properties.stylesCommon?has_content>
+ <#list properties.stylesCommon?split(' ') as style>
+
+ #list>
+ #if>
+ <#if properties.styles?has_content>
+ <#list properties.styles?split(' ') as style>
+
+ #list>
+ #if>
+ <#if properties.scripts?has_content>
+ <#list properties.scripts?split(' ') as script>
+
+ #list>
+ #if>
+
+
+ <#if scripts??>
+ <#list scripts as script>
+
+ #list>
+ #if>
+
+
+ <#if authenticationSession??>
+
+ #if>
+
+
+
+
+
+
+#macro>
diff --git a/keycloak/themes/mytheme/login/terms.ftl b/keycloak/themes/mytheme/login/terms.ftl
new file mode 100644
index 0000000..687b192
--- /dev/null
+++ b/keycloak/themes/mytheme/login/terms.ftl
@@ -0,0 +1,15 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=false; section>
+ <#if section = "header">
+ ${msg("termsTitle")}
+ <#elseif section = "form">
+
+ ${kcSanitize(msg("termsText"))?no_esc}
+
+
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/update-email.ftl b/keycloak/themes/mytheme/login/update-email.ftl
new file mode 100644
index 0000000..1650e25
--- /dev/null
+++ b/keycloak/themes/mytheme/login/update-email.ftl
@@ -0,0 +1,30 @@
+<#import "template.ftl" as layout>
+<#import "password-commons.ftl" as passwordCommons>
+<#import "user-profile-commons.ftl" as userProfileCommons>
+<@layout.registrationLayout displayMessage=messagesPerField.exists('global') displayRequiredFields=true; section>
+ <#if section = "header">
+ ${msg("updateEmailTitle")}
+ <#elseif section = "form">
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/user-profile-commons.ftl b/keycloak/themes/mytheme/login/user-profile-commons.ftl
new file mode 100644
index 0000000..0beda89
--- /dev/null
+++ b/keycloak/themes/mytheme/login/user-profile-commons.ftl
@@ -0,0 +1,210 @@
+<#macro userProfileFormFields>
+ <#assign currentGroup="">
+
+ <#list profile.attributes as attribute>
+
+ <#if attribute.name=='locale' && realm.internationalizationEnabled && locale.currentLanguageTag?has_content>
+
+ <#else>
+
+ <#assign group = (attribute.group)!"">
+ <#if group != currentGroup>
+ <#assign currentGroup=group>
+ <#if currentGroup != "">
+
+ #if>
+ #if>
+
+ <#nested "beforeField" attribute>
+
+ <#nested "afterField" attribute>
+
+ #if>
+ #list>
+
+ <#list profile.html5DataAnnotations?keys as key>
+
+ #list>
+#macro>
+
+<#macro inputFieldByType attribute>
+ <#switch attribute.annotations.inputType!''>
+ <#case 'textarea'>
+ <@textareaTag attribute=attribute/>
+ <#break>
+ <#case 'select'>
+ <#case 'multiselect'>
+ <@selectTag attribute=attribute/>
+ <#break>
+ <#case 'select-radiobuttons'>
+ <#case 'multiselect-checkboxes'>
+ <@inputTagSelects attribute=attribute/>
+ <#break>
+ <#default>
+ <#if attribute.multivalued && attribute.values?has_content>
+ <#list attribute.values as value>
+ <@inputTag attribute=attribute value=value!''/>
+ #list>
+ <#else>
+ <@inputTag attribute=attribute value=attribute.value!''/>
+ #if>
+ #switch>
+#macro>
+
+<#macro inputTag attribute value>
+ disabled#if>
+ <#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}"#if>
+ <#if attribute.annotations.inputTypePlaceholder??>placeholder="${advancedMsg(attribute.annotations.inputTypePlaceholder)}"#if>
+ <#if attribute.annotations.inputTypePattern??>pattern="${attribute.annotations.inputTypePattern}"#if>
+ <#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"#if>
+ <#if attribute.annotations.inputTypeMaxlength??>maxlength="${attribute.annotations.inputTypeMaxlength}"#if>
+ <#if attribute.annotations.inputTypeMinlength??>minlength="${attribute.annotations.inputTypeMinlength}"#if>
+ <#if attribute.annotations.inputTypeMax??>max="${attribute.annotations.inputTypeMax}"#if>
+ <#if attribute.annotations.inputTypeMin??>min="${attribute.annotations.inputTypeMin}"#if>
+ <#if attribute.annotations.inputTypeStep??>step="${attribute.annotations.inputTypeStep}"#if>
+ <#list attribute.html5DataAnnotations as key, value>
+ data-${key}="${value}"
+ #list>
+ />
+#macro>
+
+<#macro inputTagType attribute>
+ <#compress>
+ <#if attribute.annotations.inputType??>
+ <#if attribute.annotations.inputType?starts_with("html5-")>
+ ${attribute.annotations.inputType[6..]}
+ <#else>
+ ${attribute.annotations.inputType}
+ #if>
+ <#else>
+ text
+ #if>
+ #compress>
+#macro>
+
+<#macro textareaTag attribute>
+
+#macro>
+
+<#macro selectTag attribute>
+ disabled#if>
+ <#if attribute.annotations.inputType=='multiselect'>multiple#if>
+ <#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}"#if>
+ >
+ <#if attribute.annotations.inputType=='select'>
+
+ #if>
+
+ <#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
+ <#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
+ <#elseif attribute.validators.options?? && attribute.validators.options.options??>
+ <#assign options=attribute.validators.options.options>
+ <#else>
+ <#assign options=[]>
+ #if>
+
+ <#list options as option>
+ selected#if>><@selectOptionLabelText attribute=attribute option=option/>
+ #list>
+
+
+#macro>
+
+<#macro inputTagSelects attribute>
+ <#if attribute.annotations.inputType=='select-radiobuttons'>
+ <#assign inputType='radio'>
+ <#assign classDiv=properties.kcInputClassRadio!>
+ <#assign classInput=properties.kcInputClassRadioInput!>
+ <#assign classLabel=properties.kcInputClassRadioLabel!>
+ <#else>
+ <#assign inputType='checkbox'>
+ <#assign classDiv=properties.kcInputClassCheckbox!>
+ <#assign classInput=properties.kcInputClassCheckboxInput!>
+ <#assign classLabel=properties.kcInputClassCheckboxLabel!>
+ #if>
+
+ <#if attribute.annotations.inputOptionsFromValidation?? && attribute.validators[attribute.annotations.inputOptionsFromValidation]?? && attribute.validators[attribute.annotations.inputOptionsFromValidation].options??>
+ <#assign options=attribute.validators[attribute.annotations.inputOptionsFromValidation].options>
+ <#elseif attribute.validators.options?? && attribute.validators.options.options??>
+ <#assign options=attribute.validators.options.options>
+ <#else>
+ <#assign options=[]>
+ #if>
+
+ <#list options as option>
+
+ disabled#if>
+ <#if attribute.values?seq_contains(option)>checked#if>
+ />
+ <@selectOptionLabelText attribute=attribute option=option/>
+
+ #list>
+#macro>
+
+<#macro selectOptionLabelText attribute option>
+ <#compress>
+ <#if attribute.annotations.inputOptionLabels??>
+ ${advancedMsg(attribute.annotations.inputOptionLabels[option]!option)}
+ <#else>
+ <#if attribute.annotations.inputOptionLabelsI18nPrefix??>
+ ${msg(attribute.annotations.inputOptionLabelsI18nPrefix + '.' + option)}
+ <#else>
+ ${option}
+ #if>
+ #if>
+ #compress>
+#macro>
diff --git a/keycloak/themes/mytheme/login/webauthn-authenticate.ftl b/keycloak/themes/mytheme/login/webauthn-authenticate.ftl
new file mode 100644
index 0000000..8148e80
--- /dev/null
+++ b/keycloak/themes/mytheme/login/webauthn-authenticate.ftl
@@ -0,0 +1,102 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayInfo=(realm.registrationAllowed && !registrationDisabled??); section>
+ <#if section = "title">
+ title
+ <#elseif section = "header">
+ ${kcSanitize(msg("webauthn-login-title"))?no_esc}
+ <#elseif section = "form">
+
+
+
+
+ <#elseif section = "info">
+ <#if realm.registrationAllowed && !registrationDisabled??>
+
+ #if>
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/login/webauthn-error.ftl b/keycloak/themes/mytheme/login/webauthn-error.ftl
new file mode 100644
index 0000000..2c31dab
--- /dev/null
+++ b/keycloak/themes/mytheme/login/webauthn-error.ftl
@@ -0,0 +1,36 @@
+<#import "template.ftl" as layout>
+<@layout.registrationLayout displayMessage=true; section>
+ <#if section = "header">
+ ${kcSanitize(msg("webauthn-error-title"))?no_esc}
+ <#elseif section = "form">
+
+
+
+
+
+
+
+ <#if isAppInitiatedAction??>
+
+ #if>
+
+ #if>
+@layout.registrationLayout>
\ No newline at end of file
diff --git a/keycloak/themes/mytheme/login/webauthn-register.ftl b/keycloak/themes/mytheme/login/webauthn-register.ftl
new file mode 100644
index 0000000..af4c950
--- /dev/null
+++ b/keycloak/themes/mytheme/login/webauthn-register.ftl
@@ -0,0 +1,64 @@
+<#import "template.ftl" as layout>
+<#import "password-commons.ftl" as passwordCommons>
+
+<@layout.registrationLayout; section>
+ <#if section = "title">
+ title
+ <#elseif section = "header">
+
+ ${kcSanitize(msg("webauthn-registration-title"))?no_esc}
+ <#elseif section = "form">
+
+
+
+
+
+
+
+ <#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
+
+ #if>
+
+ #if>
+@layout.registrationLayout>
diff --git a/keycloak/themes/mytheme/theme.properties b/keycloak/themes/mytheme/theme.properties
new file mode 100644
index 0000000..128ad2c
--- /dev/null
+++ b/keycloak/themes/mytheme/theme.properties
@@ -0,0 +1,3 @@
+parent=base
+name=mytheme
+types=login
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index cd1fa84..0be5595 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,24 @@
org.springframework.boot
spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.projectlombok
+ lombok
+ true
+ 1.18.38
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
@@ -25,6 +43,14 @@
org.springframework.boot
spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
diff --git a/src/main/java/com/example/keycloakdemo/DemoController.java b/src/main/java/com/example/keycloakdemo/DemoController.java
deleted file mode 100644
index 76af2bb..0000000
--- a/src/main/java/com/example/keycloakdemo/DemoController.java
+++ /dev/null
@@ -1,21 +0,0 @@
-
-package com.example.keycloakdemo;
-
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-@RestController
-@RequestMapping("/api")
-public class DemoController {
-
- @GetMapping("/public")
- public String publicEndpoint() {
- return "This is a public endpoint accessible by anyone.";
- }
-
- @GetMapping("/protected")
- public String protectedEndpoint() {
- return "This is a protected endpoint accessible only by authenticated users.";
- }
-}
diff --git a/src/main/java/com/example/keycloakdemo/components/KeycloakRoleConverter.java b/src/main/java/com/example/keycloakdemo/components/KeycloakRoleConverter.java
new file mode 100644
index 0000000..f8aa0fa
--- /dev/null
+++ b/src/main/java/com/example/keycloakdemo/components/KeycloakRoleConverter.java
@@ -0,0 +1,50 @@
+package com.example.keycloakdemo.components;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static com.example.keycloakdemo.others.ConstantsClass.ROLES;
+
+@Component
+@Slf4j
+public class KeycloakRoleConverter implements Converter {
+
+ @Override
+ public AbstractAuthenticationToken convert(Jwt jwt) {
+ log.info(">>> Converting JWT for subject: " + jwt.getSubject());
+
+ Collection authorities = new ArrayList<>();
+
+ // Client roles
+ Map resourceAccess = jwt.getClaimAsMap("resource_access");
+ if (resourceAccess != null) {
+ for (Map.Entry mapEntry : resourceAccess.entrySet()) {
+ Object value = mapEntry.getValue();
+ Map clientId = (Map) value;
+ if (clientId != null && clientId.containsKey(ROLES)) {
+ List roles = (List) clientId.get(ROLES);
+ authorities.addAll(roles.stream()
+ .map(role -> {
+
+ /*String cleanRole = role.replaceAll("^.*_", "");*/
+ return new SimpleGrantedAuthority("ROLE_" + role);
+ }
+ )
+ .toList());
+ }
+ }
+ }
+ return new JwtAuthenticationToken(jwt, authorities);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/keycloakdemo/components/MultiTenantJwtAuthManagerResolver.java b/src/main/java/com/example/keycloakdemo/components/MultiTenantJwtAuthManagerResolver.java
new file mode 100644
index 0000000..9490d55
--- /dev/null
+++ b/src/main/java/com/example/keycloakdemo/components/MultiTenantJwtAuthManagerResolver.java
@@ -0,0 +1,68 @@
+package com.example.keycloakdemo.components;
+
+import com.nimbusds.jwt.SignedJWT;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.security.authentication.*;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.example.keycloakdemo.others.ConstantsClass.PROTOCOL_URL;
+
+@Component
+@Slf4j
+public class MultiTenantJwtAuthManagerResolver implements AuthenticationManagerResolver {
+
+ private final Map authenticationManagers = new HashMap<>();
+ private final Converter jwtGrantedAuthoritiesConverter;
+
+ @Autowired
+ public MultiTenantJwtAuthManagerResolver(@Value("${keycloak.realms}") String[] realms,
+ Converter jwtGrantedAuthoritiesConverter) {
+ log.info(">>> Configured realms: {}", Arrays.toString(realms));
+ this.jwtGrantedAuthoritiesConverter = jwtGrantedAuthoritiesConverter;
+
+ for (String realm : realms) {
+ String jwkSetUri = realm + PROTOCOL_URL;
+ var decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
+ var provider = new JwtAuthenticationProvider(decoder);
+ provider.setJwtAuthenticationConverter(jwtGrantedAuthoritiesConverter);
+ authenticationManagers.put(realm, new ProviderManager(provider));
+ }
+ }
+
+ @Override
+ public AuthenticationManager resolve(HttpServletRequest request) {
+ String authHeader = request.getHeader("Authorization");
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ log.info(">>> No Bearer token, returning null");
+ return null;
+ }
+ try {
+ String token = authHeader.substring(7);
+ SignedJWT jwt = SignedJWT.parse(token);
+ String issuer = jwt.getJWTClaimsSet().getIssuer();
+ log.info(">>> Token issuer: {}", issuer);
+ AuthenticationManager manager = authenticationManagers.get(issuer);
+ if (manager == null) {
+ log.warn(">>> No AuthenticationManager found for issuer {}", issuer);
+ // можно здесь бросить ошибку, чтобы не возвращать null
+ throw new AuthenticationCredentialsNotFoundException("Unknown issuer: " + issuer);
+ }
+ log.info(">>> Found AuthenticationManager for issuer {}", issuer);
+ return manager;
+ } catch (Exception e) {
+ log.error(">>> Error parsing JWT", e);
+ throw new RuntimeException("Invalid token", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/keycloakdemo/config/SecurityConfiguration.java b/src/main/java/com/example/keycloakdemo/config/SecurityConfiguration.java
new file mode 100644
index 0000000..6ced93b
--- /dev/null
+++ b/src/main/java/com/example/keycloakdemo/config/SecurityConfiguration.java
@@ -0,0 +1,41 @@
+package com.example.keycloakdemo.config;
+
+import com.example.keycloakdemo.components.MultiTenantJwtAuthManagerResolver;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.web.SecurityFilterChain;
+
+@Setter
+
+@Configuration
+@EnableWebSecurity
+@Slf4j
+public class SecurityConfiguration {
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
+ MultiTenantJwtAuthManagerResolver resolver
+ ) throws Exception {
+ return httpSecurity
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(registry -> {
+ registry.requestMatchers("/one").hasRole("ADMIN");
+ registry.requestMatchers("/two").hasRole("USER");
+ registry.requestMatchers("/three").hasRole("MODERATOR");
+ registry.anyRequest().authenticated();
+ })
+ .oauth2ResourceServer(oauth2 -> oauth2
+ .authenticationManagerResolver(resolver)
+ )
+ .sessionManagement(session -> session
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/example/keycloakdemo/controller/DemoController.java b/src/main/java/com/example/keycloakdemo/controller/DemoController.java
new file mode 100644
index 0000000..1013f3a
--- /dev/null
+++ b/src/main/java/com/example/keycloakdemo/controller/DemoController.java
@@ -0,0 +1,26 @@
+
+package com.example.keycloakdemo.controller;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping
+public class DemoController {
+
+ @GetMapping("/one")
+ String oneEndpoint() {
+ return "One";
+ }
+
+ @GetMapping("/two")
+ String twoEndpoint() {
+ return "Two";
+ }
+
+ @GetMapping("/three")
+ String threeEndpoint() {
+ return "Three";
+ }
+}
diff --git a/src/main/java/com/example/keycloakdemo/others/ConstantsClass.java b/src/main/java/com/example/keycloakdemo/others/ConstantsClass.java
new file mode 100644
index 0000000..1a7ef54
--- /dev/null
+++ b/src/main/java/com/example/keycloakdemo/others/ConstantsClass.java
@@ -0,0 +1,14 @@
+package com.example.keycloakdemo.others;
+
+/**
+ *
+ * Класс с константами
+ *
+ */
+public class ConstantsClass {
+ public static final String SECRETKEY = "F510C4B0354B33E8ED73016958603787042439D41EC31038150B1FA6986B6AA532415E9E6DCF1A60A3354F14CAA6CB2EBF573130E104FF296BDF20ED65D4BC4B";
+
+ public static final String PROTOCOL_URL = "/protocol/openid-connect/certs";
+
+ public static final String ROLES = "roles";
+}
diff --git a/src/main/java/com/example/keycloakdemo/others/UserRoles.java b/src/main/java/com/example/keycloakdemo/others/UserRoles.java
new file mode 100644
index 0000000..6af7c46
--- /dev/null
+++ b/src/main/java/com/example/keycloakdemo/others/UserRoles.java
@@ -0,0 +1,18 @@
+package com.example.keycloakdemo.others;
+
+public enum UserRoles {
+
+ ADMIN("ADMIN"),
+ USER("USER"),
+ MODERATOR("MODERATOR");
+
+ private String userRoles;
+
+ UserRoles(String userRoles) {
+ this.userRoles = userRoles;
+ }
+
+ public String getUserRoles() {
+ return userRoles;
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index e69de29..8ffc173 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+keycloak.realms=http://localhost:8080/realms/my-realm
+server.port=8081
\ No newline at end of file