Skip to content

Commit a4f07f6

Browse files
elenaizaguirrepetermetz
authored andcommitted
feat: add jwt authorization to supply chain example
1. The JWT token to be used is printed by the Supply Chain App after it successfully bootstrapped itself (pulled up the ledgers, API server) 2. The web application uses a native window prompt for getting the token which might need to be refactored in the future because there are ideas floating around on the internet that the window prompt/alert APIs should be discontinued in web browsers altogether. 3. The image built from this source code has been pushed to ghcr.io as: ghcr.io/hyperledger/cactus-example-supply-chain-app:2022-04-05--feat-1579 closes #1579 Signed-off-by: Elena Izaguirre <[email protected]> Signed-off-by: Peter Somogyvari <[email protected]>
1 parent 932df10 commit a4f07f6

File tree

10 files changed

+168
-23
lines changed

10 files changed

+168
-23
lines changed

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ As blockchain technology proliferates, blockchain integration will become an inc
3232
-p 4000:4000 \
3333
-p 4100:4100 \
3434
-p 4200:4200 \
35-
ghcr.io/hyperledger/cactus-example-supply-chain-app:2021-09-08--docs-1312
35+
ghcr.io/hyperledger/cactus-example-supply-chain-app:2022-04-05--feat-1579
3636
```
37-
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3100`
38-
4. Visit http://localhost:3100 in a web browser with Javascript enabled
39-
5. Use the graphical user interface to create data on both ledgers and observe that a consistent view of the data from different ledgers is provided.
37+
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3200`
38+
4. Token generated by the application is displayed below
39+
5. Visit http://localhost:3200 in a web browser with Javascript enabled and insert the token when prompted
40+
6. Use the graphical user interface to create data on both ledgers and observe that a consistent view of the data from different ledgers is provided.
4041

4142
Once the last command has finished executing, open link printed on the console with a web browser of your choice
4243

examples/cactus-example-supply-chain-backend/src/main/typescript/supply-chain-app.ts

+96-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { AddressInfo } from "net";
22
import { Server } from "http";
33

4-
import { exportPKCS8, generateKeyPair, KeyLike, exportSPKI } from "jose";
4+
import {
5+
exportPKCS8,
6+
generateKeyPair,
7+
KeyLike,
8+
exportSPKI,
9+
SignJWT,
10+
} from "jose";
11+
import expressJwt from "express-jwt";
12+
513
import { v4 as uuidv4 } from "uuid";
614
import exitHook, { IAsyncExitHookDoneCallback } from "async-exit-hook";
715

@@ -24,7 +32,12 @@ import {
2432
Servers,
2533
} from "@hyperledger/cactus-common";
2634

27-
import { ApiServer, ConfigService } from "@hyperledger/cactus-cmd-api-server";
35+
import {
36+
ApiServer,
37+
AuthorizationProtocol,
38+
ConfigService,
39+
IAuthorizationConfig,
40+
} from "@hyperledger/cactus-cmd-api-server";
2841

2942
import { PluginConsortiumManual } from "@hyperledger/cactus-plugin-consortium-manual";
3043
import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory";
@@ -73,6 +86,8 @@ export class SupplyChainApp {
7386
private _besuApiClient?: BesuApi;
7487
private _quorumApiClient?: QuorumApi;
7588
private _fabricApiClient?: FabricApi;
89+
private authorizationConfig?: IAuthorizationConfig;
90+
private token?: string;
7691

7792
public get besuApiClientOrThrow(): BesuApi {
7893
if (this._besuApiClient) {
@@ -130,6 +145,48 @@ export class SupplyChainApp {
130145
this.shutdownHooks = [];
131146
}
132147

148+
async getOrCreateToken(): Promise<string> {
149+
if (!this.token) {
150+
await this.createAuthorizationConfig();
151+
}
152+
return this.token as string;
153+
}
154+
155+
async getOrCreateAuthorizationConfig(): Promise<IAuthorizationConfig> {
156+
if (!this.authorizationConfig) {
157+
await this.createAuthorizationConfig();
158+
}
159+
return this.authorizationConfig as IAuthorizationConfig;
160+
}
161+
162+
async createAuthorizationConfig(): Promise<void> {
163+
const jwtKeyPair = await generateKeyPair("RS256", { modulusLength: 4096 });
164+
const jwtPrivateKeyPem = await exportPKCS8(jwtKeyPair.privateKey);
165+
const expressJwtOptions: expressJwt.Options = {
166+
algorithms: ["RS256"],
167+
secret: jwtPrivateKeyPem,
168+
audience: uuidv4(),
169+
issuer: uuidv4(),
170+
};
171+
172+
const jwtPayload = { name: "Peter", location: "London" };
173+
this.token = await new SignJWT(jwtPayload)
174+
.setProtectedHeader({
175+
alg: "RS256",
176+
})
177+
.setIssuer(expressJwtOptions.issuer)
178+
.setAudience(expressJwtOptions.audience)
179+
.sign(jwtKeyPair.privateKey);
180+
181+
this.authorizationConfig = {
182+
unprotectedEndpointExemptions: [],
183+
expressJwtOptions,
184+
socketIoJwtOptions: {
185+
secret: jwtPrivateKeyPem,
186+
},
187+
};
188+
}
189+
133190
public async start(): Promise<IStartInfo> {
134191
this.log.debug(`Starting SupplyChainApp...`);
135192

@@ -174,9 +231,21 @@ export class SupplyChainApp {
174231
const addressInfoC = httpApiC.address() as AddressInfo;
175232
const nodeApiHostC = `http://localhost:${addressInfoC.port}`;
176233

177-
const besuConfig = new Configuration({ basePath: nodeApiHostA });
178-
const quorumConfig = new Configuration({ basePath: nodeApiHostB });
179-
const fabricConfig = new Configuration({ basePath: nodeApiHostC });
234+
const token = await this.getOrCreateToken();
235+
const baseOptions = { headers: { Authorization: `Bearer ${token}` } };
236+
237+
const besuConfig = new Configuration({
238+
basePath: nodeApiHostA,
239+
baseOptions,
240+
});
241+
const quorumConfig = new Configuration({
242+
basePath: nodeApiHostB,
243+
baseOptions,
244+
});
245+
const fabricConfig = new Configuration({
246+
basePath: nodeApiHostC,
247+
baseOptions,
248+
});
180249

181250
const besuApiClient = new BesuApi(besuConfig);
182251
const quorumApiClient = new QuorumApi(quorumConfig);
@@ -217,6 +286,11 @@ export class SupplyChainApp {
217286
consortiumDatabase,
218287
keyPairPem: keyPairPemA,
219288
logLevel: this.options.logLevel,
289+
ctorArgs: {
290+
baseOptions: {
291+
headers: { Authorization: `Bearer ${token}` },
292+
},
293+
},
220294
}),
221295
new SupplyChainCactusPlugin({
222296
logLevel: this.options.logLevel,
@@ -256,6 +330,11 @@ export class SupplyChainApp {
256330
consortiumDatabase,
257331
keyPairPem: keyPairPemB,
258332
logLevel: this.options.logLevel,
333+
ctorArgs: {
334+
baseOptions: {
335+
headers: { Authorization: `Bearer ${token}` },
336+
},
337+
},
259338
}),
260339
new SupplyChainCactusPlugin({
261340
logLevel: this.options.logLevel,
@@ -294,6 +373,11 @@ export class SupplyChainApp {
294373
consortiumDatabase,
295374
keyPairPem: keyPairPemC,
296375
logLevel: "INFO",
376+
ctorArgs: {
377+
baseOptions: {
378+
headers: { Authorization: `Bearer ${token}` },
379+
},
380+
},
297381
}),
298382
new SupplyChainCactusPlugin({
299383
logLevel: "INFO",
@@ -333,6 +417,8 @@ export class SupplyChainApp {
333417

334418
const apiServerC = await this.startNode(httpApiC, httpGuiC, registryC);
335419

420+
this.log.info(`JWT generated by the application: ${token}`);
421+
336422
return {
337423
apiServerA,
338424
apiServerB,
@@ -341,13 +427,13 @@ export class SupplyChainApp {
341427
fabricApiClient,
342428
quorumApiClient,
343429
supplyChainApiClientA: new SupplyChainApi(
344-
new Configuration({ basePath: nodeApiHostA }),
430+
new Configuration({ basePath: nodeApiHostA, baseOptions }),
345431
),
346432
supplyChainApiClientB: new SupplyChainApi(
347-
new Configuration({ basePath: nodeApiHostA }),
433+
new Configuration({ basePath: nodeApiHostA, baseOptions }),
348434
),
349435
supplyChainApiClientC: new SupplyChainApi(
350-
new Configuration({ basePath: nodeApiHostA }),
436+
new Configuration({ basePath: nodeApiHostA, baseOptions }),
351437
),
352438
};
353439
}
@@ -498,6 +584,8 @@ export class SupplyChainApp {
498584
properties.cockpitPort = addressInfoCockpit.port;
499585
properties.grpcPort = 0; // TODO - make this configurable as well
500586
properties.logLevel = this.options.logLevel || "INFO";
587+
properties.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN;
588+
properties.authorizationConfigJson = await this.getOrCreateAuthorizationConfig();
501589

502590
const apiServer = new ApiServer({
503591
config: properties,

examples/cactus-example-supply-chain-frontend/src/app/app.module.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,26 @@ import {
1717
FABRIC_DEMO_LEDGER_ID,
1818
} from "src/constants";
1919
import { ApiClient } from "@hyperledger/cactus-api-client";
20+
import { AuthConfig } from "./common/auth-config";
2021

2122
LoggerProvider.setLogLevel("TRACE");
2223

2324
const log: Logger = LoggerProvider.getOrCreate({ label: "app-module" });
2425

26+
let token = "";
27+
while (token == "") {
28+
token = window.prompt("Introduce the token generated by the application");
29+
}
30+
AuthConfig.authToken = token;
31+
log.info(`Inserted token: ${AuthConfig.authToken}`);
32+
2533
log.info("Running AppModule...");
2634
const cactusApiUrl = location.origin;
2735
log.info("Instantiating ApiClient with CACTUS_API_URL=%o", cactusApiUrl);
28-
const configuration = new Configuration({ basePath: cactusApiUrl });
36+
const configuration = new Configuration({
37+
basePath: cactusApiUrl,
38+
baseOptions: { headers: { Authorization: `Bearer ${AuthConfig.authToken}` } },
39+
});
2940
const apiClient = new ApiClient(configuration);
3041

3142
@NgModule({

examples/cactus-example-supply-chain-frontend/src/app/bamboo-harvest/bamboo-harvest-list/bamboo-harvest-list.page.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { QUORUM_DEMO_LEDGER_ID } from "../../../constants";
1111
import { BambooHarvestDetailPage } from "../bamboo-harvest-detail/bamboo-harvest-detail.page";
1212
import { ModalController } from "@ionic/angular";
1313

14+
import { AuthConfig } from "../../common/auth-config";
15+
1416
@Component({
1517
selector: "app-bamboo-harvest-list",
1618
templateUrl: "./bamboo-harvest-list.page.html",
@@ -42,7 +44,11 @@ export class BambooHarvestListPage implements OnInit {
4244
this._supplyChainApi = await this.baseClient.ofLedger(
4345
this.quorumLedgerId,
4446
SupplyChainApi,
45-
{},
47+
{
48+
baseOptions: {
49+
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
50+
},
51+
},
4652
);
4753
await this.loadData();
4854
}

examples/cactus-example-supply-chain-frontend/src/app/bookshelf/bookshelf-detail/bookshelf-detail.page.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
import { Logger, LoggerProvider } from "@hyperledger/cactus-common";
1616
import { QUORUM_DEMO_LEDGER_ID } from "src/constants";
1717

18+
import { AuthConfig } from "../../common/auth-config";
19+
1820
@Component({
1921
selector: "app-bookshelf-detail",
2022
templateUrl: "./bookshelf-detail.page.html",
@@ -52,7 +54,11 @@ export class BookshelfDetailPage implements OnInit {
5254
this._supplyChainApi = await this.baseClient.ofLedger(
5355
this.quorumLedgerId,
5456
SupplyChainApi,
55-
{},
57+
{
58+
baseOptions: {
59+
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
60+
},
61+
},
5662
);
5763

5864
if (!this.bookshelf) {

examples/cactus-example-supply-chain-frontend/src/app/bookshelf/bookshelf-list/bookshelf-list.page.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { BESU_DEMO_LEDGER_ID } from "../../../constants";
1111
import { BookshelfDetailPage } from "../bookshelf-detail/bookshelf-detail.page";
1212
import { ModalController } from "@ionic/angular";
1313

14+
import { AuthConfig } from "../../common/auth-config";
15+
1416
@Component({
1517
selector: "app-bookshelf-list",
1618
templateUrl: "./bookshelf-list.page.html",
@@ -42,7 +44,11 @@ export class BookshelfListPage implements OnInit {
4244
this._supplyChainApi = await this.baseClient.ofLedger(
4345
this.ledgerId,
4446
SupplyChainApi,
45-
{},
47+
{
48+
baseOptions: {
49+
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
50+
},
51+
},
4652
);
4753
await this.loadData();
4854
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export class AuthConfig {
2+
static authToken: string;
3+
}

examples/cactus-example-supply-chain-frontend/src/app/shipment/shipment-detail/shipment-detail.page.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
import { Logger, LoggerProvider } from "@hyperledger/cactus-common";
1616
import { QUORUM_DEMO_LEDGER_ID } from "src/constants";
1717

18+
import { AuthConfig } from "../../common/auth-config";
19+
1820
@Component({
1921
selector: "app-shipment-detail",
2022
templateUrl: "./shipment-detail.page.html",
@@ -52,7 +54,11 @@ export class ShipmentDetailPage implements OnInit {
5254
this._supplyChainApi = await this.baseClient.ofLedger(
5355
this.quorumLedgerId,
5456
SupplyChainApi,
55-
{},
57+
{
58+
baseOptions: {
59+
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
60+
},
61+
},
5662
);
5763

5864
if (!this.shipment) {

examples/cactus-example-supply-chain-frontend/src/app/shipment/shipment-list/shipment-list.page.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { BESU_DEMO_LEDGER_ID } from "../../../constants";
1111
import { ShipmentDetailPage } from "../shipment-detail/shipment-detail.page";
1212
import { ModalController } from "@ionic/angular";
1313

14+
import { AuthConfig } from "../../common/auth-config";
15+
1416
@Component({
1517
selector: "app-shipment-list",
1618
templateUrl: "./shipment-list.page.html",
@@ -42,7 +44,11 @@ export class ShipmentListPage implements OnInit {
4244
this._supplyChainApi = await this.baseClient.ofLedger(
4345
this.ledgerId,
4446
SupplyChainApi,
45-
{},
47+
{
48+
baseOptions: {
49+
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
50+
},
51+
},
4652
);
4753
await this.loadData();
4854
}

examples/supply-chain-app/README.md

+17-5
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,20 @@
1414
-p 4000:4000 \
1515
-p 4100:4100 \
1616
-p 4200:4200 \
17-
ghcr.io/hyperledger/cactus-example-supply-chain-app:2021-09-08--docs-1312
17+
ghcr.io/hyperledger/cactus-example-supply-chain-app:2022-04-05--feat-1579
1818
```
1919
2. Observe the example application pulling up in the logs
2020
1. the test ledger containers,
2121
2. a test consortium with multiple members and their Cactus nodes
22-
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3100`
23-
4. Visit http://0.0.0.0:3100 in your web browser with Javascript enabled
22+
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3200`
23+
4. Visit http://0.0.0.0:3200 in your web browser with Javascript enabled
2424

2525
## Building and running the container locally
2626

2727
```sh
2828
# Change directories to the project root
2929
30-
# Build the dockar image and tag it as "scaeb" for supply chain app example backend
30+
# Build the docker image and tag it as "scaeb" for supply chain app example backend
3131
DOCKER_BUILDKIT=1 docker build -f ./examples/supply-chain-app/Dockerfile . -t scaeb
3232
3333
# Run the built image with ports mapped to the host machine as you see fit
@@ -38,6 +38,16 @@ DOCKER_BUILDKIT=1 docker build -f ./examples/supply-chain-app/Dockerfile . -t sc
3838
docker run --rm -it --privileged -p 3000:3000 -p 3100:3100 -p 3200:3200 -p 4000:4000 -p 4100:4100 -p 4200:4200 scaeb
3939
```
4040

41+
Building the image with a specific npm package version:
42+
43+
```sh
44+
DOCKER_BUILDKIT=1 docker build \
45+
--build-arg NPM_PKG_VERSION=jwt-supply-chain \
46+
--file ./examples/supply-chain-app/Dockerfile \
47+
--tag scaeb \
48+
./
49+
```
50+
4151
## Running the Example Application Locally
4252

4353
> Make sure you have all the dependencies set up as explained in `BUILD.md`
@@ -61,4 +71,6 @@ On the terminal, issue the following commands (steps 1 to 6) and then perform th
6171
7. Locate the `.vscode/template.launch.json` file
6272
8. Within that file locate the entry named `"Example: Supply Chain App"`
6373
9. Copy the VSCode debug definition object from 2) to your `.vscode/launch.json` file
64-
10. At this point the VSCode `Run and Debug` panel on the left should have an option also titled `"Example: Supply Chain App"` which
74+
10. At this point the VSCode `Run and Debug` panel on the left should have an option also titled `"Example: Supply Chain App"` which starts the application
75+
11. When the application finishes loading, token generated is displayed on the terminal
76+
12. Visit http://localhost:3200 in a web browser with Javascript enabled and insert the token when prompted

0 commit comments

Comments
 (0)