Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
35b21bd
IS-11327 LWA: WebAuthn ceremony errors as HAAPI step AppErrors
aleixsuau May 20, 2026
cb48e2b
IS-11327 Rename metadata.messages to metadata.viewData; extract getHa…
aleixsuau May 20, 2026
65476cc
IS-11327: reduce doc useless verbosity
aleixsuau May 20, 2026
16cc0cb
IS-11327: refactor webauthn error handling
aleixsuau May 20, 2026
58574ef
IS-11327 Move shared ClientOperationResult to operations/typings.ts
aleixsuau May 21, 2026
2818521
IS-11368 LWA: forward HaapiBaseFormField.required to rendered field i…
aleixsuau May 25, 2026
57743ad
IS-11368 Default createMockFormAction fields to required: true.
aleixsuau May 25, 2026
b7b8fad
IS-11368 Default required attr to true when field.required is omitted.
aleixsuau May 26, 2026
20d5905
IS-11368: remove default field required
aleixsuau May 26, 2026
6fec68f
IS-11345 Display the configured logo in the LWA.
aleixsuau May 26, 2026
01600a7
IS-11318 LWA: rework page symbols data in bootstrap configuration
luisgoncalves May 26, 2026
a944e6a
IS-5161: remove duplicated wells
aleixsuau May 27, 2026
1d13d9b
Merge branch 'integration/IS-5161/login-web-app' into feature/IS-1136…
aleixsuau May 27, 2026
90f61bd
Merge pull request #205 from curityio/feature/IS-5161/IS-11318-update…
luisgoncalves May 27, 2026
f4f9c2b
IS-11345: remove non-used deps
aleixsuau May 27, 2026
a4c4f2a
Merge pull request #200 from curityio/feature/IS-11368-required-attr-…
aleixsuau May 27, 2026
3a4d2d5
IS-11318 LWA: fix propagation of bootstrap configuration in the dev s…
luisgoncalves May 27, 2026
7224b9f
IS-5161 Remove header, move app-layout side outside well
May 27, 2026
4eec80c
IS-5161 Logo spacing tweaks
May 27, 2026
9fc10cb
IS-11345: refactor to use config provider
aleixsuau May 27, 2026
cd3eb5a
IS-11345: logo tests
aleixsuau May 27, 2026
78ff304
Merge pull request #204 from curityio/fix/integration/IS-5161/login-w…
aleixsuau May 27, 2026
c1dec04
IS-11327 Rethrow non-DOMException from getWebAuthnErrorType
aleixsuau May 28, 2026
7dbbaf6
IS-11327 Type WebAuthn runner mocks against their real signatures
aleixsuau May 28, 2026
f21525f
IS-11327: remove useless comment
aleixsuau May 28, 2026
66bea62
IS-11327: return failed error for null credentials
aleixsuau May 28, 2026
a1cdd99
IS-11327: remove viewData error messages
aleixsuau May 28, 2026
c582a9d
IS-5161 Wrap HaapiStepperStepUI in App.tsx with HaapiStepperErrorNoti…
aleixsuau May 26, 2026
69660e4
Merge pull request #203 from curityio/feature/IS-5161/wrap-haapi-step…
aleixsuau May 28, 2026
da52027
Merge pull request #206 from curityio/feature/IS-5161/IS-11318-dev-lo…
luisgoncalves May 28, 2026
3db12cb
IS-11327 LWA: WebAuthn ceremony errors as HAAPI step AppErrors
aleixsuau May 20, 2026
78d2938
IS-11327 Rename metadata.messages to metadata.viewData; extract getHa…
aleixsuau May 20, 2026
e1211c7
IS-11327: reduce doc useless verbosity
aleixsuau May 20, 2026
bd01831
IS-11327: refactor webauthn error handling
aleixsuau May 20, 2026
836c0d7
IS-11327 Move shared ClientOperationResult to operations/typings.ts
aleixsuau May 21, 2026
15a4654
IS-11327 Rethrow non-DOMException from getWebAuthnErrorType
aleixsuau May 28, 2026
7b76888
IS-11327 Type WebAuthn runner mocks against their real signatures
aleixsuau May 28, 2026
c65a98f
IS-11327: remove useless comment
aleixsuau May 28, 2026
e7eacaf
IS-11327: return failed error for null credentials
aleixsuau May 28, 2026
1784e8c
IS-11327: remove viewData error messages
aleixsuau May 28, 2026
e6b4e00
Merge branch 'feature/IS-11327/webauthn-error-handling' of github.com…
aleixsuau May 29, 2026
ebd5201
IS-11327 Move getHaapiStepperError to operations/helpers.ts
aleixsuau May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#*
* Copyright (C) 2026 Curity AB. All rights reserved.
*
* The contents of this file are the property of Curity AB.
* You may not copy or use this file, in either source code
* or executable form, except in compliance with terms
* set by Curity AB.
*
* For further information, please contact Curity AB.
*#

{
logo: {
path: '$!logo_path',
isInsideWell: #if ($logo_inside) true #else false #end,
},
#if ($show_symbol)
#macro(symbolPath $jsonKey $pathVariable)
#if($pathVariable)'$jsonKey': '$!pathVariable',#end
#end

pageSymbols: {
## Map of plugin implementation type to symbol path.
## Used as a fallback if an exact match for the page symbol is not found in 'views'.
plugins: {
#* Authenticators *#
#symbolPath("bankid" $page_symbol_authenticate_bankid)
#symbolPath("duo" $page_symbol_authenticate_pair_device)
#symbolPath("email" $page_symbol_authenticate_email)
#symbolPath("html-form" $page_symbol_authenticate_htmlform)
#symbolPath("openid-wallet" $page_symbol_openid_wallet)
#symbolPath("passkeys" $page_symbol_authenticate_passkeys)
#symbolPath("sms" $page_symbol_authenticate_sms)
#symbolPath("totp" $page_symbol_authenticate_totp)
#symbolPath("webauthn" $page_symbol_authenticate_webauthn)

#* Authentication ACtions *#
#symbolPath("opt-in-mfa" $page_symbol_authenticate_opt_in_mfa)
#symbolPath("require-active-account" $page_symbol_authenticate_htmlform)
#symbolPath("reset-password" $page_symbol_authenticate_htmlform)
#symbolPath("signup" $page_symbol_authenticate_htmlform)

#* Consentors *#
#symbolPath("bankid-signing-consentor" $page_symbol_authenticate_bankid)
},
## Map of view/template name to symbol path.
views: {
## Nothing to add for now
},
default: '$!page_symbol',
}
#end
}
33 changes: 1 addition & 32 deletions src/identity-server/templates/core/views/api-driven-ui/index.vm
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,7 @@
clientId: '$_apiDrivenClientInfo.clientId',
tokenEndpoint: '$_apiDrivenClientInfo.tokenEndpoint',
},
theme: {
logo: {
path: '$!logo_path',
isInsideWell: #if ($logo_inside) true #else false #end,
},
#if ($show_symbol)
pageSymbols: {
## Free form mapping of page symbols names to full paths.
#macro(pageSymbolPath $jsonKey $variable)
#if($variable)$jsonKey: '$!variable',#end
#end
paths: {
#pageSymbolPath("authenticate_desktop" $page_symbol_authenticate_desktop)
#pageSymbolPath("authenticate_mobile" $page_symbol_authenticate_mobile)
#pageSymbolPath("authenticate_email" $page_symbol_authenticate_email)
#pageSymbolPath("authenticate_card" $page_symbol_authenticate_card)
#pageSymbolPath("authenticate_sms" $page_symbol_authenticate_sms)
#pageSymbolPath("authenticate_pair_device" $page_symbol_authenticate_pair_device)
#pageSymbolPath("authenticate_htmlform" $page_symbol_authenticate_htmlform)
#pageSymbolPath("authenticate_totp" $page_symbol_authenticate_totp)
#pageSymbolPath("authenticate_bankid" $page_symbol_authenticate_bankid)
#pageSymbolPath("authenticate_webauthn" $page_symbol_authenticate_webauthn)
#pageSymbolPath("authenticate_opt_in_mfa" $page_symbol_authenticate_opt_in_mfa)
#pageSymbolPath("authenticate_passkeys" $page_symbol_authenticate_passkeys)
#pageSymbolPath("authenticate_openid_wallet" $page_symbol_openid_wallet)
#pageSymbolPath("error_generic" $page_symbol_error_generic)
},
## Added in case the UI needs to build paths for other page symbols.
basePath: '$!page_symbol_path',
},
#end
},
theme: #parse("fragments/api-driven-ui/theme")
};
</script>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion src/login-web-app/configure-idsvr-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ API_DRIVEN_UI_DIR=$OVERRIDES_DIR/views/api-driven-ui

mkdir -p $API_DRIVEN_UI_DIR

cp ./loader-dev.vm.html $API_DRIVEN_UI_DIR/index.vm
cp ./loader-dev.vm $API_DRIVEN_UI_DIR/index.vm
8 changes: 1 addition & 7 deletions src/login-web-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,7 @@
}

// Set the configuration object expected by the application
window.__CONFIG__ = {
initialUrl: arg('initialUrl'),
haapi: {
clientId: arg('haapi_clientId'),
tokenEndpoint: arg('haapi_tokenEndpoint'),
},
};
window.__CONFIG__ = JSON.parse(arg('configuration'));

// Change the current URL to match the original request to Identity Server, so that it looks like the
// application was loaded directly by the server's template
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,27 @@
</head>
<body>
<script>
const configuration = {
initialUrl: '$_apiDrivenInitialUrl',
haapi: {
clientId: '$_apiDrivenClientInfo.clientId',
tokenEndpoint: '$_apiDrivenClientInfo.tokenEndpoint',
},
theme: {
logo: {
path: '$!logo_path',
isInsideWell: #if ($logo_inside) true #else false #end,
},
#if ($show_symbol)
pageSymbols: {
default: '$!page_symbol',
}
#end
},
};
const query = new URLSearchParams({
originalUrl: window.location.href,
initialUrl: '$_apiDrivenInitialUrl',
haapi_clientId: '$_apiDrivenClientInfo.clientId',
haapi_tokenEndpoint: '$_apiDrivenClientInfo.tokenEndpoint',
configuration: JSON.stringify(configuration),
});

const redirect = new URL('/', window.location.href);
Expand Down
20 changes: 13 additions & 7 deletions src/login-web-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,24 @@
*/

import { Layout } from './shared/ui/Layout';
import { AppConfig } from './shared/feature/app-config/AppConfig';
import { ErrorBoundary } from './shared/feature/error-handling/ErrorBoundary';
import { HaapiStepperStepUI } from './haapi-stepper/feature/steps/HaapiStepperStepUI';
import { HaapiStepper } from './haapi-stepper/feature/stepper/HaapiStepper';
import { HaapiStepperErrorNotifier } from './haapi-stepper/feature/stepper/HaapiStepperErrorNotifier';

export function App() {
return (
<ErrorBoundary>
<HaapiStepper>
<Layout>
<HaapiStepperStepUI />
</Layout>
</HaapiStepper>
</ErrorBoundary>
<AppConfig>
<ErrorBoundary>
<HaapiStepper>
<Layout>
<HaapiStepperErrorNotifier>
<HaapiStepperStepUI />
</HaapiStepperErrorNotifier>
</Layout>
</HaapiStepper>
</ErrorBoundary>
</AppConfig>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import { HaapiConfiguration } from '@curity/identityserver-haapi-web-driver';
export interface BootstrapConfiguration {
initialUrl: string;
haapi: HaapiConfiguration;
theme: {
logo: {
path: string;
isInsideWell: boolean;
};
};
}

// @ts-expect-error window.__CONFIG__ is not declared on the Window type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,30 +184,27 @@ export interface HaapiExternalBrowserArguments {
href: string;
}

/**
* Client operation WebAuthn registration action
*/
export interface HaapiWebAuthnRegistrationClientOperationAction extends HaapiClientOperationAction {
model: HaapiWebAuthnRegistrationClientOperationModel;
}
export type HaapiWebAuthnRegistrationClientOperationAction =
| HaapiWebAuthnPasskeysRegistrationAction
| HaapiWebAuthnAnyDeviceRegistrationAction;

export interface HaapiWebAuthnRegistrationClientOperationModel extends HaapiBaseClientOperationModel {
name: HAAPI_ACTION_CLIENT_OPERATIONS.WEBAUTHN_REGISTRATION;
arguments: HaapiWebAuthnRegistrationArgs;
continueActions: [HaapiFormAction];
}

export type HaapiWebAuthnPasskeysRegistrationAction = Omit<HaapiWebAuthnRegistrationClientOperationAction, 'model'> & {
export interface HaapiWebAuthnPasskeysRegistrationAction extends HaapiClientOperationAction {
model: Omit<HaapiWebAuthnRegistrationClientOperationModel, 'arguments'> & {
arguments: HaapiWebAuthnPasskeysArgs;
};
};
}

export type HaapiWebAuthnAnyDeviceRegistrationAction = Omit<HaapiWebAuthnRegistrationClientOperationAction, 'model'> & {
export interface HaapiWebAuthnAnyDeviceRegistrationAction extends HaapiClientOperationAction {
model: Omit<HaapiWebAuthnRegistrationClientOperationModel, 'arguments'> & {
arguments: HaapiWebAuthnAnyDeviceArgs;
};
};
}

/**
* Discriminated union of `webauthn-registration` action arguments.
Expand All @@ -223,10 +220,15 @@ export interface HaapiWebAuthnPasskeysArgs {
credentialCreationOptions: HaapiPublicKeyCredentialCreationOptions;
}

export interface HaapiWebAuthnAnyDeviceArgs {
platformCredentialCreationOptions?: HaapiPublicKeyCredentialCreationOptions;
crossPlatformCredentialCreationOptions?: HaapiPublicKeyCredentialCreationOptions;
}
export type HaapiWebAuthnAnyDeviceArgs =
| {
platformCredentialCreationOptions: HaapiPublicKeyCredentialCreationOptions;
crossPlatformCredentialCreationOptions?: HaapiPublicKeyCredentialCreationOptions;
}
| {
platformCredentialCreationOptions?: undefined;
crossPlatformCredentialCreationOptions: HaapiPublicKeyCredentialCreationOptions;
};

/**
* Continue-action payload key for the `webauthn-registration` operation. The value matches the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ import {
HAAPI_ACTION_TYPES,
HaapiAction,
} from '../../../../data-access/types/haapi-action.types';
import { HaapiLink } from '../../../../data-access/types/haapi-step.types';
import { HaapiLink, HaapiStep } from '../../../../data-access/types/haapi-step.types';
import { RefObject } from 'react';
import { HaapiStepperAction, HaapiStepperLink } from '../../../stepper/haapi-stepper.types';
import { HaapiFetchFormAction } from '../../../../data-access/types/haapi-fetch.types';
import { isBankIdClientOperation, runBankIdAuthentication } from './bankid';
import { isExternalBrowserFlowClientOperation, runExternalBrowserFlow } from './external-browser-flow';
import {
Expand All @@ -26,51 +25,37 @@ import {
runWebAuthnAuthentication,
runWebAuthnRegistration,
} from './webauthn';
import { ClientOperationResult } from './typings';

export function isClientOperation(
action: HaapiAction | HaapiStepperAction | HaapiLink | HaapiStepperLink
): action is HaapiClientOperationAction {
return 'template' in action && action.template === HAAPI_ACTION_TYPES.CLIENT_OPERATION;
}

/**
* Performs a client operation, returning a continuation action and values if further action is required, or null if
* no further action is required or if the operation was aborted.
*/
export async function performClientOperation(
action: HaapiClientOperationAction,
pendingOperation: RefObject<AbortController | NodeJS.Timeout | null>
): Promise<HaapiFetchFormAction | null> {
pendingOperation: RefObject<AbortController | NodeJS.Timeout | null>,
currentStep: HaapiStep | null
): Promise<ClientOperationResult> {
const abortController = new AbortController();
pendingOperation.current = abortController;
const signal = abortController.signal;

try {
if (isExternalBrowserFlowClientOperation(action)) {
return await runExternalBrowserFlow(action, 2500, abortController.signal);
}

if (isWebAuthnRegistrationClientOperation(action)) {
return await runWebAuthnRegistration(action, abortController.signal);
}
if (isWebAuthnRegistrationClientOperation(action)) {
return runWebAuthnRegistration(action, signal, currentStep);
}

if (isWebAuthnAuthenticationClientOperation(action)) {
return await runWebAuthnAuthentication(action, abortController.signal);
}
if (isWebAuthnAuthenticationClientOperation(action)) {
return runWebAuthnAuthentication(action, signal, currentStep);
}

if (isBankIdClientOperation(action)) {
return await runBankIdAuthentication(action);
}
} catch (err) {
/**
* If the operation was aborted by the caller, convert to null - i.e. no further action - instead of error
* Note that the cancellation is triggered by code on this file and a 'reason' is not provided, so we can rely on
* the error being the default AbortError.
*/
if (abortController.signal.aborted && err instanceof DOMException && err.name === 'AbortError') {
return null;
Comment thread
aleixsuau marked this conversation as resolved.
}
if (isExternalBrowserFlowClientOperation(action)) {
return runExternalBrowserFlow(action, 2500, signal).then(clientOperationData => ({ clientOperationData }));
Comment thread
aleixsuau marked this conversation as resolved.
}

throw err;
if (isBankIdClientOperation(action)) {
return runBankIdAuthentication(action).then(clientOperationData => ({ clientOperationData }));
Comment thread
aleixsuau marked this conversation as resolved.
}

throw new Error(`Unsupported client operation: ${action.model.name}`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2026 Curity AB. All rights reserved.
*
* The contents of this file are the property of Curity AB.
* You may not copy or use this file, in either source code
* or executable form, except in compliance with terms
* set by Curity AB.
*
* For further information, please contact Curity AB.
*/

import { HAAPI_PROBLEM_STEPS, HaapiUserMessage } from '../../../../data-access/types/haapi-step.types';
import { HaapiStepperError } from '../../../stepper/haapi-stepper.types';
import { formatErrorStepData } from '../../../stepper/data-formatters/problem-step';

/**
* Synthesises a {@link HaapiStepperError} for a client-operation failure (IS-11327).
*
* Client-operation failures (WebAuthn ceremony cancel / timeout / parse error / unsupported
* API today; BankID / EBF on the same pattern when their per-operation error handling lands)
* happen on the client and aren't part of the HAAPI response, so the stepper has no native
* category for them. We treat them as `AppError`-class problems of the current step — building
* a `HaapiUnexpectedProblemStep` via {@link formatErrorStepData} — so they surface via
* `useHaapiStepper().error.app` like any server-driven problem and consumers handle them
* through the same channel (e.g. `HaapiStepperErrorNotifier`).
*/
export function getHaapiStepperError(messageText: string | undefined): HaapiStepperError {
const messages: HaapiUserMessage[] = messageText ? [{ text: messageText }] : [];
return formatErrorStepData({
type: HAAPI_PROBLEM_STEPS.UNEXPECTED,
messages,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (C) 2025 Curity AB. All rights reserved.
*
* The contents of this file are the property of Curity AB.
* You may not copy or use this file, in either source code
* or executable form, except in compliance with terms
* set by Curity AB.
*
* For further information, please contact Curity AB.
*/

import { HaapiFetchFormAction } from '../../../../data-access';
import { HaapiStepperError } from '../../../stepper/haapi-stepper.types';

/**
* Discriminated-union return shape shared by all client-operation runners (WebAuthn,
* external-browser-flow, BankID — as each ports onto this pattern per IS-11327).
*
* Runners always resolve. Success carries the continuation form action + payload; failure
* carries a synthesised {@link HaapiStepperError} which `performClientOperation` forwards to the
* stepper, which routes it through `setError` → `useHaapiStepper().error.app`. Programming
* bugs / unexpected runtime errors are not represented here — those still throw and escape to
* the React error boundary.
*/
export type ClientOperationResult =
| { clientOperationData: HaapiFetchFormAction; clientOperationError?: undefined }
| { clientOperationData?: undefined; clientOperationError: HaapiStepperError };
Loading
Loading