├── .prettierrc ├── tsconfig.build.json ├── src ├── app.service.ts ├── app.controller.ts ├── app.controller.spec.ts ├── app.module.ts └── main.ts ├── libs └── zitadel-auth │ ├── src │ ├── interfaces │ │ ├── zitadel-auth-module-config.interface.ts │ │ └── zitadel-user.request.ts │ ├── zitadel-auth.module-definition.ts │ ├── index.ts │ ├── zitadel-auth.module.ts │ └── strategy │ │ └── zitadel.strategy.ts │ └── tsconfig.lib.json ├── @types └── express │ └── index.d.ts ├── .env.sample ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── .gitignore ├── nest-cli.json ├── .github └── dependabot.yml ├── .eslintrc.js ├── tsconfig.json ├── LICENSE.md ├── package.json └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/zitadel-auth/src/interfaces/zitadel-auth-module-config.interface.ts: -------------------------------------------------------------------------------- 1 | import { ZitadelIntrospectionOptions } from 'passport-zitadel'; 2 | 3 | // we merely define an alias type for the interface here 4 | export type ZitadelAuthModuleConfig = ZitadelIntrospectionOptions; 5 | -------------------------------------------------------------------------------- /@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ZitadelUser } from 'libs/zitadel-auth/src/interfaces/zitadel-user.request'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 4 | namespace Express { 5 | export interface Request { 6 | user?: ZitadelUser; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/zitadel-auth/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "outDir": "../../dist/libs/zitadel-auth" 6 | }, 7 | "include": ["src/**/*"], 8 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | APP_PORT=8080 2 | NODE_ENV="development" 3 | 4 | OPENAPI_CLIENT_ID= 5 | OPENAPI_CLIENT_SECRET= 6 | 7 | IDP_AUTHORITY= 8 | IDP_AUTHORIZATION_TYPE= 9 | IDP_AUTHORIZATION_PROFILE_TYPE= 10 | IDP_AUTHORIZATION_PROFILE_KEY_ID= 11 | IDP_AUTHORIZATION_PROFILE_KEY= 12 | IDP_AUTHORIZATION_PROFILE_APP_ID= 13 | IDP_AUTHORIZATION_PROFILE_CLIENT_ID= -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testEnvironment": "node", 9 | "testRegex": ".e2e-spec.ts$", 10 | "transform": { 11 | "^.+\\.(t|j)s$": "ts-jest" 12 | }, 13 | "moduleNameMapper": { 14 | "@auth/zitadel-auth/(.*)": "/../libs/zitadel-auth/src/$1", 15 | "@auth/zitadel-auth": "/../libs/zitadel-auth/src" 16 | } 17 | } -------------------------------------------------------------------------------- /libs/zitadel-auth/src/zitadel-auth.module-definition.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurableModuleBuilder } from '@nestjs/common'; 2 | import { ZitadelAuthModuleConfig } from './interfaces/zitadel-auth-module-config.interface'; 3 | 4 | // https://docs.nestjs.com/fundamentals/dynamic-modules#configurable-module-builder 5 | export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = 6 | new ConfigurableModuleBuilder() 7 | .setClassMethodName('forRoot') 8 | .build(); 9 | -------------------------------------------------------------------------------- /libs/zitadel-auth/src/index.ts: -------------------------------------------------------------------------------- 1 | // entrypoint - export your stuff here 2 | export * from './zitadel-auth.module'; 3 | 4 | // export token which is used to inject AuthModule config 5 | export { MODULE_OPTIONS_TOKEN as AUTH_OPTIONS_TOKEN } from './zitadel-auth.module-definition'; 6 | 7 | // export backend-facing interfaces 8 | export { ZitadelUser } from './interfaces/zitadel-user.request'; 9 | export { ZitadelAuthModuleConfig } from './interfaces/zitadel-auth-module-config.interface'; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # OS 15 | .DS_Store 16 | 17 | # Tests 18 | /coverage 19 | /.nyc_output 20 | 21 | # IDEs and editors 22 | /.idea 23 | .project 24 | .classpath 25 | .c9/ 26 | *.launch 27 | .settings/ 28 | *.sublime-workspace 29 | 30 | # IDE - VSCode 31 | .vscode/* 32 | !.vscode/settings.json 33 | !.vscode/tasks.json 34 | !.vscode/launch.json 35 | !.vscode/extensions.json -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src", 5 | "compilerOptions": { 6 | "deleteOutDir": true, 7 | "webpack": true 8 | }, 9 | "projects": { 10 | "zitadel-auth": { 11 | "type": "library", 12 | "root": "libs/zitadel-auth", 13 | "entryFile": "index", 14 | "sourceRoot": "libs/zitadel-auth/src", 15 | "compilerOptions": { 16 | "tsConfigPath": "libs/zitadel-auth/tsconfig.lib.json" 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /libs/zitadel-auth/src/zitadel-auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | import { 4 | ConfigurableModuleClass, 5 | MODULE_OPTIONS_TOKEN, 6 | } from './zitadel-auth.module-definition'; 7 | import { ZitadelStrategy } from './strategy/zitadel.strategy'; 8 | 9 | @Module({ 10 | imports: [PassportModule], 11 | providers: [ 12 | { 13 | provide: MODULE_OPTIONS_TOKEN, 14 | useValue: MODULE_OPTIONS_TOKEN, 15 | }, 16 | ZitadelStrategy, 17 | ], 18 | exports: [ZitadelStrategy], 19 | }) 20 | export class ZitadelAuthModule extends ConfigurableModuleClass {} 21 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpStatus, UseGuards } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | import { ApiBearerAuth, ApiResponse } from '@nestjs/swagger'; 4 | import { AuthGuard } from '@nestjs/passport'; 5 | 6 | @ApiBearerAuth('zitadel-jwt') 7 | @ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Not authorized' }) 8 | @UseGuards(AuthGuard('zitadel')) 9 | @Controller({ path: 'app', version: '1' }) 10 | export class AppController { 11 | constructor(private readonly appService: AppService) {} 12 | 13 | @Get() 14 | getHello(): string { 15 | return this.appService.getHello(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir: __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "ES2021", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strictNullChecks": false, 17 | "noImplicitAny": false, 18 | "strictBindCallApply": false, 19 | "forceConsistentCasingInFileNames": false, 20 | "noFallthroughCasesInSwitch": false, 21 | // "typeRoots": ["./@types", ".yarn/cache/**/node_modules/@types"] 22 | "paths": { 23 | "@auth/zitadel-auth": ["libs/zitadel-auth/src"], 24 | "@auth/zitadel-auth/*": ["libs/zitadel-auth/src/*"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /libs/zitadel-auth/src/strategy/zitadel.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ZitadelIntrospectionStrategy } from 'passport-zitadel'; 4 | import { MODULE_OPTIONS_TOKEN } from '../zitadel-auth.module-definition'; 5 | import { ZitadelAuthModuleConfig } from '../interfaces/zitadel-auth-module-config.interface'; 6 | import { ZitadelUser } from '../interfaces/zitadel-user.request'; 7 | 8 | @Injectable() 9 | export class ZitadelStrategy extends PassportStrategy( 10 | ZitadelIntrospectionStrategy, 11 | 'zitadel', 12 | ) { 13 | constructor( 14 | @Inject(MODULE_OPTIONS_TOKEN) 15 | options: ZitadelAuthModuleConfig, 16 | ) { 17 | super(options); 18 | } 19 | // Required due to PassportStrategy's abstract declaration 20 | async validate(payload: ZitadelUser): Promise { 21 | // Return the payload as-is since it already contains all the user information 22 | return payload; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 EHW+ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /libs/zitadel-auth/src/interfaces/zitadel-user.request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a Zitadel user with information about their authentication token. 3 | */ 4 | export interface ZitadelUser { 5 | /** 6 | * Whether this Zitadel user is currently active. 7 | * If active is true, further information will be provided. 8 | */ 9 | active: boolean; 10 | 11 | /** 12 | * A whitespace-separated string that contains the scopes of the access_token. 13 | * These scopes might differ from the provided scope parameter. 14 | */ 15 | scope: string; 16 | 17 | /** 18 | * The client ID of the Zitadel application in the form of `@p`. 19 | */ 20 | client_id: string; 21 | 22 | /** 23 | * Type of the access_token. The value is always 'Bearer'. 24 | */ 25 | token_type: string; 26 | 27 | /** 28 | * The expiration time of the token as a Unix timestamp. 29 | */ 30 | exp: unknown; 31 | 32 | /** 33 | * The time when the token was issued as a Unix timestamp. 34 | */ 35 | iat: string; 36 | 37 | /** 38 | * The time before which the token must not be used as a Unix timestamp. 39 | */ 40 | nbf: string; 41 | 42 | /** 43 | * The subject identifier of the user. 44 | */ 45 | sub: string; 46 | 47 | /** 48 | * The audience of the token, which can be a string or an array of strings. 49 | */ 50 | aud: string | string[]; 51 | 52 | /** 53 | * The issuer of the token. 54 | */ 55 | iss: string; 56 | 57 | /** 58 | * The unique identifier of the token. 59 | */ 60 | jti: string; 61 | 62 | /** 63 | * The ZITADEL login name of the user, consisting of username@primarydomain. 64 | */ 65 | username: string; 66 | 67 | /** 68 | * The full name of the user. 69 | */ 70 | name: string; 71 | 72 | /** 73 | * The given name or first name of the user. 74 | */ 75 | given_name: string; 76 | 77 | /** 78 | * The family name or last name of the user. 79 | */ 80 | family_name: string; 81 | 82 | /** 83 | * The user's preferred locale. 84 | */ 85 | locale: string; 86 | 87 | /** 88 | * The time when the user's information was last updated as a Unix timestamp. 89 | */ 90 | updated_at: string; 91 | 92 | /** 93 | * The preferred username of the user. 94 | */ 95 | preferred_username: string; 96 | 97 | /** 98 | * The user's email address. 99 | */ 100 | email: string; 101 | 102 | /** 103 | * Indicates whether the user's email has been verified. 104 | */ 105 | email_verified: boolean; 106 | } 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zitadel-nodejs-nestjs", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "MIT", 8 | "scripts": { 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"libs/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^11.1.8", 24 | "@nestjs/config": "^4.0.2", 25 | "@nestjs/core": "^10.4.15", 26 | "@nestjs/passport": "^11.0.5", 27 | "@nestjs/platform-express": "^10.4.19", 28 | "@nestjs/swagger": "^8.1.1", 29 | "class-transformer": "^0.5.1", 30 | "class-validator": "^0.14.2", 31 | "joi": "^18.0.1", 32 | "passport-zitadel": "^1.2.3", 33 | "reflect-metadata": "^0.2.2", 34 | "rxjs": "^7.8.2" 35 | }, 36 | "devDependencies": { 37 | "@nestjs/cli": "^11.0.10", 38 | "@nestjs/schematics": "^11.0.9", 39 | "@nestjs/testing": "^10.4.8", 40 | "@types/express": "^5.0.5", 41 | "@types/jest": "^30.0.0", 42 | "@types/node": "^24.10.0", 43 | "@types/supertest": "^6.0.3", 44 | "@typescript-eslint/eslint-plugin": "^7.0.0", 45 | "@typescript-eslint/parser": "^6.21.0", 46 | "eslint": "^8.57.1", 47 | "eslint-config-prettier": "^10.1.8", 48 | "eslint-plugin-prettier": "^5.5.4", 49 | "jest": "^30.2.0", 50 | "prettier": "^3.6.2", 51 | "source-map-support": "^0.5.21", 52 | "supertest": "^7.1.4", 53 | "ts-jest": "^29.4.5", 54 | "ts-loader": "^9.5.4", 55 | "ts-node": "^10.9.2", 56 | "tsconfig-paths": "^4.2.0", 57 | "typescript": "^5.9.3" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": ".", 66 | "testRegex": ".*\\.spec\\.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "collectCoverageFrom": [ 71 | "**/*.(t|j)s" 72 | ], 73 | "coverageDirectory": "./coverage", 74 | "testEnvironment": "node", 75 | "roots": [ 76 | "/src/", 77 | "/libs/" 78 | ], 79 | "moduleNameMapper": { 80 | "^@auth/zitadel-auth(|/.*)$": "/libs/zitadel-auth/src/$1" 81 | } 82 | }, 83 | "packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72" 84 | } 85 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { ConfigModule, ConfigService } from '@nestjs/config'; 5 | import * as Joi from 'joi'; 6 | import { ZitadelAuthModule, ZitadelAuthModuleConfig } from '@auth/zitadel-auth'; 7 | 8 | @Module({ 9 | imports: [ 10 | ConfigModule.forRoot({ 11 | isGlobal: true, 12 | validationSchema: Joi.object({ 13 | NODE_ENV: Joi.string() 14 | .valid('development', 'production', 'test', 'provision') 15 | .default('production'), 16 | OPENAPI_CLIENT_ID: Joi.string().when('NODE_ENV', { 17 | is: 'production', 18 | then: Joi.optional(), 19 | otherwise: Joi.required(), 20 | }), 21 | OPENAPI_CLIENT_SECRET: Joi.string().when('NODE_ENV', { 22 | is: 'production', 23 | then: Joi.optional(), 24 | otherwise: Joi.required(), 25 | }), 26 | IDP_AUTHORITY: Joi.string().required(), 27 | IDP_AUTHORIZATION_TYPE: Joi.string() 28 | .valid('jwt-profile') 29 | .default('jwt-profile') 30 | .optional(), 31 | IDP_AUTHORIZATION_PROFILE_TYPE: Joi.string() 32 | .valid('application') 33 | .default('application') 34 | .optional(), 35 | IDP_AUTHORIZATION_PROFILE_KEY_ID: Joi.string().required(), 36 | IDP_AUTHORIZATION_PROFILE_KEY: Joi.string().required(), 37 | IDP_AUTHORIZATION_PROFILE_APP_ID: Joi.string().required(), 38 | IDP_AUTHORIZATION_PROFILE_CLIENT_ID: Joi.string().required(), 39 | }), 40 | validationOptions: { 41 | // allowUnknown: false, 42 | abortEarly: true, 43 | }, 44 | }), 45 | ZitadelAuthModule.forRootAsync({ 46 | imports: [ConfigModule], 47 | useFactory: (config: ConfigService): ZitadelAuthModuleConfig => { 48 | return { 49 | authority: config.getOrThrow('IDP_AUTHORITY'), 50 | authorization: { 51 | type: config.getOrThrow<'jwt-profile'>('IDP_AUTHORIZATION_TYPE'), 52 | profile: { 53 | type: config.getOrThrow<'application'>( 54 | 'IDP_AUTHORIZATION_PROFILE_TYPE', 55 | ), 56 | keyId: config.getOrThrow( 57 | 'IDP_AUTHORIZATION_PROFILE_KEY_ID', 58 | ), 59 | key: config.getOrThrow('IDP_AUTHORIZATION_PROFILE_KEY'), 60 | appId: config.getOrThrow( 61 | 'IDP_AUTHORIZATION_PROFILE_APP_ID', 62 | ), 63 | clientId: config.getOrThrow( 64 | 'IDP_AUTHORIZATION_PROFILE_CLIENT_ID', 65 | ), 66 | }, 67 | }, 68 | }; 69 | }, 70 | inject: [ConfigService], 71 | }), 72 | ], 73 | controllers: [AppController], 74 | providers: [ConfigService, AppService], 75 | }) 76 | export class AppModule {} 77 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClassSerializerInterceptor, 3 | NestApplicationOptions, 4 | ValidationPipe, 5 | ValidationPipeOptions, 6 | } from '@nestjs/common'; 7 | import { 8 | CorsOptions, 9 | CorsOptionsDelegate, 10 | } from '@nestjs/common/interfaces/external/cors-options.interface'; 11 | import { ConfigService } from '@nestjs/config'; 12 | import { NestFactory, Reflector } from '@nestjs/core'; 13 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 14 | import * as fs from 'fs'; 15 | import { AppModule } from './app.module'; 16 | import * as packageJson from '../package.json'; 17 | import * as path from 'path'; 18 | 19 | async function bootstrap() { 20 | const appOptions: NestApplicationOptions = { 21 | logger: ['error', 'warn', 'debug', 'log'], 22 | }; 23 | const corsOptions: CorsOptions | CorsOptionsDelegate = {}; 24 | const validationOptions: ValidationPipeOptions = { 25 | transform: true, 26 | always: true, 27 | forbidUnknownValues: true, 28 | }; 29 | 30 | const globalApiPrefix: string = '/api'; 31 | const scopes: string[] = ['openid', 'profile', 'email', 'offline_access']; 32 | let redirectUri: string; 33 | 34 | const app = await NestFactory.create(AppModule, appOptions); 35 | app.enableCors(corsOptions); 36 | app.useGlobalPipes(new ValidationPipe(validationOptions)); 37 | app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector))); 38 | app.enableVersioning(); 39 | app.setGlobalPrefix(globalApiPrefix); 40 | 41 | const config: ConfigService = app.get(ConfigService); 42 | 43 | const port: number = config.getOrThrow('APP_PORT'); 44 | const clientId: string = config.getOrThrow('OPENAPI_CLIENT_ID'); 45 | const clientSecret: string = config.getOrThrow( 46 | 'OPENAPI_CLIENT_SECRET', 47 | ); 48 | const authority: string = config.getOrThrow('IDP_AUTHORITY'); 49 | 50 | const swaggerDocument = new DocumentBuilder() 51 | .setTitle('Zitadel NestJs Example') 52 | .setTermsOfService('http://swagger.io/terms/') 53 | .setExternalDoc('Find out more about Swagger', 'http://swagger.io/') 54 | .setContact('Contact the developer', '', 'mail@example.com') 55 | .setLicense('Apache 2.0', 'http://www.apache.org/licenses/LICENSE-2.0.html') 56 | .setVersion(packageJson.version) 57 | // Authentication security by token introspection 58 | .addSecurity('zitadel-jwt', { 59 | type: 'openIdConnect', 60 | openIdConnectUrl: `${authority}/.well-known/openid-configuration`, 61 | name: 'Zitadel', 62 | }); 63 | if (config.get('NODE_ENV') !== 'production') { 64 | swaggerDocument.addServer(`http://localhost:${port}`); 65 | redirectUri = `http://localhost:${port}`; 66 | } else { 67 | redirectUri = 'YOUR PROD URL HERE'; 68 | throw new Error('SET YOUR PROD URL HERE AND REMOVE THIS THROW'); 69 | } 70 | 71 | const document = SwaggerModule.createDocument(app, swaggerDocument.build()); 72 | SwaggerModule.setup(globalApiPrefix.slice(1), app, document, { 73 | swaggerOptions: { 74 | persistAuthorization: true, 75 | oauth2RedirectUrl: `${redirectUri}${globalApiPrefix}/oauth2-redirect.html`, 76 | initOAuth: { 77 | clientId, 78 | clientSecret, 79 | scopes, 80 | }, 81 | }, 82 | }); 83 | if (config.get('NODE_ENV') !== 'production') { 84 | fs.writeFileSync( 85 | path.join(__dirname, '..', 'swagger.json'), 86 | JSON.stringify(document), 87 | ); 88 | } 89 | await app.listen(port); 90 | } 91 | bootstrap().then(); 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zitadel-nodejs-nestjs 2 | 3 | Welcome to the `zitadel-nodejs-nestjs` repository! This example NestJs application demonstrates how to implement authentication using the Zitadel OIDC (OpenID Connect) flow with the help of Passport. Please note that this example exclusively focuses on authentication and does not include authorization. 4 | 5 | ## Table of Contents 6 | 7 | - [Introduction](#introduction) 8 | - [Getting Started](#getting-started) 9 | - [Prerequisites](#prerequisites) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Contributing](#contributing) 13 | - [License](#license) 14 | 15 | ## Introduction 16 | 17 | [Zitadel](https://zitadel.ch/) is a comprehensive Identity and Access Management (IAM) solution that offers robust authentication and authorization capabilities. This example application showcases how to leverage Zitadel for authentication within a NestJs application. 18 | 19 | :warning: Please be aware that this example only deals with authentication and does not cover the authorization aspect of your application. 20 | 21 | ## Getting Started 22 | 23 | Follow these steps to set up and run the `zitadel-nodejs-nestjs` application on your local machine. 24 | 25 | ### Prerequisites 26 | 27 | Before you begin, ensure you have the following prerequisites in place: 28 | 29 | - Node.js and npm installed on your system 30 | - Yarn package manager installed (you can install it with `npm install -g yarn`) 31 | - Basic knowledge of NestJs, Passport, and OIDC authentication 32 | - A Zitadel account and OIDC client credentials (client ID and client secret) 33 | 34 | Your Zitadel should be configured to look like this: 35 | 36 | ```mermaid 37 | graph TB 38 | 39 | subgraph cluster_zitadel 40 | Zitadel((Zitadel Instance)) 41 | Organization((Your Organization)) 42 | Project((Development Project)) 43 | APIApp((API Type Application)) 44 | WebApp((Web Type Application)) 45 | end 46 | 47 | %%subgraph cluster_app 48 | %% NestJs((NestJs Backend)) 49 | %% NestJs ---> OpenAPI((NestJs Swagger)) 50 | %%end 51 | 52 | Zitadel --> Organization 53 | Organization --> Project 54 | Project --> APIApp 55 | Project --> WebApp 56 | %% NestJs -.- APIApp 57 | %% OpenAPI -.- WebApp 58 | ``` 59 | 60 | ### Installation 61 | 62 | 1. Clone the repository: 63 | 64 | ```shell 65 | git clone https://github.com/ehwplus/zitadel-nodejs-nestjs.git 66 | cd zitadel-nodejs-nestjs 67 | ``` 68 | 69 | Install the project dependencies using Yarn: 70 | 71 | ```shell 72 | yarn install 73 | ``` 74 | 75 | 2. Create a .env file in the project root. You can use the provided .env.example file as a template and set the following environment variables: 76 | 77 | ``` 78 | APP_PORT=8080 79 | NODE_ENV="development" 80 | 81 | OPENAPI_CLIENT_ID= 82 | OPENAPI_CLIENT_SECRET= 83 | 84 | IDP_AUTHORITY= 85 | IDP_AUTHORIZATION_TYPE= 86 | IDP_AUTHORIZATION_PROFILE_TYPE= 87 | IDP_AUTHORIZATION_PROFILE_KEY_ID= 88 | IDP_AUTHORIZATION_PROFILE_KEY= 89 | IDP_AUTHORIZATION_PROFILE_APP_ID= 90 | IDP_AUTHORIZATION_PROFILE_CLIENT_ID= 91 | ``` 92 | 93 | 3. Replace the values as needed, especially the Zitadel OIDC client credentials and issuer URL. 94 | 95 | 4. Start the application: 96 | ```shell 97 | yarn start 98 | ``` 99 | 100 | Your zitadel-nodejs-nestjs application should now be up and running, configured to use Zitadel OIDC authentication. 101 | 102 | ## Usage 103 | 104 | This example demonstrates the basic setup for integrating Zitadel OIDC authentication into a NestJs application. It showcases the login flow and user authentication. However, remember that this project doesn't cover authorization, and you should implement your own authorization logic according to your application's needs. 105 | 106 | Feel free to explore, modify, and extend the code to meet your specific requirements. 107 | 108 | ## Contributing 109 | 110 | We welcome contributions from the community. If you find issues or have ideas for improvements, please open an issue or submit a pull request. Your input is highly valued. 111 | 112 | We unfortunately neither have a CONTRIBUTING.md nor have the resources to provide a helpful CONTRIBUTING.md. 113 | 114 | 115 | 116 | ## License 117 | 118 | This project is licensed under the MIT License. You are free to use, modify, and distribute the code as per the terms specified in the license. 119 | --------------------------------------------------------------------------------