Express + TypeScript + OpenFGA 权限控制实践指南

作者:API传播员 · 2025-10-24 · 阅读时间:6分钟
本文详细介绍了如何在Express + Typescript Node.js API中集成OpenFGA和Auth0来实现细粒度的API授权。内容包括使用Auth0 CLI注册API、创建令牌验证中间件、配置环境变量、启动MongoDB数据库、使用OpenFGA初始化授权模型以及添加细粒度授权。通过具体的代码示例和步骤指导,帮助开发者在项目中实现高效的权限管理。

添加 API 授权

在设置 API 授权时,首先需要通过命令行工具生成设备确认码,并在浏览器中激活设备。如果不使用不透明令牌,则无需在 Auth0 中为 API 创建客户端应用程序,但必须在租户中注册 API。以下是使用 Auth0 CLI 注册 API 的命令:

auth0 apis create 
  --name "Document API" 
  --identifier https://document-api.okta.com 
  --offline-access=false

在提示时,将作用域留空并保留默认值。

接下来,克隆文档 API 的代码存储库,该存储库已经实现了基本的请求处理逻辑:

git clone https://github.com/indiepopart/express-typescript-fga.git

存储库包含两个文件夹:“start”和“final”。“start”文件夹中是一个基础的 Node.js 项目。使用您喜欢的 IDE 打开该项目,并安装 express-[oauth](https://www.explinks.com/wiki/oauth/)2-jwt-bearer 依赖:

cd express-typescript-fga/start
npm install express-oauth2-jwt-bearer

创建令牌验证中间件

src/middleware/auth0.middleware.ts 文件中创建一个用于验证令牌的中间件,代码如下:

// src/middleware/auth0.middleware.ts
import * as dotenv from "dotenv";
import { auth } from "express-oauth2-jwt-bearer";

dotenv.config();export const validateAccessToken = auth({
  issuerBaseURL: https://${process.env.AUTH0_DOMAIN},
  audience: process.env.AUTH0_AUDIENCE,
});

在路由中调用 validateAccessToken,例如:

// src/documents/document.router.ts
documentRouter.get("/", validateAccessToken, async (req, res, next) => {
  try {
    const documents = await getAllDocuments();
    res.status(200).json(documents);
  } catch (error) {
    next(error);
  }
});

添加错误处理

error.middleware.ts 文件中添加错误处理逻辑,代码如下:

import { Request, Response, NextFunction } from "express";
import { InvalidTokenError, UnauthorizedError } from "express-oauth2-jwt-bearer";
import mongoose from "mongoose";

export const errorHandler = (
  error: any,
  request: Request,
  response: Response,
  next: NextFunction
) => {
  console.log(error);  if (error instanceof InvalidTokenError) {
    response.status(error.status).json({ message: "Bad credentials" });
    return;
  }  if (error instanceof UnauthorizedError) {
    response.status(error.status).json({ message: "Requires authentication" });
    return;
  }  if (error instanceof mongoose.Error.ValidationError) {
    response.status(400).json({ message: "Bad Request" });
    return;
  }  if (error instanceof mongoose.Error.CastError) {
    response.status(400).json({ message: "Bad Request" });
    return;
  }  response.status(500).json({ message: "Internal Server Error" });
};

配置环境变量

.env.example 文件复制为 .env,并添加以下内容:

AUTH0_AUDIENCE=https://document-api.okta.com
AUTH0_DOMAIN=

启动 MongoDB 数据库

使用以下命令启动 MongoDB 数据库和管理界面:

docker compose up mongodb mongo-express

使用 Auth0 CLI 获取测试访问令牌:

auth0 test token -a https://document-api.okta.com -s openid

然后使用 cURL 创建文档:

curl -i -X POST 
  -H "Authorization:Bearer $ACCESS_TOKEN" 
  -H "Content-Type: application/json" 
  -d '{"name": "planning.doc"}' 
  http://localhost:6060/api/documents

成功的响应示例如下:

{
  "name": "planning.doc",
  "_id": "66feb9c1f106b84c28644d3e",
  "__v": 0
}

在 OpenFGA 中初始化授权模型

授权模型通过定义用户类型、对象类型及其关系来实现。在本指南中,我们将使用一个简化的授权模型。首先,在 start/OpenFGA 目录下创建文件 auth-model.fga,内容如下:

模型模式1.1
类型 用户
类型 文档
关系定义 所有者:[用户,域#成员]或父级所有者
定义 编写者:[用户、域#成员].或父级的所有者或编写者
定义 评论者:[用户和域#成员][父级的编写者或评论者
定义 查看器:[user,user:*,域#会员]或父定义父级的评论者或查看器:[文档]
类型 域
关系定义 成员:[用户]

将模型转换为 JSON 格式:

fga model transform --file=auth-model.fga > auth-model.json

启动 OpenFGA 服务:

docker compose up openfga

创建一个存储:

export FGA_API_URL=http://localhost:8090
fga store create --name "documents-fga"

将输出的 store-id 设置为环境变量,并写入模型:

export FGA_STORE_ID=
fga model write --store-id=${FGA_STORE_ID} --file auth-model.json

使用 OpenFGA 添加细粒度授权

.env 文件中添加以下变量:

FGA_API_URL=http://localhost:8090
FGA_STORE_ID=
FGA_MODEL_ID=

安装 OpenFGA Node.js SDK:

npm install @openfga/sdk

创建权限检查中间件,代码如下:

// src/middleware/openfga.middleware.ts
import * as dotenv from "dotenv";
import { NextFunction, Request, Response } from "express";
import { ClientCheckRequest, OpenFgaClient } from "@openfga/sdk";

dotenv.config();export class PermissionDenied extends Error {
  constructor(message: string) {
    super(message);
  }
}const fgaClient = new OpenFgaClient({
  apiUrl: process.env.FGA_API_URL,
  storeId: process.env.FGA_STORE_ID,
  authorizationModelId: process.env.FGA_MODEL_ID,
});export const forView = (req: Request): ClientCheckRequest => {
  const userId = req.auth?.payload.sub;
  return { user: user:${userId}, object: document:${req.params.id}, relation: "viewer" };
};// 其他权限检查方法省略...export const checkPermissions = (createTuple: (req: Request) => ClientCheckRequest | null) => {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      const tuple = createTuple(req);
      if (!tuple) {
        next();
        return;
      }
      const result = await fgaClient.check(tuple);
      if (!result.allowed) {
        next(new PermissionDenied("Permission denied"));
        return;
      }
      next();
    } catch (error) {
      next(error);
    }
  };
};

在路由中调用中间件:

documentRouter.get(
  "/:id",
  validateAccessToken,
  checkPermissions(forView),
  async (req, res, next) => {
    try {
      const document = await findDocumentById(req.params.id);
      if (!document) {
        res.status(404).json({ message: "Document not found" });
        return;
      }
      res.status(200).json(document);
    } catch (error) {
      next(error);
    }
  }
);

向 Express API 发送请求

运行 API 并尝试读取操作:

curl -i -H "Authorization: Bearer $ACCESS_TOKEN" localhost:6060/api/documents

如果权限不足,将返回 403 Forbidden

{ "message": "Permission denied" }

使用 FGA CLI 授予读取权限后,重试操作即可成功。

fga tuple write --store-id=${FGA_STORE_ID} --model-id=$FGA_MODEL_ID 
  'user:' viewer document:
原文链接: https://auth0.com/blog/express-typescript-fga/