import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constants"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; import type { MessageKey } from "../i18n/i18n"; type ExtractAfterStartingWith = StrEnum extends `${Prefix}${infer U}` ? U : never; /** Take theses type definition with a grain of salt. * Some values might be undefined on some pages. * (ex: url.loginAction is undefined on error.ftl) */ export type KcContext = | KcContext.Login | KcContext.Register | KcContext.Info | KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail | KcContext.Terms | KcContext.LoginDeviceVerifyUserCode | KcContext.LoginOauthGrant | KcContext.LoginOtp | KcContext.LoginUsername | KcContext.WebauthnAuthenticate | KcContext.WebauthnRegister | KcContext.LoginPassword | KcContext.LoginUpdatePassword | KcContext.LoginUpdateProfile | KcContext.LoginIdpLinkConfirm | KcContext.LoginIdpLinkEmail | KcContext.LoginPageExpired | KcContext.LoginConfigTotp | KcContext.LogoutConfirm | KcContext.IdpReviewUserProfile | KcContext.UpdateEmail | KcContext.SelectAuthenticator | KcContext.SamlPostForm | KcContext.DeleteCredential | KcContext.Code | KcContext.DeleteAccountConfirm | KcContext.FrontchannelLogout | KcContext.LoginRecoveryAuthnCodeConfig | KcContext.LoginRecoveryAuthnCodeInput | KcContext.LoginResetOtp | KcContext.LoginX509Info | KcContext.WebauthnError; assert(); export declare namespace KcContext { export type Common = { themeVersion: string; keycloakifyVersion: string; themeType: "login"; themeName: string; url: { loginAction: string; resourcesPath: string; resourcesCommonPath: string; loginRestartFlowUrl: string; loginUrl: string; ssoLoginInOtherTabsUrl: string; }; realm: { name: string; displayName?: string; displayNameHtml?: string; internationalizationEnabled: boolean; registrationEmailAsUsername: boolean; }; /** Undefined if !realm.internationalizationEnabled */ locale?: { supported: { url: string; label: string; languageTag: string; }[]; currentLanguageTag: string; }; auth?: { showUsername?: boolean; showResetCredentials?: boolean; showTryAnotherWayLink?: boolean; attemptedUsername?: string; }; scripts: string[]; message?: { type: "success" | "warning" | "error" | "info"; summary: string; }; client: { clientId: string; name?: string; description?: string; attributes: Record; }; isAppInitiatedAction?: boolean; messagesPerField: { /** * Return text if message for given field exists. Useful eg. to add css styles for fields with message. * * @param fieldName to check for * @param text to return * @return text if message exists for given field, else undefined */ printIfExists: (fieldName: string, text: T) => T | undefined; /** * Check if exists error message for given fields * * @param fields * @return boolean */ existsError: (fieldName: string, ...otherFiledNames: string[]) => boolean; /** * Get message for given field. * * @param fieldName * @return message text or empty string */ get: (fieldName: string) => string; /** * Check if message for given field exists * * @param field * @return boolean */ exists: (fieldName: string) => boolean; getFirstError: (...fieldNames: string[]) => string; }; properties: Record; authenticationSession?: { authSessionId: string; tabId: string; ssoLoginInOtherTabsUrl: string; }; }; export type SamlPostForm = Common & { pageId: "saml-post-form.ftl"; samlPost: { url: string; SAMLRequest?: string; SAMLResponse?: string; relayState?: string; }; }; export type Login = Common & { pageId: "login.ftl"; url: { loginResetCredentialsUrl: string; registrationUrl: string; }; realm: { loginWithEmailAllowed: boolean; rememberMe: boolean; password: boolean; resetPasswordAllowed: boolean; registrationAllowed: boolean; }; auth: { selectedCredential?: string; }; registrationDisabled: boolean; login: { username?: string; rememberMe?: string; // "on" | undefined password?: string; }; usernameHidden?: boolean; social: { displayInfo: boolean; providers?: { loginUrl: string; alias: string; providerId: string; displayName: string; iconClasses?: string; }[]; }; }; export type Register = Common & { pageId: "register.ftl"; profile: UserProfile; url: { registrationAction: string; }; passwordRequired: boolean; recaptchaRequired: boolean; recaptchaSiteKey?: string; /** * Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension * A Keycloak Java extension used as dependency in Keycloakify. */ passwordPolicies?: PasswordPolicies; termsAcceptanceRequired?: boolean; }; export type Info = Common & { pageId: "info.ftl"; messageHeader?: string; requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKey>[]; skipLink: boolean; pageRedirectUri?: string; actionUri?: string; client: { baseUrl?: string; }; }; export type Error = Common & { pageId: "error.ftl"; client?: { baseUrl?: string; }; message: NonNullable; skipLink?: boolean; }; export type LoginResetPassword = Common & { pageId: "login-reset-password.ftl"; realm: { loginWithEmailAllowed: boolean; duplicateEmailsAllowed: boolean; }; url: { loginResetCredentialsUrl: string; }; auth: { attemptedUsername?: string; }; }; export type LoginVerifyEmail = Common & { pageId: "login-verify-email.ftl"; //NOTE: Optional because maybe it wasn't defined in older keycloak versions. user?: { email: string; }; }; export type Terms = Common & { pageId: "terms.ftl"; //NOTE: Optional because maybe it wasn't defined in older keycloak versions. user?: { id: string; username: string; attributes: Record; email: string; emailVerified: boolean; firstName?: string; lastName?: string; markedForEviction?: boolean; }; }; export type LoginDeviceVerifyUserCode = Common & { pageId: "login-oauth2-device-verify-user-code.ftl"; url: { oauth2DeviceVerificationAction: string; }; }; export type LoginOauthGrant = Common & { pageId: "login-oauth-grant.ftl"; oauth: { code: string; client: string; clientScopesRequested: { consentScreenText: string; dynamicScopeParameter?: string; }[]; }; url: { oauthAction: string; }; }; export type LoginOtp = Common & { pageId: "login-otp.ftl"; otpLogin: { userOtpCredentials: { id: string; userLabel: string; }[]; selectedCredentialId?: string; }; }; export type LoginUsername = Common & { pageId: "login-username.ftl"; url: { loginResetCredentialsUrl: string; registrationUrl: string; }; realm: { loginWithEmailAllowed: boolean; rememberMe: boolean; password: boolean; resetPasswordAllowed: boolean; registrationAllowed: boolean; }; registrationDisabled: boolean; login: { username?: string; rememberMe?: string; }; usernameHidden?: boolean; social: Login["social"]; }; export type LoginPassword = Common & { pageId: "login-password.ftl"; url: { loginResetCredentialsUrl: string; registrationUrl: string; }; realm: { resetPasswordAllowed: boolean; }; auth?: { showUsername?: boolean; showResetCredentials?: boolean; showTryAnotherWayLink?: boolean; attemptedUsername?: string; }; social: { displayInfo: boolean; }; }; export type WebauthnAuthenticate = Common & { pageId: "webauthn-authenticate.ftl"; authenticators: { authenticators: WebauthnAuthenticate.WebauthnAuthenticator[]; }; challenge: string; // I hate this: userVerification: UserVerificationRequirement | "not specified"; rpId: string; createTimeout: string; isUserIdentified: "true" | "false"; shouldDisplayAuthenticators: boolean; social: { displayInfo: boolean; }; login: {}; realm: { password: boolean; registrationAllowed: boolean; }; registrationDisabled?: boolean; url: { registrationUrl?: string; }; }; export namespace WebauthnAuthenticate { export type WebauthnAuthenticator = { credentialId: string; transports: { iconClass: string; displayNameProperties?: MessageKey[]; }; label: string; createdAt: string; }; } export type WebauthnRegister = Common & { pageId: "webauthn-register.ftl"; challenge: string; userid: string; username: string; signatureAlgorithms: string[]; rpEntityName: string; rpId: string; attestationConveyancePreference: string; authenticatorAttachment: string; requireResidentKey: string; userVerificationRequirement: string; createTimeout: number; excludeCredentialIds: string; isSetRetry?: boolean; isAppInitiatedAction?: boolean; }; export type LoginUpdatePassword = Common & { pageId: "login-update-password.ftl"; }; export type LoginIdpLinkConfirm = Common & { pageId: "login-idp-link-confirm.ftl"; idpAlias: string; }; export type LoginIdpLinkEmail = Common & { pageId: "login-idp-link-email.ftl"; brokerContext: { username: string; }; idpAlias: string; }; export type LoginPageExpired = Common & { pageId: "login-page-expired.ftl"; }; export type LoginConfigTotp = Common & { pageId: "login-config-totp.ftl"; mode?: "qr" | "manual" | undefined | null; totp: { totpSecretEncoded: string; qrUrl: string; policy: { algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512"; digits: number; lookAheadWindow: number; getAlgorithmKey: () => string; } & ( | { type: "totp"; period: number; } | { type: "hotp"; initialCounter: number; } ); supportedApplications: string[]; totpSecretQrCode: string; manualUrl: string; totpSecret: string; otpCredentials: { id: string; userLabel: string }[]; }; }; export type LogoutConfirm = Common & { pageId: "logout-confirm.ftl"; url: { logoutConfirmAction: string; }; client: { baseUrl?: string; }; logoutConfirm: { code: string; skipLink?: boolean; }; }; export type LoginUpdateProfile = Common & { pageId: "login-update-profile.ftl"; profile: UserProfile; }; export type IdpReviewUserProfile = Common & { pageId: "idp-review-user-profile.ftl"; profile: UserProfile; }; export type UpdateEmail = Common & { pageId: "update-email.ftl"; profile: UserProfile; }; export type SelectAuthenticator = Common & { pageId: "select-authenticator.ftl"; auth: { authenticationSelections: SelectAuthenticator.AuthenticationSelection[]; }; }; export namespace SelectAuthenticator { export type AuthenticationSelection = { authExecId: string; displayName: | "otp-display-name" | "password-display-name" | "auth-username-form-display-name" | "auth-username-password-form-display-name" | "webauthn-display-name" | "webauthn-passwordless-display-name"; helpText: | "otp-help-text" | "password-help-text" | "auth-username-form-help-text" | "auth-username-password-form-help-text" | "webauthn-help-text" | "webauthn-passwordless-help-text"; iconCssClass?: | "kcAuthenticatorDefaultClass" | "kcAuthenticatorPasswordClass" | "kcAuthenticatorOTPClass" | "kcAuthenticatorWebAuthnClass" | "kcAuthenticatorWebAuthnPasswordlessClass"; }; } export type DeleteCredential = Common & { pageId: "delete-credential.ftl"; credentialLabel: string; }; export type Code = Common & { pageId: "code.ftl"; code: { success: boolean; code?: string; error?: string; }; }; export type DeleteAccountConfirm = Common & { pageId: "delete-account-confirm.ftl"; triggered_from_aia: boolean; }; export type FrontchannelLogout = Common & { pageId: "frontchannel-logout.ftl"; logout: { clients: { name: string; frontChannelLogoutUrl: string; }[]; logoutRedirectUri?: string; }; }; export type LoginRecoveryAuthnCodeConfig = Common & { pageId: "login-recovery-authn-code-config.ftl"; recoveryAuthnCodesConfigBean: { generatedRecoveryAuthnCodesList: string[]; generatedRecoveryAuthnCodesAsString: string; generatedAt: number; }; }; export type LoginRecoveryAuthnCodeInput = Common & { pageId: "login-recovery-authn-code-input.ftl"; recoveryAuthnCodesInputBean: { codeNumber: number; }; }; export type LoginResetOtp = Common & { pageId: "login-reset-otp.ftl"; configuredOtpCredentials: { userOtpCredentials: { id: string; userLabel: string; }[]; selectedCredentialId: string; }; }; export type LoginX509Info = Common & { pageId: "login-x509-info.ftl"; x509: { formData: { subjectDN?: string; isUserEnabled?: boolean; username?: string; }; }; }; export type WebauthnError = Common & { pageId: "webauthn-error.ftl"; isAppInitiatedAction?: boolean; }; } export type UserProfile = { attributes: Attribute[]; attributesByName: Record; html5DataAnnotations?: Record; }; export type Attribute = { name: string; displayName?: string; required: boolean; value?: string; values?: string[]; group?: { html5DataAnnotations: Record; displayHeader?: string; name: string; displayDescription?: string; }; html5DataAnnotations?: { kcNumberFormat?: string; kcNumberUnFormat?: string; }; readOnly: boolean; validators: Validators; annotations: { inputType?: string; inputTypeSize?: `${number}` | number; inputOptionsFromValidation?: string; inputOptionLabels?: Record; inputOptionLabelsI18nPrefix?: string; inputTypeCols?: `${number}` | number; inputTypeRows?: `${number}` | number; inputTypeMaxlength?: `${number}` | number; inputHelperTextBefore?: string; inputHelperTextAfter?: string; inputTypePlaceholder?: string; inputTypePattern?: string; inputTypeMinlength?: `${number}` | number; inputTypeMax?: string; inputTypeMin?: string; inputTypeStep?: string; }; multivalued?: boolean; autocomplete?: | "on" | "off" | "name" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "email" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "url" | "photo"; }; export type Validators = Partial<{ length: Validators.DoIgnoreEmpty & Validators.Range; integer: Validators.DoIgnoreEmpty & Validators.Range; email: Validators.DoIgnoreEmpty; pattern: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string }; options: Validators.Options; multivalued: Validators.DoIgnoreEmpty & Validators.Range; // NOTE: Following are the validators for which we don't implement client side validation yet // or for which the validation can't be performed on the client side. /* double: Validators.DoIgnoreEmpty & Validators.Range; "up-immutable-attribute": {}; "up-attribute-required-by-metadata-value": {}; "up-username-has-value": {}; "up-duplicate-username": {}; "up-username-mutation": {}; "up-email-exists-as-username": {}; "up-blank-attribute-value": Validators.ErrorMessage & { "fail-on-null": boolean; }; "up-duplicate-email": {}; "local-date": Validators.DoIgnoreEmpty; "person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage; uri: Validators.DoIgnoreEmpty; "username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage; */ }>; export declare namespace Validators { export type DoIgnoreEmpty = { "ignore.empty.value"?: boolean; }; export type ErrorMessage = { "error-message"?: string; }; export type Range = { min?: `${number}` | number; max?: `${number}` | number; }; export type Options = { options: string[]; }; } { type Got = KcContext["pageId"]; type Expected = LoginThemePageId; type OnlyInGot = Exclude; type OnlyInExpected = Exclude; assert>(); assert>(); } export type PasswordPolicies = { /** The minimum length of the password */ length?: number; /** The minimum number of digits required in the password */ digits?: number; /** The minimum number of lowercase characters required in the password */ lowerCase?: number; /** The minimum number of uppercase characters required in the password */ upperCase?: number; /** The minimum number of special characters required in the password */ specialChars?: number; /** Whether the password can be the username */ notUsername?: boolean; /** Whether the password can be the email address */ notEmail?: boolean; };