Part III: Adding Resource Server Authorization to OpenID with Node.js
Tags: openid, resourceserver, node, authorizationserver
Published: 2022-06-17 07:42:31

## Introduction

In this article, we will continue building upon our OIDC ([OpenID Connect 1.0](https://openid.net/specs/openid-connect-core-1_0.html)) implementation with Node.js to add resource server authorization. Resource server authorization ensures that only authorized clients can access protected resources. We will create a separate client specifically for the resource server, which will have restricted access and cannot issue new tokens. 

## Let's start

We call every entity or action that is accessible through a URI a **resource**. Authorization server only grants access for **resource owner** with valid **scopes**.

### Update configuration

We will create a separate client for resource server. This client is a restricted client which only can access resources. We can't issue new token with this client. Also we can remove it to revoke every user access to resources. We have enabled **introspection** feature which able us to validate a token. **resourceIndicators** is were we define oure resource server.

_`./oidc/src/configs/configuration.ts`_

```ts
export const configuration: Configuration = {
  clients: [
    {
      client_id: "api",
      client_secret: "night-wolf",
      redirect_uris: [],
      response_types: [],
      grant_types: ["client_credentials"],
      scope: "openid email profile phone address",
    },
  ],
  features: {
    introspection: {
      enabled: true,
      allowedPolicy(ctx, client, token) {
        if (
          client.introspectionEndpointAuthMethod === "none" &&
          token.clientId !== ctx.oidc.client?.clientId
        ) {
          return false;
        }
        return true;
      },
    },
    resourceIndicators: {
      defaultResource(ctx) {
        return Array.isArray(ctx.oidc.params?.resource)
          ? ctx.oidc.params?.resource[0]
          : ctx.oidc.params?.resource;
      },
      getResourceServerInfo(ctx, resourceIndicator, client) {
        return {
          scope: "api:read offline_access",
        };
      },
    },
  },
};
```

### Add API service

To run the API server follow these steps. We will set up everything under _`./api`_ directory.

### 1. Add dependencies

```
$ yarn add koa
$ yarn add @types/koa -D
```

### 2. Add controller

We will create a mocked service. It will return PI number and we treat it as top secret information!

_`./api/src/controllers/api.controller.ts`_

```ts
import { Middleware } from "koa";

export default (): { [key: string]: Middleware } => ({
  pi: async (ctx) => {
    ctx.status = 200;
    ctx.message = Math.PI.toString();
  },
});
```

### 3. Add middlewares

We reached the magical part! Here we check if the user has access to the resource. Then we will pass session information to the next controller in the chain.

_`./api/src/middlewares/auth.middleware.ts`_

```ts
import { Middleware } from "koa";
import fetch from "node-fetch";

export const authenticate: Middleware = async (ctx, next) => {
  const body = new URLSearchParams();
  if (!ctx.request.headers.authorization) return ctx.throw(401);
  body.append(
    "token",
    ctx.request.headers.authorization.replace(/^Bearer /, "")
  );
  body.append("client_id", process.env.CLIENT_ID as string);
  body.append("client_secret", process.env.CLIENT_SECRET as string);
  const url = `${process.env.AUTH_ISSUER}/token/introspection`;
  const response = await fetch(url, {
    method: "POST",
    headers: {
      ["Content-Type"]: "application/x-www-form-urlencoded",
    },
    body: body,
  });
  if (response.status !== 200) ctx.throw(401);
  const json = await response.json();
  const { active, aud } = json;
  // Resource URI and audience (aud) must be equal
  if (active && aud.trim() === ctx.request.href.split("?")[0]) {
    ctx.state.session = json;
    await next();
  } else {
    ctx.throw(401);
  }
};

// Check if scope is valid
export const authorize =
  (...scopes: string[]): Middleware =>
  async (ctx, next) => {
    if (
      ctx.state.session &&
      scopes.every((scope) => ctx.state.session.scope.includes(scope))
    ) {
      await next();
    } else {
      ctx.throw(401);
    }
  };
```

### 4. Add router

Here we bind the router to the controller. Also, we give the required scopes for _`./pi`_ controller.

_`./api/src/routes/api.router.ts`_

```ts
import Router from "koa-router";
import apiController from "../controllers/api.controller";
import { authenticate, authorize } from "../middlewares/auth.middleware";

export default () => {
  const router = new Router();

  const { pi } = apiController();

  router.get("/pi", authenticate, authorize("api:read"), pi);

  return router;
};
```

_`./api/src/routes/index.ts`_

```ts
import Router from "koa-router";
import appRouter from "./api.router";

export default () => {
  const router = new Router();

  router.use(appRouter().routes());

  return router;
};
```

### 5. Start the server

Server startup script

_`./api/src/index.ts`_

```ts
import cors from "@koa/cors";
import dotenv from "dotenv";
import Koa from "koa";
import path from "path";
import router from "./routes";

dotenv.config({ path: path.resolve("api/.env") });

const app = new Koa();

app.use(cors());
app.use(router().routes());

app.listen(process.env.PORT, () => {
  console.log(
    `api listening on port ${process.env.PORT}, check http://localhost:${process.env.PORT}`
  );
});
```

## Add service page in app

As the final piece, we create a consumer app under _`./app`_ directory which accesses API server to access PI resource.

### Add html file

_`./app/src/views/pi.ejs`_

```html
<!DOCTYPE html>
<html>
  <%- include('components/head'); -%>
  <body class="app">
    <div class="login-card">
      <h1><%= title %></h1>
      <form autocomplete="off">
        <label>Token</label>
        <input id="token" required name="token" placeholder="Token" />
        <p id="pi" style="margin-top: 0">Value: -</p>

        <button type="button" class="login login-submit" onclick="onClick()">
          Fetch
        </button>
      </form>
    </div>
  </body>
  <script>
    async function onClick() {
      try {
        const response = await fetch("<%= apiUrl %>/pi", {
          headers: {
            ["Authorization"]: `Bearer ${
              document.getElementById("token").value
            }`,
          },
        });
        if (response.status === 401) {
          return alert("You are not authorized to access PI.");
        } else if (response.status !== 200) {
          return alert(" Failed to fetch PI.");
        }
        const pi = await response.text();
        document.getElementById("pi").innerText = `Value: ${pi}`;
      } catch (error) {
        alert("Error encountered.");
      }
    }
  </script>
</html>
```

### Add controllers

_`./app/src/controllers/app.controller.ts`_

```ts
export default (): { [key: string]: Middleware } => ({
  pi: async (ctx) => {
    return ctx.render("pi", { title: "PI", apiUrl: process.env.API_URL });
  },
});
```

### Add router

_`./app/src/routes/app.router.ts`_

```ts
export default () => {
  const router = new Router();

  const { pi } = appController();

  router.get("/pi", pi);

  return router;
};
```

### Summary

In this section, we created a resource server with PI number as a restricted resource. Then we integrated this with our authorization server to grant user access. To see the result we created a minimal web app to view everything in action.