UNPKG

@okta/okta-signin-widget

Version:
634 lines (560 loc) 16 kB
/* * Copyright (c) 2022-present, Okta, Inc. and/or its affiliates. All rights reserved. * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.") * * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0. * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and limitations under the License. */ import HCaptcha from '@hcaptcha/react-hcaptcha'; import { IdxAuthenticator, IdxMessage, IdxTransaction, Input, WebauthnVerificationValues, } from '@okta/okta-auth-js'; import { IdxOption } from '@okta/okta-auth-js/types/lib/idx/types/idx-js'; import { HTMLReactParserOptions } from 'html-react-parser'; import { FunctionComponent } from 'preact'; import { Ref } from 'preact/hooks'; import ReCAPTCHA from 'react-google-recaptcha'; import { IStepperContext, IWidgetContext } from './context'; import { ClickHandler } from './handlers'; import { Modify } from './jsonforms'; import { ListItem, PasswordSettings } from './password'; import { UserInfo } from './userInfo'; type GeneralDataSchemaBag = Record<string, DataSchema>; export type DataSchemaBag = GeneralDataSchemaBag & { submit: ActionOptions; fieldsToTrim: string[]; fieldsToValidate: string[]; fieldsToExclude: (data: FormBag['data']) => string[]; captchaRef?: Ref<ReCAPTCHA | HCaptcha>; }; export type FormBag = { schema: Record<string, unknown>; uischema: UISchemaLayout; data: Record<string, unknown>; // temp schema bag to handle client validation and form submission dataSchema: DataSchemaBag; }; export type WidgetMessage = Modify<IdxMessage, { class?: string; i18n?: { key: string; params?: unknown[]; }; message?: string | WidgetMessage[]; title?: string; name?: string; description?: string; links?: WidgetMessageLink[]; listStyleType?: ListStyleType, }>; export type ListStyleType = 'circle' | 'disc' | 'square' | 'decimal'; export type WidgetMessageLink = { label: string, url: string, }; export type AutoCompleteValue = 'username' | 'current-password' | 'one-time-code' | 'new-password' | 'tel-national' | 'given-name' | 'family-name' | 'email' | 'off'; export type InputModeValue = 'numeric' | 'decimal' | 'tel' | 'email' | 'url' | 'search'; export type PhoneVerificationMethodType = 'sms' | 'voice'; export type InputAttributes = { autocomplete?: AutoCompleteValue; inputmode?: InputModeValue; }; // flat params export type ActionParams = { [key: string]: string | boolean | number | null; }; export interface ActionOptions { actionParams?: ActionParams; isActionStep?: boolean; step: string; includeData?: boolean; includeImmutableData?: boolean; } /** * WebAuthNEnrollmentPayload */ export type WebAuthNEnrollmentPayload = { credentials: { /** * Represents the client data that was passed * to CredentialsContainer.create() */ clientData: string; /** * BtoA String containing authenticator data and an attestation statement * for a newly-created key pair. */ attestation: string; } }; /** * WebAuthNVerificationPayload */ export type WebAuthNVerificationPayload = { credentials: WebauthnVerificationValues }; export type WebAuthNEnrollmentHandler = (transaction: IdxTransaction) => Promise<WebAuthNEnrollmentPayload>; export type WebAuthNAuthenticationHandler = (transaction: IdxTransaction) => Promise<WebAuthNVerificationPayload>; export type ElementContentType = 'subtitle' | 'footer'; export type LanguageDirection = 'rtl' | 'ltr'; /** * @description Token value to search for in a translated string */ export type TokenSearchValue = '$1' | '$2'; /** * @description Record containing properties to use in the replacement of a translated string * @prop {string} element - Target element with which to replace a token * @prop {Object} attributes - Object containing any optional attributes that can be added to the target element * @prop {string} attributes.class - Class name to apply to the target element * @prop {string} attributes.href - href value to apply to the target element */ export type TokenReplacementValue = { element: 'span' | 'a' | 'p' | 'h1' | 'h2'; attributes?: { class?: string; href?: string; target?: AnchorTargetType; rel?: 'noopener noreferrer'; }; }; export type AnchorTargetType = '_self' | '_blank' | '_parent' | 'top'; export type TokenReplacement = Partial<Record<TokenSearchValue, TokenReplacementValue>>; export type RegistrationElementSchema = Modify<Input, { 'label-top'?: boolean; placeholder?: string; 'data-se'?: string; options?: IdxOption[] | Record<string, string>; sublabel?: string; wide?: boolean; }>; export type ConsentScope = { name: string; label: string; value: string; desc?: string; }; export interface UISchemaElement { type: string; id?: string; key?: string; // TODO: make this field required translations?: TranslationInfo[]; /** * @deprecated */ label?: string; noMargin?: boolean; focus?: boolean; ariaDescribedBy?: string; contentType?: ElementContentType; /** * Each index of the elements * array within {@link StepperLayout} corresponds to a singular view (group of elements). * This property maps to / matches the index value of the group of elements in the * {@link StepperLayout} elements array. This property allows you to determine which * view/step within the {@link StepperLayout} this element belongs to. */ viewIndex?: number; noTranslate?: boolean; /** * The purpose of this property is to customize how HTML elements are parsed * and rendered in the UI. See htmlContentParserUtils.tsx for reference. */ parserOptions?: HTMLReactParserOptions; dir?: 'ltr' | 'rtl' } /** * Parent interface for all Layout types with common properties */ interface Layout { type: string; key?: string; elements: unknown[]; options?: Record<string, unknown>; } export enum UISchemaLayoutType { HORIZONTAL = 'HorizontalLayout', VERTICAL = 'VerticalLayout', STEPPER = 'Stepper', ACCORDION = 'Accordion', } export interface UISchemaLayout extends Layout { type: UISchemaLayoutType; elements: (UISchemaElement | UISchemaLayout | StepperLayout | AccordionLayout)[]; options?: { onClick?: ClickHandler; } } export interface StepperLayout extends Layout { type: UISchemaLayoutType.STEPPER; elements: Omit<UISchemaLayout['elements'], 'StepperLayout'>; options?: { defaultStepIndex: () => number; } } export interface AccordionLayout extends Layout { type: UISchemaLayoutType.ACCORDION; elements: AccordionPanelElement[]; } export interface CustomLayout extends Layout { type: UISchemaLayoutType; elements: (CustomLayout | UISchemaElement | PickerSchema)[]; } export function isUISchemaLayoutType(type: string): boolean { return Object.values(UISchemaLayoutType).includes(type as UISchemaLayoutType); } export type PickerSchema = { tester: (schema: UISchemaElement) => boolean; mapper?: (schema: UISchemaElement) => UISchemaElement; }; export interface FieldElement extends UISchemaElement { type: 'Field'; key: string; /** * @description Determines if the app should display an asterisk next to the field label * This only applies for profile enrollment view. */ showAsterisk?: boolean; /** * @description Allows inputs to be formatted/masked by matching 'pattern' and modifying it to be 'replacement' * This mask is applied to input values in useOnChange */ inputMask?: { pattern: RegExp | string; replacement: string; }; options: { inputMeta: Input; format?: 'select' | 'radio'; attributes?: InputAttributes; type?: string; customOptions?: IdxOption[], dataSe?: string; }; } // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#attr-type export enum ButtonType { SUBMIT = 'submit', BUTTON = 'button', RESET = 'reset', } // TODO: use type instead of format in tester function export interface ButtonElement extends UISchemaElement { type: 'Button', options: ActionOptions & { type: ButtonType; variant?: 'primary' | 'floating' | 'secondary'; wide?: boolean; deviceChallengeUrl?: string; dataType?: 'cancel' | 'save'; dataSe?: string; stepToRender?: string; ariaLabel?: string; disabled?: boolean; Icon?: FunctionComponent | string; iconAlt?: string; onClick?: (widgetContext: IWidgetContext) => unknown; }; } export interface AuthenticatorButtonElement extends UISchemaElement { type: 'AuthenticatorButton'; label: string; options: ButtonElement['options'] & { key: string; ariaLabel: string; authenticator?: IdxAuthenticator; isEnroll?: boolean; isAdditionalEnroll?: boolean; ctaLabel: string; description?: string; nickname?: string; usageDescription?: string; logoUri?: string; iconName?: string; iconDescr?: string; }; } export interface AuthenticatorButtonListElement extends UISchemaElement { type: 'AuthenticatorButtonList'; options: { buttons: AuthenticatorButtonElement[]; dataSe: string; }; } export interface WebAuthNButtonElement extends UISchemaElement { type: 'WebAuthNSubmitButton'; options: { step: string; onClick: (() => Promise<WebAuthNEnrollmentPayload>) | (() => Promise<WebAuthNVerificationPayload>) submitOnLoad?: boolean; }; } export interface PIVButtonElement extends UISchemaElement { type: 'PIVButton'; } export interface LaunchAuthenticatorButtonElement extends UISchemaElement { type: 'LaunchAuthenticatorButton'; options: { step: string; deviceChallengeUrl?: string; challengeMethod?: string; }; } export interface OpenOktaVerifyFPButtonElement extends UISchemaElement { type: 'OpenOktaVerifyFPButton'; options: { step: string; href?: string; challengeMethod?: string; }; } export interface LoopbackProbeElement extends UISchemaElement { type: 'LoopbackProbe'; options: { deviceChallengePayload: { ports: string[]; domain: string; challengeRequest: string; probeTimeoutMillis?: number; }; step: string; cancelStep: string; }; } export interface ChromeDtcContainerElement extends UISchemaElement { type: 'ChromeDtcContainer'; options: { href: string; }; } export interface TitleElement extends UISchemaElement { type: 'Title'; options: { content: string; }; } export interface HeadingElement extends UISchemaElement { type: 'Heading'; options: { // https://mui.com/material-ui/api/typography/ level: 1 | 2 | 3 | 4 | 5 | 6; visualLevel: 1 | 2 | 3 | 4 | 5 | 6; content: string; dataSe?: string; }; } export interface DescriptionElement extends UISchemaElement { type: 'Description'; options: { content: string; dataSe?: string; variant?: 'body1' | 'subtitle1' | 'legend'; }; } export interface TextWithActionLinkElement extends UISchemaElement { type: 'TextWithActionLink'; options: ActionOptions & { content: string; contentClassname: string; stepToRender?: string; }; } export interface ReminderElement extends UISchemaElement { type: 'Reminder'; options: ActionOptions & { /** * The call to action text in the reminder content area */ content: string; /** * Override the default timeout before reminder appears */ contentHasHtml?: boolean; timeout?: number; buttonText?: string; contentClassname?: string; }; } export interface ListElement extends UISchemaElement { type: 'List'; options: { /** * Items to render in the list. * * **NOTE**: Only string and UISchemaElement with type * 'Button' or 'Description' * are supported. Other UISchemaElement types will * not render and print a warning to the console. */ items: (string | UISchemaLayout)[]; type: 'ul' | 'ol'; description?: string; }; } export interface PasswordRequirementsElement extends UISchemaElement { type: 'PasswordRequirements', options: { id: string; header: string; userInfo: UserInfo; settings: PasswordSettings; requirements: ListItem[]; validationDelayMs: number; } } export interface PasswordMatchesElement extends UISchemaElement { type: 'PasswordMatches', options: { validationDelayMs: number; } } export interface LinkElement extends UISchemaElement { type: 'Link'; options: ActionOptions & { label: string; href?: string; dataSe?: string; target?: AnchorTargetType; onClick?: (widgetContext?: IWidgetContext) => unknown; }; } export interface AccordionPanelElement extends UISchemaElement { type: 'AccordionPanel', options: { id: string; summary: string; content: Omit<UISchemaLayout, 'AccordionLayout'>; }; } export interface ImageWithTextElement extends UISchemaElement { type: 'ImageWithText'; options: { id: string; SVGIcon: FunctionComponent; textContent?: string; alignment?: string; }; } export interface QRCodeElement extends UISchemaElement { type: 'QRCode'; options: { data: string; }; } export interface SpinnerElement extends UISchemaElement { type: 'Spinner'; } export interface InfoboxElement extends UISchemaElement { type: 'InfoBox', options: { message: WidgetMessage | WidgetMessage[]; class: string; dataSe?: string; } } export interface StepperButtonElement extends UISchemaElement { type: 'StepperButton', label: string; options: Omit<ButtonElement['options'], 'step'> & { nextStepIndex: number | ((widgetContext: IWidgetContext) => number); } } export interface StepperLinkElement extends UISchemaElement { type: 'StepperLink', label: string; options: Omit<LinkElement['options'], 'step'> & { nextStepIndex: number | ((widgetContext: IWidgetContext) => number); } } export interface StepperNavigatorElement extends UISchemaElement { type: 'StepperNavigator', options: { callback: (stepperContext: IStepperContext) => void; } } export interface StepperRadioElement extends UISchemaElement { type: 'StepperRadio', options: { customOptions: Array<IdxOption & { key?: string; callback: (widgetContext: IWidgetContext, stepIndex: number) => void; }>, name: string; defaultValue: (widgetContext: IWidgetContext, stepIndex: number) => string | number | boolean; } } export interface RedirectElement extends UISchemaElement { type: 'Redirect', options: { url: string; }, } export interface AutoSubmitElement extends UISchemaElement { type: 'AutoSubmit', options: ActionOptions, } export interface HiddenInputElement extends UISchemaElement { type: 'HiddenInput'; options: { name: string; value: string; }; } type ValidateFunction = (data: FormBag['data']) => WidgetMessage[] | undefined; export interface DataSchema { validate?: ValidateFunction; } export interface TranslationInfo { name: string; i18nKey: string; value: string; noTranslate?: boolean; } export interface DividerElement extends UISchemaElement { type: 'Divider'; options?: { text: string; }; } export interface DuoWindowElement extends UISchemaElement { type: 'DuoWindow'; options: { title: string; host: string; signedToken: string; step: string; }; } export interface CaptchaContainerElement extends UISchemaElement { type: 'CaptchaContainer'; options: { captchaId: string; siteKey: string; type: 'HCAPTCHA' | 'RECAPTCHA_V2'; }; } export interface IdentifierContainerElement extends UISchemaElement { type: 'IdentifierContainer'; options: { identifier: string; }; }