Skip to content

Commit 15d9667

Browse files
author
Steve Hobbs
committed
Migrate calling-an-api tutorial from docs
1 parent 26d1157 commit 15d9667

File tree

2 files changed

+385
-0
lines changed

2 files changed

+385
-0
lines changed

02-Calling-an-API/TUTORIAL.md

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
# Tutorial
2+
3+
_This tutorial demonstrates how to make API calls for protected resources on your server._
4+
Most single-page apps use resources from data APIs. You may want to restrict access to those resources, so that only authenticated users with sufficient privileges can access them. Auth0 lets you manage access to these resources using [API Authorization](/api-auth).
5+
6+
This tutorial shows you how to create a simple API using [Express](https://expressjs.com) that serves resources protected by a middleware that looks for and validates access tokens. You will then see how to call this API using an access token granted by the Auth0 authorization server.
7+
8+
Most single page applications use resources from data APIs. You may want to restrict access to those resources, so that only authenticated users with sufficient privileges can access them. Auth0 lets you manage access to these resources using [API Authorization](/api-auth).
9+
10+
This tutorial shows you how to create a simple API using [Express](https://expressjs.com) that validates incoming JSON Web Tokens. You will then see how to call this API using an Access Token granted by the Auth0 authorization server.
11+
12+
## Create an API
13+
14+
In the [APIs section](https://manage.auth0.com/#/apis) of the Auth0 dashboard, click **Create API**. Provide a name and an identifier for your API.
15+
You will use the identifier later when you're configuring your Javascript Auth0 application instance.
16+
For **Signing Algorithm**, select **RS256**.
17+
18+
![Create API](media/create-api.png)
19+
20+
## Create the Backend API
21+
22+
For this example, you'll create an [Express](https://expressjs.com/) server that acts as the backend API. This API will expose an endpoint to validate incoming [JWT-formatted access tokens](https://auth0.com/docs/tokens/concepts/jwts) before returning a response.
23+
24+
Start by installing the following packages:
25+
26+
```bash
27+
npm install cors express express-jwt jwks-rsa npm-run-all
28+
```
29+
30+
- [`express`](https://github.com/expressjs/express) - a lightweight web server for Node
31+
- [`express-jwt`](https://www.npmjs.com/package/express-jwt) - middleware to validate JsonWebTokens
32+
- [`cors`](https://github.com/expressjs/cors) - middleware to enable CORS
33+
- [`jwks-rsa`](https://www.npmjs.com/package/jwks-rsa) - retrieves RSA signing keys from a JWKS endpoint
34+
- [`npm-run-all`](https://www.npmjs.com/package/npm-run-all) - a helper to run the SPA and backend API concurrently
35+
36+
Next, create a new file `server.js` with the following code:
37+
38+
```js
39+
const express = require("express");
40+
const cors = require("cors");
41+
const jwt = require("express-jwt");
42+
const jwksRsa = require("jwks-rsa");
43+
44+
// Create a new Express app
45+
const app = express();
46+
47+
// Accept cross-origin requests from the frontend app
48+
app.use(cors({ origin: "http://localhost:3000" }));
49+
50+
// Set up Auth0 configuration
51+
const authConfig = {
52+
domain: "YOUR_AUTH0_DOMAIN",
53+
audience: "YOUR_API_IDENTIFIER",
54+
};
55+
56+
// Define middleware that validates incoming bearer tokens
57+
// using JWKS from ${account.namespace}
58+
const checkJwt = jwt({
59+
secret: jwksRsa.expressJwtSecret({
60+
cache: true,
61+
rateLimit: true,
62+
jwksRequestsPerMinute: 5,
63+
jwksUri: `https://YOUR_AUTH0_DOMAIN/.well-known/jwks.json`,
64+
}),
65+
66+
audience: authConfig.audience,
67+
issuer: `https://YOUR_AUTH0_DOMAIN/`,
68+
algorithm: ["RS256"],
69+
});
70+
71+
// Define an endpoint that must be called with an access token
72+
app.get("/api/external", checkJwt, (req, res) => {
73+
res.send({
74+
msg: "Your Access Token was successfully validated!",
75+
});
76+
});
77+
78+
// Start the app
79+
app.listen(3001, () => console.log("API listening on 3001"));
80+
```
81+
82+
The above API has one available endpoint, `/api/external`, that returns a JSON response to the caller. This endpoint uses the `checkJwt` middleware to validate the supplied bearer token using your tenant's [JSON Web Key Set](https://auth0.com/docs/jwks). If the token is valid, the request is allowed to continue. Otherwise, the server returns a 401 Unauthorized response.
83+
84+
:::note
85+
If you are not logged in when viewing this tutorial, the sample above will show the `domain` and `audience` values as "YOUR_TENANT" and "YOUR_API_IDENTIFIER" respectively. These should be replaced with the values from your Auth0 application.
86+
:::
87+
88+
Finally, modify `package.json` to add two new scripts: `dev` and `server`. Running the `dev` script will now start both the backend server and the serve the Angular application at the same time:
89+
90+
```json
91+
"scripts": {
92+
...,
93+
"server": "node server.js",
94+
"dev": "npm-run-all --parallel start server"
95+
},
96+
```
97+
98+
To start the project for this sample, run the `dev` script from the terminal:
99+
100+
```bash
101+
npm run dev
102+
```
103+
104+
This will start both the backend API and the frontend application together.
105+
106+
### Proxy to Backend API
107+
108+
In this example, the Node backend and Angular app run on two different ports. In order to call the API from the Angular application, the development server must be configured to proxy requests through to the backend API. This is so that the Angular app can make a request to `/api/external` and it will be correctly proxied through to the backend API at `http://localhost:3001/api/external`.
109+
110+
To do this, create a `proxy.conf.json` file in the root of the project and add the following code:
111+
112+
```json
113+
{
114+
"/api": {
115+
"target": "http://localhost:3001",
116+
"secure": false
117+
}
118+
}
119+
```
120+
121+
Now open `angular.json` and add a reference to the proxy config. In the `serve` node, include the following reference to the proxy config file:
122+
123+
```json
124+
...
125+
"serve": {
126+
...,
127+
"options": {
128+
...,
129+
"proxyConfig": "proxy.conf.json"
130+
},
131+
...
132+
```
133+
134+
::: note
135+
In order for these changes to take effect, you will need to stop and restart the run script.
136+
:::
137+
138+
## Update the Authentication Service
139+
140+
We'll now make several updates in the `src/app/auth.service.ts` file.
141+
142+
### Add Audience
143+
144+
First, add the `audience` value to the creation of the Auth0 client instance:
145+
146+
```js
147+
// Create an observable of Auth0 instance of client
148+
auth0Client$ = (from(
149+
createAuth0Client({
150+
...,
151+
audience: "YOUR_AUTH0_API_IDENTIFIER"
152+
})
153+
```
154+
155+
This setting tells the authorization server that your application would like to call the API with the identifier \${apiIdentifier} on the user's behalf. This will cause the authorization server to prompt the user for consent the next time they log in. It will also return an access token that can be used to call the API.
156+
157+
### Manage Access Token
158+
159+
We'll define an observable method to retrieve the access token and make it available for use in our application. Add the following to the `AuthService` class:
160+
161+
```ts
162+
getTokenSilently$(options?): Observable<string> {
163+
return this.auth0Client$.pipe(
164+
concatMap((client: Auth0Client) => from(client.getTokenSilently(options)))
165+
);
166+
}
167+
```
168+
169+
If you'd like to [pass options to `getTokenSilently`](https://auth0.github.io/auth0-spa-js/classes/auth0client.html#gettokensilently) when calling the method, you can do so.
170+
171+
:::note
172+
**Why isn't the token stored in browser storage?** Historically, it was common to store tokens in local or session storage. However, browser storage is [not a secure place to store sensitive data](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage). The [`auth0-spa-js` SDK](https://auth0.com/docs/libraries/auth0-spa-js) manages session retrieval for you so that you no longer need to store sensitive data in browser storage in order to restore sessions after refreshing a Single Page Application.
173+
174+
With `auth0-spa-js`, we can simply request the token from the SDK when we need it (e.g., in an HTTP interceptor) rather than storing it locally in the Angular app or browser. The SDK manages token freshness as well, so we don't need to worry about renewing tokens when they expire.
175+
:::
176+
177+
## Create an HTTP Interceptor
178+
179+
In order to add the access token to the header of secured API requests, we'll create an [HTTP interceptor](https://angular.io/api/common/http/HttpInterceptor). Interceptors intercept and handle or modify HTTP requests or responses. Interceptors in Angular are services.
180+
181+
Create an interceptor service with the following CLI command:
182+
183+
```bash
184+
ng generate service interceptor
185+
```
186+
187+
Open the generated `src/app/interceptor.service.ts` file and add the following code:
188+
189+
```ts
190+
import { Injectable } from "@angular/core";
191+
import {
192+
HttpRequest,
193+
HttpHandler,
194+
HttpEvent,
195+
HttpInterceptor,
196+
} from "@angular/common/http";
197+
import { AuthService } from "./auth.service";
198+
import { Observable, throwError } from "rxjs";
199+
import { mergeMap, catchError } from "rxjs/operators";
200+
201+
@Injectable({
202+
providedIn: "root",
203+
})
204+
export class InterceptorService implements HttpInterceptor {
205+
constructor(private auth: AuthService) {}
206+
207+
intercept(
208+
req: HttpRequest<any>,
209+
next: HttpHandler
210+
): Observable<HttpEvent<any>> {
211+
return this.auth.getTokenSilently$().pipe(
212+
mergeMap((token) => {
213+
const tokenReq = req.clone({
214+
setHeaders: { Authorization: `Bearer <%= "${token}" %>` },
215+
});
216+
return next.handle(tokenReq);
217+
}),
218+
catchError((err) => throwError(err))
219+
);
220+
}
221+
}
222+
```
223+
224+
The `AuthService` is provided so that we can access the `getTokenSilently$()` stream. Using this stream in the interceptor ensures that requests wait for the access token to be available before firing.
225+
226+
The `intercept()` method returns an observable of an HTTP event. In it, we do the following:
227+
228+
- `mergeMap` ensures that any new requests coming through don't cancel previous requests that may not have completed yet; this is useful if you have multiple HTTP requests on the same page
229+
- `clone` the outgoing request and attach the [`Authorization` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization) with access token, then send the cloned, authorized request on its way
230+
231+
In order to put the interceptor to work in the application, open the `src/app/app-routing.module.ts` routing module and add:
232+
233+
```js
234+
...
235+
import { HTTP_INTERCEPTORS } from '@angular/common/http';
236+
import { InterceptorService } from './interceptor.service';
237+
...
238+
@NgModule({
239+
imports: [RouterModule.forRoot(routes)],
240+
exports: [RouterModule],
241+
providers: [
242+
{
243+
provide: HTTP_INTERCEPTORS,
244+
useClass: InterceptorService,
245+
multi: true
246+
}
247+
]
248+
})
249+
```
250+
251+
In the `providers` array, we're providing the `HTTP_INTERCEPTORS` injection token and our `InterceptorService` class. We then set `multi: true` because we could potentially use multiple interceptors (which would be called in the order provided).
252+
253+
::: note
254+
The interceptor will now run on every outgoing HTTP request. If you have public endpoints as well as protected endpoints, you could use conditional logic in the interceptor to only add the token to requests that require it.
255+
:::
256+
257+
## Call the API
258+
259+
Now we're ready to call the secure API endpoint and display its results in the application. To make HTTP requests in our application, we'll first add the `HttpClientModule` to the `src/app/app.module.ts`:
260+
261+
```js
262+
...
263+
import { HttpClientModule } from '@angular/common/http';
264+
...
265+
@NgModule({
266+
...,
267+
imports: [
268+
BrowserModule,
269+
AppRoutingModule,
270+
HttpClientModule
271+
],
272+
...
273+
})
274+
...
275+
```
276+
277+
The `HttpClientModule` must be imported and then added to the NgModule's `imports` array.
278+
279+
Next, create an API service using the Angular CLI:
280+
281+
```bash
282+
ng generate service api
283+
```
284+
285+
Open the generated `src/app/api.service.ts` file and add:
286+
287+
```ts
288+
import { Injectable } from "@angular/core";
289+
import { HttpClient } from "@angular/common/http";
290+
import { Observable } from "rxjs";
291+
292+
@Injectable({
293+
providedIn: "root",
294+
})
295+
export class ApiService {
296+
constructor(private http: HttpClient) {}
297+
298+
ping$(): Observable<any> {
299+
return this.http.get("/api/external");
300+
}
301+
}
302+
```
303+
304+
This creates a `ping$()` method that returns an observable of the HTTP GET request to the API. This can now be used in the application to call the `api/external` endpoint.
305+
306+
Now we need somewhere to call the API and display the results. Create a new page component:
307+
308+
```bash
309+
ng generate component external-api
310+
```
311+
312+
Open `src/app/external-api/external-api.component.ts` and add this code:
313+
314+
```ts
315+
import { Component, OnInit } from "@angular/core";
316+
import { ApiService } from "src/app/api.service";
317+
318+
@Component({
319+
selector: "app-external-api",
320+
templateUrl: "./external-api.component.html",
321+
styleUrls: ["./external-api.component.css"],
322+
})
323+
export class ExternalApiComponent implements OnInit {
324+
responseJson: string;
325+
326+
constructor(private api: ApiService) {}
327+
328+
ngOnInit() {}
329+
330+
pingApi() {
331+
this.api.ping$().subscribe((res) => (this.responseJson = res));
332+
}
333+
}
334+
```
335+
336+
The API service is provided and a named subscription to `api.ping$()` is created. The act of subscribing fires off the HTTP call, which we'll do on a button click in the UI. When data comes back from the API, the results are set in a local property (`responseJson`).
337+
338+
:::note
339+
We do _not_ need to unsubscribe from the `api.ping$()` observable because it completes once the HTTP request is finished.
340+
:::
341+
342+
Open `src/app/external-api/external-api.component.html` and replace its contents with the following:
343+
344+
```html
345+
<button (click)="pingApi()">Ping API</button>
346+
347+
<pre *ngIf="responseJson">
348+
<code>{{ responseJson | json }}</code>
349+
</pre>
350+
```
351+
352+
Clicking the button calls the API. The response is displayed in the `pre` element, which only renders if `responseJson` is present.
353+
354+
This route now needs to be added to our application. Open `src/app/app-routing.module.ts` and add the `/external-api` route like so:
355+
356+
```js
357+
...
358+
import { ExternalApiComponent } from './external-api/external-api.component';
359+
...
360+
const routes: Routes = [
361+
...,
362+
{
363+
path: 'external-api',
364+
component: ExternalApiComponent,
365+
canActivate: [AuthGuard]
366+
}
367+
];
368+
...
369+
```
370+
371+
The `/external-api` route is also guarded with `AuthGuard` since it requires an authenticated user with an access token.
372+
373+
Finally, add a link to the navigation bar. Open `src/app/nav-bar/nav-bar.component.html` and add:
374+
375+
```html
376+
<header>
377+
... &nbsp;<a routerLink="external-api" *ngIf="auth.loggedIn">External API</a>
378+
</header>
379+
```
380+
381+
::: note
382+
If, at any time, the application isn't working as expected, the Angular CLI server may need to be restarted. Enter `Ctrl+C` in the terminal to stop the application, then run it again using `npm run dev`.
383+
:::
384+
385+
> **Checkpoint:** You should now be able to log in, browse to the External API page, and click the **Ping API** button to make an API request. The response should then display in the browser.
201 KB
Loading

0 commit comments

Comments
 (0)