mirror of
https://github.com/standardnotes/app
synced 2026-10-16 00:13:46 -04:00
fix: Fixes web clipper hardware key auth for Firefox (#3040)
* fix: Fixes web clipper hardware key auth for Firefox * chore: fix ts errors
This commit is contained in:
@@ -5,6 +5,20 @@ import packageInfo from '../../package.json'
|
||||
|
||||
const isFirefox = navigator.userAgent.indexOf('Firefox/') !== -1
|
||||
|
||||
const openClipperAuthPanel = async (pane: 'sign-in' | 'register') => {
|
||||
const popupPath = await browserAction.getPopup({})
|
||||
const url = new URL(popupPath, runtime.getURL('/'))
|
||||
url.searchParams.set('route', 'extension')
|
||||
url.searchParams.set('pane', pane)
|
||||
|
||||
await windows.create({
|
||||
type: 'detached_panel',
|
||||
url: url.toString(),
|
||||
width: 350,
|
||||
height: 300,
|
||||
})
|
||||
}
|
||||
|
||||
const openPopupAndClipSelection = async (payload: ClipPayload) => {
|
||||
await storage.local.set({ clip: payload })
|
||||
|
||||
@@ -33,6 +47,8 @@ runtime.onMessage.addListener(async (message: RuntimeMessage) => {
|
||||
return
|
||||
}
|
||||
void openPopupAndClipSelection(message.payload)
|
||||
} else if (message.type === RuntimeMessageTypes.OpenClipperAuthPanel) {
|
||||
await openClipperAuthPanel(message.pane)
|
||||
} else if (message.type === RuntimeMessageTypes.CaptureVisibleTab) {
|
||||
return await tabs.captureVisibleTab(undefined, {
|
||||
format: 'png',
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{9f917dfe-accd-4d3a-9685-33c3ac0ca643}",
|
||||
"strict_min_version": "48.0"
|
||||
"strict_min_version": "150.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "{9f917dfe-accd-4d3a-9685-33c3ac0ca643}",
|
||||
"strict_min_version": "48.0"
|
||||
"strict_min_version": "150.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ export const RuntimeMessageTypes = {
|
||||
ToggleScreenshotMode: 'toggle-screenshot-mode',
|
||||
CaptureVisibleTab: 'capture-visible-tab',
|
||||
RunHttpRequest: 'run-http-request',
|
||||
OpenClipperAuthPanel: 'open-clipper-auth-panel',
|
||||
} as const
|
||||
|
||||
export type RuntimeMessageType = (typeof RuntimeMessageTypes)[keyof typeof RuntimeMessageTypes]
|
||||
@@ -33,6 +34,7 @@ export type RuntimeMessageReturnTypes = {
|
||||
[RuntimeMessageTypes.StartNodeSelection]: void
|
||||
[RuntimeMessageTypes.ToggleScreenshotMode]: void
|
||||
[RuntimeMessageTypes.RunHttpRequest]: void
|
||||
[RuntimeMessageTypes.OpenClipperAuthPanel]: void
|
||||
}
|
||||
|
||||
export type RuntimeMessage =
|
||||
@@ -48,11 +50,16 @@ export type RuntimeMessage =
|
||||
type: typeof RuntimeMessageTypes.ToggleScreenshotMode
|
||||
enabled: boolean
|
||||
}
|
||||
| {
|
||||
type: typeof RuntimeMessageTypes.OpenClipperAuthPanel
|
||||
pane: 'sign-in' | 'register'
|
||||
}
|
||||
| {
|
||||
type: Exclude<
|
||||
RuntimeMessageType,
|
||||
| MessagesWithClipPayload
|
||||
| typeof RuntimeMessageTypes.ToggleScreenshotMode
|
||||
| typeof RuntimeMessageTypes.RunHttpRequest
|
||||
| typeof RuntimeMessageTypes.OpenClipperAuthPanel
|
||||
>
|
||||
}
|
||||
|
||||
@@ -4,11 +4,15 @@ import { RuntimeMessage, RuntimeMessageReturnTypes } from '../types/message'
|
||||
export default async function sendMessageToActiveTab<T extends RuntimeMessage>(
|
||||
message: T,
|
||||
): Promise<RuntimeMessageReturnTypes[T['type']] | undefined> {
|
||||
const [activeTab] = await tabs.query({ active: true, currentWindow: true, windowType: 'normal' })
|
||||
const [activeTab] = await tabs.query({ active: true, lastFocusedWindow: true })
|
||||
|
||||
if (!activeTab || !activeTab.id) {
|
||||
if (!activeTab?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
return await tabs.sendMessage(activeTab.id, message)
|
||||
try {
|
||||
return await tabs.sendMessage(activeTab.id, message)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
CreateEncryptedBackupFile,
|
||||
WebSocketsService,
|
||||
PreferencesServiceEvent,
|
||||
AuthenticatorManager,
|
||||
} from '@standardnotes/services'
|
||||
import {
|
||||
SNNote,
|
||||
@@ -955,6 +956,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
||||
return this.dependencies.get<AddAuthenticator>(TYPES.AddAuthenticator)
|
||||
}
|
||||
|
||||
fetchAuthenticatorRegistrationOptions(): Promise<Record<string, unknown> | null> {
|
||||
return this.dependencies.get<AuthenticatorManager>(TYPES.AuthenticatorManager).generateRegistrationOptions()
|
||||
}
|
||||
|
||||
get listAuthenticators(): ListAuthenticators {
|
||||
return this.dependencies.get<ListAuthenticators>(TYPES.ListAuthenticators)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ export class AddAuthenticator implements UseCaseInterface<void> {
|
||||
)
|
||||
}
|
||||
|
||||
const registrationOptions = await this.authenticatorClient.generateRegistrationOptions()
|
||||
const registrationOptions =
|
||||
dto.registrationOptions ?? (await this.authenticatorClient.generateRegistrationOptions())
|
||||
if (registrationOptions === null) {
|
||||
return Result.fail('Could not generate authenticator registration options')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export interface AddAuthenticatorDTO {
|
||||
userUuid: string
|
||||
authenticatorName: string
|
||||
/** When provided, skips the server round-trip so WebAuthn can run in the same user gesture. */
|
||||
registrationOptions?: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { action, computed, makeObservable, observable } from 'mobx'
|
||||
import { startAuthentication, startRegistration } from '@simplewebauthn/browser'
|
||||
import { PanelResizedData } from '@/Types/PanelResizedData'
|
||||
import { getBlobFromBase64, isDesktopApplication, isDev } from '@/Utils'
|
||||
import { prepareWebAuthnRegistrationOptions } from '@/Utils/prepareWebAuthnRegistrationOptions'
|
||||
import {
|
||||
ArchiveManager,
|
||||
AutolockService,
|
||||
@@ -128,9 +129,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
||||
deviceInterface.environment === Environment.Mobile ? 250 : ApplicationOptionsDefaults.sleepBetweenBatches,
|
||||
allowMultipleSelection: deviceInterface.environment !== Environment.Mobile,
|
||||
allowNoteSelectionStatePersistence: deviceInterface.environment !== Environment.Mobile,
|
||||
u2fAuthenticatorRegistrationPromptFunction: startRegistration as unknown as (
|
||||
registrationOptions: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>,
|
||||
u2fAuthenticatorRegistrationPromptFunction: ((registrationOptions: Record<string, unknown>) =>
|
||||
startRegistration(
|
||||
prepareWebAuthnRegistrationOptions(registrationOptions) as unknown as Parameters<typeof startRegistration>[0],
|
||||
)) as unknown as (registrationOptions: Record<string, unknown>) => Promise<Record<string, unknown>>,
|
||||
u2fAuthenticatorVerificationPromptFunction: startAuthentication as unknown as (
|
||||
authenticationOptions: Record<string, unknown>,
|
||||
) => Promise<Record<string, unknown>>,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { Username } from '@standardnotes/snjs'
|
||||
import { Environment, Username } from '@standardnotes/snjs'
|
||||
import { ChallengePrompt } from '@standardnotes/services'
|
||||
import { RefObject, useState } from 'react'
|
||||
import { RefObject, useCallback, useState } from 'react'
|
||||
import { c } from 'ttag'
|
||||
|
||||
import { WebApplication } from '@/Application/WebApplication'
|
||||
import { IS_FIREFOX } from '@/Components/SuperEditor/Lexical/Shared/environment'
|
||||
|
||||
import Button from '../Button/Button'
|
||||
import Icon from '../Icon/Icon'
|
||||
|
||||
import { InputValue } from './InputValue'
|
||||
import U2FPromptIframeContainer from './U2FPromptIframeContainer'
|
||||
import U2FPromptFirefoxNative from './U2FPromptFirefoxNative'
|
||||
import { isAndroid } from '@standardnotes/ui-services'
|
||||
|
||||
type Props = {
|
||||
@@ -24,11 +26,31 @@ const U2FPrompt = ({ application, onValueChange, prompt, buttonRef, contextData
|
||||
const [authenticatorResponse, setAuthenticatorResponse] = useState<Record<string, unknown> | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleNativeResponse = useCallback(
|
||||
(response: Record<string, unknown>) => {
|
||||
onValueChange(response, prompt)
|
||||
},
|
||||
[onValueChange, prompt],
|
||||
)
|
||||
|
||||
if (!application.isFullU2FClient && !isAndroid()) {
|
||||
const apiHost = application.getHost.execute().getValue() || window.defaultSyncServer
|
||||
|
||||
if (application.environment === Environment.Clipper && IS_FIREFOX) {
|
||||
return (
|
||||
<U2FPromptFirefoxNative
|
||||
contextData={contextData}
|
||||
apiHost={apiHost}
|
||||
buttonRef={buttonRef}
|
||||
onResponse={handleNativeResponse}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<U2FPromptIframeContainer
|
||||
contextData={contextData}
|
||||
apiHost={application.getHost.execute().getValue() || window.defaultSyncServer}
|
||||
apiHost={apiHost}
|
||||
onResponse={(response) => {
|
||||
onValueChange(response, prompt)
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { log, LoggingDomain } from '@/Logging'
|
||||
import { getU2fRelyingPartyId } from '@/Constants/U2FConstants'
|
||||
import { startAuthentication } from '@simplewebauthn/browser'
|
||||
import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/typescript-types'
|
||||
import { RefObject, useCallback, useState } from 'react'
|
||||
import { c } from 'ttag'
|
||||
|
||||
import Button from '../Button/Button'
|
||||
|
||||
type Props = {
|
||||
contextData?: Record<string, unknown>
|
||||
onResponse: (response: Record<string, unknown>) => void
|
||||
apiHost: string
|
||||
buttonRef: RefObject<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const U2FPromptFirefoxNative = ({ contextData, onResponse, apiHost, buttonRef }: Props) => {
|
||||
const [pending, setPending] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const authenticateWithSecurityKey = useCallback(async () => {
|
||||
const username = (contextData as { username: string } | undefined)?.username
|
||||
if (!username) {
|
||||
setError(c('B1.Account.SignIn.Error').t`No username provided`)
|
||||
return
|
||||
}
|
||||
|
||||
setPending(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiHost}/v1/authenticators/generate-authentication-options`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username }),
|
||||
})
|
||||
|
||||
const jsonResponse = await response.json()
|
||||
if (!jsonResponse.data?.options) {
|
||||
throw new Error(c('B1.Account.SignIn.Error').t`No options returned from server`)
|
||||
}
|
||||
|
||||
const options = jsonResponse.data.options as PublicKeyCredentialRequestOptionsJSON
|
||||
|
||||
const rpId = getU2fRelyingPartyId(apiHost, options)
|
||||
|
||||
log(LoggingDomain.U2F, 'Starting native WebAuthn authentication', {
|
||||
username,
|
||||
apiHost,
|
||||
rpId,
|
||||
})
|
||||
|
||||
const assertionResponse = await startAuthentication({
|
||||
...options,
|
||||
rpId,
|
||||
})
|
||||
|
||||
log(LoggingDomain.U2F, 'Native WebAuthn authentication completed', { id: assertionResponse.id })
|
||||
|
||||
onResponse(assertionResponse as unknown as Record<string, unknown>)
|
||||
} catch (authError) {
|
||||
setError(authError instanceof Error ? authError.message : String(authError))
|
||||
console.error(authError)
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}, [contextData, apiHost, onResponse])
|
||||
|
||||
return (
|
||||
<div className="min-w-76">
|
||||
{error && <div className="text-danger">{error}</div>}
|
||||
<Button primary fullWidth onClick={authenticateWithSecurityKey} disabled={pending} ref={buttonRef}>
|
||||
{pending
|
||||
? c('B1.Account.SignIn.Status').t`Waiting for security key...`
|
||||
: c('B5.SecuritySync.Challenge.Action').t`Authenticate Device`}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default U2FPromptFirefoxNative
|
||||
@@ -39,6 +39,18 @@ import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||
import Spinner from '../Spinner/Spinner'
|
||||
import { IS_FIREFOX } from '@/Components/SuperEditor/Lexical/Shared/environment'
|
||||
|
||||
const getAuthPaneFromUrl = () => {
|
||||
const pane = new URLSearchParams(window.location.search).get('pane')
|
||||
if (pane === 'sign-in') {
|
||||
return AccountMenuPane.SignIn
|
||||
}
|
||||
if (pane === 'register') {
|
||||
return AccountMenuPane.Register
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGroup }) => {
|
||||
const application = useApplication()
|
||||
@@ -54,6 +66,8 @@ const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGro
|
||||
})
|
||||
.catch(console.error)
|
||||
}, [])
|
||||
const isDetachedPanel = (currentWindow?.type as string | undefined) === 'detached_panel'
|
||||
const isFirefoxAuthPanel = IS_FIREFOX && getAuthPaneFromUrl() !== undefined
|
||||
const isFirefoxPopup = !!currentWindow && currentWindow.type === 'popup' && currentWindow.incognito === false
|
||||
|
||||
const [user, setUser] = useState(() => application.sessions.getUser())
|
||||
@@ -74,6 +88,16 @@ const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGro
|
||||
return application.addEventObserver(async (event) => {
|
||||
switch (event) {
|
||||
case ApplicationEvent.SignedIn:
|
||||
setUser(application.sessions.getUser())
|
||||
setIsEntitled(
|
||||
application.features.getFeatureStatus(
|
||||
NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.Clipper).getValue(),
|
||||
) === FeatureStatus.Entitled,
|
||||
)
|
||||
if (isFirefoxAuthPanel) {
|
||||
window.close()
|
||||
}
|
||||
break
|
||||
case ApplicationEvent.SignedOut:
|
||||
case ApplicationEvent.UserRolesChanged:
|
||||
setUser(application.sessions.getUser())
|
||||
@@ -126,15 +150,37 @@ const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGro
|
||||
void application.setPreference(PrefKey.ClipperDefaultTagUuid, undefined)
|
||||
}, [application])
|
||||
|
||||
const [menuPane, setMenuPane] = useState<AccountMenuPane>()
|
||||
const [menuPane, setMenuPane] = useState<AccountMenuPane | undefined>(getAuthPaneFromUrl)
|
||||
|
||||
const openFirefoxAuthPanel = useCallback(async (pane: 'sign-in' | 'register') => {
|
||||
try {
|
||||
await runtime.sendMessage({ type: RuntimeMessageTypes.OpenClipperAuthPanel, pane })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
setMenuPane(pane === 'sign-in' ? AccountMenuPane.SignIn : AccountMenuPane.Register)
|
||||
return
|
||||
}
|
||||
|
||||
window.close()
|
||||
}, [])
|
||||
|
||||
const activateRegisterPane = useCallback(() => {
|
||||
if (IS_FIREFOX && !isFirefoxAuthPanel && !isDetachedPanel) {
|
||||
void openFirefoxAuthPanel('register')
|
||||
return
|
||||
}
|
||||
|
||||
setMenuPane(AccountMenuPane.Register)
|
||||
}, [setMenuPane])
|
||||
}, [isDetachedPanel, isFirefoxAuthPanel, openFirefoxAuthPanel])
|
||||
|
||||
const activateSignInPane = useCallback(() => {
|
||||
if (IS_FIREFOX && !isFirefoxAuthPanel && !isDetachedPanel) {
|
||||
void openFirefoxAuthPanel('sign-in')
|
||||
return
|
||||
}
|
||||
|
||||
setMenuPane(AccountMenuPane.SignIn)
|
||||
}, [setMenuPane])
|
||||
}, [isDetachedPanel, isFirefoxAuthPanel, openFirefoxAuthPanel])
|
||||
|
||||
const showSignOutConfirmation = useCallback(async () => {
|
||||
if (
|
||||
@@ -155,7 +201,7 @@ const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGro
|
||||
void sendMessageToActiveTab({
|
||||
type: RuntimeMessageTypes.ToggleScreenshotMode,
|
||||
enabled: isScreenshotMode,
|
||||
})
|
||||
}).catch(() => undefined)
|
||||
}, [isScreenshotMode])
|
||||
|
||||
const [hasSelection, setHasSelection] = useState(false)
|
||||
@@ -323,16 +369,26 @@ const ClipperView = ({ applicationGroup }: { applicationGroup: WebApplicationGro
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return menuPane ? (
|
||||
<div className="py-1">
|
||||
<MenuPaneSelector
|
||||
mainApplicationGroup={applicationGroup}
|
||||
menuPane={menuPane}
|
||||
setMenuPane={setMenuPane}
|
||||
closeMenu={() => setMenuPane(undefined)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
if (menuPane || isFirefoxAuthPanel) {
|
||||
return (
|
||||
<div className="py-1">
|
||||
<MenuPaneSelector
|
||||
mainApplicationGroup={applicationGroup}
|
||||
menuPane={menuPane ?? AccountMenuPane.SignIn}
|
||||
setMenuPane={(pane) => {
|
||||
if (isFirefoxAuthPanel && pane === AccountMenuPane.GeneralMenu) {
|
||||
window.close()
|
||||
return
|
||||
}
|
||||
setMenuPane(pane)
|
||||
}}
|
||||
closeMenu={() => (isFirefoxAuthPanel ? window.close() : setMenuPane(undefined))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu a11yLabel={c('B1.Account.Session.Label').t`User account menu`}>
|
||||
<MenuItem onClick={activateRegisterPane}>
|
||||
<Icon type="user" className="mr-2 h-6 w-6 text-neutral md:h-5 md:w-5" />
|
||||
|
||||
+38
-2
@@ -1,4 +1,4 @@
|
||||
import { FunctionComponent, useCallback, useState } from 'react'
|
||||
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { AddAuthenticator } from '@standardnotes/snjs'
|
||||
|
||||
@@ -19,6 +19,27 @@ const U2FAddDeviceView: FunctionComponent<Props> = ({ addAuthenticator, onDevice
|
||||
|
||||
const [deviceName, setDeviceName] = useState('')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const [registrationOptions, setRegistrationOptions] = useState<Record<string, unknown> | null>(null)
|
||||
const [isLoadingOptions, setIsLoadingOptions] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true
|
||||
|
||||
void application.fetchAuthenticatorRegistrationOptions().then((options) => {
|
||||
if (!isActive) {
|
||||
return
|
||||
}
|
||||
setRegistrationOptions(options)
|
||||
setIsLoadingOptions(false)
|
||||
if (options === null) {
|
||||
setErrorMessage(c('B6.Preferences.Security.Error').t`Could not prepare security key registration`)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
isActive = false
|
||||
}
|
||||
}, [application])
|
||||
|
||||
const handleDeviceNameChange = useCallback((deviceName: string) => {
|
||||
setDeviceName(deviceName)
|
||||
@@ -30,6 +51,11 @@ const U2FAddDeviceView: FunctionComponent<Props> = ({ addAuthenticator, onDevice
|
||||
return
|
||||
}
|
||||
|
||||
if (!registrationOptions) {
|
||||
setErrorMessage(c('B6.Preferences.Security.Error').t`Could not prepare security key registration`)
|
||||
return
|
||||
}
|
||||
|
||||
const user = application.sessions.getUser()
|
||||
if (user === undefined) {
|
||||
setErrorMessage(c('B6.Preferences.Security.Error').t`User not found`)
|
||||
@@ -39,6 +65,7 @@ const U2FAddDeviceView: FunctionComponent<Props> = ({ addAuthenticator, onDevice
|
||||
const authenticatorAddedOrError = await addAuthenticator.execute({
|
||||
userUuid: user.uuid,
|
||||
authenticatorName: deviceName,
|
||||
registrationOptions,
|
||||
})
|
||||
if (authenticatorAddedOrError.isFailed()) {
|
||||
setErrorMessage(authenticatorAddedOrError.getError())
|
||||
@@ -47,7 +74,15 @@ const U2FAddDeviceView: FunctionComponent<Props> = ({ addAuthenticator, onDevice
|
||||
|
||||
onDeviceAddingModalToggle(false)
|
||||
await onDeviceAdded()
|
||||
}, [deviceName, setErrorMessage, application, addAuthenticator, onDeviceAddingModalToggle, onDeviceAdded])
|
||||
}, [
|
||||
deviceName,
|
||||
registrationOptions,
|
||||
setErrorMessage,
|
||||
application,
|
||||
addAuthenticator,
|
||||
onDeviceAddingModalToggle,
|
||||
onDeviceAdded,
|
||||
])
|
||||
|
||||
const closeModal = () => {
|
||||
onDeviceAddingModalToggle(false)
|
||||
@@ -77,6 +112,7 @@ const U2FAddDeviceView: FunctionComponent<Props> = ({ addAuthenticator, onDevice
|
||||
type: 'primary',
|
||||
onClick: handleAddDeviceClick,
|
||||
mobileSlot: 'right',
|
||||
disabled: isLoadingOptions || registrationOptions === null,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/** Relying party ID used when registering and authenticating hardware keys for Standard Notes. */
|
||||
export const U2F_RELYING_PARTY_ID = 'app.standardnotes.com'
|
||||
|
||||
/**
|
||||
* Stable WebAuthn origin for the Firefox clipper extension (SHA-256 of gecko.id, a–p encoded).
|
||||
* Must be included in the auth server's U2F_EXPECTED_ORIGIN allowlist.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Use_the_web_authn_api
|
||||
*/
|
||||
export const FIREFOX_CLIPPER_WEBAUTHN_ORIGIN =
|
||||
'moz-extension://mbmeiccjaghdhkomcnlbapkpmhjgcjdonnphcmpgbgnlpbepegcaeomhoinfmflc'
|
||||
|
||||
export function isLocalU2fApiHost(apiHost: string): boolean {
|
||||
try {
|
||||
const hostname = new URL(apiHost).hostname
|
||||
return hostname === 'localhost' || hostname === '127.0.0.1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Prod clipper always uses app.standardnotes.com; local servers use their configured rpId. */
|
||||
export function getU2fRelyingPartyId(apiHost: string, serverOptions?: { rpId?: string }): string {
|
||||
if (isLocalU2fApiHost(apiHost)) {
|
||||
return serverOptions?.rpId ?? new URL(apiHost).hostname
|
||||
}
|
||||
|
||||
return U2F_RELYING_PARTY_ID
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { isDev } from '@/Utils'
|
||||
|
||||
/** Align rpId with the page hostname for local WebAuthn development. */
|
||||
export function prepareWebAuthnRegistrationOptions(options: Record<string, unknown>): Record<string, unknown> {
|
||||
if (!isDev) {
|
||||
return options
|
||||
}
|
||||
|
||||
const hostname = window.location.hostname
|
||||
if (hostname !== 'localhost' && hostname !== '127.0.0.1') {
|
||||
return options
|
||||
}
|
||||
|
||||
const rp = options.rp as { id?: string; name?: string } | undefined
|
||||
if (!rp) {
|
||||
return options
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
rp: {
|
||||
...rp,
|
||||
id: hostname,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user