Overview

New versions of FusionAuth sometimes include new or updated theme templates. If a new template is not part of a custom theme, FusionAuth will render the template from the default theme. Occasionally, modifications to existing templates or helper macros introduced in a FusionAuth release will require changes to an existing custom theme in order for it to continue functioning correctly.

This page contains notes on changes in recent versions of FusionAuth that require changes to a customized theme.

Version 1.59.0

Starting in version 1.59.0, user passwords are optional and phone identities are now supported. While theme updates are not required, there are some areas that you may want to consider making changes.

TemplateChanged For Phone IdentitiesPassword
MessagesX
Account edit template (self-service)X
Account index template (self-service)X
Forgot password templateX
Forgot password sent templateX
OAuth complete registration templateX
OAuth passwordless templateX
OAuth register templateX

Messages

To properly support phone identities, several themed Messages have changed. If forgot-password-email-sent or forgot-password-email-sent-title was previously customized in a theme, the same customizations need to be applied to the new forgot-password-message-sent and forgot-password-message-sent-title messages.

Messages Before

forgot-password-email-sent=We have sent an email to %s containing a link that will allow you to reset your password. Once you receive the email follow the instructions to change your password.
forgot-password-email-sent-title=Email sent
loginId=Email
...
[PasswordlessRequestSent]=An email is on the way.

Messages After

forgot-password-message-sent=We have sent a message to %s containing a link that will allow you to reset your password. Once you receive the message follow the instructions to change your password.
forgot-password-message-sent-title=Message sent
loginId=Login
...
[PasswordlessRequestSent]=A message is on the way.

Account edit template (self-service)

To accommodate the use case of an existing user, without a password, setting a password for the first time, there is a new template variable, passwordSet, that can be used on the Helpers page to hide the “current password” field for users that do not have a password.

Before

<form action="${request.contextPath}/account/edit" method="POST" class="full" id="user-form">
  ...
  [#list fieldValues as field]
    [#if field.key == "user.password"]
      [@helpers.passwordField field application.formConfiguration.selfServiceFormConfiguration.requireCurrentPasswordOnPasswordChange/]
    [#else]
      [@helpers.customField field=field key=field.key autofocus=false placeholder=field.key label=theme.optionalMessage(field.key) leftAddon="false"/]
      [#if field.confirm]
        [@helpers.customField field "confirm.${field.key}" false "[confirm]${field.key}" /]
      [/#if]
    [/#if]
  [/#list]
</form>

After

<form action="${request.contextPath}/account/edit" method="POST" class="full" id="user-form">
  ...
  [#list fieldValues as field]
    [#if field.key == "user.password"]
      [@helpers.passwordField field=field
                              showCurrentPasswordField=(passwordSet && application.formConfiguration.selfServiceFormConfiguration.requireCurrentPasswordOnPasswordChange)/]
    [#else]
      [@helpers.customField field=field key=field.key autofocus=false placeholder=field.key label=theme.optionalMessage(field.key) leftAddon="false"/]
      [#if field.confirm]
        [@helpers.customField field "confirm.${field.key}" false "[confirm]${field.key}" /]
      [/#if]
    [/#if]
  [/#list]
</form>

Account index template (self-service)

New installations of FusionAuth show the new user.phoneNumber field on the account index page, rather than user.mobilePhone.

Before

<dl class="horizontal">
  <dt>${theme.message("user.mobilePhone")}</dt>
  <dd>${helpers.display(user, "mobilePhone")}</dd>
</dl>

After

<dl class="horizontal">
  <dt>${theme.message("user.phoneNumber")}</dt>
  <dd>${fusionAuth.phone_format(user.phoneNumber!"\x2013")}</dd>
</dl>

Forgot password template

To improve the user experience for phone number identities, the email field should be replaced with a loginId field which will use the label Login instead of Email (see Messages above). Everything will continue to function even if this change is not made, but the user experience will be improved with the new field because of the more accurate label.

Before

<fieldset class="push-less-top">
  [@helpers.input type="text" name="email" id="email" autocapitalize="none" autofocus=true autocomplete="on" autocorrect="off" placeholder=theme.message('email') leftAddon="user" required=true/]
  [@helpers.captchaBadge showCaptcha=showCaptcha captchaMethod=tenant.captchaConfiguration.captchaMethod siteKey=tenant.captchaConfiguration.siteKey/]
</fieldset>

After

<fieldset class="push-less-top">
  [@helpers.input type="text" name="loginId" id="loginId" autocapitalize="none" autofocus=true autocomplete="on" autocorrect="off" placeholder=theme.message('loginId') leftAddon="user" required=true/]
  [@helpers.captchaBadge showCaptcha=showCaptcha captchaMethod=tenant.captchaConfiguration.captchaMethod siteKey=tenant.captchaConfiguration.siteKey/]
</fieldset>

Forgot password sent template

Similar to the Forgot password template, the Forgot password sent template should also be updated to use the loginId field instead of the email field. This will improve the user experience for phone number identities.

Before

[@helpers.main title=theme.message('forgot-password-email-sent-title')]
  <p>
    ${theme.message('forgot-password-email-sent', email)}
  </p>
  <p class="mt-2">[@helpers.link url="/oauth2/authorize"]${theme.message('return-to-login')}[/@helpers.link]</p>
[/@helpers.main]

After

[@helpers.main title=theme.message('forgot-password-message-sent-title')]
  <p>
    ${theme.message('forgot-password-message-sent', loginId)}
  </p>
  <p class="mt-2">[@helpers.link url="/oauth2/authorize"]${theme.message('return-to-login')}[/@helpers.link]</p>
[/@helpers.main]

OAuth complete registration template

You may need users to have passwords, but passwords are optional. Consider the following example:

  • A user has already logged in
  • The user does not have a password
  • The user performs self-serve registration for an application that uses basic registration

In this case, FusionAuth takes the user to the OAuth complete registration page to add any required fields for the application. If the user does not have a password you can collect it on this page.

To support this, FusionAuth provides a new template variable named passwordSet, which indicates whether or not the user has a password. Use this variable to show a password field if you require the user to have a password.

Note that for a new or logged-out user that registers for this application, the password will generally be collected on the initial registration page. Logged-in users bypass the initial page.

For example, before:

Before

[#if application.registrationConfiguration.mobilePhone.enabled]
  [@helpers.input type="text" name="user.mobilePhone" id="mobilePhone" placeholder=theme.message("mobilePhone") leftAddon="phone" required=application.registrationConfiguration.mobilePhone.required/]
[/#if]
[#if application.registrationConfiguration.preferredLanguages.enabled]
  [@helpers.locale_select field="" name="user.preferredLanguages"  id="preferredLanguages" label=theme.message("preferredLanguage") required=application.registrationConfiguration.preferredLanguages.required /]
[/#if]

After

[#if application.registrationConfiguration.mobilePhone.enabled]
  [@helpers.input type="text" name="user.mobilePhone" id="mobilePhone" placeholder=theme.message("mobilePhone") leftAddon="phone" required=application.registrationConfiguration.mobilePhone.required/]
[/#if]
[#if !(passwordSet!false)]
  [@helpers.input type="password" name="user.password" id="password" autocomplete="new-password" placeholder=theme.message('password') leftAddon="lock" required=true/]
  [#if application.registrationConfiguration.confirmPassword]
    [@helpers.input type="password" name="confirm.user.password" id="passwordConfirm" autocomplete="new-password" placeholder=theme.message('passwordConfirm') leftAddon="lock" required=true/]
  [/#if]
[/#if]
[#if application.registrationConfiguration.preferredLanguages.enabled]
  [@helpers.locale_select field="" name="user.preferredLanguages"  id="preferredLanguages" label=theme.message("preferredLanguage") required=application.registrationConfiguration.preferredLanguages.required /]
[/#if]

OAuth passwordless template

To support passwordless logins with phone numbers, several changes must be made to allow submission of one-time or short codes on the passwordless page. The changes include: new hidden form fields, an if surrounding the loginId field with a new oneTimeCode field, and a change to the button text based on whether the form is being submitted or a code is being sent.

Before

<form action="${request.contextPath}/oauth2/passwordless" method="POST" class="full">
  [@helpers.oauthHiddenFields/]
  <fieldset>
    [@helpers.input type="text" name="loginId" id="loginId" autocomplete="username" autocapitalize="none" autocomplete="on" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message("loginId") leftAddon="user" required=true/]
    [@helpers.captchaBadge showCaptcha=showCaptcha captchaMethod=tenant.captchaConfiguration.captchaMethod siteKey=tenant.captchaConfiguration.siteKey/]
  </fieldset>

  [@helpers.input id="rememberDevice" type="checkbox" name="rememberDevice" label=theme.message("remember-device") value="true" uncheckedValue="false"]
  <i class="fa fa-info-circle" data-tooltip="${theme.message('{tooltip}remember-device')}"></i>[#t/]
  [/@helpers.input]

  <div class="form-row">
    [@helpers.button icon="send" text=theme.message('send')/]
    <p class="mt-2">[@helpers.link url="/oauth2/authorize"]${theme.message('return-to-login')}[/@helpers.link]</p>
  </div>
</form>

After

<form action="${request.contextPath}/oauth2/passwordless" method="POST" class="full">
  [@helpers.oauthHiddenFields/]
  [@helpers.hidden name="code"/]
  [@helpers.hidden name="formField"/]

  <fieldset>
    [#if (formField!false)]
      [@helpers.input type="text" name="oneTimeCode" id="otp" autocapitalize="none" autofocus=true autocomplete="one-time-code" autocorrect="off" placeholder="${theme.message('passwordless-code')}" leftAddon="lock"/]
    [#else]
      [@helpers.input type="text" name="loginId" id="loginId" autocomplete="username" autocapitalize="none" autocomplete="on" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message("loginId") leftAddon="user" required=true/]
    [/#if]
    [@helpers.captchaBadge showCaptcha=showCaptcha captchaMethod=tenant.captchaConfiguration.captchaMethod siteKey=tenant.captchaConfiguration.siteKey/]
  </fieldset>

  [@helpers.input id="rememberDevice" type="checkbox" name="rememberDevice" label=theme.message("remember-device") value="true" uncheckedValue="false"]
  <i class="fa fa-info-circle" data-tooltip="${theme.message('{tooltip}remember-device')}"></i>[#t/]
  [/@helpers.input]

  <div class="form-row">
    [#if (formField!false)]
      [@helpers.button text=theme.message('submit')/]
    [#else]
      [@helpers.button icon="send" text=theme.message('send')/]
    [/#if]
  </div>
</form>

OAuth register template

To support phone numbers in basic registration, a user.phoneNumber field must be added.

Before

[#if application.registrationConfiguration.loginIdType == 'email']
  [@helpers.input type="text" name="user.email" id="email" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message('email') leftAddon="user" required=true/]
[#else]
  [@helpers.input type="text" name="user.username" id="username" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message('username') leftAddon="user" required=true/]
[/#if]

After

[#if application.registrationConfiguration.loginIdType == 'email']
  [@helpers.input type="text" name="user.email" id="email" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message('email') leftAddon="user" required=true/]
[#elseif application.registrationConfiguration.loginIdType == 'phoneNumber']
  [@helpers.input type="text" name="user.phoneNumber" id="phoneNumber" autocomplete="mobile" autocapitalize="none" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message('phoneNumber') leftAddon="mobile" required=true/]
[#else]
  [@helpers.input type="text" name="user.username" id="username" autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false" autofocus=true placeholder=theme.message('username') leftAddon="user" required=true/]
[/#if]

Version 1.53.3

Version 1.53.3 includes a change to persist the value of the Keep me signed in checkbox from the hosted login pages through an external identity provider workflow. This checkbox value indicates whether the user wishes to create an SSO session after login. If the Google IdP’s loginMethod is configured as UsePopup or UseVendorJavaScript, existing custom advanced themes require an update to incorporate the fix for the Google IdP. You can update the template via the API using theme.templates.helpers or by modifying the Helpers template in the admin UI. Google IdPs configured with a loginMethod value of UseRedirect do not require this update, but you may consider making the change preemptively in case the loginMethod is changed later.

To allow the Keep me signed in value to be persisted through a Google IdP login in an existing custom advanced theme, remove the data-login_uri attribute and its value from the div with Id g_id_onload in the googleButton macro and add the data-callback attribute in its place.

Replace

<div id="g_id_onload" [#list identityProvider.lookupAPIProperties(clientId)!{} as attribute, value] data-${attribute}="${value}" [/#list]
     data-client_id="${identityProvider.lookupClientId(clientId)}"
     data-login_uri="${currentBaseURL}/oauth2/callback?state=${idpRedirectState}&identityProviderId=${identityProvider.id}" >
</div>

with

<div id="g_id_onload" [#list identityProvider.lookupAPIProperties(clientId)!{} as attribute, value] data-${attribute}="${value}" [/#list]
     data-client_id="${identityProvider.lookupClientId(clientId)}"
     data-callback="googleLoginCallback" >
</div>

Version 1.52.0

Version 1.52.0 includes a change to use the browser-default date picker to enhance the experience on mobile. Existing custom advanced themes require an update to incorporate the change. You can update the template via the API using theme.templates.helpers or by modifying the Helpers template in the admin UI.

To include the new date picker in an existing custom advanced theme, replace the Prime.Document.query('.date-picker') line in the head macro with the following:

document.querySelectorAll('.date-picker').forEach(datePicker => {
  datePicker.onfocus = () => datePicker.type = 'date';
  datePicker.onblur = () => {
    if (datePicker.value === '') {
      datePicker.type = 'text';
    }
  };
});

Version 1.50.0

Version 1.50.0 added the ability to prompt users for consent to custom OAuth scopes in third-party applications. This change requires a new themed template oauth2Consent as well as a new macro and function in the helpers template.

The oauth2Consent template from the default theme will be used until it is added to an existing custom theme. You can copy the new template from the default theme as a starting point and add it to a custom theme via the API using theme.templates.oauth2Consent or the Consent prompt template in the admin UI.

The new scopeConsentField macro and resolveScopeMessaging function must be added to an existing custom theme’s helpers template in order for the theme to continue functioning. Add these new items to the template via the API using theme.templates.helpers or the Helpers template in the admin UI. You can copy them from the default template or use the following:

[#macro scopeConsentField application scope type]
  [#-- Resolve the consent message and detail for the provided scope --]
  [#if type != "unknown"]
    [#local scopeMessage = resolveScopeMessaging('message', application, scope.name, scope.defaultConsentMessage!scope.name) /]
    [#local scopeDetail = resolveScopeMessaging('detail', application, scope.name, scope.defaultConsentDetail!'') /]
  [/#if]

  [#if type == "required"]
    [#-- Required scopes should use a hidden form field with a value of "true". The user cannot change this selection, --]
    [#-- but there should be a display element to inform the user that they must consent to the scopes to continue. --]
    <div class="form-row consent-item col-lg-offset-0">
      [@hidden name="scopeConsents['${scope.name}']" value="true" /]
      <i class="fa fa-check"></i>
      <span>
        ${scopeMessage}
        [#if scopeDetail?has_content]
          <i class="fa fa-info-circle" data-tooltip="${scopeDetail}"></i>
        [/#if]
      </span>
    </div>
  [#elseif type == "optional"]
    [#-- Optional scopes should render a checkbox to allow a user to change their selection. The available values should be "true" and "false" --]
    <div class="consent-item col-lg-offset-0">
      [@input type="checkbox" name="scopeConsents['${scope.name}']" id="${scope.name}" label=scopeMessage value="true" uncheckedValue="false" tooltip=scopeDetail /]
    </div>
  [#elseif type == "unknown"]
    [#-- Unknown scopes and the reserved "openid" and "offline_access" scopes are considered required and do not have an associated display element. --]
    [@hidden name="scopeConsents['${scope}']" value="true" /]
  [/#if]
[/#macro]

[#function resolveScopeMessaging messageType application scopeName default]
  [#-- Application specific, tenant specific, not application/tenant specific, then default --]
  [#local message = theme.optionalMessage("[{application}${application.id}]{scope-${messageType}}${scopeName}") /]
  [#local resolvedMessage = message != "[{application}${application.id}]{scope-${messageType}}${scopeName}" /]
  [#if !resolvedMessage]
     [#local message = theme.optionalMessage("[{tenant}${application.tenantId}]{scope-${messageType}}${scopeName}") /]
     [#local resolvedMessage = message != "[{tenant}${application.tenantId}]{scope-${messageType}}${scopeName}" /]
  [/#if]
  [#if !resolvedMessage]
    [#local message = theme.optionalMessage("{scope-${messageType}}${scopeName}") /]
    [#local resolvedMessage = message != "{scope-${messageType}}${scopeName}" /]
  [/#if]
  [#if !resolvedMessage]
    [#return default /]
  [#else]
    [#return message /]
  [/#if]
[/#function]