nestjs-keycloak-auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +391 -0
  3. package/dist/constants.d.ts +65 -0
  4. package/dist/constants.js +72 -0
  5. package/dist/controllers/keycloak-admin.controller.d.ts +16 -0
  6. package/dist/controllers/keycloak-admin.controller.js +94 -0
  7. package/dist/decorators/access-token.decorator.d.ts +5 -0
  8. package/dist/decorators/access-token.decorator.js +13 -0
  9. package/dist/decorators/enforcer-options.decorator.d.ts +8 -0
  10. package/dist/decorators/enforcer-options.decorator.js +12 -0
  11. package/dist/decorators/keycloak-user.decorator.d.ts +5 -0
  12. package/dist/decorators/keycloak-user.decorator.js +13 -0
  13. package/dist/decorators/public.decorator.d.ts +6 -0
  14. package/dist/decorators/public.decorator.js +11 -0
  15. package/dist/decorators/resource.decorator.d.ts +6 -0
  16. package/dist/decorators/resource.decorator.js +11 -0
  17. package/dist/decorators/roles.decorator.d.ts +10 -0
  18. package/dist/decorators/roles.decorator.js +15 -0
  19. package/dist/decorators/scopes.decorator.d.ts +19 -0
  20. package/dist/decorators/scopes.decorator.js +27 -0
  21. package/dist/decorators/token-scopes.decorator.d.ts +19 -0
  22. package/dist/decorators/token-scopes.decorator.js +24 -0
  23. package/dist/errors.d.ts +24 -0
  24. package/dist/errors.js +44 -0
  25. package/dist/guards/auth.guard.d.ts +25 -0
  26. package/dist/guards/auth.guard.js +176 -0
  27. package/dist/guards/resource.guard.d.ts +26 -0
  28. package/dist/guards/resource.guard.js +245 -0
  29. package/dist/guards/role.guard.d.ts +17 -0
  30. package/dist/guards/role.guard.js +113 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +18 -0
  33. package/dist/interface/enforcer-options.interface.d.ts +8 -0
  34. package/dist/interface/enforcer-options.interface.js +2 -0
  35. package/dist/interface/jwks.interface.d.ts +22 -0
  36. package/dist/interface/jwks.interface.js +2 -0
  37. package/dist/interface/jwt.interface.d.ts +46 -0
  38. package/dist/interface/jwt.interface.js +2 -0
  39. package/dist/interface/keycloak-auth-module-async-options.interface.d.ts +11 -0
  40. package/dist/interface/keycloak-auth-module-async-options.interface.js +2 -0
  41. package/dist/interface/keycloak-auth-options-factory.interface.d.ts +4 -0
  42. package/dist/interface/keycloak-auth-options-factory.interface.js +2 -0
  43. package/dist/interface/keycloak-auth-options.interface.d.ts +162 -0
  44. package/dist/interface/keycloak-auth-options.interface.js +3 -0
  45. package/dist/interface/keycloak-grant.interface.d.ts +33 -0
  46. package/dist/interface/keycloak-grant.interface.js +2 -0
  47. package/dist/interface/keycloak-request.interface.d.ts +8 -0
  48. package/dist/interface/keycloak-request.interface.js +2 -0
  49. package/dist/interface/oidc.interface.d.ts +14 -0
  50. package/dist/interface/oidc.interface.js +2 -0
  51. package/dist/interface/server.interface.d.ts +9 -0
  52. package/dist/interface/server.interface.js +2 -0
  53. package/dist/interface/tenant-config.interface.d.ts +13 -0
  54. package/dist/interface/tenant-config.interface.js +2 -0
  55. package/dist/internal.util.d.ts +13 -0
  56. package/dist/internal.util.js +54 -0
  57. package/dist/keycloak-auth.module.d.ts +54 -0
  58. package/dist/keycloak-auth.module.js +174 -0
  59. package/dist/keycloak-auth.providers.d.ts +7 -0
  60. package/dist/keycloak-auth.providers.js +122 -0
  61. package/dist/services/backchannel-logout.service.d.ts +30 -0
  62. package/dist/services/backchannel-logout.service.js +100 -0
  63. package/dist/services/jwks-cache.service.d.ts +25 -0
  64. package/dist/services/jwks-cache.service.js +130 -0
  65. package/dist/services/keycloak-admin.service.d.ts +37 -0
  66. package/dist/services/keycloak-admin.service.js +268 -0
  67. package/dist/services/keycloak-grant.service.d.ts +23 -0
  68. package/dist/services/keycloak-grant.service.js +88 -0
  69. package/dist/services/keycloak-http.service.d.ts +41 -0
  70. package/dist/services/keycloak-http.service.js +211 -0
  71. package/dist/services/keycloak-multitenant.service.d.ts +24 -0
  72. package/dist/services/keycloak-multitenant.service.js +161 -0
  73. package/dist/services/keycloak-url.service.d.ts +12 -0
  74. package/dist/services/keycloak-url.service.js +43 -0
  75. package/dist/services/oidc-discovery.service.d.ts +24 -0
  76. package/dist/services/oidc-discovery.service.js +86 -0
  77. package/dist/services/token-validation.service.d.ts +29 -0
  78. package/dist/services/token-validation.service.js +215 -0
  79. package/dist/token/keycloak-grant.d.ts +24 -0
  80. package/dist/token/keycloak-grant.js +37 -0
  81. package/dist/token/keycloak-token.d.ts +57 -0
  82. package/dist/token/keycloak-token.js +105 -0
  83. package/dist/types/conditional-scope.type.d.ts +2 -0
  84. package/dist/types/conditional-scope.type.js +2 -0
  85. package/dist/util.d.ts +3 -0
  86. package/dist/util.js +17 -0
  87. package/package.json +130 -0
@@ -0,0 +1,57 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import { JwtContent, JwtHeader } from '../interface/jwt.interface';
4
+ export { JwtContent, JwtHeader };
5
+ export type { JwtPermission } from '../interface/jwt.interface';
6
+ /**
7
+ * Native KeycloakToken class that replaces keycloak-connect's Token.
8
+ * Parses a JWT and provides role/permission checking methods.
9
+ */
10
+ export declare class KeycloakToken {
11
+ private readonly _token;
12
+ private readonly clientId?;
13
+ readonly header: JwtHeader;
14
+ readonly content: JwtContent;
15
+ readonly signature: Buffer;
16
+ readonly signed: string;
17
+ constructor(_token: string, clientId?: string);
18
+ /**
19
+ * The raw JWT string. Matches keycloak-connect's `token.token` property.
20
+ */
21
+ get token(): string;
22
+ /**
23
+ * Check if the token has a specific role.
24
+ * Matches keycloak-connect's Token.hasRole() logic:
25
+ * - "realm:roleName" checks realm_access.roles
26
+ * - "clientId:roleName" checks resource_access[clientId].roles
27
+ * - "roleName" checks resource_access[this.clientId].roles
28
+ */
29
+ hasRole(name: string): boolean;
30
+ /**
31
+ * Check if the token has a realm-level role.
32
+ */
33
+ hasRealmRole(roleName: string): boolean;
34
+ /**
35
+ * Check if the token has an application/client-level role.
36
+ */
37
+ hasApplicationRole(clientId: string, roleName: string): boolean;
38
+ /**
39
+ * Check if the token has a specific permission (for UMA/resource server).
40
+ * Matches keycloak-connect's Token.hasPermission() semantics:
41
+ * - If matching resource found with no scope requested, returns true.
42
+ * - If matching resource found with scope requested and scopes array
43
+ * exists with entries but does NOT include the scope, returns false.
44
+ * - If matching resource found with scope requested and scopes array
45
+ * is empty or absent, returns true (permission is granted without scope restriction).
46
+ */
47
+ hasPermission(resource: string, scope?: string): boolean;
48
+ /**
49
+ * Check if the token is expired.
50
+ * Matches keycloak-connect's behavior: exp=0 is considered expired.
51
+ */
52
+ isExpired(): boolean;
53
+ /**
54
+ * Get the raw JWT string.
55
+ */
56
+ toString(): string;
57
+ }
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeycloakToken = void 0;
4
+ /**
5
+ * Native KeycloakToken class that replaces keycloak-connect's Token.
6
+ * Parses a JWT and provides role/permission checking methods.
7
+ */
8
+ class KeycloakToken {
9
+ constructor(_token, clientId) {
10
+ this._token = _token;
11
+ this.clientId = clientId;
12
+ this.header = {};
13
+ this.content = { exp: 0 };
14
+ this.signature = Buffer.alloc(0);
15
+ this.signed = '';
16
+ if (_token) {
17
+ try {
18
+ const parts = _token.split('.');
19
+ this.header = JSON.parse(Buffer.from(parts[0], 'base64').toString('utf8'));
20
+ this.content = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
21
+ this.signature = Buffer.from(parts[2], 'base64');
22
+ this.signed = `${parts[0]}.${parts[1]}`;
23
+ }
24
+ catch (_a) {
25
+ // defaults already set above
26
+ }
27
+ }
28
+ }
29
+ /**
30
+ * The raw JWT string. Matches keycloak-connect's `token.token` property.
31
+ */
32
+ get token() {
33
+ return this._token;
34
+ }
35
+ /**
36
+ * Check if the token has a specific role.
37
+ * Matches keycloak-connect's Token.hasRole() logic:
38
+ * - "realm:roleName" checks realm_access.roles
39
+ * - "clientId:roleName" checks resource_access[clientId].roles
40
+ * - "roleName" checks resource_access[this.clientId].roles
41
+ */
42
+ hasRole(name) {
43
+ if (!this.clientId) {
44
+ return false;
45
+ }
46
+ const parts = name.split(':');
47
+ if (parts.length === 1) {
48
+ // No prefix — check the default client's resource_access
49
+ return this.hasApplicationRole(this.clientId, parts[0]);
50
+ }
51
+ if (parts[0] === 'realm') {
52
+ return this.hasRealmRole(parts[1]);
53
+ }
54
+ return this.hasApplicationRole(parts[0], parts[1]);
55
+ }
56
+ /**
57
+ * Check if the token has a realm-level role.
58
+ */
59
+ hasRealmRole(roleName) {
60
+ var _a, _b, _c;
61
+ return (_c = (_b = (_a = this.content.realm_access) === null || _a === void 0 ? void 0 : _a.roles) === null || _b === void 0 ? void 0 : _b.includes(roleName)) !== null && _c !== void 0 ? _c : false;
62
+ }
63
+ /**
64
+ * Check if the token has an application/client-level role.
65
+ */
66
+ hasApplicationRole(clientId, roleName) {
67
+ var _a, _b, _c, _d;
68
+ return ((_d = (_c = (_b = (_a = this.content.resource_access) === null || _a === void 0 ? void 0 : _a[clientId]) === null || _b === void 0 ? void 0 : _b.roles) === null || _c === void 0 ? void 0 : _c.includes(roleName)) !== null && _d !== void 0 ? _d : false);
69
+ }
70
+ /**
71
+ * Check if the token has a specific permission (for UMA/resource server).
72
+ * Matches keycloak-connect's Token.hasPermission() semantics:
73
+ * - If matching resource found with no scope requested, returns true.
74
+ * - If matching resource found with scope requested and scopes array
75
+ * exists with entries but does NOT include the scope, returns false.
76
+ * - If matching resource found with scope requested and scopes array
77
+ * is empty or absent, returns true (permission is granted without scope restriction).
78
+ */
79
+ hasPermission(resource, scope) {
80
+ var _a, _b, _c;
81
+ const match = (_b = (_a = this.content.authorization) === null || _a === void 0 ? void 0 : _a.permissions) === null || _b === void 0 ? void 0 : _b.find((p) => p.rsid === resource || p.rsname === resource);
82
+ if (!match) {
83
+ return false;
84
+ }
85
+ // Scope requested and the permission has an explicit scope list that doesn't include it
86
+ if (scope && ((_c = match.scopes) === null || _c === void 0 ? void 0 : _c.length) > 0 && !match.scopes.includes(scope)) {
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ /**
92
+ * Check if the token is expired.
93
+ * Matches keycloak-connect's behavior: exp=0 is considered expired.
94
+ */
95
+ isExpired() {
96
+ return this.content.exp * 1000 < Date.now();
97
+ }
98
+ /**
99
+ * Get the raw JWT string.
100
+ */
101
+ toString() {
102
+ return this._token;
103
+ }
104
+ }
105
+ exports.KeycloakToken = KeycloakToken;
@@ -0,0 +1,2 @@
1
+ import { KeycloakToken } from '../token/keycloak-token';
2
+ export type ConditionalScopeFn = (request: unknown, token: KeycloakToken) => string[];
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/dist/util.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { JwtPayload } from './interface/jwt.interface';
2
+ export { JwtPayload };
3
+ export declare const parseToken: (token: string) => JwtPayload;
package/dist/util.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseToken = void 0;
4
+ const errors_1 = require("./errors");
5
+ const parseToken = (token) => {
6
+ const parts = token.split('.');
7
+ if (parts.length < 2) {
8
+ throw new errors_1.KeycloakTokenError('Malformed JWT: expected at least 2 segments');
9
+ }
10
+ try {
11
+ return JSON.parse(Buffer.from(parts[1], 'base64').toString());
12
+ }
13
+ catch (_a) {
14
+ throw new errors_1.KeycloakTokenError('Malformed JWT: payload is not valid JSON');
15
+ }
16
+ };
17
+ exports.parseToken = parseToken;
package/package.json ADDED
@@ -0,0 +1,130 @@
1
+ {
2
+ "name": "nestjs-keycloak-auth",
3
+ "version": "1.0.0",
4
+ "description": "Keycloak authentication and authorization module for NestJS",
5
+ "author": "B. Joshua Adedigba",
6
+ "contributors": [
7
+ {
8
+ "name": "B. Joshua Adedigba",
9
+ "email": "tbannaarr@gmail.com",
10
+ "url": "https://github.com/bannaarr01"
11
+ }
12
+ ],
13
+ "license": "MIT",
14
+ "scripts": {
15
+ "start:dev": "tsc -w",
16
+ "clean": "rimraf dist",
17
+ "build": "tsc",
18
+ "test": "jest",
19
+ "test:cov": "jest --coverage --runInBand",
20
+ "release": "release-it",
21
+ "release:alpha": "release-it --preReleaseId=alpha --npm.tag=next --github.preRelease",
22
+ "format": "prettier src --write",
23
+ "lint": "eslint src",
24
+ "lint:fix": "eslint src --fix",
25
+ "prepare": "husky"
26
+ },
27
+ "keywords": [
28
+ "nestjs",
29
+ "nest",
30
+ "keycloak",
31
+ "nest-keycloak",
32
+ "keycloak-nest",
33
+ "typescript",
34
+ "authentication",
35
+ "authorization",
36
+ "oauth2",
37
+ "openid-connect",
38
+ "oidc",
39
+ "rbac",
40
+ "guard",
41
+ "uma",
42
+ "multi-tenant",
43
+ "jwt",
44
+ "sso",
45
+ "access-control"
46
+ ],
47
+ "homepage": "https://github.com/bannaarr01/nestjs-keycloak-auth#readme",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/bannaarr01/nestjs-keycloak-auth"
51
+ },
52
+ "bugs": "https://github.com/bannaarr01/nestjs-keycloak-auth/issues",
53
+ "files": [
54
+ "dist"
55
+ ],
56
+ "main": "./dist/index.js",
57
+ "types": "./dist/index.d.ts",
58
+ "typesVersions": {
59
+ "*": {
60
+ "dist/index.d.ts": [
61
+ "dist/index.d.ts"
62
+ ],
63
+ "*": [
64
+ "dist/*"
65
+ ]
66
+ }
67
+ },
68
+ "exports": {
69
+ ".": "./dist/index.js",
70
+ "./*": "./dist/*"
71
+ },
72
+ "peerDependencies": {
73
+ "@nestjs/axios": ">=3.0.0",
74
+ "@nestjs/common": ">=10.0.0 <12.0.0",
75
+ "@nestjs/core": ">=10.0.0 <12.0.0",
76
+ "axios": ">=1.0.0"
77
+ },
78
+ "devDependencies": {
79
+ "@nestjs/axios": "^4.0.0",
80
+ "@nestjs/common": "^11.0.6",
81
+ "@nestjs/core": "^11.0.6",
82
+ "@nestjs/graphql": "^13.0.2",
83
+ "@release-it/conventional-changelog": "^10.0.6",
84
+ "@types/express": "^4.17.21",
85
+ "@types/jest": "^30.0.0",
86
+ "@types/node": "^18.19.23",
87
+ "axios": "^1.7.0",
88
+ "class-transformer": "0.5.1",
89
+ "class-validator": "0.14.1",
90
+ "cpr": "3.0.1",
91
+ "eslint": "^9.39.4",
92
+ "eslint-config-prettier": "9.1.0",
93
+ "eslint-plugin-prettier": "5.1.3",
94
+ "eslint-plugin-unused-imports": "^4.4.1",
95
+ "graphql": "^16.10.0",
96
+ "husky": "^9.1.7",
97
+ "jest": "^30.3.0",
98
+ "lint-staged": "^15.5.2",
99
+ "prettier": "3.2.5",
100
+ "reflect-metadata": "0.2.1",
101
+ "release-it": "^19.2.4",
102
+ "rimraf": "3.0.2",
103
+ "rxjs": "7.8.1",
104
+ "ts-jest": "^29.4.6",
105
+ "ts-node": "10.8.2",
106
+ "typescript": "5.4.2",
107
+ "typescript-eslint": "^8.57.1"
108
+ },
109
+ "peerDependenciesMeta": {
110
+ "@nestjs/axios": {
111
+ "optional": false
112
+ },
113
+ "axios": {
114
+ "optional": false
115
+ }
116
+ },
117
+ "volta": {
118
+ "node": "21.7.1"
119
+ },
120
+ "lint-staged": {
121
+ "**/*.ts": [
122
+ "eslint"
123
+ ]
124
+ },
125
+ "overrides": {
126
+ "release-it": {
127
+ "undici": "^7.0.0"
128
+ }
129
+ }
130
+ }