Skip to content

Commit f1735f3

Browse files
committed
auth: add authStore and send credentials to APIs
1 parent 5f3c86b commit f1735f3

File tree

8 files changed

+166
-27
lines changed

8 files changed

+166
-27
lines changed

app/src/__stories__/StoryWrapper.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class StoryAppStorage {
2626
unit: Unit.sats,
2727
balanceMode: BalanceMode.receive,
2828
});
29+
setSession = () => undefined;
30+
getSession = () => '';
2931
}
3032

3133
// Create a store that pulls data from the mock GRPC and doesn't use

app/src/api/auth.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* A shared base class containing logic for storing the API credentials
3+
*/
4+
class AuthenticatedApi {
5+
private _credentials = '';
6+
7+
/**
8+
* Returns a metadata object containing authorization info that was
9+
* previous set if any
10+
*/
11+
protected get _meta() {
12+
return this._credentials
13+
? { authorization: `Basic ${this._credentials}` }
14+
: undefined;
15+
}
16+
17+
/**
18+
* Sets the credentials to use for all API requests
19+
* @param credentials the base64 encoded password
20+
*/
21+
setCredentials(credentials: string) {
22+
this._credentials = credentials;
23+
}
24+
}
25+
26+
export default AuthenticatedApi;

app/src/api/lnd.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import * as LND from 'types/generated/lnd_pb';
22
import { Lightning } from 'types/generated/lnd_pb_service';
3+
import AuthenticatedApi from './auth';
34
import GrpcClient from './grpc';
45

56
/**
67
* An API wrapper to communicate with the LND node via GRPC
78
*/
8-
class LndApi {
9-
9+
class LndApi extends AuthenticatedApi {
1010
private _grpc: GrpcClient;
1111

1212
constructor(grpc: GrpcClient) {
13+
super();
1314
this._grpc = grpc;
1415
}
1516

@@ -18,7 +19,7 @@ class LndApi {
1819
*/
1920
async getInfo(): Promise<LND.GetInfoResponse.AsObject> {
2021
const req = new LND.GetInfoRequest();
21-
const res = await this._grpc.request(Lightning.GetInfo, req);
22+
const res = await this._grpc.request(Lightning.GetInfo, req, this._meta);
2223
return res.toObject();
2324
}
2425

@@ -27,7 +28,7 @@ class LndApi {
2728
*/
2829
async channelBalance(): Promise<LND.ChannelBalanceResponse.AsObject> {
2930
const req = new LND.ChannelBalanceRequest();
30-
const res = await this._grpc.request(Lightning.ChannelBalance, req);
31+
const res = await this._grpc.request(Lightning.ChannelBalance, req, this._meta);
3132
return res.toObject();
3233
}
3334

@@ -36,7 +37,7 @@ class LndApi {
3637
*/
3738
async walletBalance(): Promise<LND.WalletBalanceResponse.AsObject> {
3839
const req = new LND.WalletBalanceRequest();
39-
const res = await this._grpc.request(Lightning.WalletBalance, req);
40+
const res = await this._grpc.request(Lightning.WalletBalance, req, this._meta);
4041
return res.toObject();
4142
}
4243

@@ -45,7 +46,7 @@ class LndApi {
4546
*/
4647
async listChannels(): Promise<LND.ListChannelsResponse.AsObject> {
4748
const req = new LND.ListChannelsRequest();
48-
const res = await this._grpc.request(Lightning.ListChannels, req);
49+
const res = await this._grpc.request(Lightning.ListChannels, req, this._meta);
4950
return res.toObject();
5051
}
5152
}

app/src/api/loop.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ import * as LOOP from 'types/generated/loop_pb';
22
import { SwapClient } from 'types/generated/loop_pb_service';
33
import { Quote } from 'types/state';
44
import Big from 'big.js';
5+
import AuthenticatedApi from './auth';
56
import GrpcClient from './grpc';
67

78
/**
89
* An API wrapper to communicate with the Loop daemon via GRPC
910
*/
10-
class LoopApi {
11-
11+
class LoopApi extends AuthenticatedApi {
1212
private _grpc: GrpcClient;
1313

1414
constructor(grpc: GrpcClient) {
15+
super();
1516
this._grpc = grpc;
1617
}
1718

@@ -20,7 +21,7 @@ class LoopApi {
2021
*/
2122
async listSwaps(): Promise<LOOP.ListSwapsResponse.AsObject> {
2223
const req = new LOOP.ListSwapsRequest();
23-
const res = await this._grpc.request(SwapClient.ListSwaps, req);
24+
const res = await this._grpc.request(SwapClient.ListSwaps, req, this._meta);
2425
return res.toObject();
2526
}
2627

@@ -29,7 +30,7 @@ class LoopApi {
2930
*/
3031
async getLoopInTerms(): Promise<LOOP.TermsResponse.AsObject> {
3132
const req = new LOOP.TermsRequest();
32-
const res = await this._grpc.request(SwapClient.GetLoopInTerms, req);
33+
const res = await this._grpc.request(SwapClient.GetLoopInTerms, req, this._meta);
3334
return res.toObject();
3435
}
3536

@@ -38,7 +39,7 @@ class LoopApi {
3839
*/
3940
async getLoopOutTerms(): Promise<LOOP.TermsResponse.AsObject> {
4041
const req = new LOOP.TermsRequest();
41-
const res = await this._grpc.request(SwapClient.LoopOutTerms, req);
42+
const res = await this._grpc.request(SwapClient.LoopOutTerms, req, this._meta);
4243
return res.toObject();
4344
}
4445

@@ -48,7 +49,7 @@ class LoopApi {
4849
async getLoopInQuote(amount: Big): Promise<LOOP.QuoteResponse.AsObject> {
4950
const req = new LOOP.QuoteRequest();
5051
req.setAmt(+amount);
51-
const res = await this._grpc.request(SwapClient.GetLoopInQuote, req);
52+
const res = await this._grpc.request(SwapClient.GetLoopInQuote, req, this._meta);
5253
return res.toObject();
5354
}
5455

@@ -58,7 +59,7 @@ class LoopApi {
5859
async getLoopOutQuote(amount: Big): Promise<LOOP.QuoteResponse.AsObject> {
5960
const req = new LOOP.QuoteRequest();
6061
req.setAmt(+amount);
61-
const res = await this._grpc.request(SwapClient.LoopOutQuote, req);
62+
const res = await this._grpc.request(SwapClient.LoopOutQuote, req, this._meta);
6263
return res.toObject();
6364
}
6465

@@ -75,7 +76,7 @@ class LoopApi {
7576
req.setMaxSwapFee(+quote.swapFee);
7677
req.setMaxMinerFee(+quote.minerFee);
7778
if (lastHop) req.setLastHop(Buffer.from(lastHop, 'hex').toString('base64'));
78-
const res = await this._grpc.request(SwapClient.LoopIn, req);
79+
const res = await this._grpc.request(SwapClient.LoopIn, req, this._meta);
7980
return res.toObject();
8081
}
8182

@@ -98,7 +99,7 @@ class LoopApi {
9899
req.setOutgoingChanSetList(chanIds);
99100
req.setSwapPublicationDeadline(deadline);
100101

101-
const res = await this._grpc.request(SwapClient.LoopOut, req);
102+
const res = await this._grpc.request(SwapClient.LoopOut, req, this._meta);
102103
return res.toObject();
103104
}
104105

app/src/components/auth/AuthPage.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const Styled = {
5555

5656
const AuthPage: React.FC = () => {
5757
const { l } = usePrefixedTranslation('cmps.auth.AuthPage');
58-
const { uiStore } = useStore();
58+
const store = useStore();
5959
const [pass, setPass] = useState('');
6060
const [error, setError] = useState('');
6161

@@ -64,17 +64,19 @@ const AuthPage: React.FC = () => {
6464
setError('');
6565
};
6666

67-
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
67+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
6868
e.preventDefault();
6969
try {
70-
if (!pass) throw new Error('oops, password is required');
71-
// TODO: send password to backend to validate
72-
uiStore.goToLoop();
73-
} catch (e) {
74-
setError(e.message);
70+
await store.authStore.login(pass);
71+
} catch (err) {
72+
setError(err.message);
7573
}
7674
};
7775

76+
// don't display the login UI until the app is fully initialized this prevents
77+
// a UI flicker while validating credentials stored in session storage
78+
if (!store.initialized) return null;
79+
7880
const { Wrapper, Logo, Title, Subtitle, Form, Label, ErrMessage, Submit } = Styled;
7981
return (
8082
<Wrapper>

app/src/store/store.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { observable } from 'mobx';
1+
import { observable, when } from 'mobx';
22
import { IS_DEV, IS_TEST } from 'config';
33
import AppStorage from 'util/appStorage';
44
import CsvExporter from 'util/csv';
@@ -12,6 +12,7 @@ import {
1212
SwapStore,
1313
UiStore,
1414
} from './stores';
15+
import AuthStore from './stores/authStore';
1516
import { PersistentSettings } from './stores/settingsStore';
1617

1718
/**
@@ -21,6 +22,7 @@ export class Store {
2122
//
2223
// Child Stores
2324
//
25+
authStore = new AuthStore(this);
2426
buildSwapStore = new BuildSwapStore(this);
2527
channelStore = new ChannelStore(this);
2628
swapStore = new SwapStore(this);
@@ -65,11 +67,22 @@ export class Store {
6567
*/
6668
async init() {
6769
this.settingsStore.init();
68-
await this.nodeStore.fetchInfo();
69-
await this.channelStore.fetchChannels();
70-
await this.swapStore.fetchSwaps();
71-
await this.nodeStore.fetchBalances();
70+
await this.authStore.init();
7271
this.initialized = true;
72+
73+
// go to the Loop page when the user is authenticated. it can be from
74+
// entering a password or from loading the credentials from storage
75+
when(
76+
() => this.authStore.authenticated,
77+
async () => {
78+
this.uiStore.goToLoop();
79+
// also fetch all the data we need
80+
await this.nodeStore.fetchInfo();
81+
await this.channelStore.fetchChannels();
82+
await this.swapStore.fetchSwaps();
83+
await this.nodeStore.fetchBalances();
84+
},
85+
);
7386
}
7487
}
7588

app/src/store/stores/authStore.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { action, observable, toJS } from 'mobx';
2+
import { Store } from 'store';
3+
4+
export default class AuthStore {
5+
private _store: Store;
6+
7+
/** true if the credentials have been validated by the backend */
8+
@observable authenticated = false;
9+
10+
/** the password encoded to base64 */
11+
@observable credentials = '';
12+
13+
constructor(store: Store) {
14+
this._store = store;
15+
}
16+
17+
/**
18+
* Updates the credentials in the store, session storage, and API wrappers
19+
* @param credentials the encoded password
20+
*/
21+
@action.bound
22+
setCredentials(credentials: string) {
23+
this.credentials = credentials;
24+
this._store.storage.setSession('credentials', this.credentials);
25+
this._store.api.lnd.setCredentials(credentials);
26+
this._store.api.loop.setCredentials(credentials);
27+
}
28+
29+
/**
30+
* Validate the supplied password and save for later if successful
31+
*/
32+
@action.bound
33+
async login(password: string) {
34+
this._store.log.info('attempting to login with password');
35+
if (!password) throw new Error('oops, password is required');
36+
37+
// encode the password and update the store
38+
const encoded = Buffer.from(`${password}:${password}`).toString('base64');
39+
this.setCredentials(encoded);
40+
this._store.log.info('saved credentials to sessionStorage');
41+
42+
try {
43+
// validate the the credentials are correct
44+
await this.validate();
45+
} catch (error) {
46+
// clear the credentials if incorrect
47+
this.setCredentials('');
48+
this._store.log.error('incorrect credentials');
49+
throw new Error('oops, that password is incorrect');
50+
}
51+
}
52+
53+
@action.bound
54+
async validate() {
55+
// test the credentials by making an API call to getInfo
56+
await this._store.api.lnd.getInfo();
57+
this._store.log.info('authentication successful', toJS(this));
58+
this.authenticated = true;
59+
}
60+
61+
/**
62+
* load and validate credentials from the browser's session storage
63+
*/
64+
@action.bound
65+
async init() {
66+
this._store.log.info('loading credentials from sessionStorage');
67+
const creds = this._store.storage.getSession('credentials');
68+
if (creds) {
69+
this.setCredentials(creds);
70+
this._store.log.info('found credentials. validating');
71+
try {
72+
// test the credentials by making an API call to getInfo
73+
await this.validate();
74+
} catch (error) {
75+
// clear the credentials and swallow the error
76+
this.setCredentials('');
77+
this._store.log.error('cleared invalid credentials in sessionStorage');
78+
}
79+
}
80+
}
81+
}

app/src/util/appStorage.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,17 @@ export default class AppStorage<T> {
1515
return JSON.parse(json) as T;
1616
}
1717
}
18+
/**
19+
* stores data in the browser session storage
20+
*/
21+
setSession(key: string, data: string) {
22+
sessionStorage.setItem(key, data);
23+
}
24+
25+
/**
26+
* retrieves data from the browser session storage
27+
*/
28+
getSession(key: string): string | undefined {
29+
return sessionStorage.getItem(key) || undefined;
30+
}
1831
}

0 commit comments

Comments
 (0)