发布于 2026-01-06 7 阅读
0

Use Auth0 to secure your NestJS application DEV's Worldwide Show and Tell Challenge Presented by Mux: Pitch Your Projects!

使用 Auth0 保护您的 NestJS 应用程序

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

简而言之:本文将解释如何使用Auth0保护基于NestJS的 API 。此外,还包含 Auth0 管理 API,用于接收当前登录用户的个人资料。 如果您不想阅读本文,可以直接访问GitHub 代码库


上周我遇到了将 Auth0 连接到我的 NestJS 项目的问题。
我希望限制 API 的某些端点对公众开放。
由于我不想将用户数据和密码存储在自己的服务器上,所以我决定使用 Auth0。

我使用 NestJS 作为后端应用程序框架已经有一段时间了。NestJS 让新手用户很容易上手。
它提供了一个成熟的命令行界面 (CLI),并且已经提供了许多实用功能,可以根据需要集成到项目中(例如数据库连接、验证、日志记录或 HTTP 请求)。

我当时非常绝望,找不到可行的解决方案。我在 Twitter 和 dev.to 上都寻求过帮助:

我收到了一些私信,里面有很多非常有用的建议。
可惜我还是没能成功,所以我决定休息几天,不再想这件事。
事实证明这是个非常好的主意,最终我解决了这个问题🥳。

配置 Auth0

如果您还没有 Auth0 账号,请前往他们的注册页面创建一个。Auth0
提供慷慨的免费套餐,包括无密码用户账号、两个社交身份提供商(例如 Google、Facebook、Twitter 或 GitHub)以及社区支持对于
本教程和一些小型项目而言,免费套餐应该足够用了。

创建 API

登录控制面板后,导航至API部分,然后点击“创建 API”按钮创建一个新的 API。

创建 API

接下来,系统会提示您输入 API 名称和标识符。Auth0
建议使用 URL 作为标识符,并使用一个易于理解的 API 名称。
签名算法应保留默认选项 (RS256)。

创建 API

创建应用程序

Auth0 在创建新应用时会要求指定应用类型。
在本例中,应用类型无关紧要。对于公开应用,您可能需要了解更多应用类型信息,以便选择符合您需求的类型。

创建应用程序
创建应用程序
创建应用程序

应用程序创建完成后,您需要配置允许的回调 URL。回调 URL 是用户成功通过身份验证后可以被重定向到的位置。

添加回调 URL

启用 Auth0 管理 API

由于我们之后还需要读取已认证用户的个人资料,因此必须为刚刚创建的应用程序启用 Auth0 管理 API。启用 Auth0 管理 API 的配置信息可以在“API”
选项卡 中找到 此外,还必须选择read:users权限范围。

启用 Auth0 管理 API

登录

稍后我们需要生成一个访问令牌才能使用 API。该令牌通过 Auth0 登录表单生成。
登录表单由 Auth0 提供,可通过您的 Auth0 租户域访问。
您还需要 Auth0 客户端 ID。这两个值都可以从您的应用程序配置页面复制。

获取变量

替换网址中的值,然后在浏览器中打开它。

https://$AUTH0_DOMAIN/authorize?audience=http://localhost:3000&scope=SCOPE&response_type=code&client_id=$AUTH0_CLIENT_ID&redirect_uri=http://localhost:4200/login&state=STATE?prompt=none

登录

使用单点登录 (SSO) 提供商(默认配置为 Google)或电子邮件和密码登录后,
登录表单会将您重定向到http://localhost:4200/login,该地址已预先配置。
即使没有 Web 应用程序运行也没关系,但您需要复制code查询参数的值并保存以备后用。
code值用于生成 Bearer 令牌。

登录

更新用户元数据

Auth0 的一个优点是,您可以将元数据附加到用户配置文件。
您可以使用用户元数据将用户配置文件链接到其他服务(例如 GitHub 或 dev.to 配置文件)——我相信您会发现许多其他用例。

用户
用户


创建新的 NestJS 应用程序

$ npm i -g @nestjs/cli
$ nest new nestjs-auth0-jwt
Enter fullscreen mode Exit fullscreen mode

如果一切顺利,将会创建一个新的 NestJS 应用程序,其目录结构如下:

nestjs-auth0-jwt
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── README.md
├── nest-cli.json
├── package-lock.json
├── package.json
├── tsconfig.build.json
├── tsconfig.json
└── tslint.json
Enter fullscreen mode Exit fullscreen mode

运行应用程序

导航到新目录并启动 API:

$ cd nestjs-auth0-jwt
$ npm run start:dev
Enter fullscreen mode Exit fullscreen mode

应用程序运行后,您可以打开浏览器并访问http://localhost:3000

运行应用程序

添加依赖项

$ npm install --save @nestjs/passport passport passport-jwt jwks-rsa auth0
$ npm install --save-dev @types/passport-jwt @types/auth0
Enter fullscreen mode Exit fullscreen mode

创建身份验证模块

$ nest generate module auth
Enter fullscreen mode Exit fullscreen mode

创建 JWT 策略

$ touch auth/jwt.strategy.ts
Enter fullscreen mode Exit fullscreen mode
import { passportJwtSecret } from 'jwks-rsa';
import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt';

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      secretOrKeyProvider: passportJwtSecret({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`
      }),

      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // 1
      audience: 'http://localhost:3000',
      issuer: `https://${process.env.AUTH0_DOMAIN}/`
    });
  }

  validate(payload: any, done: VerifiedCallback) {
    if (!payload) {
      done(new UnauthorizedException(), false); // 2
    }

    return done(null, payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

1:ExtractJwt.fromAuthHeaderAsBearerToken()创建一个新的提取器,用于在授权标头中查找方案为“bearer”的 JSON Web Token。

2:如果没有收到有效载荷,则身份验证失败。

更新身份验证模块

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';

import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [PassportModule],
  providers: [JwtStrategy],
  exports: [JwtStrategy]
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

创建 API 端点

import { ManagementClient, User } from 'auth0';
import * as express from 'express';

import { Controller, Get, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello(); // 1
  }

  @Get('secret')
  @UseGuards(AuthGuard('jwt')) // 2
  secretEndpoint(@Request() req: express.Request): string {
    return 'this endpoint should be protected';
  }

  @Get('profile')
  @UseGuards(AuthGuard('jwt')) // 2
  async profile(@Request() req: express.Request): Promise<any> {
    const authZero = new ManagementClient({
      // 3
      domain: process.env.AUTH0_DOMAIN,
      clientId: process.env.AUTH0_CLIENT_ID,
      clientSecret: process.env.AUTH0_CLIENT_SECRET,
      scope: 'read:users update:users'
    });

    const response = await authZero
      .getUser({ id: req.user.sub }) // 4
      .then((user: User) => {
        return user;
      })
      .catch(err => {
        return err;
      });

    return response;
  }
}
Enter fullscreen mode Exit fullscreen mode

1:公开可用的 API 端点。

2:@UseGuards(AuthGuard('jwt'))

3:创建 Auth0 管理 API 客户端。

4:JWT响应中的主题与Auth0用户ID相同。因此,该sub属性用作调用的id参数getUser,该调用会返回完整的用户配置文件。

使用登录码检索 Bearer Token

$ export AUTH0_DOMAIN=${AUTH0_DOMAIN}
$ export AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
$ export AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET}
$ export CODE=${CODE}

$ curl -X POST -H 'content-type: application/json' -d '{
  "grant_type": "authorization_code",
  "client_id": "'$AUTH0_CLIENT_ID'",
  "client_secret": "'$AUTH0_CLIENT_SECRET'",
  "code": "'$CODE'",
  "redirect_uri": "http://localhost:4200"
}' https://$AUTH0_DOMAIN/oauth/token
Enter fullscreen mode Exit fullscreen mode
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik9EQXpOMEZHTWpBd1FVUTJORFpETXpBMVJETXdSRUUyTjBSRE5qRXpNemRFUWtNMk5URTFNUSJ9.eyJpc3MiOiJodHRwczovL25lc3Rqcy1hdXRoMC1qd3QuYXUuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEzMDc2NzYwNTUyNTQ5MTY1ODQ5IiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaWF0IjoxNTY1OTg1MzU3LCJleHAiOjE1NjYwNzE3NTcsImF6cCI6IjR0eFZVSm5ZVlRhbzRsczA2dFozRGNCdm5JVk9uY01RIn0.U3dVcaFPY5ZHlz9sntx3O2Svz_HfapBpryrUB9ipIdoIHgu_Skp40FGaLu0Fx-_Uo2GE4pfvM-XR1bQgQGyVbMhALpe2CZrKKCe9k1v4VZ_zxwhYDdV8WNr99jmbMtnm_I9rZIz3YU9dyjWlV_ktHV0bPHj1wIjBUrUc9P_EF5Vw3CeNMxlFXZ2xYldT9XdYUotJHIoJ-e_KWo0hMn_qF5xvWxD-RJIQL7G2ZxEcsmMe9JovwZoAnoqaIyutMP8g7X1UfoTGs-Fa6B1xXhtrYBms--sm_FrM5w0rjIyOuyujulPTeXO8_CbuL1Yz5kCcBsuJdXTiyTcTV9R8W0f4Rg",
  "expires_in": 86400,
  "token_type": "Bearer"
}
Enter fullscreen mode Exit fullscreen mode

运行应用程序

将变量替换为您之前保存的值${AUTH0_DOMAIN}然后启动 NestJS 应用程序。${AUTH0_CLIENT_ID}${AUTH0_CLIENT_SECRET}

$ export AUTH0_DOMAIN=${AUTH0_DOMAIN}
$ export AUTH0_CLIENT_ID=${AUTH0_CLIENT_ID}
$ export AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET}
$ npm run start:dev
Enter fullscreen mode Exit fullscreen mode

无需身份验证即可发出请求

应用程序运行后,使用 cURL 访问 API 端点/secret 由于这两个端点都已标记了注解,因此无法公开访问,/profile您将收到 401(未授权)错误。
AuthGuard

$ curl http://localhost:3000/secret
Enter fullscreen mode Exit fullscreen mode
$ curl http://localhost:3000/profile
Enter fullscreen mode Exit fullscreen mode

使用身份验证发出请求

现在您可以运行相同的请求,但通过标头提供访问令牌Authorization

由于您之前已授权自己,现在您可以访问两个私有端点/secret/profile

/profile端点还会返回您从 Auth0 收到的个人资料信息,这些信息来自 Auth0 管理 API。

Auth0 管理 API 使用subJWT 响应中的 (subject) 字段来检索您的用户个人资料。
为了便于文档记录,示例应用程序中的 JWT 响应已记录到控制台。

$ curl http://localhost:3000/secret \
  -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik9EQXpOMEZHTWpBd1FVUTJORFpETXpBMVJETXdSRUUyTjBSRE5qRXpNemRFUWtNMk5URTFNUSJ9.eyJpc3MiOiJodHRwczovL25lc3Rqcy1hdXRoMC1qd3QuYXUuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEzMDc2NzYwNTUyNTQ5MTY1ODQ5IiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaWF0IjoxNTY1OTg1MzU3LCJleHAiOjE1NjYwNzE3NTcsImF6cCI6IjR0eFZVSm5ZVlRhbzRsczA2dFozRGNCdm5JVk9uY01RIn0.U3dVcaFPY5ZHlz9sntx3O2Svz_HfapBpryrUB9ipIdoIHgu_Skp40FGaLu0Fx-_Uo2GE4pfvM-XR1bQgQGyVbMhALpe2CZrKKCe9k1v4VZ_zxwhYDdV8WNr99jmbMtnm_I9rZIz3YU9dyjWlV_ktHV0bPHj1wIjBUrUc9P_EF5Vw3CeNMxlFXZ2xYldT9XdYUotJHIoJ-e_KWo0hMn_qF5xvWxD-RJIQL7G2ZxEcsmMe9JovwZoAnoqaIyutMP8g7X1UfoTGs-Fa6B1xXhtrYBms--sm_FrM5w0rjIyOuyujulPTeXO8_CbuL1Yz5kCcBsuJdXTiyTcTV9R8W0f4Rg'
Enter fullscreen mode Exit fullscreen mode
$ curl http://localhost:3000/profile \
  -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik9EQXpOMEZHTWpBd1FVUTJORFpETXpBMVJETXdSRUUyTjBSRE5qRXpNemRFUWtNMk5URTFNUSJ9.eyJpc3MiOiJodHRwczovL25lc3Rqcy1hdXRoMC1qd3QuYXUuYXV0aDAuY29tLyIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEzMDc2NzYwNTUyNTQ5MTY1ODQ5IiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwiaWF0IjoxNTY1OTg1MzU3LCJleHAiOjE1NjYwNzE3NTcsImF6cCI6IjR0eFZVSm5ZVlRhbzRsczA2dFozRGNCdm5JVk9uY01RIn0.U3dVcaFPY5ZHlz9sntx3O2Svz_HfapBpryrUB9ipIdoIHgu_Skp40FGaLu0Fx-_Uo2GE4pfvM-XR1bQgQGyVbMhALpe2CZrKKCe9k1v4VZ_zxwhYDdV8WNr99jmbMtnm_I9rZIz3YU9dyjWlV_ktHV0bPHj1wIjBUrUc9P_EF5Vw3CeNMxlFXZ2xYldT9XdYUotJHIoJ-e_KWo0hMn_qF5xvWxD-RJIQL7G2ZxEcsmMe9JovwZoAnoqaIyutMP8g7X1UfoTGs-Fa6B1xXhtrYBms--sm_FrM5w0rjIyOuyujulPTeXO8_CbuL1Yz5kCcBsuJdXTiyTcTV9R8W0f4Rg'
Enter fullscreen mode Exit fullscreen mode

我希望这篇文章能让你了解如何在 NestJS 应用程序中实现像 Auth0 这样的身份验证提供程序。

将 Auth0 和身份验证集成到我的项目中非常麻烦,但最终我还是成功了,感觉真的很棒。

我无法在我的 NestJS 项目中配置 Auth0 身份验证。我也在这里寻求过帮助:

这个问题困扰了我好几天,然后我休息了一天左右,没碰代码。之后
我头脑清醒地重新开始,一个小时就解决了问题🥳。

这就是我热爱编程的原因,你总能体验到取得成就的成就感💪。
而且通常每隔几个小时或几天(取决于项目规模)就会有小小的突破。对我来说,这就是工作中最大的动力!

顺便说一句:我正在写一篇文章,介绍我是如何解决这个问题的😉。

我也认为 NestJS 的身份验证文档可以改进,以便用户更容易正确实现并避免安全漏洞。不过,我并不抱怨,毕竟 NestJS 是开源的,任何人都可以参与文档的改进。

如果您对 NestJS、Auth0 或其他主题有任何疑问,请随时通过Twitterdev.to或邮件yo@fullstack.to私信我。我很乐意为您提供帮助!

链接


如果你喜欢我的内容,不妨在推特上关注我:@fullstack_to

封面图片来自UnsplashJason Blackeye

文章来源:https://dev.to/matthias/use-auth0-to-secure-your-nestjs-application-mbo