Skip to content

Commit c8f0d15

Browse files
committed
frontend/settings: add enable authentication switch for android
This introduce a config switch, available on Android, to enable the system-level authentication. It also adds a new `force-auth` endpoint that can be used by frontend components to force an authentication request. Here it is used to force an authentication before updating the authentication config setting.
1 parent a955f53 commit c8f0d15

File tree

8 files changed

+165
-12
lines changed

8 files changed

+165
-12
lines changed

backend/backend.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type authEventType string
116116

117117
const (
118118
authRequired authEventType = "auth-required"
119+
authForced authEventType = "auth-forced"
119120
authOk authEventType = "auth-ok"
120121
authErr authEventType = "auth-err"
121122
)
@@ -309,6 +310,22 @@ func (backend *Backend) Config() *config.Config {
309310
return backend.config
310311
}
311312

313+
// Authenticate executes a system authentication if
314+
// the authentication config flag is enabled or if the
315+
// `force` input flag is enabled (as a consequence of an
316+
// 'auth/auth-forced' notification).
317+
// Otherwise, the authentication is automatically assumed as
318+
// successful.
319+
func (backend *Backend) Authenticate(force bool) {
320+
backend.log.Info("Auth requested")
321+
if backend.config.AppConfig().Backend.Authentication || force {
322+
backend.environment.Auth()
323+
} else {
324+
backend.AuthResult(true)
325+
}
326+
}
327+
328+
// TriggerAuth triggers an auth-required notification.
312329
func (backend *Backend) TriggerAuth() {
313330
backend.Notify(observable.Event{
314331
Subject: "auth",
@@ -319,6 +336,27 @@ func (backend *Backend) TriggerAuth() {
319336
})
320337
}
321338

339+
// ForceAuth triggers an auth-forced notification
340+
// followed by an auth-required notification.
341+
func (backend *Backend) ForceAuth() {
342+
backend.Notify(observable.Event{
343+
Subject: "auth",
344+
Action: action.Replace,
345+
Object: authEventObject{
346+
Typ: authForced,
347+
},
348+
})
349+
backend.Notify(observable.Event{
350+
Subject: "auth",
351+
Action: action.Replace,
352+
Object: authEventObject{
353+
Typ: authRequired,
354+
},
355+
})
356+
}
357+
358+
// AuthResult triggers an auth-ok or auth-err notification
359+
// depending on the input value.
322360
func (backend *Backend) AuthResult(ok bool) {
323361
backend.log.Infof("Auth result: %v", ok)
324362
typ := authErr

backend/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ type Backend struct {
7474
DeprecatedLitecoinActive bool `json:"litecoinActive"`
7575
DeprecatedEthereumActive bool `json:"ethereumActive"`
7676

77+
Authentication bool `json:"authentication"`
78+
7779
BTC btcCoinConfig `json:"btc"`
7880
TBTC btcCoinConfig `json:"tbtc"`
7981
RBTC btcCoinConfig `json:"rbtc"`
@@ -155,6 +157,7 @@ func NewDefaultAppConfig() AppConfig {
155157
UseProxy: false,
156158
ProxyAddress: "",
157159
},
160+
Authentication: false,
158161
DeprecatedBitcoinActive: true,
159162
DeprecatedLitecoinActive: true,
160163
DeprecatedEthereumActive: true,

backend/handlers/handlers.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ type Backend interface {
101101
AOPPCancel()
102102
AOPPApprove()
103103
AOPPChooseAccount(code accountsTypes.Code)
104+
Authenticate(force bool)
105+
TriggerAuth()
106+
ForceAuth()
104107
GetAccountFromCode(code string) (accounts.Interface, error)
105108
HTTPClient() *http.Client
106109
CancelConnectKeystore()
@@ -194,7 +197,9 @@ func NewHandlers(
194197
getAPIRouterNoError(apiRouter)("/update", handlers.getUpdateHandler).Methods("GET")
195198
getAPIRouterNoError(apiRouter)("/banners/{key}", handlers.getBannersHandler).Methods("GET")
196199
getAPIRouterNoError(apiRouter)("/using-mobile-data", handlers.getUsingMobileDataHandler).Methods("GET")
197-
getAPIRouterNoError(apiRouter)("/auth", handlers.getAuthHandler).Methods("GET")
200+
getAPIRouterNoError(apiRouter)("/authenticate", handlers.postAuthenticateHandler).Methods("POST")
201+
getAPIRouterNoError(apiRouter)("/trigger-auth", handlers.postTriggerAuthHandler).Methods("POST")
202+
getAPIRouterNoError(apiRouter)("/force-auth", handlers.postForceAuthHandler).Methods("POST")
198203
getAPIRouter(apiRouter)("/set-dark-theme", handlers.postDarkThemeHandler).Methods("POST")
199204
getAPIRouterNoError(apiRouter)("/detect-dark-theme", handlers.getDetectDarkThemeHandler).Methods("GET")
200205
getAPIRouterNoError(apiRouter)("/version", handlers.getVersionHandler).Methods("GET")
@@ -460,9 +465,26 @@ func (handlers *Handlers) getUsingMobileDataHandler(r *http.Request) interface{}
460465
return handlers.backend.Environment().UsingMobileData()
461466
}
462467

463-
func (handlers *Handlers) getAuthHandler(r *http.Request) interface{} {
464-
handlers.log.Info("Auth requested")
465-
handlers.backend.Environment().Auth()
468+
func (handlers *Handlers) postAuthenticateHandler(r *http.Request) interface{} {
469+
var force bool
470+
if err := json.NewDecoder(r.Body).Decode(&force); err != nil {
471+
return map[string]interface{}{
472+
"success": false,
473+
"errorMessage": err.Error(),
474+
}
475+
}
476+
477+
handlers.backend.Authenticate(force)
478+
return nil
479+
}
480+
481+
func (handlers *Handlers) postTriggerAuthHandler(r *http.Request) interface{} {
482+
handlers.backend.TriggerAuth()
483+
return nil
484+
}
485+
486+
func (handlers *Handlers) postForceAuthHandler(r *http.Request) interface{} {
487+
handlers.backend.ForceAuth()
466488
return nil
467489
}
468490

frontends/web/src/api/backend.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,16 @@ export const cancelConnectKeystore = (): Promise<void> => {
108108
export const setWatchonly = (watchonly: boolean): Promise<ISuccess> => {
109109
return apiPost('set-watchonly', watchonly);
110110
};
111-
export const authenticate = (): Promise<void> => {
112-
return apiGet('auth');
111+
export const authenticate = (force: boolean = false): Promise<void> => {
112+
return apiPost('authenticate', force);
113113
};
114114

115+
export const forceAuth = (): Promise<void> => {
116+
return apiPost('force-auth');
117+
};
115118

116119
export type TAuthEventObject = {
117-
typ: 'auth-required' | 'auth-ok' | 'auth-err';
120+
typ: 'auth-required' | 'auth-forced' | 'auth-ok' | 'auth-err' ;
118121
};
119122

120123
export const subscribeAuth = (

frontends/web/src/components/auth/authrequired.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,42 @@
1616

1717
import { View } from '../view/view';
1818
import style from './authrequired.module.css';
19-
import { useEffect, useState } from 'react';
19+
import { useEffect, useRef, useState } from 'react';
2020
import { TAuthEventObject, authenticate, subscribeAuth } from '../../api/backend';
2121

2222
export const AuthRequired = () => {
2323
const [authRequired, setAuthRequired] = useState(false);
24+
const authForced = useRef(false);
2425

2526
useEffect(() => {
2627
const unsubscribe = subscribeAuth((data: TAuthEventObject) => {
2728
switch (data.typ) {
29+
case 'auth-forced':
30+
authForced.current = true;
31+
break;
2832
case 'auth-required':
2933
// It is a bit strange to call authenticate inside `setAuthRequired`,
3034
// but doing so we avoid declaring `authRequired` as a useEffect's
3135
// dependency, which would cause it to unsubscribe/subscribe every
3236
// time the state changes.
3337
setAuthRequired((prevAuthRequired) => {
3438
if (!prevAuthRequired) {
35-
authenticate();
39+
authenticate(authForced.current);
3640
}
3741
return true;
3842
});
3943
break;
4044
case 'auth-err':
41-
authenticate();
45+
authenticate(authForced.current);
4246
break;
4347
case 'auth-ok':
4448
setAuthRequired(false);
49+
authForced.current = false;
4550
}
4651
});
4752

4853
// Perform initial authentication. If the auth config is disabled,
49-
// the backend will immediately sent an auth-ok back.
54+
// the backend will immediately send an auth-ok back.
5055
setAuthRequired(true);
5156
authenticate();
5257

frontends/web/src/locales/en/app.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,10 @@
10591059
}
10601060
},
10611061
"advancedSettings": {
1062+
"authentication": {
1063+
"description": "Lock access to the app with screen lock/fingerprint.",
1064+
"title": "Screen lock"
1065+
},
10621066
"coinControl": {
10631067
"description": "Select which UTXOs are part of a transaction to help improve privacy."
10641068
},

frontends/web/src/routes/settings/advanced-settings.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { getConfig } from '../../utils/config';
2929
import { MobileHeader } from './components/mobile-header';
3030
import { Guide } from '../../components/guide/guide';
3131
import { Entry } from '../../components/guide/entry';
32+
import { EnableAuthSetting } from './components/advanced-settings/enable-auth-setting';
3233

3334
export type TProxyConfig = {
3435
proxyAddress: string;
@@ -40,8 +41,10 @@ export type TFrontendConfig = {
4041
coinControl?: boolean;
4142
}
4243

43-
type TBackendConfig = {
44+
export type TBackendConfig = {
4445
proxy?: TProxyConfig
46+
authentication?: boolean;
47+
4548
}
4649

4750
export type TConfig = {
@@ -55,6 +58,7 @@ export const AdvancedSettings = ({ deviceIDs, hasAccounts }: TPagePropsWithSetti
5558
const [config, setConfig] = useState<TConfig>();
5659

5760
const frontendConfig = config?.frontend;
61+
const backendConfig = config?.backend;
5862
const proxyConfig = config?.backend?.proxy;
5963

6064
useEffect(() => {
@@ -82,6 +86,7 @@ export const AdvancedSettings = ({ deviceIDs, hasAccounts }: TPagePropsWithSetti
8286
>
8387
<EnableCustomFeesToggleSetting frontendConfig={frontendConfig} onChangeConfig={setConfig} />
8488
<EnableCoinControlSetting frontendConfig={frontendConfig} onChangeConfig={setConfig} />
89+
<EnableAuthSetting backendConfig={backendConfig} onChangeConfig={setConfig} />
8590
<EnableTorProxySetting proxyConfig={proxyConfig} onChangeConfig={setConfig} />
8691
<ConnectFullNodeSetting />
8792
</WithSettingsTabs>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Copyright 2023 Shift Crypto AG
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ChangeEvent, Dispatch } from 'react';
18+
import { useTranslation } from 'react-i18next';
19+
import { Toggle } from '../../../../components/toggle/toggle';
20+
import { SettingsItem } from '../settingsItem/settingsItem';
21+
import { TBackendConfig, TConfig } from '../../advanced-settings';
22+
import { setConfig } from '../../../../utils/config';
23+
import { TAuthEventObject, subscribeAuth, forceAuth } from '../../../../api/backend';
24+
import { runningInAndroid } from '../../../../utils/env';
25+
26+
type TProps = {
27+
backendConfig?: TBackendConfig;
28+
onChangeConfig: Dispatch<TConfig>;
29+
}
30+
31+
export const EnableAuthSetting = ({ backendConfig, onChangeConfig }: TProps) => {
32+
const { t } = useTranslation();
33+
34+
const handleToggleAuth = async (e: ChangeEvent<HTMLInputElement>) => {
35+
// Before updating the config we need the user to authenticate.
36+
// The forceAuth is needed to force the backend to execute the
37+
// authentication even if the auth config is disabled.
38+
const unsubscribe = subscribeAuth((data: TAuthEventObject) => {
39+
if (data.typ === 'auth-ok') {
40+
updateConfig(!e.target.checked);
41+
unsubscribe();
42+
}
43+
});
44+
forceAuth();
45+
};
46+
47+
const updateConfig = async (auth: boolean) => {
48+
const config = await setConfig({
49+
backend: { authentication: auth },
50+
}) as TConfig;
51+
onChangeConfig(config);
52+
};
53+
54+
if (!runningInAndroid()) {
55+
return null;
56+
}
57+
58+
return (
59+
<SettingsItem
60+
settingName={t('newSettings.advancedSettings.authentication.title')}
61+
secondaryText={t('newSettings.advancedSettings.authentication.description')}
62+
extraComponent={
63+
backendConfig !== undefined ?
64+
<Toggle
65+
checked={backendConfig?.authentication || false}
66+
onChange={handleToggleAuth}
67+
/>
68+
:
69+
null
70+
}
71+
/>
72+
);
73+
};

0 commit comments

Comments
 (0)