diff --git a/.yarn/cache/@simplewebauthn-browser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip b/.yarn/cache/@simplewebauthn-browser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip deleted file mode 100644 index 34e817595..000000000 Binary files a/.yarn/cache/@simplewebauthn-browser-npm-7.0.0-93e13e1065-eb8d7e2d92.zip and /dev/null differ diff --git a/.yarn/cache/@simplewebauthn-browser-npm-7.1.0-356ca6f81a-925e266075.zip b/.yarn/cache/@simplewebauthn-browser-npm-7.1.0-356ca6f81a-925e266075.zip new file mode 100644 index 000000000..9a8da658d Binary files /dev/null and b/.yarn/cache/@simplewebauthn-browser-npm-7.1.0-356ca6f81a-925e266075.zip differ diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index dca909fdb..6cb91ffb0 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -79,7 +79,6 @@ import { DecryptedItemInterface, EncryptedItemInterface, Environment, - HistoryEntry, ItemStream, Platform, } from '@standardnotes/models' @@ -90,7 +89,7 @@ import { SNLog } from '../Log' import { ChallengeResponse, ListedClientInterface } from '../Services' import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions' import { ApplicationOptionsDefaults } from './Options/Defaults' -import { LegacySession, MapperInterface, Session, UseCaseInterface } from '@standardnotes/domain-core' +import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core' import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper' import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper' import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' @@ -102,7 +101,6 @@ import { DeleteAuthenticator } from '@Lib/Domain/UseCase/DeleteAuthenticator/Del import { ListRevisions } from '@Lib/Domain/UseCase/ListRevisions/ListRevisions' import { GetRevision } from '@Lib/Domain/UseCase/GetRevision/GetRevision' import { DeleteRevision } from '@Lib/Domain/UseCase/DeleteRevision/DeleteRevision' -import { RevisionMetadata } from '@Lib/Domain/Revision/RevisionMetadata' import { GetAuthenticatorAuthenticationResponse } from '@Lib/Domain/UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse' /** How often to automatically sync, in milliseconds */ @@ -266,39 +264,39 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.subscriptionManager } - get signInWithRecoveryCodes(): UseCaseInterface { + get signInWithRecoveryCodes(): SignInWithRecoveryCodes { return this._signInWithRecoveryCodes } - get getRecoveryCodes(): UseCaseInterface { + get getRecoveryCodes(): GetRecoveryCodes { return this._getRecoveryCodes } - get addAuthenticator(): UseCaseInterface { + get addAuthenticator(): AddAuthenticator { return this._addAuthenticator } - get listAuthenticators(): UseCaseInterface> { + get listAuthenticators(): ListAuthenticators { return this._listAuthenticators } - get deleteAuthenticator(): UseCaseInterface { + get deleteAuthenticator(): DeleteAuthenticator { return this._deleteAuthenticator } - get getAuthenticatorAuthenticationResponse(): UseCaseInterface> { + get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse { return this._getAuthenticatorAuthenticationResponse } - get listRevisions(): UseCaseInterface> { + get listRevisions(): ListRevisions { return this._listRevisions } - get getRevision(): UseCaseInterface { + get getRevision(): GetRevision { return this._getRevision } - get deleteRevision(): UseCaseInterface { + get deleteRevision(): DeleteRevision { return this._deleteRevision } diff --git a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts index 4399a41e6..06c72a5a4 100644 --- a/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts +++ b/packages/snjs/lib/Domain/UseCase/UseCaseContainerInterface.ts @@ -1,16 +1,21 @@ -import { HistoryEntry } from '@standardnotes/models' -import { UseCaseInterface } from '@standardnotes/domain-core' - -import { RevisionMetadata } from '../Revision/RevisionMetadata' +import { AddAuthenticator } from './AddAuthenticator/AddAuthenticator' +import { GetRecoveryCodes } from './GetRecoveryCodes/GetRecoveryCodes' +import { SignInWithRecoveryCodes } from './SignInWithRecoveryCodes/SignInWithRecoveryCodes' +import { ListAuthenticators } from './ListAuthenticators/ListAuthenticators' +import { DeleteAuthenticator } from './DeleteAuthenticator/DeleteAuthenticator' +import { GetAuthenticatorAuthenticationResponse } from './GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse' +import { ListRevisions } from './ListRevisions/ListRevisions' +import { GetRevision } from './GetRevision/GetRevision' +import { DeleteRevision } from './DeleteRevision/DeleteRevision' export interface UseCaseContainerInterface { - get signInWithRecoveryCodes(): UseCaseInterface - get getRecoveryCodes(): UseCaseInterface - get addAuthenticator(): UseCaseInterface - get listAuthenticators(): UseCaseInterface> - get deleteAuthenticator(): UseCaseInterface - get getAuthenticatorAuthenticationResponse(): UseCaseInterface> - get listRevisions(): UseCaseInterface> - get getRevision(): UseCaseInterface - get deleteRevision(): UseCaseInterface + get signInWithRecoveryCodes(): SignInWithRecoveryCodes + get getRecoveryCodes(): GetRecoveryCodes + get addAuthenticator(): AddAuthenticator + get listAuthenticators(): ListAuthenticators + get deleteAuthenticator(): DeleteAuthenticator + get getAuthenticatorAuthenticationResponse(): GetAuthenticatorAuthenticationResponse + get listRevisions(): ListRevisions + get getRevision(): GetRevision + get deleteRevision(): DeleteRevision } diff --git a/packages/snjs/lib/Domain/index.ts b/packages/snjs/lib/Domain/index.ts index a5ff50e6f..d1fe689aa 100644 --- a/packages/snjs/lib/Domain/index.ts +++ b/packages/snjs/lib/Domain/index.ts @@ -1,2 +1,18 @@ export * from './Revision/Revision' export * from './Revision/RevisionMetadata' +export * from './UseCase/AddAuthenticator/AddAuthenticator' +export * from './UseCase/AddAuthenticator/AddAuthenticatorDTO' +export * from './UseCase/DeleteAuthenticator/DeleteAuthenticator' +export * from './UseCase/DeleteAuthenticator/DeleteAuthenticatorDTO' +export * from './UseCase/DeleteRevision/DeleteRevision' +export * from './UseCase/DeleteRevision/DeleteRevisionDTO' +export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponse' +export * from './UseCase/GetAuthenticatorAuthenticationResponse/GetAuthenticatorAuthenticationResponseDTO' +export * from './UseCase/GetRecoveryCodes/GetRecoveryCodes' +export * from './UseCase/GetRevision/GetRevision' +export * from './UseCase/GetRevision/GetRevisionDTO' +export * from './UseCase/ListAuthenticators/ListAuthenticators' +export * from './UseCase/ListRevisions/ListRevisions' +export * from './UseCase/ListRevisions/ListRevisionsDTO' +export * from './UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes' +export * from './UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodesDTO' diff --git a/packages/ui-services/src/Route/Params/AppViewRouteParams.ts b/packages/ui-services/src/Route/Params/AppViewRouteParams.ts new file mode 100644 index 000000000..b191b1148 --- /dev/null +++ b/packages/ui-services/src/Route/Params/AppViewRouteParams.ts @@ -0,0 +1,3 @@ +export const ValidAppViewRoutes = ['u2f'] as const + +export type AppViewRouteParam = typeof ValidAppViewRoutes[number] diff --git a/packages/ui-services/src/Route/RootQueryParam.ts b/packages/ui-services/src/Route/RootQueryParam.ts index 216029a11..fc0943f53 100644 --- a/packages/ui-services/src/Route/RootQueryParam.ts +++ b/packages/ui-services/src/Route/RootQueryParam.ts @@ -4,4 +4,5 @@ export enum RootQueryParam { DemoToken = 'demo-token', AcceptSubscriptionInvite = 'accept-subscription-invite', UserRequest = 'user-request', + AppViewRoute = 'route', } diff --git a/packages/ui-services/src/Route/RouteParser.ts b/packages/ui-services/src/Route/RouteParser.ts index 83cc4fb78..ade6df172 100644 --- a/packages/ui-services/src/Route/RouteParser.ts +++ b/packages/ui-services/src/Route/RouteParser.ts @@ -1,5 +1,6 @@ import { UserRequestType } from '@standardnotes/common' import { PreferenceId } from './../Preferences/PreferenceId' +import { AppViewRouteParam, ValidAppViewRoutes } from './Params/AppViewRouteParams' import { DemoParams } from './Params/DemoParams' import { OnboardingParams } from './Params/OnboardingParams' import { PurchaseParams } from './Params/PurchaseParams' @@ -78,6 +79,18 @@ export class RouteParser implements RouteParserInterface { } } + get appViewRouteParam(): AppViewRouteParam | undefined { + this.checkForProperRouteType(RouteType.AppViewRoute) + + const appViewRoute = this.searchParams.get(RootQueryParam.AppViewRoute) as AppViewRouteParam + + if (!ValidAppViewRoutes.includes(appViewRoute)) { + return + } + + return this.searchParams.get(RootQueryParam.AppViewRoute) as AppViewRouteParam + } + private checkForProperRouteType(type: RouteType): void { if (this.parsedType !== type) { throw new Error('Accessing invalid params') @@ -99,6 +112,7 @@ export class RouteParser implements RouteParserInterface { [RootQueryParam.DemoToken, RouteType.Demo], [RootQueryParam.AcceptSubscriptionInvite, RouteType.AcceptSubscriptionInvite], [RootQueryParam.UserRequest, RouteType.UserRequest], + [RootQueryParam.AppViewRoute, RouteType.AppViewRoute], ]) for (const rootQueryParam of rootQueryParametersMap.keys()) { diff --git a/packages/ui-services/src/Route/RouteParserInterface.ts b/packages/ui-services/src/Route/RouteParserInterface.ts index ddc01ac81..61bf3e3a8 100644 --- a/packages/ui-services/src/Route/RouteParserInterface.ts +++ b/packages/ui-services/src/Route/RouteParserInterface.ts @@ -1,3 +1,4 @@ +import { AppViewRouteParam } from './Params/AppViewRouteParams' import { DemoParams } from './Params/DemoParams' import { OnboardingParams } from './Params/OnboardingParams' import { PurchaseParams } from './Params/PurchaseParams' @@ -13,5 +14,6 @@ export interface RouteParserInterface { get onboardingParams(): OnboardingParams get subscriptionInviteParams(): SubscriptionInviteParams get userRequestParams(): UserRequestParams + get appViewRouteParam(): AppViewRouteParam | undefined get type(): RouteType } diff --git a/packages/ui-services/src/Route/RouteType.ts b/packages/ui-services/src/Route/RouteType.ts index 5b2fe79a3..e577f14d6 100644 --- a/packages/ui-services/src/Route/RouteType.ts +++ b/packages/ui-services/src/Route/RouteType.ts @@ -5,5 +5,6 @@ export enum RouteType { AcceptSubscriptionInvite = 'accept-subscription-invite', UserRequest = 'user-request', Demo = 'demo', + AppViewRoute = 'route', None = 'none', } diff --git a/packages/web/package.json b/packages/web/package.json index 88e9b8567..cc3df8e6f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -38,6 +38,7 @@ "@reach/listbox": "^0.18.0", "@reach/tooltip": "^0.18.0", "@reach/visually-hidden": "^0.18.0", + "@simplewebauthn/browser": "^7.1.0", "@standardnotes/authenticator": "^2.3.9", "@standardnotes/autobiography-theme": "^1.2.7", "@standardnotes/bold-editor": "^1.6.4", diff --git a/packages/web/src/javascripts/App.tsx b/packages/web/src/javascripts/App.tsx index 99bd8cd10..6044f82ec 100644 --- a/packages/web/src/javascripts/App.tsx +++ b/packages/web/src/javascripts/App.tsx @@ -36,6 +36,8 @@ import { WebApplication } from './Application/Application' import { createRoot, Root } from 'react-dom/client' import { ElementIds } from './Constants/ElementIDs' import { setDefaultMonospaceFont } from './setDefaultMonospaceFont' +import { RouteParser, RouteType } from '@standardnotes/ui-services' +import U2FAuthIframe from './Components/U2FAuthIframe/U2FAuthIframe' let keyCount = 0 const getKey = () => { @@ -71,6 +73,13 @@ const startApplication: StartApplication = async function startApplication( setDefaultMonospaceFont(device.platform) + const route = new RouteParser(window.location.href) + + if (route.type === RouteType.AppViewRoute && route.appViewRouteParam === 'u2f') { + root.render() + return + } + root.render( | null>(null) const [error, setError] = useState('') + if (!application.isFullU2FClient) { + return ( + { + onValueChange(response, prompt) + }} + /> + ) + } + return (
{error &&
{error}
} @@ -27,18 +40,18 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData onClick={async () => { if (!contextData || contextData.username === undefined) { setError('No username provided') - return } const authenticatorResponseOrError = await application.getAuthenticatorAuthenticationResponse.execute({ - username: contextData.username, + username: contextData.username as string, }) + if (authenticatorResponseOrError.isFailed()) { setError(authenticatorResponseOrError.getError()) - return } + const authenticatorResponse = authenticatorResponseOrError.getValue() setAuthenticatorResponse(authenticatorResponse) diff --git a/packages/web/src/javascripts/Components/ChallengeModal/U2FPromptIframeContainer.tsx b/packages/web/src/javascripts/Components/ChallengeModal/U2FPromptIframeContainer.tsx new file mode 100644 index 000000000..549dd2534 --- /dev/null +++ b/packages/web/src/javascripts/Components/ChallengeModal/U2FPromptIframeContainer.tsx @@ -0,0 +1,66 @@ +import { log, LoggingDomain } from '@/Logging' +import { isDev } from '@/Utils' +import { useEffect, useRef } from 'react' + +type Props = { + contextData?: Record + onResponse: (response: string) => void + apiHost: string +} + +const U2F_IFRAME_ORIGIN = isDev ? 'http://localhost:3001/?route=u2f' : 'https://app.standardnotes.com/?route=u2f' + +const U2FPromptIframeContainer = ({ contextData, onResponse, apiHost }: Props) => { + const iframeRef = useRef(null) + + useEffect(() => { + const messageHandler = (event: MessageEvent) => { + log(LoggingDomain.U2F, 'Native client received message', event) + const eventDoesNotComeFromU2FIFrame = event.origin !== new URL(U2F_IFRAME_ORIGIN).origin + if (eventDoesNotComeFromU2FIFrame) { + log( + LoggingDomain.U2F, + 'Not sending data to U2F iframe; origin does not match', + event.origin, + new URL(U2F_IFRAME_ORIGIN).origin, + ) + return + } + + if (event.data.mountedAuthView) { + if (iframeRef.current?.contentWindow) { + log(LoggingDomain.U2F, 'Sending contextData to U2F iframe', contextData) + iframeRef.current.contentWindow.postMessage( + { username: (contextData as Record).username, apiHost }, + U2F_IFRAME_ORIGIN, + ) + } + return + } + + if (event.data.assertionResponse) { + log(LoggingDomain.U2F, 'Received assertion response from U2F iframe', event.data.assertionResponse) + onResponse(event.data.assertionResponse) + } + } + + window.addEventListener('message', messageHandler) + + return () => { + window.removeEventListener('message', messageHandler) + } + }, [contextData, onResponse, apiHost]) + + return ( +