├── .prettierrc
├── nest-cli.json
├── tsconfig.build.json
├── src
├── user.service.ts
├── main.ts
├── user.controller.ts
└── app.module.ts
├── test
├── jest-e2e.json
└── app.e2e-spec.ts
├── tsconfig.json
├── README.md
├── .gitignore
├── .eslintrc.js
└── package.json
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/user.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class UserService {
5 | getHello(): string {
6 | return 'Hello';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 |
4 | async function bootstrap() {
5 | const app = await NestFactory.create(AppModule);
6 | await app.listen(4000);
7 | }
8 | bootstrap();
9 |
--------------------------------------------------------------------------------
/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": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Secure NestJs Rest API with Keycloak
2 |
3 | Detailed description available in medium
4 |
5 |
6 |
7 | ## Installation
8 |
9 | ```bash
10 | $ npm install
11 | ```
12 |
13 | ## Running the app
14 |
15 | ```bash
16 | # development
17 | $ npm run start
18 |
19 | # watch mode
20 | $ npm run start:dev
21 |
22 | # production mode
23 | $ npm run start:prod
24 | ```
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | yarn-debug.log*
10 | yarn-error.log*
11 | lerna-debug.log*
12 |
13 | # OS
14 | .DS_Store
15 |
16 | # Tests
17 | /coverage
18 | /.nyc_output
19 |
20 | # IDEs and editors
21 | /.idea
22 | .project
23 | .classpath
24 | .c9/
25 | *.launch
26 | .settings/
27 | *.sublime-workspace
28 |
29 | # IDE - VSCode
30 | .vscode/*
31 | !.vscode/settings.json
32 | !.vscode/tasks.json
33 | !.vscode/launch.json
34 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'prettier/@typescript-eslint',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import {
3 | Roles,
4 | Unprotected,
5 | } from 'nest-keycloak-connect';
6 | import { UserService } from './user.service';
7 |
8 | @Controller()
9 | export class UserController {
10 | constructor(private readonly userService: UserService) {}
11 |
12 | @Get('/public')
13 | @Unprotected()
14 | getpublic(): string {
15 | return `${this.userService.getHello()} from public`;
16 | }
17 |
18 | @Get('/user')
19 | @Roles({ roles: ['user'] })
20 | // @Roles({ roles: ['realm:app-user']) // protected using realm role
21 | getUser(): string {
22 | return `${this.userService.getHello()} from user`;
23 | }
24 |
25 | @Get('/admin')
26 | @Roles({ roles: ['admin'] })
27 | getAdmin(): string {
28 | return `${this.userService.getHello()} from admin`;
29 | }
30 |
31 | @Get('/all')
32 | @Roles({ roles: ['user', 'admin'] })
33 | getAll(): string {
34 | return `${this.userService.getHello()} from all`;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { UserController } from './user.controller';
3 | import { UserService } from './user.service';
4 | import {
5 | KeycloakConnectModule,
6 | ResourceGuard,
7 | RoleGuard,
8 | AuthGuard,
9 | } from 'nest-keycloak-connect';
10 | import { APP_GUARD } from '@nestjs/core';
11 |
12 | @Module({
13 | imports: [
14 | KeycloakConnectModule.register({
15 | authServerUrl: 'http://localhost:8080/auth',
16 | realm: 'Demo-Realm',
17 | clientId: 'nest-app',
18 | secret: '83790b4f-48cd-4b6c-ac60-451a918be4b9', // Secret key of the client taken from keycloak server
19 | }),
20 | ],
21 | controllers: [UserController],
22 | providers: [
23 | UserService,
24 | // This adds a global level authentication guard, you can also have it scoped
25 | // if you like.
26 | //
27 | // Will return a 401 unauthorized when it is unable to
28 | // verify the JWT token or Bearer header is missing.
29 | {
30 | provide: APP_GUARD,
31 | useClass: AuthGuard,
32 | },
33 | // This adds a global level resource guard, which is permissive.
34 | // Only controllers annotated with @Resource and methods with @Scopes
35 | // are handled by this guard.
36 | {
37 | provide: APP_GUARD,
38 | useClass: ResourceGuard,
39 | },
40 | // New in 1.1.0
41 | // This adds a global level role guard, which is permissive.
42 | // Used by `@Roles` decorator with the optional `@AllowAnyRole` decorator for allowing any
43 | // specified role passed.
44 | {
45 | provide: APP_GUARD,
46 | useClass: RoleGuard,
47 | },
48 | ],
49 | })
50 | export class AppModule {}
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-keycloak",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "Chamith Madusanka",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "start:debug": "nest start --debug --watch",
15 | "start:prod": "node dist/main",
16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
17 | "test": "jest",
18 | "test:watch": "jest --watch",
19 | "test:cov": "jest --coverage",
20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
21 | "test:e2e": "jest --config ./test/jest-e2e.json"
22 | },
23 | "dependencies": {
24 | "@nestjs/common": "^10.4.0",
25 | "@nestjs/core": "^10.4.0",
26 | "@nestjs/platform-express": "^10.4.0",
27 | "nest-keycloak-connect": "1.10.1",
28 | "reflect-metadata": "^0.2.2",
29 | "rimraf": "^6.0.1",
30 | "rxjs": "^7.8.1"
31 | },
32 | "devDependencies": {
33 | "@nestjs/cli": "^10.4.4",
34 | "@nestjs/schematics": "^10.1.3",
35 | "@nestjs/testing": "^10.4.0",
36 | "@types/express": "^4.17.21",
37 | "@types/jest": "^29.5.12",
38 | "@types/node": "^22.2.0",
39 | "@types/supertest": "^6.0.2",
40 | "@typescript-eslint/eslint-plugin": "^8.0.1",
41 | "@typescript-eslint/parser": "^8.0.1",
42 | "eslint": "^9.9.0",
43 | "eslint-config-prettier": "9.1.0",
44 | "eslint-plugin-prettier": "^5.2.1",
45 | "jest": "^29.7.0",
46 | "prettier": "^3.3.3",
47 | "supertest": "^7.0.0",
48 | "ts-jest": "^29.2.4",
49 | "ts-loader": "^9.5.1",
50 | "ts-node": "^10.9.2",
51 | "tsconfig-paths": "^4.2.0",
52 | "typescript": "^5.5.4"
53 | },
54 | "jest": {
55 | "moduleFileExtensions": [
56 | "js",
57 | "json",
58 | "ts"
59 | ],
60 | "rootDir": "src",
61 | "testRegex": ".*\\.spec\\.ts$",
62 | "transform": {
63 | "^.+\\.(t|j)s$": "ts-jest"
64 | },
65 | "collectCoverageFrom": [
66 | "**/*.(t|j)s"
67 | ],
68 | "coverageDirectory": "../coverage",
69 | "testEnvironment": "node"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------