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))} + + <#elseif section = "form"> +
+ <#if code.success> +

${msg("copyCodeInstruction")}

+ + <#else> +

${kcSanitize(code.error)}

+ +
+ + 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"> + +
+ +
+ + ${msg("irreversibleAction")} +
+ +

${msg("deletingImplies")}

+
    +
  • ${msg("loggingOutImmediately")}
  • +
  • ${msg("errasingData")}
  • +
+ + + +
+ + <#if triggered_from_aia> + + +
+
+ + + \ 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)} +
+
+ + +
+
+ + 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"> +
+

${kcSanitize(message.summary)?no_esc}

+ <#if skipLink??> + <#else> + <#if client?? && client.baseUrl?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ + \ 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 --> + 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} + +
  • + +
+ <#if logout.logoutRedirectUri?has_content> + + ${msg("doContinue")} + + + 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"> +
+ + <@userProfileCommons.userProfileFormFields/> + +
+
+
+
+
+ +
+ +
+
+
+ + \ 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} + + <#elseif section = "form"> +
+

${message.summary}<#if requiredActions??><#list requiredActions>: <#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, <#else>

+ <#if skipLink??> + <#else> + <#if pageRedirectUri?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ <#elseif actionUri?has_content> +

${kcSanitize(msg("proceedWithAction"))?no_esc}

+ <#elseif (client.baseUrl)?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ + \ 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"> + +
+ + +
+
+ + 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"> +
    +
  1. +

    ${msg("loginTotpStep1")}

    + +
      + <#list totp.supportedApplications as app> +
    • ${msg(app)}
    • + +
    +
  2. + + <#if mode?? && mode = "manual"> +
  3. +

    ${msg("loginTotpManualStep2")}

    +

    ${totp.totpSecretEncoded}

    +

    ${msg("loginTotpScanBarcode")}

    +
  4. +
  5. +

    ${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}
    • + +
    +

    +
  6. + <#else> +
  7. +

    ${msg("loginTotpStep2")}

    + Figure: Barcode
    +

    ${msg("loginTotpUnableToScan")}

    +
  8. + +
  9. +

    ${msg("loginTotpStep3")}

    +

    ${msg("loginTotpStep3DeviceName")}

    +
  10. +
+ +
+
+
+ * +
+
+ + + <#if messagesPerField.existsError('totp')> + + ${kcSanitize(messagesPerField.get('totp'))?no_esc} + + + +
+ + <#if mode??> +
+ +
+
+ <#if totp.otpCredentials?size gte 1>* +
+ +
+ + + <#if messagesPerField.existsError('userLabel')> + + ${kcSanitize(messagesPerField.get('userLabel'))?no_esc} + + +
+
+ +
+ <@passwordCommons.logoutOtherSessions/> +
+ + <#if isAppInitiatedAction??> + + + <#else> + + +
+ + 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"> +
+ ${msg("pageExpiredMsg1")} ${msg("doClickHere")} + + +
+ + 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"> +
+
+ + +
+
+ + 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")} +

+ + \ 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 client.name?has_content> + ${msg("oauthGrantTitle",advancedMsg(client.name))} + <#else> + ${msg("oauthGrantTitle",client.clientId)} + +

+ <#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 client.attributes.policyUri?? || client.attributes.tosUri??> +

+ <#if client.name?has_content> + ${msg("oauthGrantInformation",advancedMsg(client.name))} + <#else> + ${msg("oauthGrantInformation",client.clientId)} + + <#if client.attributes.tosUri??> + ${msg("oauthGrantReview")} + ${msg("oauthGrantTos")} + + <#if client.attributes.policyUri??> + ${msg("oauthGrantReview")} + ${msg("oauthGrantPolicy")} + +

+ + +
+ +
+
+
+
+
+ +
+
+ + +
+
+
+
+
+
+ + 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"> +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+
+
+ + \ 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 otpLogin.userOtpCredentials?size gt 1> +
+
+ <#list otpLogin.userOtpCredentials as otpCredential> + checked="checked"> + + +
+
+ + +
+
+ +
+ +
+ + + <#if messagesPerField.existsError('totp')> + + ${kcSanitize(messagesPerField.get('totp'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ +
+
+
+ + \ 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")} . +

+ + 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"> +
+ + + + + + +
+ +
+ <#if authenticators??> +
+ <#list authenticators.authenticators as authenticator> + + +
+ + <#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators> + <#if authenticators.authenticators?size gt 1> +

${kcSanitize(msg("passkey-available-authenticators"))?no_esc}

+ + +
+ <#list authenticators.authenticators as authenticator> +
+
+ +
+
+
+ ${kcSanitize(msg('${authenticator.label}'))?no_esc} +
+ + <#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content> +
+ <#list authenticator.transports.displayNameProperties as nameProperty> + ${kcSanitize(msg('${nameProperty!}'))?no_esc} + <#if nameProperty?has_next> + , + + +
+ + +
+ + ${kcSanitize(msg('passkey-createdAt-label'))?no_esc} + + + ${kcSanitize(authenticator.createdAt)?no_esc} + +
+
+
+
+ +
+ + + +
+
+ <#if realm.password> + + + +
+
+
+ + + + <#elseif section = "info"> + <#if realm.registrationAllowed && !registrationDisabled??> +
+ ${msg("noAccount")} ${msg("doRegister")} +
+ + + + 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 messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+ +
+
+
+ <#if realm.resetPasswordAllowed> + ${msg("doForgotPassword")} + +
+
+
+ +
+ +
+
+
+
+ + + + 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> +
  1. ${code?counter}: ${code[0..3]}-${code[4..7]}-${code[8..]}
  2. + +
+ + +
+ + + +
+ + +
+ + +
+ +
+ + + + <@passwordCommons.logoutOtherSessions/> + + <#if isAppInitiatedAction??> + + + <#else> + + +
+ + + + 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 messagesPerField.existsError('recoveryCodeInput')> + + ${kcSanitize(messagesPerField.get('recoveryCodeInput'))?no_esc} + + +
+
+ +
+
+
+
+
+ +
+ +
+
+
+ + \ 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"> +
+
+
+

${msg("otp-reset-description")}

+ + <#list configuredOtpCredentials.userOtpCredentials as otpCredential> + checked="checked"> + + + +
+
+ +
+
+
+
+
+ + 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"> +
+
+
+ +
+
+ + <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+
+ +
+ <#elseif section = "info" > + <#if realm.duplicateEmailsAllowed> + ${msg("emailInstructionUsername")} + <#else> + ${msg("emailInstruction")} + + + 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 messagesPerField.existsError('password')> + + ${kcSanitize(messagesPerField.get('password'))?no_esc} + + +
+
+ +
+
+ +
+
+
+ + +
+ + <#if messagesPerField.existsError('password-confirm')> + + ${kcSanitize(messagesPerField.get('password-confirm'))?no_esc} + + + +
+
+ +
+ <@passwordCommons.logoutOtherSessions/> + +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + + 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"> +
+ + <@userProfileCommons.userProfileFormFields/> + +
+
+
+
+
+ +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + \ 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"> +
+
+ <#if realm.password> +
+ <#if !usernameHidden??> +
+ + + + + <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+ + +
+
+ <#if realm.rememberMe && !usernameHidden??> +
+ +
+ +
+
+ +
+ +
+
+ +
+
+ + <#elseif section = "info" > + <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+ ${msg("noAccount")} ${msg("doRegister")} +
+ + <#elseif section = "socialProviders" > + <#if realm.password && social?? && social.providers?has_content> +
+
+

${msg("identity-provider-login-label")}

+ + +
+ + + + 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 isAppInitiatedAction??> +
+
+
+ <#if verifyEmail??> + + <#else> + + + +
+
+
+ + <#elseif section = "info"> + <#if !isAppInitiatedAction??> +

+ ${msg("emailVerifyInstruction2")} +
+ ${msg("doClickHere")} ${msg("emailVerifyInstruction3")} +

+ + + 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 x509.formData.subjectDN??> +
+ +
+ <#else> +
+ +
+ +
+ +
+ + <#if x509.formData.isUserEnabled??> +
+ +
+
+ +
+ + +
+ +
+
+
+
+
+ +
+
+ + <#if x509.formData.isUserEnabled??> + + +
+
+
+
+ + + 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"> +
+
+ <#if realm.password> +
+ <#if !usernameHidden??> +
+ + + + + <#if messagesPerField.existsError('username','password')> + + ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} + + + +
+ + +
+ + +
+ + +
+ + <#if usernameHidden?? && messagesPerField.existsError('username','password')> + + ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} + + + +
+ +
+
+ <#if realm.rememberMe && !usernameHidden??> +
+ +
+ +
+
+ <#if realm.resetPasswordAllowed> + ${msg("doForgotPassword")} + +
+ +
+ +
+ value="${auth.selectedCredential}"/> + +
+
+ +
+
+ + <#elseif section = "info" > + <#if realm.password && realm.registrationAllowed && !registrationDisabled??> +
+
+ ${msg("noAccount")} ${msg("doRegister")} +
+
+ + <#elseif section = "socialProviders" > + <#if realm.password && social?? && social.providers?has_content> +
+
+

${msg("identity-provider-login-label")}

+ + +
+ + + + 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 logoutConfirm.skipLink> + <#else> + <#if (client.baseUrl)?has_content> +

${kcSanitize(msg("backToApplication"))?no_esc}

+ + +
+ +
+
+ + 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> +
+
+
+ +
+
+
+ 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??> +
+
+ ${msg("termsTitle")} +
+ ${kcSanitize(msg("termsText"))?no_esc} +
+
+
+
+
+ + +
+ <#if messagesPerField.existsError('termsAccepted')> +
+ + ${kcSanitize(messagesPerField.get('termsAccepted'))?no_esc} + +
+ +
+ + 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")} + + <#elseif section = "form"> +
+ + <@userProfileCommons.userProfileFormFields; callback, attribute> + <#if callback == "afterField" && passwordRequired?? && (attribute.name == 'username' || (attribute.name == 'email' && realm.registrationEmailAsUsername))> + +
+
+ * +
+
+ +
+
+ + + + +
+
+ +
+
+ +
+
+ + <@registerCommons.termsAcceptance/> + <#if recaptchaRequired?? && (recaptchaVisible!false)> +
+
+
+
+
+ + +
+
+ +
+
+
+ + + \ 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 samlPost.SAMLRequest??> + + + <#if samlPost.SAMLResponse??> + + + <#if samlPost.relayState??> + + + + +
+ + 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")} + + <#elseif section = "form"> + +
+
+ <#list auth.authenticationSelections as authenticationSelection> + + +
+
+ + + + 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"> +
+
+

${msg("organization.select")}

+ + +
+ +
+ + + 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 properties.meta?has_content> + <#list properties.meta?split(' ') as meta> + + + + ${msg("loginTitle",(realm.displayName!''))} + + <#if properties.stylesCommon?has_content> + <#list properties.stylesCommon?split(' ') as style> + + + + <#if properties.styles?has_content> + <#list properties.styles?split(' ') as style> + + + + <#if properties.scripts?has_content> + <#list properties.scripts?split(' ') as script> + + + + + + <#if scripts??> + <#list scripts as script> + + + + + + <#if authenticationSession??> + + + + + +
+
+
${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}
+
+
+
+ <#if realm.internationalizationEnabled && locale.supported?size gt 1> +
+
+ +
+
+ + <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+

<#nested "header">

+
+
+ <#else> +

<#nested "header">

+ + <#else> + <#if displayRequiredFields> +
+
+ * ${msg("requiredFields")} +
+
+ <#nested "show-username"> +
+ + + + +
+
+
+ <#else> + <#nested "show-username"> +
+ + + + +
+ + +
+
+
+ + <#-- App-initiated actions should not see warning messages about the need to complete the action --> + <#-- during login. --> + <#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)> +
+
+ <#if message.type = 'success'> + <#if message.type = 'warning'> + <#if message.type = 'error'> + <#if message.type = 'info'> +
+ ${kcSanitize(message.summary)?no_esc} +
+ + + <#nested "form"> + + <#if auth?has_content && auth.showTryAnotherWayLink()> +
+ +
+ + + <#nested "socialProviders"> + + <#if displayInfo> +
+
+ <#nested "info"> +
+
+ +
+
+ + <@loginFooter.content/> +
+
+ + + 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} +
+
+ + +
+
+ + 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"> +
+ <@userProfileCommons.userProfileFormFields/> + +
+
+
+
+
+ + <@passwordCommons.logoutOtherSessions/> + +
+ <#if isAppInitiatedAction??> + + + <#else> + + +
+
+
+ + 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 != ""> +
+ data-${key}="${value}" + + > + + <#assign groupDisplayHeader=group.displayHeader!""> + <#if groupDisplayHeader != ""> + <#assign groupHeaderText=advancedMsg(groupDisplayHeader)!group> + <#else> + <#assign groupHeaderText=group.name!""> + +
+ +
+ + <#assign groupDisplayDescription=group.displayDescription!""> + <#if groupDisplayDescription != ""> + <#assign groupDescriptionText=advancedMsg(groupDisplayDescription)!""> +
+ +
+ +
+ + + + <#nested "beforeField" attribute> +
+
+ + <#if attribute.required>* +
+
+ <#if attribute.annotations.inputHelperTextBefore??> +
${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextBefore))?no_esc}
+ + <@inputFieldByType attribute=attribute/> + <#if messagesPerField.existsError('${attribute.name}')> + + ${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc} + + + <#if attribute.annotations.inputHelperTextAfter??> +
${kcSanitize(advancedMsg(attribute.annotations.inputHelperTextAfter))?no_esc}
+ +
+
+ <#nested "afterField" attribute> + + + + + <#list profile.html5DataAnnotations?keys as key> + + + + +<#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!''/> + + <#else> + <@inputTag attribute=attribute value=attribute.value!''/> + + + + +<#macro inputTag attribute value> + disabled + <#if attribute.autocomplete??>autocomplete="${attribute.autocomplete}" + <#if attribute.annotations.inputTypePlaceholder??>placeholder="${advancedMsg(attribute.annotations.inputTypePlaceholder)}" + <#if attribute.annotations.inputTypePattern??>pattern="${attribute.annotations.inputTypePattern}" + <#if attribute.annotations.inputTypeSize??>size="${attribute.annotations.inputTypeSize}" + <#if attribute.annotations.inputTypeMaxlength??>maxlength="${attribute.annotations.inputTypeMaxlength}" + <#if attribute.annotations.inputTypeMinlength??>minlength="${attribute.annotations.inputTypeMinlength}" + <#if attribute.annotations.inputTypeMax??>max="${attribute.annotations.inputTypeMax}" + <#if attribute.annotations.inputTypeMin??>min="${attribute.annotations.inputTypeMin}" + <#if attribute.annotations.inputTypeStep??>step="${attribute.annotations.inputTypeStep}" + <#list attribute.html5DataAnnotations as key, value> + data-${key}="${value}" + + /> + + +<#macro inputTagType attribute> + <#compress> + <#if attribute.annotations.inputType??> + <#if attribute.annotations.inputType?starts_with("html5-")> + ${attribute.annotations.inputType[6..]} + <#else> + ${attribute.annotations.inputType} + + <#else> + text + + + + +<#macro textareaTag attribute> + + + +<#macro selectTag attribute> + + + +<#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 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=[]> + + + <#list options as option> +
+ disabled + <#if attribute.values?seq_contains(option)>checked + /> + +
+ + + +<#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} + + + + 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"> +
+
+ + + + + + +
+ +
+ <#if authenticators??> +
+ <#list authenticators.authenticators as authenticator> + + +
+ + <#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators> + <#if authenticators.authenticators?size gt 1> +

${kcSanitize(msg("webauthn-available-authenticators"))?no_esc}

+ + +
+ <#list authenticators.authenticators as authenticator> +
+
+ +
+
+
+ ${kcSanitize(msg('${authenticator.label}'))?no_esc} +
+ + <#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content> +
+ <#list authenticator.transports.displayNameProperties as nameProperty> + ${kcSanitize(msg('${nameProperty!}'))?no_esc} + <#if nameProperty?has_next> + , + + +
+ + +
+ + ${kcSanitize(msg('webauthn-createdAt-label'))?no_esc} + + + ${kcSanitize(authenticator.createdAt)?no_esc} + +
+
+
+
+ +
+ + + +
+ +
+
+
+ + + + <#elseif section = "info"> + <#if realm.registrationAllowed && !registrationDisabled??> +
+ ${msg("noAccount")} ${msg("doRegister")} +
+ + + 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??> +
+ +
+ + + + \ 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"> + +
+
+ + + + + + + <@passwordCommons.logoutOtherSessions/> +
+
+ + + + + + <#if !isSetRetry?has_content && isAppInitiatedAction?has_content> +
+ +
+ + + + 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