Skip to content

Commit 55e897b

Browse files
authored
Express sample (#4)
* add ams-expressjs-shopping project * set-up separate github workflows for respective sample folders
1 parent 1cb4909 commit 55e897b

26 files changed

+6726
-2
lines changed

.github/workflows/build_and_test_cap.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
22
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
33

4-
name: CAP sample - Build and Test
4+
name: CAP sample
55

66
on:
77
push:
88
branches: [ "main" ]
9+
paths:
10+
- "ams-cap-nodejs-bookshop/**"
11+
- ".github/workflows/build_and_test_cap.yml"
912
pull_request:
1013
branches: [ "**" ]
14+
paths:
15+
- "ams-cap-nodejs-bookshop/**"
16+
- ".github/workflows/build_and_test_cap.yml"
1117

1218
jobs:
1319
npm:
@@ -16,7 +22,7 @@ jobs:
1622
JAVA_HOME: /usr/lib/jvm/java-17-openjdk
1723
strategy:
1824
matrix:
19-
node-version: [18.x, 20.x, 22.x]
25+
node-version: [20.x, 22.x]
2026
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
2127

2228
steps:
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3+
4+
name: Express sample
5+
6+
on:
7+
push:
8+
branches: [ "main" ]
9+
paths:
10+
- "ams-expressjs-shopping/**"
11+
- ".github/workflows/build_and_test_expressjs.yml"
12+
pull_request:
13+
branches: [ "**" ]
14+
paths:
15+
- "ams-expressjs-shopping/**"
16+
- ".github/workflows/build_and_test_expressjs.yml"
17+
18+
jobs:
19+
npm:
20+
runs-on: [ ubuntu-latest ]
21+
env:
22+
JAVA_HOME: /usr/lib/jvm/java-17-openjdk
23+
strategy:
24+
matrix:
25+
node-version: [20.x, 22.x]
26+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
27+
28+
steps:
29+
- uses: actions/checkout@v4
30+
- name: Use Node.js ${{ matrix.node-version }}
31+
uses: actions/setup-node@v4
32+
with:
33+
node-version: ${{ matrix.node-version }}
34+
cache: 'npm'
35+
cache-dependency-path: ams-expressjs-shopping
36+
- name: Set up JDK 17 # for DCL -> DCN compilation to work before tests
37+
uses: actions/setup-java@v4
38+
with:
39+
java-version: '17'
40+
distribution: 'temurin'
41+
- run: npm ci
42+
working-directory: ams-expressjs-shopping
43+
- run: npm test
44+
working-directory: ams-expressjs-shopping

ams-expressjs-shopping/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
3+
test/dcn

ams-expressjs-shopping/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# SAP Identity Service Authentication and Authorization Sample
2+
3+
This project is a sample application that demonstrates how to use SAP Identity Service for authentication and authorization via the client libraries [`@sap/xssec`](https://www.npmjs.com/package/@sap/xssec) and [`@sap/ams`](https://www.npmjs.com/package/@sap/ams).
4+
5+
---
6+
7+
## Demonstrated Features
8+
9+
1. **Authentication via IAS (Identity Authentication Service)**:
10+
- Uses the `@sap/xssec` library to authenticate users via SAP Identity Service.
11+
12+
1. **Authorization via AMS (Authorization Management Service)**:
13+
- Uses the `@sap/ams` library to check user privileges for specific actions and resources and provide instance-based authorization.
14+
- Demonstrates how to configure and use the `IdentityServiceAuthProvider` for authorization based on IAS tokens.
15+
16+
1. **Middleware Integration**:
17+
- Demonstrates how to integrate the client libraries into an Express application for seamless access to the security context and authorization checks in any place where the `req` object is available.
18+
19+
1. **Technical communication** via SAP Identity Service APIs
20+
- Demonstrates how the `IdentityServiceAuthProvider` can authorize principal propagation requests from other systems that consume SAP Identity Service APIs of this application. The resulting authorizations are the intersection of the user's policies and the policies defined for the consumed API.
21+
22+
1. **Mocking security contexts for Testing**:
23+
- Shows how different security contexts can be mocked for testing without authenticating via a real IAS instance.
24+
- Demonstrates how to compile DCL policies locally to DCN and use users with mocked policy assignments for testing without a real AMS instance.
25+
26+
1. **Privilege-Based UI Rendering**:
27+
- Includes an example of how to retrieve potential user privileges to determine which UI elements to display.
28+
29+
---
30+
31+
## Project Structure
32+
33+
- **`server.js`**:
34+
- The main application server setup.
35+
36+
- **`auth/authenticate.js` and `auth/authorize.js`**:
37+
- Contains the main logic for setting up authentication and authorization.
38+
39+
- **`auth/dcl`**:
40+
- Defines the authorization policies.
41+
42+
- **`db`**:
43+
- A mock database with dummy data.
44+
45+
- **`service`**:
46+
- Service layer of the application with request handlers for the REST API.
47+
48+
- **`ui`**:
49+
- A simple UI to demonstrate features of this project when logged in with users that have different policies assigned.
50+
51+
- **`test`**:
52+
- Tests against the REST API that demonstrate the expected result of the authorization checks.
53+
54+
---
55+
56+
## Testing
57+
58+
### Run Unit Tests
59+
```bash
60+
npm i
61+
npm test
62+
```
63+
64+
### Start server for local testing
65+
```bash
66+
NODE_ENV=test npm start
67+
```
68+
69+
The application UI will be accessible under https://localhost:3000.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const TECHNICAL_USER_APIS = [
2+
"GetProducts"
3+
]
4+
5+
const PRINCIPAL_PROPAGATION_APIS = [
6+
"GetProducts",
7+
"ExternalOrder"
8+
]
9+
10+
function mapTechnicalUserApi(api) {
11+
if (TECHNICAL_USER_APIS.includes(api)) {
12+
return `internal.${api}`;
13+
}
14+
}
15+
16+
function mapPrincipalPropagationApi(api) {
17+
if (PRINCIPAL_PROPAGATION_APIS.includes(api)) {
18+
return `internal.${api}`;
19+
}
20+
}
21+
22+
module.exports = {
23+
mapTechnicalUserApi,
24+
mapPrincipalPropagationApi
25+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const { SECURITY_CONTEXT } = require('@sap/xssec'); // USER_AGENT is just a placeholder during development until the SECURITY_CONTEXT Symbol is available
2+
const { createSecurityContext, IdentityService, IdentityServiceToken, IdentityServiceSecurityContext, errors: { ValidationError } } = require('@sap/xssec');
3+
4+
// --- Sets up AUTHENTICATION as pre-condition for AUTHORIZATION ---
5+
6+
const authenticate = process.env.NODE_ENV === 'test' ? buildMockAuthMiddleware() : buildAuthMiddleware();
7+
8+
/*
9+
Middleware for SAP Identity Service based authentication
10+
following the example in the @sap/xssec documentation:
11+
https://www.npmjs.com/package/@sap/xssec#example
12+
*/
13+
function buildAuthMiddleware() {
14+
const identityService = require('./identityService');
15+
16+
return async function authenticate(req, res, next) {
17+
try {
18+
const secContext = await createSecurityContext(identityService, { req });
19+
req[SECURITY_CONTEXT] = secContext;
20+
return next();
21+
} catch (e) {
22+
if (e instanceof ValidationError) {
23+
console.error("Unauthenticated request: ", e);
24+
return res.sendStatus(401);
25+
}
26+
27+
throw e; // handled in express error handler
28+
}
29+
}
30+
}
31+
32+
/*
33+
Middleware for mocked authentication
34+
following the recommendation in the @sap/xssec documentation for testing without real JWTs:
35+
https://www.npmjs.com/package/@sap/xssec#testing
36+
*/
37+
function buildMockAuthMiddleware() {
38+
return async function mockAuthentication(req, res, next) {
39+
const basicAuthUser = req.headers['authorization']?.split(' ')[1];
40+
if (!basicAuthUser) {
41+
return res.sendStatus(401);
42+
}
43+
44+
const user = Buffer.from(basicAuthUser, 'base64').toString().split(':')[0];
45+
const [username, api] = user.split('|');
46+
const mockPayload = {
47+
app_tid: "default",
48+
scim_id: username,
49+
sub: username,
50+
azp: "client_id",
51+
ias_apis: api ? [api] : []
52+
};
53+
54+
const mockToken = new IdentityServiceToken(null, { header: {}, payload: mockPayload });
55+
const mockService = new IdentityService({});
56+
const mockedSecurityContext = new IdentityServiceSecurityContext(mockService, mockToken, {});
57+
58+
req[SECURITY_CONTEXT] = mockedSecurityContext;
59+
next();
60+
}
61+
}
62+
63+
module.exports = authenticate;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
const { AuthorizationManagementService, IdentityServiceAuthProvider, TECHNICAL_USER_FLOW, PRINCIPAL_PROPAGATION_FLOW } = require("@sap/ams");
2+
const { mapTechnicalUserApi, mapPrincipalPropagationApi } = require("./apis");
3+
4+
let ams;
5+
if (process.env.NODE_ENV === 'test') {
6+
ams = AuthorizationManagementService.fromLocalDcn("./test/dcn", {
7+
assignments: "./test/mockPolicyAssignments.json"
8+
});
9+
} else {
10+
// production
11+
const identityService = require('./identityService');
12+
ams = AuthorizationManagementService.fromIdentityService(identityService);
13+
}
14+
15+
const authProvider = new IdentityServiceAuthProvider(ams)
16+
.withApiMapper(mapTechnicalUserApi, TECHNICAL_USER_FLOW)
17+
.withApiMapper(mapPrincipalPropagationApi, PRINCIPAL_PROPAGATION_FLOW);
18+
const amsMw = authProvider.getMiddleware();
19+
const authorize = amsMw.authorize();
20+
21+
if (process.env.DEBUG?.split(",").includes("ams")) {
22+
ams.on("authorizationCheck", event => {
23+
if (event.type === "checkPrivilege") {
24+
if (event.decision.isGranted()) {
25+
console.log(`Privilege '${event.action} ${event.resource}' for ${event.context.token.scimId} was granted based on input`, event.input);
26+
} else if(event.decision.isDenied()) {
27+
console.log(`Privilege '${event.action} ${event.resource}' for ${event.context.token.scimId} was denied based on input`, event.input);
28+
} else {
29+
console.log(`Privilege '${event.action} ${event.resource}' for ${event.context.token.scimId} was conditionally granted based on input`, event.input);
30+
}
31+
}
32+
});
33+
34+
ams.on("error", event => {
35+
if (event.type === "bundleRefreshError") {
36+
console.warn("AMS bundle refresh error:", event.error);
37+
}
38+
});
39+
}
40+
41+
module.exports = {
42+
authorize,
43+
ams,
44+
amsMw,
45+
authProvider
46+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
INTERNAL POLICY GetProducts {
2+
USE shopping.ReadProducts;
3+
}
4+
5+
INTERNAL POLICY ExternalOrder {
6+
USE shopping.CreateOrders RESTRICT order.total < 100, product.category IS NOT RESTRICTED;
7+
USE shopping.DeleteOrders;
8+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
POLICY OrderAccessory {
2+
USE shopping.CreateOrders RESTRICT product.category = 'accessory', order.total IS NOT RESTRICTED;
3+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
SCHEMA {
2+
"$user": {
3+
scim_id: String
4+
},
5+
order: {
6+
total: Number,
7+
createdBy: String
8+
},
9+
product: {
10+
category: String
11+
}
12+
}

0 commit comments

Comments
 (0)