Compare commits

..

6 commits
25.0.6 ... main

27 changed files with 949 additions and 821 deletions

View file

@ -16,8 +16,8 @@ To start a Dev Keycloak instance that can show the pub.solar theme, you need to
-e KEYCLOAK_ADMIN_PASSWORD=admin \ -e KEYCLOAK_ADMIN_PASSWORD=admin \
-v $(pwd):/opt/keycloak/themes/pub.solar \ -v $(pwd):/opt/keycloak/themes/pub.solar \
-v $(pwd)/.dev-import:/opt/keycloak/data/import \ -v $(pwd)/.dev-import:/opt/keycloak/data/import \
quay.io/keycloak/keycloak:25.0.6 \ quay.io/keycloak/keycloak:23.0.6 \
start-dev --import-realm start-dev --import-realm --features="declarative-user-profile"
``` ```
3. After this, you can start and stop the container using `docker start keycloak-theme-dev` and `docker-stop keycloak-theme-dev`. 3. After this, you can start and stop the container using `docker start keycloak-theme-dev` and `docker-stop keycloak-theme-dev`.

View file

@ -21,8 +21,10 @@ html {
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
margin: 0; margin: 0;
font-size: 20px; } font-size: 16px; }
@media screen and (min-width: 1200px) {
html {
font-size: 20px; } }
*:focus-visible { *:focus-visible {
outline: 0.2rem solid var(--accent); } outline: 0.2rem solid var(--accent); }
@ -57,23 +59,26 @@ html {
padding: 0rem 2rem; } padding: 0rem 2rem; }
.ps-button { .ps-button {
padding: 0.5rem 1rem;
font-size: 1rem; font-size: 1rem;
line-height: 1.2rem; padding: 0.5em 1em;
border: 2px solid var(--foreground); line-height: 1.2em;
border-radius: 1.5rem; border: 0.125em solid var(--foreground);
border-radius: 1.5em;
background-color: var(--background-darker-2); background-color: var(--background-darker-2);
cursor: pointer; } cursor: pointer; }
.ps-button:hover, .ps-button:focus { .ps-button:hover, .ps-button:focus {
border-color: var(--accent); } border-color: var(--accent); }
.ps-button_primary { .ps-button_primary {
border: 4px solid var(--foreground); border: 0.25em solid var(--foreground);
background-color: var(--background); background-color: var(--background);
color: var(--foreground); color: var(--foreground);
font-weight: bold; } font-weight: bold; }
.ps-button_primary:focus, .ps-button_primary:hover { .ps-button_primary:focus, .ps-button_primary:hover {
background-color: var(--foreground); background-color: var(--foreground);
color: var(--background); } color: var(--background); }
.ps-button_small {
font-size: 0.8rem;
padding: 0.25em 0.7em; }
.ps-input { .ps-input {
padding: 0.5rem 0.5rem; padding: 0.5rem 0.5rem;
@ -116,18 +121,20 @@ html {
display: flex; display: flex;
flex-direction: column; } flex-direction: column; }
.ps-form-group--label { .ps-form-group--label {
margin-bottom: 0.5rem; margin-bottom: 0.25rem;
display: flex; display: inline-block;
font-weight: bold; } font-weight: bold;
margin-top: 0.5rem; }
.ps-form-group .ps-button { .ps-form-group .ps-button {
align-self: flex-start; } align-self: flex-start; }
.ps-form-group--error { .ps-form-group--error {
margin-top: 0.25rem; margin-top: 0.25rem;
color: var(--accent); color: var(--accent);
font-weight: bold; } font-weight: bold; }
.ps-form-group--buttons {
margin: 0.5rem 0; }
.ps-homelink { .ps-homelink {
z-index: 100;
pointer-events: all; pointer-events: all;
color: var(--foreground); color: var(--foreground);
background: white; background: white;
@ -197,7 +204,9 @@ html {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0; padding: 0;
margin: 0; } margin: 0;
overflow-x: auto;
z-index: 100; }
.ps-header--title { .ps-header--title {
font-size: 1.5rem; font-size: 1.5rem;
padding: 0 1rem; padding: 0 1rem;
@ -206,8 +215,9 @@ html {
background-color: var(--background); background-color: var(--background);
border-right: 0.5rem solid var(--foreground); border-right: 0.5rem solid var(--foreground);
pointer-events: all; } pointer-events: all; }
.ps-header--i18n {
margin-left: auto; }
.ps-header--nav { .ps-header--nav {
margin-left: auto;
display: flex; display: flex;
border-bottom: 0.5rem solid var(--foreground); border-bottom: 0.5rem solid var(--foreground);
border-left: 0.5rem solid var(--foreground); border-left: 0.5rem solid var(--foreground);
@ -260,6 +270,9 @@ html {
border-bottom: 0.5rem solid var(--foreground); border-bottom: 0.5rem solid var(--foreground);
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
margin: 2rem; } margin: 2rem; }
.ps-page--subtitle {
padding-bottom: 0.25rem;
margin: 2rem; }
.ps-page--section { .ps-page--section {
border: 12px solid black; border: 12px solid black;
margin-top: 2rem; margin-top: 2rem;
@ -357,7 +370,8 @@ html {
left: 0; left: 0;
width: 100%; width: 100%;
background-color: var(--background); background-color: var(--background);
border-bottom: 2px solid var(--foreground); } border-bottom: 2px solid var(--foreground);
overflow-x: auto; }
.ps-section-nav--list { .ps-section-nav--list {
list-style: none; list-style: none;
display: flex; display: flex;

View file

@ -3,9 +3,10 @@
flex-direction: column; flex-direction: column;
&--label { &--label {
margin-bottom: 0.5rem; margin-bottom: 0.25rem;
display: flex; display: inline-block;
font-weight: bold; font-weight: bold;
margin-top: 0.5rem;
} }
.ps-button { .ps-button {
@ -18,4 +19,8 @@
font-weight: bold; font-weight: bold;
// font-family: monospace; // font-family: monospace;
} }
&--buttons {
margin: 0.5rem 0;
};
} }

View file

@ -36,6 +36,11 @@
margin: 2rem; margin: 2rem;
} }
&--subtitle {
padding-bottom: 0.25rem;
margin: 2rem;
};
&--section { &--section {
border: 12px solid black; border: 12px solid black;
margin-top: 2rem; margin-top: 2rem;

View file

@ -4,7 +4,7 @@
<#if code.success> <#if code.success>
${msg("codeSuccessTitle")} ${msg("codeSuccessTitle")}
<#else> <#else>
${kcSanitize(msg("codeErrorTitle", code.error))} ${msg("codeErrorTitle", code.error)}
</#if> </#if>
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-code"> <div id="kc-code">
@ -12,7 +12,7 @@
<p>${msg("copyCodeInstruction")}</p> <p>${msg("copyCodeInstruction")}</p>
<input id="code" class="${properties.kcTextareaClass!}" value="${code.code}"/> <input id="code" class="${properties.kcTextareaClass!}" value="${code.code}"/>
<#else> <#else>
<p id="error">${kcSanitize(code.error)}</p> <p id="error">${code.error}</p>
</#if> </#if>
</div> </div>
</#if> </#if>

View file

@ -1,15 +0,0 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=false; section>
<#if section = "header">
${msg("deleteCredentialTitle", credentialLabel)}
<#elseif section = "form">
<div id="kc-delete-text">
${msg("deleteCredentialMessage", credentialLabel)}
</div>
<form class="form-actions" action="${url.loginAction}" method="POST">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="accept" id="kc-accept" type="submit" value="${msg("doConfirmDelete")}"/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel-aia" value="${msg("doCancel")}" id="kc-decline" type="submit" />
</form>
<div class="clearfix"></div>
</#if>
</@layout.registrationLayout>

View file

@ -2,13 +2,13 @@
<@layout.registrationLayout displayMessage=false; section> <@layout.registrationLayout displayMessage=false; section>
<#if section = "header"> <#if section = "header">
<#if messageHeader??> <#if messageHeader??>
${kcSanitize(msg("${messageHeader}"))?no_esc} ${messageHeader}
<#else> <#else>
${message.summary} ${message.summary}
</#if> </#if>
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-info-message"> <div id="kc-info-message">
<p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${kcSanitize(msg("requiredAction.${reqActionItem}"))?no_esc}<#sep>, </#items></b></#list><#else></#if></p> <p class="instruction">${message.summary}<#if requiredActions??><#list requiredActions>: <b><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, </#items></b></#list><#else></#if></p>
<#if skipLink??> <#if skipLink??>
<#else> <#else>
<#if pageRedirectUri?has_content> <#if pageRedirectUri?has_content>

View file

@ -1,12 +0,0 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout; section>
<#if section = "header">
${msg("confirmOverrideIdpTitle")}
<#elseif section = "form">
<form id="kc-register-form" action="${url.loginAction}" method="post">
${msg("pageExpiredMsg1")} <a id="loginRestartLink" href="${url.loginRestartFlowUrl}">${msg("doClickHere")}</a>
<button type="submit" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="submitAction" id="confirmOverride" value="confirmOverride">${msg("confirmOverrideIdpContinue", idpDisplayName)}</button>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -3,11 +3,14 @@
<#if section="header"> <#if section="header">
${msg("doLogIn")} ${msg("doLogIn")}
<#elseif section="form"> <#elseif section="form">
<form id="kc-otp-login-form" class="${properties.kcFormClass!}" action="${url.loginAction}" <form
method="post"> id="kc-otp-login-form"
class="ps-container"
action="${url.loginAction}"
method="post"
>
<#if otpLogin.userOtpCredentials?size gt 1> <#if otpLogin.userOtpCredentials?size gt 1>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcInputWrapperClass!}">
<#list otpLogin.userOtpCredentials as otpCredential> <#list otpLogin.userOtpCredentials as otpCredential>
<input id="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListInputClass!}" type="radio" name="selectedCredentialId" value="${otpCredential.id}" <#if otpCredential.id == otpLogin.selectedCredentialId>checked="checked"</#if>> <input id="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListInputClass!}" type="radio" name="selectedCredentialId" value="${otpCredential.id}" <#if otpCredential.id == otpLogin.selectedCredentialId>checked="checked"</#if>>
<label for="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListClass!}" tabindex="${otpCredential?index}"> <label for="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListClass!}" tabindex="${otpCredential?index}">
@ -20,17 +23,19 @@
</label> </label>
</#list> </#list>
</div> </div>
</div>
</#if> </#if>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="otp" class="${properties.kcLabelClass!}">${msg("loginOtpOneTime")}</label> <label for="otp" class="${properties.kcLabelClass!}">${msg("loginOtpOneTime")}</label>
</div>
<div class="${properties.kcInputWrapperClass!}"> <input
<input id="otp" name="otp" autocomplete="off" type="text" class="${properties.kcInputClass!}" id="otp"
autofocus aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"/> name="otp"
autocomplete="off"
type="text"
class="${properties.kcInputClass!}"
autofocus aria-invalid="<#if messagesPerField.existsError('totp')>true</#if>"
/>
<#if messagesPerField.existsError('totp')> <#if messagesPerField.existsError('totp')>
<span id="input-error-otp-code" class="${properties.kcInputErrorMessageClass!}" <span id="input-error-otp-code" class="${properties.kcInputErrorMessageClass!}"
@ -39,19 +44,14 @@
</span> </span>
</#if> </#if>
</div> </div>
</div>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}"> <button
<div class="${properties.kcFormOptionsWrapperClass!}">
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login" id="kc-login" type="submit" value="${msg("doLogIn")}" /> name="login"
</div> id="kc-login"
type="submit"
>${msg("doLogIn")}</button>
</div> </div>
</form> </form>
</#if> </#if>

View file

@ -1,143 +0,0 @@
<#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">
<form id="webauth" action="${url.loginAction}" method="post">
<input type="hidden" id="clientDataJSON" name="clientDataJSON"/>
<input type="hidden" id="authenticatorData" name="authenticatorData"/>
<input type="hidden" id="signature" name="signature"/>
<input type="hidden" id="credentialId" name="credentialId"/>
<input type="hidden" id="userHandle" name="userHandle"/>
<input type="hidden" id="error" name="error"/>
</form>
<div class="${properties.kcFormGroupClass!} no-bottom-margin">
<#if authenticators??>
<form id="authn_select" class="${properties.kcFormClass!}">
<#list authenticators.authenticators as authenticator>
<input type="hidden" name="authn_use_chk" value="${authenticator.credentialId}"/>
</#list>
</form>
<#if shouldDisplayAuthenticators?? && shouldDisplayAuthenticators>
<#if authenticators.authenticators?size gt 1>
<p class="${properties.kcSelectAuthListItemTitle!}">${kcSanitize(msg("passkey-available-authenticators"))?no_esc}</p>
</#if>
<div class="${properties.kcFormClass!}">
<#list authenticators.authenticators as authenticator>
<div id="kc-webauthn-authenticator-item-${authenticator?index}" class="${properties.kcSelectAuthListItemClass!}">
<div class="${properties.kcSelectAuthListItemIconClass!}">
<i class="${(properties['${authenticator.transports.iconClass}'])!'${properties.kcWebAuthnDefaultIcon!}'} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i>
</div>
<div class="${properties.kcSelectAuthListItemBodyClass!}">
<div id="kc-webauthn-authenticator-label-${authenticator?index}"
class="${properties.kcSelectAuthListItemHeadingClass!}">
${kcSanitize(msg('${authenticator.label}'))?no_esc}
</div>
<#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content>
<div id="kc-webauthn-authenticator-transport-${authenticator?index}"
class="${properties.kcSelectAuthListItemDescriptionClass!}">
<#list authenticator.transports.displayNameProperties as nameProperty>
<span>${kcSanitize(msg('${nameProperty!}'))?no_esc}</span>
<#if nameProperty?has_next>
<span>, </span>
</#if>
</#list>
</div>
</#if>
<div class="${properties.kcSelectAuthListItemDescriptionClass!}">
<span id="kc-webauthn-authenticator-createdlabel-${authenticator?index}">
${kcSanitize(msg('passkey-createdAt-label'))?no_esc}
</span>
<span id="kc-webauthn-authenticator-created-${authenticator?index}">
${kcSanitize(authenticator.createdAt)?no_esc}
</span>
</div>
</div>
<div class="${properties.kcSelectAuthListItemFillClass!}"></div>
</div>
</#list>
</div>
</#if>
</#if>
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post" style="display:none">
<#if !usernameHidden??>
<div class="${properties.kcFormGroupClass!}">
<label for="username" class="${properties.kcLabelClass!}">${msg("passkey-autofill-select")}</label>
<input tabindex="1" id="username"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
class="${properties.kcInputClass!}" name="username"
value="${(login.username!'')}"
autocomplete="username webauthn"
type="text" autofocus autocomplete="off"/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</#if>
</form>
</#if>
<div id="kc-form-passkey-button" class="${properties.kcFormButtonsClass!}" style="display:none">
<input id="authenticateWebAuthnButton" type="button" onclick="doAuthenticate([], "${rpId}", "${challenge}", ${isUserIdentified}, ${createTimeout}, "${userVerification}", "${msg("passkey-unsupported-browser-text")?no_esc}")" autofocus="autofocus"
value="${kcSanitize(msg("passkey-doAuthenticate"))}"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
</div>
<div id="kc-form-passkey-button" class="${properties.kcFormButtonsClass!}" style="display:none">
<input id="authenticateWebAuthnButton" type="button" autofocus="autofocus"
value="${kcSanitize(msg("passkey-doAuthenticate"))}"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
</div>
</div>
</div>
</div>
<script type="module">
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
import { initAuthenticate } from "${url.resourcesPath}/js/passkeysConditionalAuth.js";
const authButton = document.getElementById('authenticateWebAuthnButton');
const input = {
isUserIdentified : ${isUserIdentified},
challenge : '${challenge}',
userVerification : '${userVerification}',
rpId : '${rpId}',
createTimeout : ${createTimeout},
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
};
authButton.addEventListener("click", () => {
authenticateByWebAuthn(input);
});
const args = {
isUserIdentified : ${isUserIdentified},
challenge : '${challenge}',
userVerification : '${userVerification}',
rpId : '${rpId}',
createTimeout : ${createTimeout},
errmsg : "${msg("passkey-unsupported-browser-text")?no_esc}"
};
document.addEventListener("DOMContentLoaded", (event) => initAuthenticate(args));
</script>
<#elseif section = "info">
<#if realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</#if>
</#if>
</@layout.registrationLayout>

View file

@ -3,25 +3,25 @@
<#if section = "header"> <#if section = "header">
${msg("doLogIn")} ${msg("doLogIn")}
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-form"> <form
<div id="kc-form-wrapper"> id="kc-form-login"
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" class="ps-container"
method="post"> onsubmit="login.disabled = true; return true;"
<div class="${properties.kcFormGroupClass!} no-bottom-margin"> action="${url.loginAction}"
<hr/> method="post"
>
<div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> <label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<div class="${properties.kcInputGroup!}"> <input
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" tabindex="2"
type="password" autocomplete="on" autofocus id="password"
class="${properties.kcInputClass!}"
name="password"
type="password"
autocomplete="on"
autofocus
aria-invalid="<#if messagesPerField.existsError('password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password')>true</#if>"
/> />
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password')> <#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('password'))?no_esc} ${kcSanitize(messagesPerField.get('password'))?no_esc}
@ -30,23 +30,26 @@
</div> </div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}"> <div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
</div>
<div class="${properties.kcFormOptionsWrapperClass!}">
<#if realm.resetPasswordAllowed> <#if realm.resetPasswordAllowed>
<span><a tabindex="5" <span><a tabindex="5"
href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span> href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
</#if> </#if>
</div> </div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}"> <div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input tabindex="4" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/> <button
tabindex="4"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login"
id="kc-login"
type="submit"
>
${msg("doLogIn")}
</button>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
<script type="module" src="${url.resourcesPath}/js/passwordVisibility.js"></script>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -6,7 +6,7 @@
${msg("recovery-code-config-header")} ${msg("recovery-code-config-header")}
<#elseif section = "form"> <#elseif section = "form">
<!-- warning --> <!-- warning -->
<div class="pf-c-alert pf-m-warning pf-m-inline ${properties.kcRecoveryCodesWarning!}" aria-label="Warning alert"> <div class="pf-c-alert pf-m-warning pf-m-inline ${properties.kcRecoveryCodesWarning}" aria-label="Warning alert">
<div class="pf-c-alert__icon"> <div class="pf-c-alert__icon">
<i class="pficon-warning-triangle-o" aria-hidden="true"></i> <i class="pficon-warning-triangle-o" aria-hidden="true"></i>
</div> </div>
@ -26,7 +26,7 @@
</ol> </ol>
<!-- actions --> <!-- actions -->
<div class="${properties.kcRecoveryCodesActions!}"> <div class="${properties.kcRecoveryCodesActions}">
<button id="printRecoveryCodes" class="pf-c-button pf-m-link" type="button"> <button id="printRecoveryCodes" class="pf-c-button pf-m-link" type="button">
<i class="pficon-print"></i> ${msg("recovery-codes-print")} <i class="pficon-print"></i> ${msg("recovery-codes-print")}
</button> </button>
@ -40,7 +40,7 @@
<!-- confirmation checkbox --> <!-- confirmation checkbox -->
<div class="${properties.kcFormOptionsClass!}"> <div class="${properties.kcFormOptionsClass!}">
<input class="${properties.kcCheckInputClass!}" type="checkbox" id="kcRecoveryCodesConfirmationCheck" name="kcRecoveryCodesConfirmationCheck" <input class="${properties.kcCheckInputClass}" type="checkbox" id="kcRecoveryCodesConfirmationCheck" name="kcRecoveryCodesConfirmationCheck"
onchange="document.getElementById('saveRecoveryAuthnCodesBtn').disabled = !this.checked;" onchange="document.getElementById('saveRecoveryAuthnCodesBtn').disabled = !this.checked;"
/> />
<label for="kcRecoveryCodesConfirmationCheck">${msg("recovery-codes-confirmation-message")}</label> <label for="kcRecoveryCodesConfirmationCheck">${msg("recovery-codes-confirmation-message")}</label>
@ -75,7 +75,7 @@
/* copy recovery codes */ /* copy recovery codes */
function copyRecoveryCodes() { function copyRecoveryCodes() {
var tmpTextarea = document.createElement("textarea"); var tmpTextarea = document.createElement("textarea");
var codes = document.querySelectorAll("#kc-recovery-codes-list li"); var codes = document.getElementById("kc-recovery-codes-list").getElementsByTagName("li");
for (i = 0; i < codes.length; i++) { for (i = 0; i < codes.length; i++) {
tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\n"; tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\n";
} }
@ -106,7 +106,7 @@
} }
function parseRecoveryCodeList() { function parseRecoveryCodeList() {
var recoveryCodes = document.querySelectorAll("#kc-recovery-codes-list li"); var recoveryCodes = document.querySelectorAll(".kc-recovery-codes-list li");
var recoveryCodeList = ""; var recoveryCodeList = "";
for (var i = 0; i < recoveryCodes.length; i++) { for (var i = 0; i < recoveryCodes.length; i++) {
@ -160,7 +160,7 @@
`@page { size: auto; margin-top: 0; } `@page { size: auto; margin-top: 0; }
body { width: 480px; } body { width: 480px; }
div { list-style-type: none; font-family: monospace } div { list-style-type: none; font-family: monospace }
p:first-of-type { margin-top: 48px }`; p:first-of-type { margin-top: 48px }`
return printFileContent = return printFileContent =
"<html><style>" + styles + "</style><body>" + "<html><style>" + styles + "</style><body>" +

View file

@ -1,5 +1,5 @@
<#import "template.ftl" as layout> <#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('recoveryCodeInput'); section> <@layout.registrationLayout; section>
<#if section = "header"> <#if section = "header">
${msg("auth-recovery-code-header")} ${msg("auth-recovery-code-header")}
@ -11,19 +11,7 @@
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<input tabindex="1" id="recoveryCodeInput" <input id="recoveryCodeInput" name="recoveryCodeInput" autocomplete="off" type="text" class="${properties.kcInputClass!}" autofocus/>
name="recoveryCodeInput"
aria-invalid="<#if messagesPerField.existsError('recoveryCodeInput')>true</#if>"
autocomplete="off"
type="text"
class="${properties.kcInputClass!}"
autofocus/>
<#if messagesPerField.existsError('recoveryCodeInput')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('recoveryCodeInput'))?no_esc}
</span>
</#if>
</div> </div>
</div> </div>

View file

@ -1,33 +0,0 @@
<#import "template.ftl" as layout>
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('totp'); section>
<#if section="header">
${msg("doLogIn")}
<#elseif section="form">
<form id="kc-otp-reset-form" class="${properties.kcFormClass!}" action="${url.loginAction}"
method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcInputWrapperClass!}">
<p id="kc-otp-reset-form-description">${msg("otp-reset-description")}</p>
<#list configuredOtpCredentials.userOtpCredentials as otpCredential>
<input id="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListInputClass!}" type="radio" name="selectedCredentialId" value="${otpCredential.id}" <#if otpCredential.id == configuredOtpCredentials.selectedCredentialId>checked="checked"</#if>>
<label for="kc-otp-credential-${otpCredential?index}" class="${properties.kcLoginOTPListClass!}" tabindex="${otpCredential?index}">
<span class="${properties.kcLoginOTPListItemHeaderClass!}">
<span class="${properties.kcLoginOTPListItemIconBodyClass!}">
<i class="${properties.kcLoginOTPListItemIconClass!}" aria-hidden="true"></i>
</span>
<span class="${properties.kcLoginOTPListItemTitleClass!}">${otpCredential.userLabel}</span>
</span>
</label>
</#list>
<div class="${properties.kcFormGroupClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input id="kc-otp-reset-form-submit" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"/>
</div>
</div>
</div>
</div>
</form>
</#if>
</@layout.registrationLayout>

View file

@ -2,38 +2,49 @@
<@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section> <@layout.registrationLayout displayInfo=true displayMessage=!messagesPerField.existsError('username'); section>
<#if section = "header"> <#if section = "header">
${msg("emailForgotTitle")} ${msg("emailForgotTitle")}
<#elseif section = "form">
<form id="kc-reset-password-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
</div>
<div class="${properties.kcInputWrapperClass!}">
<input type="text" id="username" name="username" class="${properties.kcInputClass!}" autofocus value="${(auth.attemptedUsername!'')}" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}"/>
</div>
</div>
</form>
<#elseif section = "info" > <#elseif section = "info" >
<#if realm.duplicateEmailsAllowed> <#if realm.duplicateEmailsAllowed>
${msg("emailInstructionUsername")} ${msg("emailInstructionUsername")}
<#else> <#else>
${msg("emailInstruction")} ${msg("emailInstruction")}
</#if> </#if>
<#elseif section = "form">
<form id="kc-reset-password-form" class="ps-container" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<label
for="username"
class="${properties.kcLabelClass!}"
>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</label>
<input
type="text"
id="username"
name="username"
class="${properties.kcInputClass!}"
autofocus
value="${(auth.attemptedUsername!'')}"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
/>
<#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('username'))?no_esc}
</span>
</#if>
</div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<button
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
>${msg("doSubmit")}</button>
</div>
</div>
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
</div>
</div>
</form>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -5,23 +5,19 @@
${msg("updatePasswordTitle")} ${msg("updatePasswordTitle")}
<#elseif section = "form"> <#elseif section = "form">
<form id="kc-passwd-update-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <form id="kc-passwd-update-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<input type="text" id="username" name="username" value="${username}" autocomplete="username"
readonly="readonly" style="display:none;"/>
<input type="password" id="password" name="password" autocomplete="current-password" style="display:none;"/>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div class="${properties.kcLabelWrapperClass!}"> <div class="${properties.kcLabelWrapperClass!}">
<label for="password-new" class="${properties.kcLabelClass!}">${msg("passwordNew")}</label> <label for="password-new" class="${properties.kcLabelClass!}">${msg("passwordNew")}</label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}">
<input type="password" id="password-new" name="password-new" class="${properties.kcInputClass!}" <input type="password" id="password-new" name="password-new" class="${properties.kcInputClass!}"
autofocus autocomplete="new-password" autofocus autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/> />
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-new" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password')> <#if messagesPerField.existsError('password')>
<span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-password" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
@ -36,19 +32,11 @@
<label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label> <label for="password-confirm" class="${properties.kcLabelClass!}">${msg("passwordConfirm")}</label>
</div> </div>
<div class="${properties.kcInputWrapperClass!}"> <div class="${properties.kcInputWrapperClass!}">
<div class="${properties.kcInputGroup!}">
<input type="password" id="password-confirm" name="password-confirm" <input type="password" id="password-confirm" name="password-confirm"
class="${properties.kcInputClass!}" class="${properties.kcInputClass!}"
autocomplete="new-password" autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/> />
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-confirm" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if messagesPerField.existsError('password-confirm')> <#if messagesPerField.existsError('password-confirm')>
<span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-password-confirm" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
@ -72,6 +60,5 @@
</div> </div>
</div> </div>
</form> </form>
<script type="module" src="${url.resourcesPath}/js/passwordVisibility.js"></script>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -3,21 +3,34 @@
<#if section = "header"> <#if section = "header">
${msg("loginAccountTitle")} ${msg("loginAccountTitle")}
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password> <#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" <form
method="post"> id="kc-form-login"
onsubmit="login.disabled = true; return true;"
action="${url.loginAction}"
method="post"
class="ps-container"
>
<#if !usernameHidden??> <#if !usernameHidden??>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<label for="username" <label
class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label> for="username"
class="${properties.kcLabelClass!}"
>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</label>
<input tabindex="1" id="username" <input
tabindex="1"
id="username"
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
class="${properties.kcInputClass!}" name="username" class="${properties.kcInputClass!}"
name="username"
value="${(login.username!'')}" value="${(login.username!'')}"
type="text" autofocus autocomplete="off"/> type="text"
autofocus
autocomplete="off"
/>
<#if messagesPerField.existsError('username')> <#if messagesPerField.existsError('username')>
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
@ -27,33 +40,44 @@
</div> </div>
</#if> </#if>
<#if realm.rememberMe && !usernameHidden??>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}"> <div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options"> <div id="kc-form-options">
<#if realm.rememberMe && !usernameHidden??>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<#if login.rememberMe??> <#if login.rememberMe??>
<input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox" <input
checked> ${msg("rememberMe")} tabindex="3"
id="rememberMe"
name="rememberMe"
type="checkbox"
checked
/> ${msg("rememberMe")}
<#else> <#else>
<input tabindex="3" id="rememberMe" name="rememberMe" <input
type="checkbox"> ${msg("rememberMe")} tabindex="3"
id="rememberMe"
name="rememberMe"
type="checkbox"
/> ${msg("rememberMe")}
</#if> </#if>
</label> </label>
</div> </div>
</div>
</div>
</#if> </#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}"> <div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input tabindex="4" <button
tabindex="4"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/> name="login"
id="kc-login"
type="submit"
>${msg("doLogIn")}</button>
</div> </div>
</form> </form>
</#if> </#if>
</div>
</div>
<#elseif section = "info" > <#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??> <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
@ -62,7 +86,7 @@
</div> </div>
</#if> </#if>
<#elseif section = "socialProviders" > <#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers?has_content> <#if realm.password && social.providers??>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}"> <div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/> <hr/>
<h4>${msg("identity-provider-login-label")}</h4> <h4>${msg("identity-provider-login-label")}</h4>

View file

@ -3,20 +3,35 @@
<#if section = "header"> <#if section = "header">
${msg("loginAccountTitle")} ${msg("loginAccountTitle")}
<#elseif section = "form"> <#elseif section = "form">
<div id="kc-form">
<div id="kc-form-wrapper">
<#if realm.password> <#if realm.password>
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post"> <form class="ps-container" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
<#if !usernameHidden??> <#if !usernameHidden??>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label> <label
for="username"
class="${properties.kcLabelClass!}"
>
<#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if>
</label>
<input tabindex="2" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="username" <input
tabindex="1"
id="username"
class="${properties.kcInputClass!}"
name="username"
value="${(login.username!'')}"
type="text"
autofocus
autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/> />
<#if messagesPerField.existsError('username','password')> <#if messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span
id="input-error"
class="${properties.kcInputErrorMessageClass!}"
aria-live="polite"
>
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc} ${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
</span> </span>
</#if> </#if>
@ -27,17 +42,15 @@
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label> <label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
<div class="${properties.kcInputGroup!}"> <input
<input tabindex="3" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="current-password" tabindex="2"
id="password"
class="${properties.kcInputClass!}"
name="password"
type="password"
autocomplete="off"
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>" aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
/> />
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg("showPassword")}"
aria-controls="password" data-password-toggle tabindex="4"
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div>
<#if usernameHidden?? && messagesPerField.existsError('username','password')> <#if usernameHidden?? && messagesPerField.existsError('username','password')>
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite"> <span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
@ -45,59 +58,64 @@
</span> </span>
</#if> </#if>
<#if realm.resetPasswordAllowed>
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a tabindex="5" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
</div>
</#if>
</div> </div>
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}"> <div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
<div id="kc-form-options">
<#if realm.rememberMe && !usernameHidden??> <#if realm.rememberMe && !usernameHidden??>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<#if login.rememberMe??> <#if login.rememberMe??>
<input tabindex="5" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")} <input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox" checked> ${msg("rememberMe")}
<#else> <#else>
<input tabindex="5" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")} <input tabindex="3" id="rememberMe" name="rememberMe" type="checkbox"> ${msg("rememberMe")}
</#if> </#if>
</label> </label>
</div> </div>
</#if> </#if>
</div> </div>
<div class="${properties.kcFormOptionsWrapperClass!}">
<#if realm.resetPasswordAllowed>
<span><a tabindex="6" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</a></span>
</#if>
</div>
</div>
<div id="kc-form-buttons" class="${properties.kcFormGroupClass!}"> <div id="kc-form-buttons" class="${properties.kcFormGroupClass!}">
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/> <input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<input tabindex="7" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/> <button
tabindex="4"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
name="login"
id="kc-login"
type="submit"
>${msg("doLogIn")}</button>
</div> </div>
</form> </form>
</#if> </#if>
</div>
</div>
<script type="module" src="${url.resourcesPath}/js/passwordVisibility.js"></script>
<#elseif section = "info" > <#elseif section = "info" >
<#if realm.password && realm.registrationAllowed && !registrationDisabled??> <#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration-container"> <div id="kc-registration-container">
<div id="kc-registration"> <div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="8" <span>${msg("noAccount")}
href="${url.registrationUrl}">${msg("doRegister")}</a></span> <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a>
</span>
</div> </div>
</div> </div>
</#if> </#if>
<#elseif section = "socialProviders" > <#elseif section = "socialProviders" >
<#if realm.password && social?? && social.providers?has_content> <#if realm.password && social.providers??>
<div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}"> <div id="kc-social-providers" class="${properties.kcFormSocialAccountSectionClass!}">
<hr/> <hr/>
<h2>${msg("identity-provider-login-label")}</h2> <h4>${msg("identity-provider-login-label")}</h4>
<ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>"> <ul class="${properties.kcFormSocialAccountListClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountListGridClass!}</#if>">
<#list social.providers as p> <#list social.providers as p>
<li> <a
<a id="social-${p.alias}" class="${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>" id="social-${p.alias}"
type="button" href="${p.loginUrl}"> class="${properties.kcFormSocialAccountListButtonClass!} <#if social.providers?size gt 3>${properties.kcFormSocialAccountGridItem!}</#if>"
type="button"
href="${p.loginUrl}"
>
<#if p.iconClasses?has_content> <#if p.iconClasses?has_content>
<i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i> <i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i>
<span class="${properties.kcFormSocialAccountNameClass!} kc-social-icon-text">${p.displayName!}</span> <span class="${properties.kcFormSocialAccountNameClass!} kc-social-icon-text">${p.displayName!}</span>
@ -105,7 +123,6 @@
<span class="${properties.kcFormSocialAccountNameClass!}">${p.displayName!}</span> <span class="${properties.kcFormSocialAccountNameClass!}">${p.displayName!}</span>
</#if> </#if>
</a> </a>
</li>
</#list> </#list>
</ul> </ul>
</div> </div>

View file

@ -1,27 +0,0 @@
<#macro termsAcceptance>
<#if termsAcceptanceRequired??>
<div class="form-group">
<div class="${properties.kcInputWrapperClass!}">
${msg("termsTitle")}
<div id="kc-registration-terms-text">
${kcSanitize(msg("termsText"))?no_esc}
</div>
</div>
</div>
<div class="form-group">
<div class="${properties.kcLabelWrapperClass!}">
<input type="checkbox" id="termsAccepted" name="termsAccepted" class="${properties.kcCheckboxInputClass!}"
aria-invalid="<#if messagesPerField.existsError('termsAccepted')>true</#if>"
/>
<label for="termsAccepted" class="${properties.kcLabelClass!}">${msg("acceptTerms")}</label>
</div>
<#if messagesPerField.existsError('termsAccepted')>
<div class="${properties.kcLabelWrapperClass!}">
<span id="input-error-terms-accepted" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
${kcSanitize(messagesPerField.get('termsAccepted'))?no_esc}
</span>
</div>
</#if>
</div>
</#if>
</#macro>

View file

@ -25,12 +25,6 @@
autocomplete="new-password" autocomplete="new-password"
aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password','password-confirm')>true</#if>"
/> />
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div> </div>
<#if messagesPerField.existsError('password')> <#if messagesPerField.existsError('password')>
@ -52,12 +46,6 @@
name="password-confirm" name="password-confirm"
aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>" aria-invalid="<#if messagesPerField.existsError('password-confirm')>true</#if>"
/> />
<button class="${properties.kcFormPasswordVisibilityButtonClass!}" type="button" aria-label="${msg('showPassword')}"
aria-controls="password-confirm" data-password-toggle
data-icon-show="${properties.kcFormPasswordVisibilityIconShow!}" data-icon-hide="${properties.kcFormPasswordVisibilityIconHide!}"
data-label-show="${msg('showPassword')}" data-label-hide="${msg('hidePassword')}">
<i class="${properties.kcFormPasswordVisibilityIconShow!}" aria-hidden="true"></i>
</button>
</div> </div>
<#if messagesPerField.existsError('password-confirm')> <#if messagesPerField.existsError('password-confirm')>
@ -82,11 +70,6 @@
</#if> </#if>
<div class="${properties.kcFormGroupClass!}"> <div class="${properties.kcFormGroupClass!}">
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
</div>
</div>
<#if recaptchaRequired?? && !(recaptchaVisible!false)> <#if recaptchaRequired?? && !(recaptchaVisible!false)>
<script> <script>
@ -104,9 +87,13 @@
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}"> <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doRegister")}"/> <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doRegister")}"/>
</div> </div>
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
<div class="${properties.kcFormOptionsWrapperClass!}">
<span><a href="${url.loginUrl}">${kcSanitize(msg("backToLogin"))?no_esc}</a></span>
</div>
</div>
</#if> </#if>
</div> </div>
</form> </form>
<script type="module" src="${url.resourcesPath}/js/passwordVisibility.js"></script>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -0,0 +1,115 @@
// for embedded scripts, quoted and modified from https://github.com/swansontec/rfc4648.js by William Swanson
'use strict';
var base64url = base64url || {};
(function(base64url) {
function parse (string, encoding, opts = {}) {
// Build the character lookup table:
if (!encoding.codes) {
encoding.codes = {};
for (let i = 0; i < encoding.chars.length; ++i) {
encoding.codes[encoding.chars[i]] = i;
}
}
// The string must have a whole number of bytes:
if (!opts.loose && (string.length * encoding.bits) & 7) {
throw new SyntaxError('Invalid padding');
}
// Count the padding bytes:
let end = string.length;
while (string[end - 1] === '=') {
--end;
// If we get a whole number of bytes, there is too much padding:
if (!opts.loose && !(((string.length - end) * encoding.bits) & 7)) {
throw new SyntaxError('Invalid padding');
}
}
// Allocate the output:
const out = new (opts.out || Uint8Array)(((end * encoding.bits) / 8) | 0);
// Parse the data:
let bits = 0; // Number of bits currently in the buffer
let buffer = 0; // Bits waiting to be written out, MSB first
let written = 0; // Next byte to write
for (let i = 0; i < end; ++i) {
// Read one character from the string:
const value = encoding.codes[string[i]];
if (value === void 0) {
throw new SyntaxError('Invalid character ' + string[i]);
}
// Append the bits to the buffer:
buffer = (buffer << encoding.bits) | value;
bits += encoding.bits;
// Write out some bits if the buffer has a byte's worth:
if (bits >= 8) {
bits -= 8;
out[written++] = 0xff & (buffer >> bits);
}
}
// Verify that we have received just enough bits:
if (bits >= encoding.bits || 0xff & (buffer << (8 - bits))) {
throw new SyntaxError('Unexpected end of data');
}
return out
}
function stringify (data, encoding, opts = {}) {
const { pad = true } = opts;
const mask = (1 << encoding.bits) - 1;
let out = '';
let bits = 0; // Number of bits currently in the buffer
let buffer = 0; // Bits waiting to be written out, MSB first
for (let i = 0; i < data.length; ++i) {
// Slurp data into the buffer:
buffer = (buffer << 8) | (0xff & data[i]);
bits += 8;
// Write out as much as we can:
while (bits > encoding.bits) {
bits -= encoding.bits;
out += encoding.chars[mask & (buffer >> bits)];
}
}
// Partial character:
if (bits) {
out += encoding.chars[mask & (buffer << (encoding.bits - bits))];
}
// Add padding characters until we hit a byte boundary:
if (pad) {
while ((out.length * encoding.bits) & 7) {
out += '=';
}
}
return out
}
const encoding = {
chars: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
bits: 6
}
base64url.decode = function (string, opts) {
return parse(string, encoding, opts);
}
base64url.encode = function (data, opts) {
return stringify(data, encoding, opts)
}
return base64url;
}(base64url));

View file

@ -0,0 +1,15 @@
const toggle = (button) => {
const passwordElement = document.getElementById(button.getAttribute('aria-controls'));
if (passwordElement.type === "password") {
passwordElement.type = "text";
button.children.item(0).className = button.dataset.iconHide;
button.setAttribute("aria-label", button.dataset.labelHide);
} else if(passwordElement.type === "text") {
passwordElement.type = "password";
button.children.item(0).className = button.dataset.iconShow;
button.setAttribute("aria-label", button.dataset.labelShow);
}
}
document.querySelectorAll('[data-password-toggle]')
.forEach(button => button.onclick = () => toggle(button));

View file

@ -1,6 +1,12 @@
<#import "template.ftl" as layout> <#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=false; section> <@layout.registrationLayout displayInfo=false; section>
<#if section = "header" || section = "show-username"> <#if section = "header" || section = "show-username">
<script type="text/javascript">
function fillAndSubmit(authExecId) {
document.getElementById('authexec-hidden-input').value = authExecId;
document.getElementById('kc-select-credential-form').submit();
}
</script>
<#if section = "header"> <#if section = "header">
${msg("loginChooseAuthenticator")} ${msg("loginChooseAuthenticator")}
</#if> </#if>
@ -9,13 +15,13 @@
<form id="kc-select-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post"> <form id="kc-select-credential-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
<div class="${properties.kcSelectAuthListClass!}"> <div class="${properties.kcSelectAuthListClass!}">
<#list auth.authenticationSelections as authenticationSelection> <#list auth.authenticationSelections as authenticationSelection>
<button class="${properties.kcSelectAuthListItemClass!}" type="submit" name="authenticationExecution" value="${authenticationSelection.authExecId}"> <div class="${properties.kcSelectAuthListItemClass!}" onclick="fillAndSubmit('${authenticationSelection.authExecId}')">
<div class="${properties.kcSelectAuthListItemIconClass!}"> <div class="${properties.kcSelectAuthListItemIconClass!}">
<i class="${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i> <i class="${properties['${authenticationSelection.iconCssClass}']!authenticationSelection.iconCssClass} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i>
</div> </div>
<div class="${properties.kcSelectAuthListItemBodyClass!}"> <div class="${properties.kcSelectAuthListItemBodyClass!}">
<div class="${properties.kcSelectAuthListItemHeadingClass!}"> <div class="${properties.kcSelectAuthListItemHeadingClass!} ${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}">
${msg('${authenticationSelection.displayName}')} ${msg('${authenticationSelection.displayName}')}
</div> </div>
<div class="${properties.kcSelectAuthListItemDescriptionClass!}"> <div class="${properties.kcSelectAuthListItemDescriptionClass!}">
@ -26,8 +32,9 @@
<div class="${properties.kcSelectAuthListItemArrowClass!}"> <div class="${properties.kcSelectAuthListItemArrowClass!}">
<i class="${properties.kcSelectAuthListItemArrowIconClass!}"></i> <i class="${properties.kcSelectAuthListItemArrowIconClass!}"></i>
</div> </div>
</button> </div>
</#list> </#list>
<input type="hidden" id="authexec-hidden-input" name="authenticationExecution" />
</div> </div>
</form> </form>

View file

@ -1,17 +1,13 @@
<#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false> <#macro registrationLayout bodyClass="" displayInfo=false displayMessage=true displayRequiredFields=false>
<!DOCTYPE html> <!doctype html>
<html class="${properties.kcHtmlClass!}"<#if realm.internationalizationEnabled> lang="${locale.currentLanguageTag}"</#if>> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<#if properties.meta?has_content> <meta name="viewport" content="width=device-width,initial-scale=1">
<#list properties.meta?split(' ') as meta>
<meta name="${meta?split('==')[0]}" content="${meta?split('==')[1]}"/>
</#list>
</#if>
<title>${msg("loginTitle",(realm.displayName!''))}</title> <title>${msg("loginTitle",(realm.displayName!''))}</title>
@ -34,29 +30,14 @@
</#if> </#if>
<#if properties.scripts?has_content> <#if properties.scripts?has_content>
<#list properties.scripts?split(' ') as script> <#list properties.scripts?split(' ') as script>
<script src="${url.resourcesPath}/${script}" type="text/javascript"></script> <script defer src="${url.resourcesPath}/${script}" type="text/javascript"></script>
</#list> </#list>
</#if> </#if>
<script type="importmap"> <#if properties.scriptsCommon?has_content>
{ <#list properties.scriptsCommon?split(' ') as script>
"imports": { <script defer src="${url.resourcesCommonPath}/${script}" type="text/javascript"></script>
"rfc4648": "${url.resourcesCommonPath}/node_modules/rfc4648/lib/rfc4648.js"
}
}
</script>
<script src="${url.resourcesPath}/js/menu-button-links.js" type="module"></script>
<#if scripts??>
<#list scripts as script>
<script src="${script}" type="text/javascript"></script>
</#list> </#list>
</#if> </#if>
<script type="module">
import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";
checkCookiesAndSetTimer(
"${url.ssoLoginInOtherTabsUrl?no_esc}"
);
</script>
</head> </head>
<body class="ps-main ps-main_full"> <body class="ps-main ps-main_full">
@ -104,11 +85,13 @@
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<h1 class="ps-page--title"><#nested "header"></h1> <h1 class="ps-page--title"><#nested "header"></h1>
<div class="ps-page--subtitle">
<#if displayRequiredFields> <#if displayRequiredFields>
<div class="${properties.kcLabelWrapperClass!} subtitle"> <div class="${properties.kcLabelWrapperClass!} subtitle">
<span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span> <span class="subtitle"><span class="required">*</span> ${msg("requiredFields")}</span>
</div> </div>
</#if> </#if>
</div>
<#else> <#else>
<#if displayRequiredFields> <#if displayRequiredFields>
<div class="${properties.kcContentWrapperClass!}"> <div class="${properties.kcContentWrapperClass!}">

View file

@ -12,14 +12,10 @@ kcButtonLargeClass=ps-button_large
kcFormGroupClass=ps-form-group kcFormGroupClass=ps-form-group
kcLabelClass=ps-form-group--label kcLabelClass=ps-form-group--label
kcInputErrorMessageClass=ps-form-group--error kcInputErrorMessageClass=ps-form-group--error
kcFormOptionsClass=ps-form-group--options
kcFormOptionsWrapperClass=ps-form-group--options-wrapper
kcFormButtonsClass=ps-form-group--buttons
kcInputClass=ps-input kcInputClass=ps-input
kcWebAuthnKeyIcon=pficon pficon-key kcWebAuthnKeyIcon=pficon pficon-key
kcAuthenticatorPasswordClass=ps-button
kcAuthenticatorWebAuthnClass=ps-button
kcAuthenticatorWebAuthnPasswordlessClass=ps-button
kcFormPasswordVisibilityIconShow=fa fa-eye
kcFormPasswordVisibilityIconHide=fa fa-eye-slash

View file

@ -1,5 +1,5 @@
<#import "template.ftl" as layout> <#import "template.ftl" as layout>
<@layout.registrationLayout displayInfo=(realm.registrationAllowed && !registrationDisabled??); section> <@layout.registrationLayout; section>
<#if section = "title"> <#if section = "title">
title title
<#elseif section = "header"> <#elseif section = "header">
@ -30,18 +30,18 @@
<div class="${properties.kcFormClass!}"> <div class="${properties.kcFormClass!}">
<#list authenticators.authenticators as authenticator> <#list authenticators.authenticators as authenticator>
<div id="kc-webauthn-authenticator-item-${authenticator?index}" class="${properties.kcSelectAuthListItemClass!}"> <div id="kc-webauthn-authenticator" class="${properties.kcSelectAuthListItemClass!}">
<div class="${properties.kcSelectAuthListItemIconClass!}"> <div class="${properties.kcSelectAuthListItemIconClass!}">
<i class="${(properties['${authenticator.transports.iconClass}'])!'${properties.kcWebAuthnDefaultIcon!}'} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i> <i class="${(properties['${authenticator.transports.iconClass}'])!'${properties.kcWebAuthnDefaultIcon!}'} ${properties.kcSelectAuthListItemIconPropertyClass!}"></i>
</div> </div>
<div class="${properties.kcSelectAuthListItemBodyClass!}"> <div class="${properties.kcSelectAuthListItemBodyClass!}">
<div id="kc-webauthn-authenticator-label-${authenticator?index}" <div id="kc-webauthn-authenticator-label"
class="${properties.kcSelectAuthListItemHeadingClass!}"> class="${properties.kcSelectAuthListItemHeadingClass!}">
${kcSanitize(msg('${authenticator.label}'))?no_esc} ${kcSanitize(msg('${authenticator.label}'))?no_esc}
</div> </div>
<#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content> <#if authenticator.transports?? && authenticator.transports.displayNameProperties?has_content>
<div id="kc-webauthn-authenticator-transport-${authenticator?index}" <div id="kc-webauthn-authenticator-transport"
class="${properties.kcSelectAuthListItemDescriptionClass!}"> class="${properties.kcSelectAuthListItemDescriptionClass!}">
<#list authenticator.transports.displayNameProperties as nameProperty> <#list authenticator.transports.displayNameProperties as nameProperty>
<span>${kcSanitize(msg('${nameProperty!}'))?no_esc}</span> <span>${kcSanitize(msg('${nameProperty!}'))?no_esc}</span>
@ -53,10 +53,10 @@
</#if> </#if>
<div class="${properties.kcSelectAuthListItemDescriptionClass!}"> <div class="${properties.kcSelectAuthListItemDescriptionClass!}">
<span id="kc-webauthn-authenticator-createdlabel-${authenticator?index}"> <span id="kc-webauthn-authenticator-created-label">
${kcSanitize(msg('webauthn-createdAt-label'))?no_esc} ${kcSanitize(msg('webauthn-createdAt-label'))?no_esc}
</span> </span>
<span id="kc-webauthn-authenticator-created-${authenticator?index}"> <span id="kc-webauthn-authenticator-created">
${kcSanitize(authenticator.createdAt)?no_esc} ${kcSanitize(authenticator.createdAt)?no_esc}
</span> </span>
</div> </div>
@ -69,34 +69,100 @@
</#if> </#if>
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}"> <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<input id="authenticateWebAuthnButton" type="button" autofocus="autofocus" <input id="authenticateWebAuthnButton" type="button" onclick="webAuthnAuthenticate()" autofocus="autofocus"
value="${kcSanitize(msg("webauthn-doAuthenticate"))}" value="${kcSanitize(msg("webauthn-doAuthenticate"))}"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/> class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"/>
</div> </div>
</div> </div>
</div> </div>
<script type="module"> <script type="text/javascript" src="${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js"></script>
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js"; <script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
const authButton = document.getElementById('authenticateWebAuthnButton'); <script type="text/javascript">
authButton.addEventListener("click", function() { function webAuthnAuthenticate() {
const input = { let isUserIdentified = ${isUserIdentified};
isUserIdentified : ${isUserIdentified}, if (!isUserIdentified) {
challenge : '${challenge}', doAuthenticate([]);
userVerification : '${userVerification}', return;
rpId : '${rpId}', }
createTimeout : ${createTimeout}, checkAllowCredentials();
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}" }
};
authenticateByWebAuthn(input);
});
</script>
function checkAllowCredentials() {
let allowCredentials = [];
let authn_use = document.forms['authn_select'].authn_use_chk;
if (authn_use !== undefined) {
if (authn_use.length === undefined) {
allowCredentials.push({
id: base64url.decode(authn_use.value, {loose: true}),
type: 'public-key',
});
} else {
for (let i = 0; i < authn_use.length; i++) {
allowCredentials.push({
id: base64url.decode(authn_use[i].value, {loose: true}),
type: 'public-key',
});
}
}
}
doAuthenticate(allowCredentials);
}
function doAuthenticate(allowCredentials) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
$("#error").val("${msg("webauthn-unsupported-browser-text")?no_esc}");
$("#webauth").submit();
return;
}
let challenge = "${challenge}";
let userVerification = "${userVerification}";
let rpId = "${rpId}";
let publicKey = {
rpId : rpId,
challenge: base64url.decode(challenge, { loose: true })
};
let createTimeout = ${createTimeout};
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
if (allowCredentials.length) {
publicKey.allowCredentials = allowCredentials;
}
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
navigator.credentials.get({publicKey})
.then((result) => {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
let authenticatorData = result.response.authenticatorData;
let signature = result.response.signature;
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
$("#authenticatorData").val(base64url.encode(new Uint8Array(authenticatorData), { pad: false }));
$("#signature").val(base64url.encode(new Uint8Array(signature), { pad: false }));
$("#credentialId").val(result.id);
if(result.response.userHandle) {
$("#userHandle").val(base64url.encode(new Uint8Array(result.response.userHandle), { pad: false }));
}
$("#webauth").submit();
})
.catch((err) => {
$("#error").val(err);
$("#webauth").submit();
})
;
}
</script>
<#elseif section = "info"> <#elseif section = "info">
<#if realm.registrationAllowed && !registrationDisabled??>
<div id="kc-registration">
<span>${msg("noAccount")} <a tabindex="6" href="${url.registrationUrl}">${msg("doRegister")}</a></span>
</div>
</#if>
</#if> </#if>
</@layout.registrationLayout> </@layout.registrationLayout>

View file

@ -5,7 +5,7 @@
<#if section = "title"> <#if section = "title">
title title
<#elseif section = "header"> <#elseif section = "header">
<span class="${properties.kcWebAuthnKeyIcon!}"></span> <span class="${properties.kcWebAuthnKeyIcon}"></span>
${kcSanitize(msg("webauthn-registration-title"))?no_esc} ${kcSanitize(msg("webauthn-registration-title"))?no_esc}
<#elseif section = "form"> <#elseif section = "form">
@ -21,42 +21,177 @@
</div> </div>
</form> </form>
<script type="module"> <script type="text/javascript" src="${url.resourcesPath}/js/base64url.js"></script>
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js"; <script type="text/javascript">
const registerButton = document.getElementById('registerWebAuthn');
registerButton.addEventListener("click", function() { function registerSecurityKey() {
const input = {
challenge : '${challenge}', // Check if WebAuthn is supported by this browser
userid : '${userid}', if (!window.PublicKeyCredential) {
username : '${username}', document.getElementById('error').value = '${msg("webauthn-unsupported-browser-text")?no_esc}';
signatureAlgorithms : [<#list signatureAlgorithms as sigAlg>${sigAlg?c},</#list>], document.getElementById('register').submit();
rpEntityName : '${rpEntityName}', return;
rpId : '${rpId}', }
attestationConveyancePreference : '${attestationConveyancePreference}',
authenticatorAttachment : '${authenticatorAttachment}', // mandatory parameters
requireResidentKey : '${requireResidentKey}', let challenge = "${challenge}";
userVerificationRequirement : '${userVerificationRequirement}', let userid = "${userid}";
createTimeout : ${createTimeout}, let username = "${username}";
excludeCredentialIds : '${excludeCredentialIds}',
initLabel : "${msg("webauthn-registration-init-label")?no_esc}", let signatureAlgorithms =[<#list signatureAlgorithms as sigAlg>${sigAlg},</#list>]
initLabelPrompt : "${msg("webauthn-registration-init-label-prompt")?no_esc}", let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
errmsg : "${msg("webauthn-unsupported-browser-text")?no_esc}"
let rpEntityName = "${rpEntityName}";
let rp = {name: rpEntityName};
let publicKey = {
challenge: base64url.decode(challenge, {loose: true}),
rp: rp,
user: {
id: base64url.decode(userid, {loose: true}),
name: username,
displayName: username
},
pubKeyCredParams: pubKeyCredParams,
}; };
registerByWebAuthn(input);
// optional parameters
let rpId = "${rpId}";
publicKey.rp.id = rpId;
let attestationConveyancePreference = "${attestationConveyancePreference}";
if (attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
let authenticatorSelection = {};
let isAuthenticatorSelectionSpecified = false;
let authenticatorAttachment = "${authenticatorAttachment}";
if (authenticatorAttachment !== 'not specified') {
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
isAuthenticatorSelectionSpecified = true;
}
let requireResidentKey = "${requireResidentKey}";
if (requireResidentKey !== 'not specified') {
if (requireResidentKey === 'Yes')
authenticatorSelection.requireResidentKey = true;
else
authenticatorSelection.requireResidentKey = false;
isAuthenticatorSelectionSpecified = true;
}
let userVerificationRequirement = "${userVerificationRequirement}";
if (userVerificationRequirement !== 'not specified') {
authenticatorSelection.userVerification = userVerificationRequirement;
isAuthenticatorSelectionSpecified = true;
}
if (isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
let createTimeout = ${createTimeout};
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
let excludeCredentialIds = "${excludeCredentialIds}";
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
navigator.credentials.create({publicKey})
.then(function (result) {
window.result = result;
let clientDataJSON = result.response.clientDataJSON;
let attestationObject = result.response.attestationObject;
let publicKeyCredentialId = result.rawId;
document.getElementById('clientDataJSON').value = base64url.encode(new Uint8Array(clientDataJSON), {pad: false});
document.getElementById('attestationObject').value = base64url.encode(new Uint8Array(attestationObject), {pad: false});
document.getElementById('publicKeyCredentialId').value = base64url.encode(new Uint8Array(publicKeyCredentialId), {pad: false});
if (typeof result.response.getTransports === "function") {
let transports = result.response.getTransports();
if (transports) {
document.getElementById('transports').value = getTransportsAsString(transports);
}
} else {
console.log("Your browser is not able to recognize supported transport media for the authenticator.");
}
let initLabel = "WebAuthn Authenticator (Default Label)";
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
if (labelResult === null) labelResult = initLabel;
document.getElementById("authenticatorLabel").value = labelResult;
document.getElementById('register').submit();
})
.catch(function (err) {
document.getElementById("error").value = err;
document.getElementById("register").submit();
}); });
}
function getPubKeyCredParams(signatureAlgorithmsList) {
let pubKeyCredParams = [];
if (signatureAlgorithmsList === []) {
pubKeyCredParams.push({type: "public-key", alg: -7});
return pubKeyCredParams;
}
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
pubKeyCredParams.push({
type: "public-key",
alg: signatureAlgorithmsList[i]
});
}
return pubKeyCredParams;
}
function getExcludeCredentials(excludeCredentialIds) {
let excludeCredentials = [];
if (excludeCredentialIds === "") return excludeCredentials;
let excludeCredentialIdsList = excludeCredentialIds.split(',');
for (let i = 0; i < excludeCredentialIdsList.length; i++) {
excludeCredentials.push({
type: "public-key",
id: base64url.decode(excludeCredentialIdsList[i],
{loose: true})
});
}
return excludeCredentials;
}
function getTransportsAsString(transportsList) {
if (transportsList === '' || transportsList.constructor !== Array) return "";
let transportsString = "";
for (let i = 0; i < transportsList.length; i++) {
transportsString += transportsList[i] + ",";
}
return transportsString.slice(0, -1);
}
</script> </script>
<input type="submit" <button
type="submit"
class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
id="registerWebAuthn" value="${msg("doRegisterSecurityKey")}"/> id="registerWebAuthn"
onclick="registerSecurityKey()"
>${msg("doRegister")}</button>
<#if !isSetRetry?has_content && isAppInitiatedAction?has_content> <#if !isSetRetry?has_content && isAppInitiatedAction?has_content>
<form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form" <form action="${url.loginAction}" class="${properties.kcFormClass!}" id="kc-webauthn-settings-form"
method="post"> method="post">
<button type="submit" <button
type="submit"
class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}"
id="cancelWebAuthnAIA" name="cancel-aia" value="true">${msg("doCancel")} id="cancelWebAuthnAIA"
</button> name="cancel-aia"
value="true"
>${msg("doCancel")}</button>
</form> </form>
</#if> </#if>