Skip to content

Commit

Permalink
feat: add jwt authorization to supply chain example
Browse files Browse the repository at this point in the history
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 <e.izaguirre.equiza@accenture.com>
Signed-off-by: Peter Somogyvari <peter.somogyvari@accenture.com>
  • Loading branch information
elenaizaguirre authored and petermetz committed Apr 6, 2022
1 parent 932df10 commit a4f07f6
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 23 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ As blockchain technology proliferates, blockchain integration will become an inc
-p 4000:4000 \
-p 4100:4100 \
-p 4200:4200 \
ghcr.io/hyperledger/cactus-example-supply-chain-app:2021-09-08--docs-1312
ghcr.io/hyperledger/cactus-example-supply-chain-app:2022-04-05--feat-1579
```
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3100`
4. Visit http://localhost:3100 in a web browser with Javascript enabled
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.
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3200`
4. Token generated by the application is displayed below
5. Visit http://localhost:3200 in a web browser with Javascript enabled and insert the token when prompted
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.

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { AddressInfo } from "net";
import { Server } from "http";

import { exportPKCS8, generateKeyPair, KeyLike, exportSPKI } from "jose";
import {
exportPKCS8,
generateKeyPair,
KeyLike,
exportSPKI,
SignJWT,
} from "jose";
import expressJwt from "express-jwt";

import { v4 as uuidv4 } from "uuid";
import exitHook, { IAsyncExitHookDoneCallback } from "async-exit-hook";

Expand All @@ -24,7 +32,12 @@ import {
Servers,
} from "@hyperledger/cactus-common";

import { ApiServer, ConfigService } from "@hyperledger/cactus-cmd-api-server";
import {
ApiServer,
AuthorizationProtocol,
ConfigService,
IAuthorizationConfig,
} from "@hyperledger/cactus-cmd-api-server";

import { PluginConsortiumManual } from "@hyperledger/cactus-plugin-consortium-manual";
import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory";
Expand Down Expand Up @@ -73,6 +86,8 @@ export class SupplyChainApp {
private _besuApiClient?: BesuApi;
private _quorumApiClient?: QuorumApi;
private _fabricApiClient?: FabricApi;
private authorizationConfig?: IAuthorizationConfig;
private token?: string;

public get besuApiClientOrThrow(): BesuApi {
if (this._besuApiClient) {
Expand Down Expand Up @@ -130,6 +145,48 @@ export class SupplyChainApp {
this.shutdownHooks = [];
}

async getOrCreateToken(): Promise<string> {
if (!this.token) {
await this.createAuthorizationConfig();
}
return this.token as string;
}

async getOrCreateAuthorizationConfig(): Promise<IAuthorizationConfig> {
if (!this.authorizationConfig) {
await this.createAuthorizationConfig();
}
return this.authorizationConfig as IAuthorizationConfig;
}

async createAuthorizationConfig(): Promise<void> {
const jwtKeyPair = await generateKeyPair("RS256", { modulusLength: 4096 });
const jwtPrivateKeyPem = await exportPKCS8(jwtKeyPair.privateKey);
const expressJwtOptions: expressJwt.Options = {
algorithms: ["RS256"],
secret: jwtPrivateKeyPem,
audience: uuidv4(),
issuer: uuidv4(),
};

const jwtPayload = { name: "Peter", location: "London" };
this.token = await new SignJWT(jwtPayload)
.setProtectedHeader({
alg: "RS256",
})
.setIssuer(expressJwtOptions.issuer)
.setAudience(expressJwtOptions.audience)
.sign(jwtKeyPair.privateKey);

this.authorizationConfig = {
unprotectedEndpointExemptions: [],
expressJwtOptions,
socketIoJwtOptions: {
secret: jwtPrivateKeyPem,
},
};
}

public async start(): Promise<IStartInfo> {
this.log.debug(`Starting SupplyChainApp...`);

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

const besuConfig = new Configuration({ basePath: nodeApiHostA });
const quorumConfig = new Configuration({ basePath: nodeApiHostB });
const fabricConfig = new Configuration({ basePath: nodeApiHostC });
const token = await this.getOrCreateToken();
const baseOptions = { headers: { Authorization: `Bearer ${token}` } };

const besuConfig = new Configuration({
basePath: nodeApiHostA,
baseOptions,
});
const quorumConfig = new Configuration({
basePath: nodeApiHostB,
baseOptions,
});
const fabricConfig = new Configuration({
basePath: nodeApiHostC,
baseOptions,
});

const besuApiClient = new BesuApi(besuConfig);
const quorumApiClient = new QuorumApi(quorumConfig);
Expand Down Expand Up @@ -217,6 +286,11 @@ export class SupplyChainApp {
consortiumDatabase,
keyPairPem: keyPairPemA,
logLevel: this.options.logLevel,
ctorArgs: {
baseOptions: {
headers: { Authorization: `Bearer ${token}` },
},
},
}),
new SupplyChainCactusPlugin({
logLevel: this.options.logLevel,
Expand Down Expand Up @@ -256,6 +330,11 @@ export class SupplyChainApp {
consortiumDatabase,
keyPairPem: keyPairPemB,
logLevel: this.options.logLevel,
ctorArgs: {
baseOptions: {
headers: { Authorization: `Bearer ${token}` },
},
},
}),
new SupplyChainCactusPlugin({
logLevel: this.options.logLevel,
Expand Down Expand Up @@ -294,6 +373,11 @@ export class SupplyChainApp {
consortiumDatabase,
keyPairPem: keyPairPemC,
logLevel: "INFO",
ctorArgs: {
baseOptions: {
headers: { Authorization: `Bearer ${token}` },
},
},
}),
new SupplyChainCactusPlugin({
logLevel: "INFO",
Expand Down Expand Up @@ -333,6 +417,8 @@ export class SupplyChainApp {

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

this.log.info(`JWT generated by the application: ${token}`);

return {
apiServerA,
apiServerB,
Expand All @@ -341,13 +427,13 @@ export class SupplyChainApp {
fabricApiClient,
quorumApiClient,
supplyChainApiClientA: new SupplyChainApi(
new Configuration({ basePath: nodeApiHostA }),
new Configuration({ basePath: nodeApiHostA, baseOptions }),
),
supplyChainApiClientB: new SupplyChainApi(
new Configuration({ basePath: nodeApiHostA }),
new Configuration({ basePath: nodeApiHostA, baseOptions }),
),
supplyChainApiClientC: new SupplyChainApi(
new Configuration({ basePath: nodeApiHostA }),
new Configuration({ basePath: nodeApiHostA, baseOptions }),
),
};
}
Expand Down Expand Up @@ -498,6 +584,8 @@ export class SupplyChainApp {
properties.cockpitPort = addressInfoCockpit.port;
properties.grpcPort = 0; // TODO - make this configurable as well
properties.logLevel = this.options.logLevel || "INFO";
properties.authorizationProtocol = AuthorizationProtocol.JSON_WEB_TOKEN;
properties.authorizationConfigJson = await this.getOrCreateAuthorizationConfig();

const apiServer = new ApiServer({
config: properties,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,26 @@ import {
FABRIC_DEMO_LEDGER_ID,
} from "src/constants";
import { ApiClient } from "@hyperledger/cactus-api-client";
import { AuthConfig } from "./common/auth-config";

LoggerProvider.setLogLevel("TRACE");

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

let token = "";
while (token == "") {
token = window.prompt("Introduce the token generated by the application");
}
AuthConfig.authToken = token;
log.info(`Inserted token: ${AuthConfig.authToken}`);

log.info("Running AppModule...");
const cactusApiUrl = location.origin;
log.info("Instantiating ApiClient with CACTUS_API_URL=%o", cactusApiUrl);
const configuration = new Configuration({ basePath: cactusApiUrl });
const configuration = new Configuration({
basePath: cactusApiUrl,
baseOptions: { headers: { Authorization: `Bearer ${AuthConfig.authToken}` } },
});
const apiClient = new ApiClient(configuration);

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { QUORUM_DEMO_LEDGER_ID } from "../../../constants";
import { BambooHarvestDetailPage } from "../bamboo-harvest-detail/bamboo-harvest-detail.page";
import { ModalController } from "@ionic/angular";

import { AuthConfig } from "../../common/auth-config";

@Component({
selector: "app-bamboo-harvest-list",
templateUrl: "./bamboo-harvest-list.page.html",
Expand Down Expand Up @@ -42,7 +44,11 @@ export class BambooHarvestListPage implements OnInit {
this._supplyChainApi = await this.baseClient.ofLedger(
this.quorumLedgerId,
SupplyChainApi,
{},
{
baseOptions: {
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
},
},
);
await this.loadData();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import { Logger, LoggerProvider } from "@hyperledger/cactus-common";
import { QUORUM_DEMO_LEDGER_ID } from "src/constants";

import { AuthConfig } from "../../common/auth-config";

@Component({
selector: "app-bookshelf-detail",
templateUrl: "./bookshelf-detail.page.html",
Expand Down Expand Up @@ -52,7 +54,11 @@ export class BookshelfDetailPage implements OnInit {
this._supplyChainApi = await this.baseClient.ofLedger(
this.quorumLedgerId,
SupplyChainApi,
{},
{
baseOptions: {
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
},
},
);

if (!this.bookshelf) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { BESU_DEMO_LEDGER_ID } from "../../../constants";
import { BookshelfDetailPage } from "../bookshelf-detail/bookshelf-detail.page";
import { ModalController } from "@ionic/angular";

import { AuthConfig } from "../../common/auth-config";

@Component({
selector: "app-bookshelf-list",
templateUrl: "./bookshelf-list.page.html",
Expand Down Expand Up @@ -42,7 +44,11 @@ export class BookshelfListPage implements OnInit {
this._supplyChainApi = await this.baseClient.ofLedger(
this.ledgerId,
SupplyChainApi,
{},
{
baseOptions: {
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
},
},
);
await this.loadData();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class AuthConfig {
static authToken: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
import { Logger, LoggerProvider } from "@hyperledger/cactus-common";
import { QUORUM_DEMO_LEDGER_ID } from "src/constants";

import { AuthConfig } from "../../common/auth-config";

@Component({
selector: "app-shipment-detail",
templateUrl: "./shipment-detail.page.html",
Expand Down Expand Up @@ -52,7 +54,11 @@ export class ShipmentDetailPage implements OnInit {
this._supplyChainApi = await this.baseClient.ofLedger(
this.quorumLedgerId,
SupplyChainApi,
{},
{
baseOptions: {
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
},
},
);

if (!this.shipment) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { BESU_DEMO_LEDGER_ID } from "../../../constants";
import { ShipmentDetailPage } from "../shipment-detail/shipment-detail.page";
import { ModalController } from "@ionic/angular";

import { AuthConfig } from "../../common/auth-config";

@Component({
selector: "app-shipment-list",
templateUrl: "./shipment-list.page.html",
Expand Down Expand Up @@ -42,7 +44,11 @@ export class ShipmentListPage implements OnInit {
this._supplyChainApi = await this.baseClient.ofLedger(
this.ledgerId,
SupplyChainApi,
{},
{
baseOptions: {
headers: { Authorization: `Bearer ${AuthConfig.authToken}` },
},
},
);
await this.loadData();
}
Expand Down
22 changes: 17 additions & 5 deletions examples/supply-chain-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,20 @@
-p 4000:4000 \
-p 4100:4100 \
-p 4200:4200 \
ghcr.io/hyperledger/cactus-example-supply-chain-app:2021-09-08--docs-1312
ghcr.io/hyperledger/cactus-example-supply-chain-app:2022-04-05--feat-1579
```
2. Observe the example application pulling up in the logs
1. the test ledger containers,
2. a test consortium with multiple members and their Cactus nodes
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3100`
4. Visit http://0.0.0.0:3100 in your web browser with Javascript enabled
3. Wait for the output to show the message `INFO (api-server): Cactus Cockpit reachable http://0.0.0.0:3200`
4. Visit http://0.0.0.0:3200 in your web browser with Javascript enabled

## Building and running the container locally

```sh
# Change directories to the project root

# Build the dockar image and tag it as "scaeb" for supply chain app example backend
# Build the docker image and tag it as "scaeb" for supply chain app example backend
DOCKER_BUILDKIT=1 docker build -f ./examples/supply-chain-app/Dockerfile . -t scaeb

# Run the built image with ports mapped to the host machine as you see fit
Expand All @@ -38,6 +38,16 @@ DOCKER_BUILDKIT=1 docker build -f ./examples/supply-chain-app/Dockerfile . -t sc
docker run --rm -it --privileged -p 3000:3000 -p 3100:3100 -p 3200:3200 -p 4000:4000 -p 4100:4100 -p 4200:4200 scaeb
```

Building the image with a specific npm package version:

```sh
DOCKER_BUILDKIT=1 docker build \
--build-arg NPM_PKG_VERSION=jwt-supply-chain \
--file ./examples/supply-chain-app/Dockerfile \
--tag scaeb \
./
```

## Running the Example Application Locally

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

0 comments on commit a4f07f6

Please sign in to comment.