|
| 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 | + |
| 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 | + ... <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. |
0 commit comments