Skip to content

Commit 21489e1

Browse files
jasonjohnmetulev
andauthored
Added Node.js proxy provider sample (#739)
* Added Node.js proxy provider sample * Update index.html Made changes suggested by review Co-authored-by: Nikola Metulev <[email protected]>
1 parent 41e5cad commit 21489e1

File tree

12 files changed

+619
-0
lines changed

12 files changed

+619
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
build/
3+
4+
yarn-error.log
5+
.env
6+
config.js
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
# Node.js Graph Proxy sample
2+
3+
This sample implements a Node.js server that acts as a proxy for Microsoft Graph. This can be used along with the Microsoft Graph Toolkit's [proxy provider](https://docs.microsoft.com/graph/toolkit/providers/proxy) to make all Microsoft Graph API calls from a backend service.
4+
5+
## Authorization modes
6+
7+
The proxy service supports to authorization modes: pass-through and on-behalf-of.
8+
9+
> **NOTE**
10+
> The [sample client application](./client) requires using the on-behalf-of mode.
11+
12+
### Pass-through
13+
14+
In pass-through mode, the service takes whatever bearer token is sent in the `Authorization` header from the client and tries to use that to call Microsoft Graph. This mode is primarily for ease of testing.
15+
16+
### On-behalf-of
17+
18+
In on-behalf-of mode, the service uses the Microsoft identity platform's [on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to exchange the token sent by the calling client for a Microsoft Graph token. The next section gives instructions on how to register the app in Azure Active Directory.
19+
20+
#### App registration
21+
22+
This sample requires two app registrations. This is needed to take advantage of [combined consent](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow#default-and-combined-consent) with the on-behalf-of flow in the Microsoft identity platform. Create an app registration for:
23+
24+
- The Graph Proxy service
25+
- The React front-end app
26+
27+
##### Graph Proxy service
28+
29+
1. Open a browser and navigate to the [Azure Active Directory admin center](https://aad.portal.azure.com). Login using a **personal account** (aka: Microsoft Account) or **Work or School Account**.
30+
31+
1. Select **Azure Active Directory** in the left-hand navigation, then select **App registrations** under **Manage**.
32+
33+
1. Select **New registration**. On the **Register an application** page, set the values as follows.
34+
35+
- Set **Name** to `Node.js Graph Proxy`.
36+
- Set **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts**.
37+
- Under **Redirect URI**, leave the value empty.
38+
39+
1. Select **Register**. On the **Node.js Graph Proxy** page, copy the value of the **Application (client) ID** and **Directory (tenant) ID**. Save them, you will need them in the next step.
40+
41+
1. Select **Certificates & secrets** under **Manage**. Select the **New client secret** button. Enter a value in **Description** and select one of the options for **Expires** and select **Add**.
42+
43+
1. Copy the client secret value before you leave this page. You will need it in the next step.
44+
45+
> **IMPORTANT**
46+
> This client secret is never shown again, so make sure you copy it now.
47+
48+
1. Select **API permissions** under **Manage**, then select **Add a permission**.
49+
50+
1. Select **Microsoft Graph**, then **Delegated permissions**.
51+
52+
1. Select the following permissions, then select **Add permissions**.
53+
54+
- **Presence.Read** - this will allow the app to read the authenticated user's presence.
55+
- **Tasks.ReadWrite** - this allows the app to read and write the user's To-Do tasks.
56+
- **User.Read** - this will allow the app to read the user's profile and photo.
57+
58+
> **NOTE**
59+
> These are the permissions required for the Microsoft Graph Toolkit components used in the client application. If you use different components, you may require additional permissions. See the [documentation](https://docs.microsoft.com/graph/toolkit/overview) for each component for details on required permissions.
60+
61+
1. Select **Expose an API**. Select the **Set** link next to **Application ID URI**. Accept the default and select **Save**.
62+
63+
1. In the **Scopes defined by this API** section, select **Add a scope**. Fill in the fields as follows and select **Add scope**.
64+
65+
- **Scope name:** `access_as_user`
66+
- **Who can consent?: Admins and users**
67+
- **Admin consent display name:** `Access Graph Proxy as the user`
68+
- **Admin consent description:** `Allows the app to call Microsoft Graph through a proxy service on users' behalf.`
69+
- **User consent display name:** `Access Graph Proxy as you`
70+
- **User consent description:** `Allows the app to call Microsoft Graph through a proxy service as you.`
71+
- **State: Enabled**
72+
73+
1. Copy the new scope. You'll need this later.
74+
75+
##### React front-end app
76+
77+
1. Return to **App registration** in the Azure portal, then select **New registration**.
78+
79+
1. On the **Register an application** page, set the values as follows.
80+
81+
- Set **Name** to `Node.js Graph Proxy Client`.
82+
- Set **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts**.
83+
- Under **Redirect URI**, set the first drop-down to `Single-page application (SPA)` and set the value to `http://localhost:8000/authcomplete`.
84+
85+
1. Select **Register**. On the **Node.js Graph Client** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step.
86+
87+
1. Select **API permissions** under **Manage**, then select **Add a permission**.
88+
89+
1. Select **APIs my organization uses**, then search for `Node.js Graph Proxy`. Select **Node.js Graph Proxy** in the list.
90+
91+
1. Select the **access_as_user** permission, then select **Add permissions**.
92+
93+
1. In the **Configured permissions** list, remove the **User.Read** permission under **Microsoft Graph**.
94+
95+
##### Add client to proxy's known applications
96+
97+
1. Return to the **Node.js Graph Proxy** app registration in the Azure portal, then select **Manifest** under **Manage**.
98+
99+
1. Locate the `"knownClientApplications": [],` line and replace it with the following, where `CLIENT_APP_ID` is the application ID of the **Node.js Graph Proxy Client** registration.
100+
101+
```json
102+
"knownClientApplications": ["CLIENT_APP_ID"],
103+
```
104+
105+
1. Select **Save**.
106+
107+
## Configuring the sample
108+
109+
1. Rename the example.env file to .env, and set the values as follows.
110+
111+
| Setting | Value |
112+
|---------|-------|
113+
| GRAPH_HOST | Set to the specific [Graph endpoint](https://docs.microsoft.com/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints) for your organization. |
114+
| AUTH_PASS_THROUGH | Set to `true` to enable pass-through mode, `false` for on-behalf-of mode. |
115+
| PROXY_APP_ID | The application ID of your **Node.js Graph Proxy** app registration |
116+
| PROXY_APP_TENANT_ID | The tenant ID from your **Node.js Graph Proxy** app registration |
117+
| PROXY_APP_SECRET | The client secret from your **Node.js Graph Proxy** app registration |
118+
119+
1. Rename the ./client/config.example.js to config.js and set the values as follows.
120+
121+
- Replace `YOUR_PROXY_CLIENT_APP_ID` with the application ID of your **Node.js Graph Proxy Client** app registration.
122+
- Replace `YOUR_PROXY_APP_ID` with the application ID of your **Node.js Graph Proxy** app registration.
123+
124+
## Run the sample
125+
126+
1. Open your command-line interface (CLI) in the root of this project and run the following command to install dependencies.
127+
128+
```Shell
129+
yarn install
130+
```
131+
132+
> **NOTE**
133+
> This only needs to be done once.
134+
135+
1. Run the following command to start the sample.
136+
137+
```Shell
138+
yarn start
139+
```
140+
141+
1. Open your browser and go to `http://localhost:8000`.
142+
143+
1. Select the **Sign In** button to sign in. After signing in and granting consent, the app should load data into the components.
144+
145+
### How do I know it worked?
146+
147+
If everything worked correctly, the components should load data just as they would normally without using the proxy provider. You can verify that requests are going through the proxy (rather than directly to Graph) by reviewing the console output in your CLI where you ran `yarn start`.
148+
149+
```Shell
150+
⚡️[server]: Server is running at http://localhost:8000
151+
Auth mode: on-behalf-of
152+
GET /v1.0/me
153+
Accept: */*
154+
POST /v1.0/$batch
155+
Accept: */*
156+
Content-Type: application/json
157+
GET /v1.0/me
158+
Accept: */*
159+
POST /v1.0/$batch
160+
Accept: */*
161+
Content-Type: application/json
162+
GET /beta/me/outlook/taskGroups
163+
Accept: */*
164+
```
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import jwt, { SigningKeyCallback, JwtHeader } from 'jsonwebtoken';
5+
import jwksClient from 'jwks-rsa';
6+
import * as msal from '@azure/msal-node';
7+
8+
const keyClient = jwksClient({
9+
jwksUri: 'https://login.microsoftonline.com/common/discovery/keys'
10+
});
11+
12+
/**
13+
* Parses the JWT header and retrieves the appropriate public key
14+
* @param {JwtHeader} header - The JWT header
15+
* @param {SigningKeyCallback} callback - Callback function
16+
*/
17+
function getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void {
18+
if (header) {
19+
keyClient.getSigningKey(header.kid!, (err, key) => {
20+
if (err) {
21+
callback(err, undefined);
22+
} else {
23+
callback(null, key.getPublicKey());
24+
}
25+
});
26+
}
27+
}
28+
29+
/**
30+
* Validates a JWT
31+
* @param {string} authHeader - The Authorization header value containing a JWT bearer token
32+
* @returns {Promise<string | null>} - Returns the token if valid, returns null if invalid
33+
*/
34+
async function validateJwt(authHeader: string): Promise<string | null> {
35+
return new Promise((resolve, reject) => {
36+
const token = authHeader.split(' ')[1];
37+
38+
const validationOptions = {
39+
audience: process.env.PROXY_APP_ID,
40+
issuer: `https://login.microsoftonline.com/${process.env.PROXY_APP_TENANT_ID}/v2.0`
41+
};
42+
43+
jwt.verify(token, getSigningKey, validationOptions, (err, payload) => {
44+
if (err) {
45+
resolve(null);
46+
}
47+
48+
resolve(token);
49+
});
50+
});
51+
}
52+
53+
/**
54+
* Gets an access token for the user using the on-behalf-of flow
55+
* @param authHeader - The Authorization header value containing a JWT bearer token
56+
* @returns {Promise<string | null>} - Returns the access token if successful, null if not
57+
*/
58+
export default async function getAccessTokenOnBehalfOf(authHeader: string): Promise<string | null> {
59+
// Validate the token
60+
const token = await validateJwt(authHeader);
61+
62+
if (token) {
63+
// Create an MSAL client
64+
const msalClient = new msal.ConfidentialClientApplication({
65+
auth: {
66+
clientId: process.env.PROXY_APP_ID!,
67+
clientSecret: process.env.PROXY_APP_SECRET
68+
}
69+
});
70+
71+
try {
72+
// Make the on-behalf-of request
73+
// This exchanges the incoming token (which is scoped for the proxy service)
74+
// for a new token that is scoped for Microsoft Graph
75+
const result = await msalClient.acquireTokenOnBehalfOf({
76+
oboAssertion: token,
77+
skipCache: true,
78+
scopes: ['https://graph.microsoft.com/.default']
79+
});
80+
81+
return result.accessToken;
82+
} catch (error) {
83+
console.log(`Token error: ${error}`);
84+
return null;
85+
}
86+
87+
} else {
88+
return null;
89+
}
90+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
const msalConfig = {
5+
auth: {
6+
clientId: 'YOUR_PROXY_CLIENT_APP_ID',
7+
redirectUri: 'http://localhost:8000/authcomplete'
8+
}
9+
}
10+
11+
const apiScopes = ['api://YOUR_PROXY_APP_ID/.default']
96.5 KB
Loading
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<!-- Copyright (c) Microsoft Corporation.
2+
Licensed under the MIT License. -->
3+
4+
<!DOCTYPE html>
5+
<html lang="en">
6+
<head>
7+
<meta charset="utf-8">
8+
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
9+
<title>Node.js Graph Proxy Client</title>
10+
11+
<link rel="shortcut icon" href="g-raph.png">
12+
<link href="style.css" rel="stylesheet" type="text/css" />
13+
</head>
14+
15+
<body>
16+
<main id="main-container" role="main" class="container">
17+
<h2>Login component</h2>
18+
<mgt-login></mgt-login>
19+
<h2>Person component</h2>
20+
<mgt-person person-query="me" person-card="hover" show-presence="true" view="twolines"></mgt-person>
21+
<h2>Tasks component</h2>
22+
<mgt-tasks data-source="todo"></mgt-tasks>
23+
</main>
24+
25+
<!-- MSAL -->
26+
<script src="https://alcdn.msauth.net/browser/2.5.2/js/msal-browser.min.js"></script>
27+
28+
<!-- Graph Toolkit -->
29+
<script src="https://unpkg.com/@microsoft/mgt/dist/bundle/mgt-loader.js"></script>
30+
31+
<!-- Configuration -->
32+
<script src="config.js"></script>
33+
34+
<script>
35+
// This is used to sign in the user and get a token that
36+
// allows the client to call the proxy
37+
const msalClient = new msal.PublicClientApplication(msalConfig);
38+
39+
const provider = new mgt.ProxyProvider('http://localhost:8000/apiproxy', async() => {
40+
// This code executes for each call to the proxy to
41+
// get any headers that it should add to the request.
42+
43+
// Get the user account name
44+
const account = sessionStorage.getItem('msalAccount');
45+
if (!account) {
46+
throw new Error(
47+
'User account missing from session. Please sign out and sign in again.');
48+
}
49+
50+
// Build a silent token request - this takes advantage of
51+
// token caching
52+
const silentTokenRequest = {
53+
// Permission scope is for the proxy, NOT Graph
54+
scopes: apiScopes,
55+
account: msalClient.getAccountByUsername(account)
56+
}
57+
58+
try {
59+
// Get the token and return it as an Authorization header
60+
const result = await msalClient.acquireTokenSilent(silentTokenRequest);
61+
return { Authorization: `Bearer ${result.accessToken}` };
62+
} catch (silentError) {
63+
// If silent requests fails with InteractionRequiredAuthError,
64+
// attempt to get the token interactively
65+
if (silentError instanceof msal.InteractionRequiredAuthError) {
66+
const interactiveResult = await msalClient.acquireTokenPopup({
67+
scopes: apiScopes,
68+
prompt: 'consent'
69+
});
70+
return { Authorization: `Bearer ${interactiveResult.accessToken}` };
71+
} else {
72+
throw silentError;
73+
}
74+
}
75+
});
76+
77+
provider.login = async () => {
78+
// Use MSAL to login
79+
const authResult = await msalClient.loginPopup({
80+
scopes: apiScopes
81+
});
82+
83+
console.log(`Access token: ${authResult.accessToken}`);
84+
85+
sessionStorage.setItem('msalAccount', authResult.account.username);
86+
87+
provider.setState(mgt.ProviderState.SignedIn);
88+
};
89+
90+
provider.logout = () => {
91+
msalClient.logout();
92+
sessionStorage.removeItem('msalAccount');
93+
provider.setState(mgt.ProviderState.SignedOut);
94+
};
95+
96+
mgt.Providers.globalProvider = provider;
97+
</script>
98+
</body>
99+
</html>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
body {
2+
font-family: "Segoe UI", "Segoe UI Web (West European)", "Segoe UI", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif;
3+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
GRAPH_HOST='https://graph.microsoft.com'
2+
AUTH_PASS_THROUGH=false
3+
PROXY_APP_ID='YOUR_PROXY_APP_ID'
4+
PROXY_APP_TENANT_ID='YOUR_TENANT_ID'
5+
PROXY_APP_SECRET='YOUR_PROXY_APP_SECRET'

0 commit comments

Comments
 (0)