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 0000000..b05c7a0
Binary files /dev/null and b/keycloak/providers/keycloak-eventlistner-1.0-SNAPSHOT.jar differ
diff --git a/keycloak/themes/mytheme/login/cli_splash.ftl b/keycloak/themes/mytheme/login/cli_splash.ftl
new file mode 100644
index 0000000..cd9ebbb
--- /dev/null
+++ b/keycloak/themes/mytheme/login/cli_splash.ftl
@@ -0,0 +1,7 @@
+ _ __ _ _
+| |/ /___ _ _ ___| | ___ __ _| | __
+| ' // _ \ | | |/ __| |/ _ \ / _` | |/ /
+| . \ __/ |_| | (__| | (_) | (_| | <
+|_|\_\___|\__, |\___|_|\___/ \__,_|_|\_\
+ |___/
+
diff --git a/keycloak/themes/mytheme/login/code.ftl b/keycloak/themes/mytheme/login/code.ftl
new file mode 100644
index 0000000..bb0621d
--- /dev/null
+++ b/keycloak/themes/mytheme/login/code.ftl
@@ -0,0 +1,19 @@
+<#import "template.ftl" as layout>
+<@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