Skip to content

Commit

Permalink
feat: new authentication mechanism (access/refresh token) (#665)
Browse files Browse the repository at this point in the history
* dev(auth): add method postToken to JmWalletApi

* refactor: remove type ServiceInfoUpdate

* dev(auth): add Single- and RefreshTokenContext types

* Revert "config: use joinmarket v0.9.10 (#664)"

Commit was temporarily till the new auth mechanism can be handled.
This reverts commit c31ba67.

* dev(auth): save auth context to session

* dev(auth): refresh token periodically

* dev(auth): maintain CurrentWallet reference when renewing auth token

* refactor: server.version must be present

* dev(auth): lower token refresh interval in dev mode

* dev(auth): add distinct method to parse auth props
  • Loading branch information
theborakompanioni authored Oct 8, 2023
1 parent 69e61e0 commit 4c76d6a
Show file tree
Hide file tree
Showing 21 changed files with 254 additions and 115 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN apt-get update \

ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver
ENV REPO_BRANCH master
ENV REPO_REF v0.9.10
ENV REPO_REF master

WORKDIR /src
RUN git clone "$REPO" . --depth=10 --branch "$REPO_BRANCH" && git checkout "$REPO_REF"
Expand Down
12 changes: 7 additions & 5 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CurrentWallet,
useCurrentWallet,
useSetCurrentWallet,
useClearCurrentWallet,
useReloadCurrentWalletInfo,
} from '../context/WalletContext'
import { clearSession, setSession } from '../session'
Expand All @@ -44,24 +45,25 @@ export default function App() {
const settings = useSettings()
const currentWallet = useCurrentWallet()
const setCurrentWallet = useSetCurrentWallet()
const clearCurrentWallet = useClearCurrentWallet()
const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
const serviceInfo = useServiceInfo()
const sessionConnectionError = useSessionConnectionError()
const [reloadingWalletInfoCounter, setReloadingWalletInfoCounter] = useState(0)
const isReloadingWalletInfo = useMemo(() => reloadingWalletInfoCounter > 0, [reloadingWalletInfoCounter])

const startWallet = useCallback(
(name: Api.WalletName, token: Api.ApiToken) => {
setSession({ name, token })
setCurrentWallet({ name, token })
(name: Api.WalletName, auth: Api.ApiAuthContext) => {
setSession({ name, auth })
setCurrentWallet({ name, token: auth.token })
},
[setCurrentWallet],
)

const stopWallet = useCallback(() => {
clearCurrentWallet()
clearSession()
setCurrentWallet(null)
}, [setCurrentWallet])
}, [clearCurrentWallet])

const reloadWalletInfo = useCallback(
(delay: Milliseconds) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/BitcoinQR.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import { useEffect, useState } from 'react'
import QRCode from 'qrcode'

import { satsToBtc } from '../utils'
Expand Down
4 changes: 2 additions & 2 deletions src/components/CoinjoinPreconditionViolationAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { forwardRef } from 'react'
import { Ref, forwardRef } from 'react'
import * as rb from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSettings } from '../context/SettingsContext'
Expand All @@ -14,7 +14,7 @@ interface CoinjoinPreconditionViolationAlertProps {
}

export const CoinjoinPreconditionViolationAlert = forwardRef(
({ summary, i18nPrefix = '' }: CoinjoinPreconditionViolationAlertProps, ref: React.Ref<HTMLDivElement>) => {
({ summary, i18nPrefix = '' }: CoinjoinPreconditionViolationAlertProps, ref: Ref<HTMLDivElement>) => {
const { t } = useTranslation()
const settings = useSettings()

Expand Down
9 changes: 5 additions & 4 deletions src/components/CreateWallet.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,9 @@ export default function CreateWallet({ parentRoute, startWallet }) {
const res = await Api.postWalletCreate({}, { walletname: walletName, password })
const body = await (res.ok ? res.json() : Api.Helper.throwError(res))

const { seedphrase, token, walletname: createdWalletFileName } = body
setCreatedWallet({ walletFileName: createdWalletFileName, seedphrase, password, token })
const { seedphrase, walletname: createdWalletFileName } = body
const auth = Api.Helper.parseAuthProps(body)
setCreatedWallet({ walletFileName: createdWalletFileName, seedphrase, password, auth })
} catch (e) {
const message = t('create_wallet.error_creating_failed', {
reason: e.message || 'Unknown reason',
Expand All @@ -112,9 +113,9 @@ export default function CreateWallet({ parentRoute, startWallet }) {
)

const walletConfirmed = useCallback(() => {
if (createdWallet?.walletFileName && createdWallet?.token) {
if (createdWallet?.walletFileName && createdWallet?.auth) {
setAlert(null)
startWallet(createdWallet.walletFileName, createdWallet.token)
startWallet(createdWallet.walletFileName, createdWallet.auth)
navigate(routes.wallet)
} else {
setAlert({ variant: 'danger', message: t('create_wallet.alert_confirmation_failed') })
Expand Down
1 change: 0 additions & 1 deletion src/components/EarnReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useTheme } from '@table-library/react-table-library/theme'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import * as Api from '../libs/JmWalletApi'
// @ts-ignore
import { useSettings } from '../context/SettingsContext'
import Balance from './Balance'
import Sprite from './Sprite'
Expand Down
20 changes: 11 additions & 9 deletions src/components/ImportWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ enum ImportWalletSteps {

interface ImportWalletProps {
parentRoute: Route
startWallet: (name: Api.WalletName, token: Api.ApiToken) => void
startWallet: (name: Api.WalletName, auth: Api.ApiAuthContext) => void
}

export default function ImportWallet({ parentRoute, startWallet }: ImportWalletProps) {
Expand All @@ -388,9 +388,9 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
const [alert, setAlert] = useState<SimpleAlert>()
const [createWalletFormValues, setCreateWalletFormValues] = useState<CreateWalletFormValues>()
const [importDetailsFormValues, setImportDetailsFormValues] = useState<ImportWalletDetailsFormValues>()
const [recoveredWallet, setRecoveredWallet] = useState<{ walletFileName: Api.WalletName; token: Api.ApiToken }>()
const [recoveredWallet, setRecoveredWallet] = useState<{ walletFileName: Api.WalletName; auth: Api.ApiAuthContext }>()

const isRecovered = useMemo(() => !!recoveredWallet?.walletFileName && recoveredWallet?.token, [recoveredWallet])
const isRecovered = useMemo(() => !!recoveredWallet?.walletFileName && recoveredWallet?.auth, [recoveredWallet])
const canRecover = useMemo(
() => !isRecovered && !serviceInfo?.walletName && !serviceInfo?.rescanning,
[isRecovered, serviceInfo],
Expand Down Expand Up @@ -441,13 +441,14 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
const recoverBody = await (recoverResponse.ok ? recoverResponse.json() : Api.Helper.throwError(recoverResponse))

const { walletname: importedWalletFileName } = recoverBody
setRecoveredWallet({ walletFileName: importedWalletFileName, token: recoverBody.token })
let auth: Api.ApiAuthContext = Api.Helper.parseAuthProps(recoverBody)
setRecoveredWallet({ walletFileName: importedWalletFileName, auth })

// Step #2: update the gaplimit config value if necessary
const originalGaplimit = await refreshConfigValues({
signal,
keys: [JM_GAPLIMIT_CONFIGKEY],
wallet: { name: importedWalletFileName, token: recoverBody.token },
wallet: { name: importedWalletFileName, token: auth.token },
})
.then((it) => it[JM_GAPLIMIT_CONFIGKEY.section] || {})
.then((it) => parseInt(it[JM_GAPLIMIT_CONFIGKEY.field] || String(JM_GAPLIMIT_DEFAULT), 10))
Expand All @@ -465,16 +466,17 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
value: String(gaplimit),
},
],
wallet: { name: importedWalletFileName, token: recoverBody.token },
wallet: { name: importedWalletFileName, token: auth.token },
})
}

// Step #3: lock and unlock the wallet (for new addresses to be imported)
const lockResponse = await Api.getWalletLock({ walletName: importedWalletFileName, token: recoverBody.token })
const lockResponse = await Api.getWalletLock({ walletName: importedWalletFileName, token: auth.token })
if (!lockResponse.ok) await Api.Helper.throwError(lockResponse)

const unlockResponse = await Api.postWalletUnlock({ walletName: importedWalletFileName }, { password })
const unlockBody = await (unlockResponse.ok ? unlockResponse.json() : Api.Helper.throwError(unlockResponse))
auth = Api.Helper.parseAuthProps(unlockBody)

// Step #4: reset `gaplimit´ to previous value if necessary
if (gaplimitUpdateNecessary) {
Expand All @@ -487,7 +489,7 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
value: String(originalGaplimit),
},
],
wallet: { name: importedWalletFileName, token: unlockBody.token },
wallet: { name: importedWalletFileName, token: auth.token },
})
}

Expand All @@ -508,7 +510,7 @@ export default function ImportWallet({ parentRoute, startWallet }: ImportWalletP
})
}

startWallet(importedWalletFileName, unlockBody.token)
startWallet(importedWalletFileName, auth)
navigate(routes.wallet)
} catch (e: any) {
if (signal.aborted) return
Expand Down
3 changes: 1 addition & 2 deletions src/components/LogOverlay.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { Helper as ApiHelper } from '../libs/JmWalletApi'
import { fetchLog } from '../libs/JamApi'
// @ts-ignore
import { useSettings } from '../context/SettingsContext'
import { CurrentWallet } from '../context/WalletContext'
import Sprite from './Sprite'
Expand Down
4 changes: 2 additions & 2 deletions src/components/ToggleSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react'
import { ChangeEvent } from 'react'
import styles from './ToggleSwitch.module.css'

interface ToggleSwitchProps {
Expand All @@ -16,7 +16,7 @@ export default function ToggleSwitch({
toggledOn,
disabled = false,
}: ToggleSwitchProps) {
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
e.stopPropagation()
onToggle(e.currentTarget.checked)
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Wallets.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,9 @@ export default function Wallets({ currentWallet, startWallet, stopWallet }) {

setUnlockWalletName(undefined)

const { walletname: unlockedWalletName, token } = body
startWallet(unlockedWalletName, token)
const auth = Api.Helper.parseAuthProps(body)

startWallet(body.walletname, auth)
navigate(routes.wallet)
} catch (e) {
const message = e.message.replace('Wallet', walletName)
Expand Down
12 changes: 10 additions & 2 deletions src/components/Wallets.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,12 @@ describe('<Wallets />', () => {
})
apiMock.postWalletUnlock.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ walletname: dummyWalletName, token: dummyToken }),
json: () =>
Promise.resolve({
walletname: dummyWalletName,
token: dummyToken,
refresh_token: dummyToken,
}),
})

await act(async () => setup({}))
Expand All @@ -223,7 +228,10 @@ describe('<Wallets />', () => {
await waitFor(() => screen.findByText('wallets.wallet_preview.button_unlock'))
})

expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, dummyToken)
expect(mockStartWallet).toHaveBeenCalledWith(dummyWalletName, {
token: dummyToken,
refresh_token: dummyToken,
})
expect(mockedNavigate).toHaveBeenCalledWith('/wallet')
})

Expand Down
6 changes: 2 additions & 4 deletions src/components/jar_details/DisplayBranch.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React from 'react'
import { ReactNode } from 'react'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
// @ts-ignore
import Balance from '../Balance'
// @ts-ignore
import { useSettings } from '../../context/SettingsContext'
import { Branch, BranchEntry } from '../../context/WalletContext'
import styles from './DisplayBranch.module.css'
Expand All @@ -28,7 +26,7 @@ const toSimpleStatus = (value: string) => {
return value.substring(0, indexOfBracket).trim()
}

const toLabelNode = (simpleStatus: string): React.ReactNode => {
const toLabelNode = (simpleStatus: string): ReactNode => {
if (simpleStatus === 'new') return <rb.Badge bg="success">{simpleStatus}</rb.Badge>
if (simpleStatus === 'used') return <rb.Badge bg="secondary">{simpleStatus}</rb.Badge>

Expand Down
2 changes: 2 additions & 0 deletions src/constants/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export const JM_MINIMUM_MAKERS_DEFAULT = 4
export const CJ_STATE_TAKER_RUNNING = 0
export const CJ_STATE_MAKER_RUNNING = 1
export const CJ_STATE_NONE_RUNNING = 2

export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.round(0.5 * 60 * 60 * 1_000)
2 changes: 1 addition & 1 deletion src/constants/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ const __isFeatureEnabled = (name: Feature, version: SemVer): boolean => {
}

export const isFeatureEnabled = (name: Feature, serviceInfo: ServiceInfo): boolean => {
return !!serviceInfo.server?.version && __isFeatureEnabled(name, serviceInfo.server.version)
return !!serviceInfo.server && __isFeatureEnabled(name, serviceInfo.server.version)
}
5 changes: 2 additions & 3 deletions src/context/ServiceConfigContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react'
// @ts-ignore
import { PropsWithChildren, createContext, useCallback, useContext, useEffect, useRef } from 'react'
import { CurrentWallet, useCurrentWallet } from './WalletContext'

import * as Api from '../libs/JmWalletApi'
Expand Down Expand Up @@ -105,7 +104,7 @@ export interface ServiceConfigContextEntry {

const ServiceConfigContext = createContext<ServiceConfigContextEntry | undefined>(undefined)

const ServiceConfigProvider = ({ children }: React.PropsWithChildren<{}>) => {
const ServiceConfigProvider = ({ children }: PropsWithChildren<{}>) => {
const currentWallet = useCurrentWallet()
const serviceConfig = useRef<ServiceConfig | null>(null)

Expand Down
Loading

0 comments on commit 4c76d6a

Please sign in to comment.