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.61.0 - prompt parameter

To ensure that FusionAuth is able to forward the prompt parameter to third party identity providers, you will need to make the following modifications.

Helpers template

Update the oauthHiddenFields, link and logoutLink macros to add the max_age parameter. When editing a theme in the UI, this page will be shown as Helpers, and the API template name is helpers.

Macro oauthHiddenFields

Add max_age to the oauthHiddenFields macro.

Add prompt

[@hidden name="max_age"/]

For example, this is a macro that includes max_age:

After

[#macro oauthHiddenFields]
  [@hidden name="captcha_token"/]
  [@hidden name="client_id"/]
  [@hidden name="code_challenge"/]
  [@hidden name="code_challenge_method"/]
  [@hidden name="metaData.device.name"/]
  [@hidden name="metaData.device.type"/]
  [@hidden name="nonce"/]
  [@hidden name="oauth_context"/]
  [@hidden name="max_age"/]
  [@hidden name="pendingIdPLinkId"/]
  [@hidden name="prompt"/]
  [@hidden name="redirect_uri"/]
  [@hidden name="response_mode"/]
  [@hidden name="response_type"/]
  [@hidden name="scope"/]
  [@hidden name="state"/]
  [@hidden name="tenantId"/]
  [@hidden name="timezone"/]
  [@hidden name="user_code"/]
[/#macro]

Add max_age to the link macro.

Add max_age to the query string

&max_age=${(max_age?url)!''}

After

[#macro link url extraParameters=""]
  <a href="${url}?tenantId=${(tenantId)!''}&client_id=${(client_id)!''}&nonce=${(nonce?url)!''}&pendingIdPLinkId=${(pendingIdPLinkId)!''}&redirect_uri=${(redirect_uri?url)!''}&response_mode=${(response_mode?url)!''}&response_type=${(response_type?url)!''}&scope=${(scope?url)!''}&state=${(state?url)!''}&timezone=${(timezone?url)!''}&metaData.device.name=${(metaData.device.name?url)!''}&metaData.device.type=${(metaData.device.type?url)!''}${(extraParameters!'')?no_esc}&code_challenge=${(code_challenge?url)!''}&code_challenge_method=${(code_challenge_method?url)!''}&user_code=${(user_code?url)!''}&prompt=${(prompt?url)!''}&max_age=${(max_age?url)!''}">
      [#nested/]
  </a>
[/#macro]

Add max_age to the logoutLink macro.

Add max_age to the query string

&max_age=${(max_age?url)!''}

After

[#macro logoutLink redirectURI extraParameters=""]
[#-- Note that in order for the post_logout_redirect_uri to be correctly URL escaped, you must use this syntax for assignment --]
    [#local post_logout_redirect_uri]${redirectURI}?tenantId=${(tenantId)!''}&client_id=${(client_id)!''}&nonce=${(nonce?url)!''}&pendingIdPLinkId=${(pendingIdPLinkId)!''}&redirect_uri=${(redirect_uri?url)!''}&response_mode=${(response_mode?url)!''}&response_type=${(response_type?url)!''}&scope=${(scope?url)!''}&state=${(state?url)!''}&timezone=${(timezone?url)!''}&metaData.device.name=${(metaData.device.name?url)!''}&metaData.device.type=${(metaData.device.type?url)!''}${(extraParameters?no_esc)!''}&code_challenge=${(code_challenge?url)!''}&code_challenge_method=${(code_challenge_method?url)!''}&user_code=${(user_code?url)!''}&prompt=${(prompt?url)!''}&max_age=${(max_age?url)!''}[/#local]
  <a
  href="/oauth2/logout?tenantId=${(tenantId)!''}&client_id=${(client_id)!''}&post_logout_redirect_uri=${post_logout_redirect_uri?markup_string?url}">[#t]
    [#nested/][#t]
  </a>[#t]
[/#macro]

Version 1.61.0 - Multiple identity provider configurations

Previously the following identity provider types were limited to a single configuration per FusionAuth instance: Apple, Epic Games, Facebook, Google, HYPR, LinkedIn, Nintendo, Sony PlayStation Network, Steam, Twitch, Twitter, and Xbox.

To enable support for multiple identity provider configurations of these types on a single login page, you will need to make the following modifications. The default theme is available on every FusionAuth instance as a point of comparison or guide to theme upgrades.

Helpers template

Update the alternativeLoginsScript, alternativeLogins, and various external identity provider button rendering macros. When editing a theme in the UI, this page will be shown as Helpers, and the API template name is helpers.

Macro alternativeLoginsScript

Remove the data-app-id attribute from the <script> tag including Facebook.js. The attribute must be set by each facebookButton macro invocation to support multiple Facebook IdP configurations.

Remove data-app-id

data-app-id="${identityProviders["Facebook"][0].lookupAppId(clientId)}"

Here is an example <script> tag without the attribute

After

<script src="${request.contextPath}/js/identityProvider/Facebook.js?version=${version}"></script>

Remove the data-client-id attribute from the <script> tag including Twitter.js. The attribute must be set by each twitterButton macro invocation to support multiple Twitter IdP configurations.

Remove data-client-id

data-client-id="${clientId}"

Here is an example <script> tag without the attribute

After

<script src="${request.contextPath}/js/identityProvider/Twitter.js?version=${version}"></script>

Replace the [#if] block including Google’s GSI client JavaScript and Google.js. Only one Google identity provider using the GSI client can be included on the page. See Google IdP limitations for more detail.

The old [#if] block

[#if identityProviders["Google"]?has_content && identityProviders["Google"][0].lookupLoginMethod(clientId) != "UseRedirect"]
  <script src="https://accounts.google.com/gsi/client" async></script>
  <script src="${request.contextPath}/js/identityProvider/Google.js?version=${version}"
          data-client-id="${identityProviders["Google"][0].lookupClientId(clientId)}"></script>
[/#if]

Here is the updated [#if] block. The updated logic selects the first Google IdP configured to use the GSI client and stores it in a gsiIdentityProvider variable to be used later in the alternativeLogins macro.

The updated [#if] block

[#if identityProviders["Google"]?has_content]
  [#list identityProviders["Google"] as idp]
    [#if idp.lookupLoginMethod(clientId) != "UseRedirect"]
      [#-- Only one Google IdP can use the GSI APIs on a page. Assign the IdP for later use and load scripts. --]
      [#assign gsiIdentityProvider = idp/]
      <script src="https://accounts.google.com/gsi/client" async></script>
      <script src="${request.contextPath}/js/identityProvider/Google.js?version=${version}"
              data-client-id="${gsiIdentityProvider.lookupClientId(clientId)}"
              data-identity-provider-id="${gsiIdentityProvider.id}"></script>
      [#break]
    [/#if]
  [/#list]
[/#if]

Macro alternativeLogins

The alternativeLogins macro checks for various IdP types in the identityProviders Freemarker variable and renders the appropriate button for that type. Several identity provider types now support multiple configurations and need to update rendering a single button to looping over the IdPs of that type.alternativeLogins

Here is an example rendering the “Sign in with Apple” button hard-coded to expect a single Apple IdP configuration.

Single configuration

[#if identityProviders["Apple"]?has_content]
  <div class="form-row push-less-top">
      [@appleButton identityProvider=identityProviders["Apple"][0] clientId=clientId/]
  </div>
[/#if]

Here is an example that supports rendering zero or more buttons based on available Apple IdP configurations by looping over all Apple IdP configurations and rendering the button with the identityProvider for that loop.

Multiple configurations

[#if identityProviders["Apple"]?has_content]
  [#list identityProviders["Apple"] as identityProvider]
    <div class="form-row push-less-top">
        [@appleButton identityProvider=identityProvider clientId=clientId/]
    </div>
  [/#list]
[/#if]

The above is just one example of several identity provider types requiring updates in alternativeLogins to render multiple buttons by replacing the hard-coded single instance with a loop. As you work through the different identity provider types, pay special attention to the key in the identityProviders map and the name of the button rendering macro in each case. The following identity provider types require this update in the alternativeLogins macro: Apple, Epic Games, Facebook, LinkedIn, Nintendo, Sony PlayStation Network, Steam, Twitch, Twitter, Xbox.

The Google identity provider type requires a bit more modification in the alternativeLogins macro in order to render the single button using the GSI client in addition to other Google IdP configurations with the UseRedirect login method. Remove the existing [#if] block and replace with the updated code below.

Remove the [#if] block

[#if identityProviders["Google"]?has_content]
  <div class="form-row push-less-top">
      [@googleButton identityProvider=identityProviders["Google"][0] clientId=clientId idpRedirectState=idpRedirectState/]
  </div>
[/#if]

In its place add two [#if] blocks. The first block checks whether it should render a GSI client button using the gsiIdentityProvider variable assigned in the alternativeLoginsScript macro. The second block calls the googleButton macro to render a login button for Google IdPs configured with the UseRedirect login method.

Render GSI and UseRedirect Google buttons

[#-- Check whether a Google IdP was assigned to use GSI APIs and render the button --]
[#if gsiIdentityProvider?has_content]
  <div class="form-row push-less-top">
      [@googleGsiButton identityProvider=gsiIdentityProvider clientId=clientId idpRedirectState=idpRedirectState/]
  </div>
[/#if]

[#if identityProviders["Google"]?has_content]
  [#list identityProviders["Google"] as identityProvider]
    <div class="form-row push-less-top">
        [#-- The googleButton macro only renders buttons for IdPs configured with the UseRedirect login methods --]
        [@googleButton identityProvider=identityProvider clientId=clientId idpRedirectState=idpRedirectState/]
    </div>
  [/#list]
[/#if]

Macro appleButton

Replace the opening tag of the <button> element inside the macro with the following

Apple <button>

<button class="apple login-button" data-scope="${identityProvider.lookupScope(clientId)!''}"
          data-services-id="${identityProvider.lookupServicesId(clientId)}"
          data-identity-provider-id="${identityProvider.id}">

Macro epicButton

Replace the opening tag of the <button> element inside the macro with the following

Epic Games <button>

<button class="epicgames login-button" data-login-method="UseRedirect"
          data-scope="${identityProvider.lookupScope(clientId)!''}" data-identity-provider-id="${identityProvider.id}">

Macro facebookButton

Replace the opening tag of the <button> element inside the macro with the following

Facebook <button>

<button class="facebook login-button" data-login-method="${identityProvider.lookupLoginMethod(clientId)!''}"
          data-permissions="${identityProvider.lookupPermissions(clientId)!''}" data-identity-provider-id="${identityProvider.id}"
          data-app-id="${identityProvider.lookupAppId(clientId)}">

Macro googleButton

This macro should now only render buttons for Google IdP configurations with the UseRedirect login method. Replace the opening tag of the <button> element inside the macro with the following

Google <button>

<button class="google login-button" data-login-method="UseRedirect"
              data-scope="${identityProvider.lookupScope(clientId)!''}" data-identity-provider-id="${identityProvider.id}">

Remove the [#else] block that renders <div>s with Ids g_id_onload and g_id_signin. The updated macro structure looks like this with the <button> children removed for brevity

googleButton macro structure

[#macro googleButton identityProvider clientId idpRedirectState=""]
    [#-- When using this loginMethod - the Google JavaScript API is not used at all. --]
    [#if identityProvider.lookupLoginMethod(clientId) == "UseRedirect"]
      <button class="google login-button" data-login-method="UseRedirect"
              data-scope="${identityProvider.lookupScope(clientId)!''}" data-identity-provider-id="${identityProvider.id}">
        ...
      </button>
    [/#if]
[/#macro]

New macro googleGsiButton

The [#else] block removed from googleButton macro gets its own macro. Add the following macro to your theme

New googleGsiButton macro

[#macro googleGsiButton identityProvider clientId idpRedirectState=""]
  [#-- UsePopup or UseVendorJavaScript --]
  [#--
   Use the Google Identity Service (GIS) API.
   https://developers.google.com/identity/gsi/web/reference/html-reference
  --]
  <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>
  [#-- This the Google Signin button. If only using One tap, you can delete or comment out this element --]
  <div class="g_id_signin" [#list identityProvider.lookupButtonProperties(clientId)!{} as attribute, value] data-${attribute}="${value}" [/#list]
       [#-- Optional click handler, when using ux_mode=popup. --]
       data-click_listener="googleButtonClickHandler">
  </div>
[/#macro]

Macro linkedInBottom

You read that correctly. If you’d like, update the name of the macro to linkedInButton along with the invocation from the alternativeLogins macro. Replace the opening tag of the <button> element inside the macro with the following

LinkedIn <button>

<button class="linkedin login-button" data-login-method="UseRedirect" data-identity-provider-id="${identityProvider.id}">

Macro sonypsnButton

Replace the opening tag of the <button> element inside the macro with the following

Sony PlayStation Network <button>

<button class="sonypsn login-button" data-login-method="UseRedirect"
          data-scope="${identityProvider.lookupScope(clientId)!''}" data-identity-provider-id="${identityProvider.id}">

Macro steamButton

Replace the opening tag of the <button> element inside the macro with the following

Steam <button>

<button class="steam login-button" data-login-method="UseRedirect" data-scope="${identityProvider.lookupScope(clientId)!''}"
          data-identity-provider-id="${identityProvider.id}">

Macro twitchButton

Replace the opening tag of the <button> element inside the macro with the following

Twitch <button>

<button class="twitch login-button" data-login-method="UseRedirect"
          data-scope="${identityProvider.lookupScope(clientId)!''}" data-identity-provider-id="${identityProvider.id}">

Macro twitterButton

Replace the opening tag of the <button> element inside the macro with the following

Twitter <button>

<button class="twitter login-button" data-client-id="${clientId}" data-identity-provider-id="${identityProvider.id}">

Macro xboxButton

Replace the opening tag of the <button> element inside the macro with the following

Xbox <button>

<button class="xbox login-button" data-login-method="UseRedirect" data-scope="${identityProvider.lookupScope(clientId)!''}"
          data-identity-provider-id="${identityProvider.id}">

Version 1.60.0

To enable support of the prompt parameter, you will need to make the following modifications.

Helpers template

Update the oauthHiddenFields, link and logoutLink macros to add the prompt parameter. When editing a theme in the UI, this page will be shown as Helpers, and the API template name is helpers.

Macro oauthHiddenFields

Add prompt to the oauthHiddenFields macro.

Add prompt

[@hidden name="prompt"/]

For example, this is a macro that includes prompt:

After

[#macro oauthHiddenFields]
  [@hidden name="captcha_token"/]
  [@hidden name="client_id"/]
  [@hidden name="code_challenge"/]
  [@hidden name="code_challenge_method"/]
  [@hidden name="metaData.device.name"/]
  [@hidden name="metaData.device.type"/]
  [@hidden name="nonce"/]
  [@hidden name="oauth_context"/]
  [@hidden name="pendingIdPLinkId"/]
  [@hidden name="prompt"/]
  [@hidden name="redirect_uri"/]
  [@hidden name="response_mode"/]
  [@hidden name="response_type"/]
  [@hidden name="scope"/]
  [@hidden name="state"/]
  [@hidden name="tenantId"/]
  [@hidden name="timezone"/]
  [@hidden name="user_code"/]
[/#macro]

Add prompt to the link macro.

Add prompt to the query string

&prompt=${(prompt?url)!''}

After

[#macro link url extraParameters=""]
  <a href="${url}?tenantId=${(tenantId)!''}&client_id=${(client_id)!''}&nonce=${(nonce?url)!''}&pendingIdPLinkId=${(pendingIdPLinkId)!''}&redirect_uri=${(redirect_uri?url)!''}&response_mode=${(response_mode?url)!''}&response_type=${(response_type?url)!''}&scope=${(scope?url)!''}&state=${(state?url)!''}&timezone=${(timezone?url)!''}&metaData.device.name=${(metaData.device.name?url)!''}&metaData.device.type=${(metaData.device.type?url)!''}${(extraParameters!'')?no_esc}&code_challenge=${(code_challenge?url)!''}&code_challenge_method=${(code_challenge_method?url)!''}&user_code=${(user_code?url)!''}&prompt=${(prompt?url)!''}">
      [#nested/]
  </a>
[/#macro]

Add prompt to the logoutLink macro.

Add prompt to the query string

&prompt=${(prompt?url)!''}

After

[#macro logoutLink redirectURI extraParameters=""]
[#-- Note that in order for the post_logout_redirect_uri to be correctly URL escaped, you must use this syntax for assignment --]
    [#local post_logout_redirect_uri]${redirectURI}?tenantId=${(tenantId)!''}&client_id=${(client_id)!''}&nonce=${(nonce?url)!''}&pendingIdPLinkId=${(pendingIdPLinkId)!''}&redirect_uri=${(redirect_uri?url)!''}&response_mode=${(response_mode?url)!''}&response_type=${(response_type?url)!''}&scope=${(scope?url)!''}&state=${(state?url)!''}&timezone=${(timezone?url)!''}&metaData.device.name=${(metaData.device.name?url)!''}&metaData.device.type=${(metaData.device.type?url)!''}${(extraParameters?no_esc)!''}&code_challenge=${(code_challenge?url)!''}&code_challenge_method=${(code_challenge_method?url)!''}&user_code=${(user_code?url)!''}&prompt=${(prompt?url)!''}[/#local]
  <a
  href="/oauth2/logout?tenantId=${(tenantId)!''}&client_id=${(client_id)!''}&post_logout_redirect_uri=${post_logout_redirect_uri?markup_string?url}">[#t]
    [#nested/][#t]
  </a>[#t]
[/#macro]

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]