@rt-tools/auth-server 0.0.0-stage → 0.1.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 (70) hide show
  1. package/README.md +77 -2
  2. package/cjs/index.js +13 -0
  3. package/cjs/index.js.map +1 -0
  4. package/cjs/lib/access-audit.js +39 -0
  5. package/cjs/lib/access-audit.js.map +1 -0
  6. package/cjs/lib/access.js +36 -0
  7. package/cjs/lib/access.js.map +1 -0
  8. package/cjs/lib/auth-server.module.js +71 -0
  9. package/cjs/lib/auth-server.module.js.map +1 -0
  10. package/cjs/lib/auth.guard.js +51 -0
  11. package/cjs/lib/auth.guard.js.map +1 -0
  12. package/cjs/lib/auth.tokens.js +10 -0
  13. package/cjs/lib/auth.tokens.js.map +1 -0
  14. package/cjs/lib/catalog-sync.js +51 -0
  15. package/cjs/lib/catalog-sync.js.map +1 -0
  16. package/cjs/lib/connect-access.js +54 -0
  17. package/cjs/lib/connect-access.js.map +1 -0
  18. package/cjs/lib/current-caller.decorator.js +14 -0
  19. package/cjs/lib/current-caller.decorator.js.map +1 -0
  20. package/cjs/lib/testing/keys.js +28 -0
  21. package/cjs/lib/testing/keys.js.map +1 -0
  22. package/cjs/lib/token-verifier.js +72 -0
  23. package/cjs/lib/token-verifier.js.map +1 -0
  24. package/cjs/package.json +3 -0
  25. package/esm/index.d.ts +10 -0
  26. package/esm/index.d.ts.map +1 -0
  27. package/esm/index.js +10 -0
  28. package/esm/index.js.map +1 -0
  29. package/esm/lib/access-audit.d.ts +13 -0
  30. package/esm/lib/access-audit.d.ts.map +1 -0
  31. package/esm/lib/access-audit.js +35 -0
  32. package/esm/lib/access-audit.js.map +1 -0
  33. package/esm/lib/access.d.ts +31 -0
  34. package/esm/lib/access.d.ts.map +1 -0
  35. package/esm/lib/access.js +29 -0
  36. package/esm/lib/access.js.map +1 -0
  37. package/esm/lib/auth-server.module.d.ts +28 -0
  38. package/esm/lib/auth-server.module.d.ts.map +1 -0
  39. package/esm/lib/auth-server.module.js +67 -0
  40. package/esm/lib/auth-server.module.js.map +1 -0
  41. package/esm/lib/auth.guard.d.ts +15 -0
  42. package/esm/lib/auth.guard.d.ts.map +1 -0
  43. package/esm/lib/auth.guard.js +48 -0
  44. package/esm/lib/auth.guard.js.map +1 -0
  45. package/esm/lib/auth.tokens.d.ts +7 -0
  46. package/esm/lib/auth.tokens.d.ts.map +1 -0
  47. package/esm/lib/auth.tokens.js +7 -0
  48. package/esm/lib/auth.tokens.js.map +1 -0
  49. package/esm/lib/catalog-sync.d.ts +27 -0
  50. package/esm/lib/catalog-sync.d.ts.map +1 -0
  51. package/esm/lib/catalog-sync.js +48 -0
  52. package/esm/lib/catalog-sync.js.map +1 -0
  53. package/esm/lib/connect-access.d.ts +35 -0
  54. package/esm/lib/connect-access.d.ts.map +1 -0
  55. package/esm/lib/connect-access.js +49 -0
  56. package/esm/lib/connect-access.js.map +1 -0
  57. package/esm/lib/current-caller.decorator.d.ts +6 -0
  58. package/esm/lib/current-caller.decorator.d.ts.map +1 -0
  59. package/esm/lib/current-caller.decorator.js +10 -0
  60. package/esm/lib/current-caller.decorator.js.map +1 -0
  61. package/esm/lib/testing/keys.d.ts +17 -0
  62. package/esm/lib/testing/keys.d.ts.map +1 -0
  63. package/esm/lib/testing/keys.js +23 -0
  64. package/esm/lib/testing/keys.js.map +1 -0
  65. package/esm/lib/token-verifier.d.ts +29 -0
  66. package/esm/lib/token-verifier.d.ts.map +1 -0
  67. package/esm/lib/token-verifier.js +67 -0
  68. package/esm/lib/token-verifier.js.map +1 -0
  69. package/esm/package.json +3 -0
  70. package/package.json +53 -4
package/README.md CHANGED
@@ -1,3 +1,78 @@
1
- # Temporary Holding Version
1
+ # @rt-tools/auth-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/Eyhenij/rt-tools/blob/main/LICENSE)
4
+
5
+ The server side of the entry module: the Keycloak token check, an access declaration on every
6
+ operation and the sync of the admin's rights to Keycloak. One check serves NestJS controllers and
7
+ Connect procedures.
8
+
9
+ ## Connecting
10
+
11
+ ```ts
12
+ import { definePermissions } from '@rt-tools/auth-contract';
13
+ import { AuthServerModule } from '@rt-tools/auth-server';
14
+
15
+ export const RIGHTS = definePermissions({ orders: ['read', 'write'] });
16
+
17
+ @Module({
18
+ imports: [
19
+ AuthServerModule.forRoot({
20
+ issuer: 'https://sso.example.com/realms/rt',
21
+ clientId: 'orders-admin',
22
+ catalog: RIGHTS,
23
+ sync: { baseUrl: 'https://sso.example.com', realm: 'rt', syncClientId: 'rt-catalog-sync', syncClientSecret: process.env.SYNC_SECRET },
24
+ }),
25
+ ],
26
+ })
27
+ export class AppModule {}
28
+ ```
29
+
30
+ The module puts a guard on every route. At start it checks that every operation declares exactly
31
+ one access. When `sync` is given, it also sends the catalog to Keycloak.
32
+
33
+ ## Access
34
+
35
+ ```ts
36
+ @Get()
37
+ @PermittedOperation('orders:read')
38
+ public list(@CurrentCaller() caller: ICaller): Order[] { … }
39
+ ```
40
+
41
+ | Decorator | The operation is open to |
42
+ | --------------------------------- | ----------------------------- |
43
+ | `@OpenOperation()` | everyone, without a token |
44
+ | `@SignedInOperation()` | anyone with an accepted token |
45
+ | `@PermittedOperation(permission)` | a caller with the right |
46
+
47
+ An operation with no declaration or with two stops the start and is named. A call without an
48
+ accepted token gets 401, a call without the right gets 403. Neither names the right.
49
+
50
+ A token is accepted when the realm signed it by its key and named itself the issuer. Its term must
51
+ hold, and it must be issued to the client of this admin (`azp`). The keys are fetched from the
52
+ realm and cached.
53
+
54
+ ## Connect
55
+
56
+ ```ts
57
+ const entries = connectAccessEntries(OrdersService, {
58
+ listOrders: { kind: 'permission', permission: 'orders:read' },
59
+ health: { kind: 'public' },
60
+ });
61
+ const interceptor = createAuthInterceptor(app.get(AUTH_TOKEN_VERIFIER), entries);
62
+ ```
63
+
64
+ The map names every method of the service exactly once, otherwise `connectAccessEntries` throws.
65
+ The refusals are `unauthenticated` and `permission_denied`. A procedure reads the caller with
66
+ `context.values.get(CONNECT_CALLER)`.
67
+
68
+ ## The catalog sync
69
+
70
+ `syncPermissionCatalog` creates the roles the client of the admin lacks and removes none. Removing
71
+ a role takes it from every person who holds it. The rights Keycloak holds beyond the catalog are
72
+ named in the log. The sync client needs a service account with `view-clients` and `manage-clients`.
73
+
74
+ ## Connecting it to an application
75
+
76
+ The order of the whole connection is in the guide of the module.
77
+ It covers Keycloak, the server, the admin, the entry screens and the move of people:
78
+ [docs/auth-integration.md](https://github.com/Eyhenij/rt-tools/blob/main/docs/auth-integration.md).
package/cjs/index.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ tslib_1.__exportStar(require("./lib/access.js"), exports);
5
+ tslib_1.__exportStar(require("./lib/access-audit.js"), exports);
6
+ tslib_1.__exportStar(require("./lib/auth-server.module.js"), exports);
7
+ tslib_1.__exportStar(require("./lib/auth.guard.js"), exports);
8
+ tslib_1.__exportStar(require("./lib/auth.tokens.js"), exports);
9
+ tslib_1.__exportStar(require("./lib/catalog-sync.js"), exports);
10
+ tslib_1.__exportStar(require("./lib/connect-access.js"), exports);
11
+ tslib_1.__exportStar(require("./lib/current-caller.decorator.js"), exports);
12
+ tslib_1.__exportStar(require("./lib/token-verifier.js"), exports);
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../projects/auth-server/src/index.ts"],"names":[],"mappings":";;;AAAA,0DAAgC;AAChC,gEAAsC;AACtC,sEAA4C;AAC5C,8DAAoC;AACpC,+DAAqC;AACrC,gEAAsC;AACtC,kEAAwC;AACxC,4EAAkD;AAClD,kEAAwC"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.undeclaredAccess = undeclaredAccess;
4
+ exports.accessAuditError = accessAuditError;
5
+ const constants_1 = require("@nestjs/common/constants");
6
+ const access_1 = require("./access");
7
+ function isRoute(handler) {
8
+ return Reflect.getMetadata(constants_1.PATH_METADATA, handler) !== undefined && Reflect.getMetadata(constants_1.METHOD_METADATA, handler) !== undefined;
9
+ }
10
+ /**
11
+ * The route handlers of the controllers that declare no access or more than one, as
12
+ * `Controller.method — none` / `— 2`.
13
+ *
14
+ * A handler is a prototype method with a route mark of the framework. An empty list means every
15
+ * handler declares exactly one access.
16
+ */
17
+ function undeclaredAccess(controllers) {
18
+ const faults = [];
19
+ for (const controller of controllers) {
20
+ const prototype = controller.prototype;
21
+ for (const name of Object.getOwnPropertyNames(prototype)) {
22
+ const handler = Reflect.get(prototype, name);
23
+ if (name === 'constructor' || typeof handler !== 'function' || !isRoute(handler)) {
24
+ continue;
25
+ }
26
+ const count = (0, access_1.accessDeclarationsOf)(handler).length;
27
+ if (count !== 1) {
28
+ faults.push(`${controller.name}.${name} — ${count === 0 ? 'none' : String(count)}`);
29
+ }
30
+ }
31
+ }
32
+ return faults;
33
+ }
34
+ /** The refusal of the start, naming every faulty operation at once. */
35
+ function accessAuditError(faults) {
36
+ const lines = faults.map((fault) => ' ' + fault).join('\n');
37
+ return new Error(`Every operation declares exactly one access; these do not:\n${lines}`);
38
+ }
39
+ //# sourceMappingURL=access-audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-audit.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/access-audit.ts"],"names":[],"mappings":";;AAkBA,4CAgBC;AAGD,4CAGC;AAxCD,wDAA0E;AAE1E,qCAAgD;AAKhD,SAAS,OAAO,CAAC,OAAe;IAC5B,OAAO,OAAO,CAAC,WAAW,CAAC,yBAAa,EAAE,OAAO,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,2BAAe,EAAE,OAAO,CAAC,KAAK,SAAS,CAAC;AACpI,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,gBAAgB,CAAC,WAAwC;IACrE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACnC,MAAM,SAAS,GAAW,UAAU,CAAC,SAAmB,CAAC;QACzD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,mBAAmB,CAAC,SAAS,CAAC,EAAE,CAAC;YACvD,MAAM,OAAO,GAAY,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACtD,IAAI,IAAI,KAAK,aAAa,IAAI,OAAO,OAAO,KAAK,UAAU,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC/E,SAAS;YACb,CAAC;YACD,MAAM,KAAK,GAAW,IAAA,6BAAoB,EAAC,OAAO,CAAC,CAAC,MAAM,CAAC;YAC3D,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,IAAI,IAAI,IAAI,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACxF,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,uEAAuE;AACvE,SAAgB,gBAAgB,CAAC,MAAyB;IACtD,MAAM,KAAK,GAAW,MAAM,CAAC,GAAG,CAAC,CAAC,KAAa,EAAU,EAAE,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrF,OAAO,IAAI,KAAK,CAAC,+DAA+D,KAAK,EAAE,CAAC,CAAC;AAC7F,CAAC"}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ACCESS_DECLARATIONS = void 0;
4
+ exports.accessDeclarationsOf = accessDeclarationsOf;
5
+ exports.OpenOperation = OpenOperation;
6
+ exports.SignedInOperation = SignedInOperation;
7
+ exports.PermittedOperation = PermittedOperation;
8
+ require("reflect-metadata");
9
+ /** The key the declarations of a handler lie under: a list, so a second one is seen, not lost. */
10
+ exports.ACCESS_DECLARATIONS = 'rt-tools.auth.access';
11
+ /** The declarations of a handler, in the order written. */
12
+ function accessDeclarationsOf(handler) {
13
+ const declared = Reflect.getMetadata(exports.ACCESS_DECLARATIONS, handler);
14
+ return Array.isArray(declared) ? declared : [];
15
+ }
16
+ function declare(access) {
17
+ return (_target, _key, descriptor) => {
18
+ const handler = descriptor.value;
19
+ Reflect.defineMetadata(exports.ACCESS_DECLARATIONS, [...accessDeclarationsOf(handler), access], handler);
20
+ };
21
+ }
22
+ /* eslint-disable sonarjs/function-name -- decorator factories are named with a capital letter: so the framework names them, and so they read at the place of use */
23
+ /** The operation is open to everyone, without a token. */
24
+ function OpenOperation() {
25
+ return declare({ kind: 'public' });
26
+ }
27
+ /** The operation is open to anyone with an accepted token. */
28
+ function SignedInOperation() {
29
+ return declare({ kind: 'signed-in' });
30
+ }
31
+ /** The operation is open to a caller with the right. */
32
+ function PermittedOperation(permission) {
33
+ return declare({ kind: 'permission', permission });
34
+ }
35
+ /* eslint-enable sonarjs/function-name */
36
+ //# sourceMappingURL=access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/access.ts"],"names":[],"mappings":";;;AAoBA,oDAGC;AAYD,sCAEC;AAGD,8CAEC;AAGD,gDAEC;AA/CD,4BAA0B;AAgB1B,kGAAkG;AACrF,QAAA,mBAAmB,GAAW,sBAAsB,CAAC;AAElE,2DAA2D;AAC3D,SAAgB,oBAAoB,CAAC,OAAe;IAChD,MAAM,QAAQ,GAAY,OAAO,CAAC,WAAW,CAAC,2BAAmB,EAAE,OAAO,CAAC,CAAC;IAC5E,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,QAAsB,CAAC,CAAC,CAAC,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,OAAO,CAAC,MAAe;IAC5B,OAAO,CAAC,OAAe,EAAE,IAAqB,EAAE,UAA8B,EAAQ,EAAE;QACpF,MAAM,OAAO,GAAW,UAAU,CAAC,KAAe,CAAC;QACnD,OAAO,CAAC,cAAc,CAAC,2BAAmB,EAAE,CAAC,GAAG,oBAAoB,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC;IACrG,CAAC,CAAC;AACN,CAAC;AAED,oKAAoK;AAEpK,0DAA0D;AAC1D,SAAgB,aAAa;IACzB,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;AACvC,CAAC;AAED,8DAA8D;AAC9D,SAAgB,iBAAiB;IAC7B,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,wDAAwD;AACxD,SAAgB,kBAAkB,CAAC,UAAuB;IACtD,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC,CAAC;AACvD,CAAC;AAED,yCAAyC"}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ var AuthServerModule_1;
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.AuthServerModule = exports.AuthStartCheck = void 0;
5
+ const tslib_1 = require("tslib");
6
+ const common_1 = require("@nestjs/common");
7
+ const core_1 = require("@nestjs/core");
8
+ const access_audit_1 = require("./access-audit");
9
+ const auth_guard_1 = require("./auth.guard");
10
+ const auth_tokens_1 = require("./auth.tokens");
11
+ const catalog_sync_1 = require("./catalog-sync");
12
+ const token_verifier_1 = require("./token-verifier");
13
+ /**
14
+ * At start: every operation declares exactly one access, and the catalog reaches Keycloak.
15
+ *
16
+ * An operation without a declaration or with two stops the start and is named; the catalog sync
17
+ * creates the roles Keycloak lacks and names the extra ones in the log.
18
+ */
19
+ class AuthStartCheck {
20
+ #discovery;
21
+ #options;
22
+ #logger = new common_1.Logger('AuthServer');
23
+ constructor(discovery, options) {
24
+ this.#discovery = discovery;
25
+ this.#options = options;
26
+ }
27
+ async onApplicationBootstrap() {
28
+ const controllers = this.#discovery
29
+ .getControllers()
30
+ .map((wrapper) => wrapper.metatype)
31
+ .filter((metatype) => typeof metatype === 'function');
32
+ const faults = (0, access_audit_1.undeclaredAccess)(controllers);
33
+ if (faults.length) {
34
+ throw (0, access_audit_1.accessAuditError)(faults);
35
+ }
36
+ if (this.#options.sync) {
37
+ const result = await (0, catalog_sync_1.syncPermissionCatalog)({ ...this.#options.sync, clientId: this.#options.clientId }, this.#options.catalog);
38
+ this.#logger.log(`catalog: ${this.#options.catalog.length} rights, created ${result.created.length}`);
39
+ if (result.extra.length) {
40
+ this.#logger.warn(`Keycloak holds rights the catalog lacks: ${result.extra.join(', ')}`);
41
+ }
42
+ }
43
+ }
44
+ }
45
+ exports.AuthStartCheck = AuthStartCheck;
46
+ /** The entry module of an admin server: the token check, the guard and the start check. */
47
+ let AuthServerModule = AuthServerModule_1 = class AuthServerModule {
48
+ static forRoot(options) {
49
+ return {
50
+ module: AuthServerModule_1,
51
+ global: true,
52
+ imports: [core_1.DiscoveryModule],
53
+ providers: [
54
+ { provide: auth_tokens_1.AUTH_SERVER_OPTIONS, useValue: options },
55
+ { provide: auth_tokens_1.AUTH_TOKEN_VERIFIER, useValue: new token_verifier_1.KeycloakTokenVerifier(options) },
56
+ { provide: core_1.APP_GUARD, useClass: auth_guard_1.AuthGuard },
57
+ {
58
+ provide: AuthStartCheck,
59
+ useFactory: (discovery) => new AuthStartCheck(discovery, options),
60
+ inject: [core_1.DiscoveryService],
61
+ },
62
+ ],
63
+ exports: [auth_tokens_1.AUTH_TOKEN_VERIFIER],
64
+ };
65
+ }
66
+ };
67
+ exports.AuthServerModule = AuthServerModule;
68
+ exports.AuthServerModule = AuthServerModule = AuthServerModule_1 = tslib_1.__decorate([
69
+ (0, common_1.Module)({})
70
+ ], AuthServerModule);
71
+ //# sourceMappingURL=auth-server.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-server.module.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/auth-server.module.ts"],"names":[],"mappings":";;;;;AAAA,2CAAuF;AACvF,uCAA4E;AAK5E,iDAAsF;AACtF,6CAAyC;AACzC,+CAAyE;AACzE,iDAAgG;AAChG,qDAA6E;AAU7E;;;;;GAKG;AACH,MAAa,cAAc;IACd,UAAU,CAAmB;IAC7B,QAAQ,CAAqB;IAC7B,OAAO,GAAW,IAAI,eAAM,CAAC,YAAY,CAAC,CAAC;IAEpD,YAAY,SAA2B,EAAE,OAA2B;QAChE,IAAI,CAAC,UAAU,GAAG,SAAS,CAAC;QAC5B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;IAC5B,CAAC;IAEM,KAAK,CAAC,sBAAsB;QAC/B,MAAM,WAAW,GAAuB,IAAI,CAAC,UAAU;aAClD,cAAc,EAAE;aAChB,GAAG,CAAC,CAAC,OAAwB,EAAW,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC;aAC5D,MAAM,CAAC,CAAC,QAAiB,EAAgC,EAAE,CAAC,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC;QACjG,MAAM,MAAM,GAAsB,IAAA,+BAAgB,EAAC,WAAW,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAA,+BAAgB,EAAC,MAAM,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACrB,MAAM,MAAM,GAAuB,MAAM,IAAA,oCAAqB,EAC1D,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAC3D,IAAI,CAAC,QAAQ,CAAC,OAAO,CACxB,CAAC;YACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,oBAAoB,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;YACtG,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBACtB,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,4CAA4C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7F,CAAC;QACL,CAAC;IACL,CAAC;CACJ;AA9BD,wCA8BC;AAED,2FAA2F;AAEpF,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAClB,MAAM,CAAC,OAAO,CAAC,OAA2B;QAC7C,OAAO;YACH,MAAM,EAAE,kBAAgB;YACxB,MAAM,EAAE,IAAI;YACZ,OAAO,EAAE,CAAC,sBAAe,CAAC;YAC1B,SAAS,EAAE;gBACP,EAAE,OAAO,EAAE,iCAAmB,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACnD,EAAE,OAAO,EAAE,iCAAmB,EAAE,QAAQ,EAAE,IAAI,sCAAqB,CAAC,OAAO,CAAC,EAAE;gBAC9E,EAAE,OAAO,EAAE,gBAAS,EAAE,QAAQ,EAAE,sBAAS,EAAE;gBAC3C;oBACI,OAAO,EAAE,cAAc;oBACvB,UAAU,EAAE,CAAC,SAA2B,EAAkB,EAAE,CAAC,IAAI,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC;oBACnG,MAAM,EAAE,CAAC,uBAAgB,CAAC;iBAC7B;aACJ;YACD,OAAO,EAAE,CAAC,iCAAmB,CAAC;SACjC,CAAC;IACN,CAAC;CACJ,CAAA;AAnBY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,gBAAgB,CAmB5B"}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AuthGuard = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const auth_contract_1 = require("@rt-tools/auth-contract");
7
+ const access_1 = require("./access");
8
+ const auth_tokens_1 = require("./auth.tokens");
9
+ /**
10
+ * The only access check of an admin server. **Closed by default.**
11
+ *
12
+ * Two refusals, told apart by the question: no accepted token is «not signed in» (401) and is
13
+ * cured by signing in; a token without the right is «not allowed» (403) and is not. Neither names
14
+ * the missing right or what in the token did not match.
15
+ */
16
+ let AuthGuard = class AuthGuard {
17
+ #verifier;
18
+ constructor(verifier) {
19
+ this.#verifier = verifier;
20
+ }
21
+ async canActivate(context) {
22
+ const declared = (0, access_1.accessDeclarationsOf)(context.getHandler());
23
+ if (declared.length !== 1) {
24
+ // The start audit stops such an application; a handler that slipped past it is refused
25
+ // rather than opened by a guess.
26
+ throw new common_1.UnauthorizedException();
27
+ }
28
+ const access = declared[0];
29
+ if (access.kind === 'public') {
30
+ return true;
31
+ }
32
+ const request = context.switchToHttp().getRequest();
33
+ const header = request.headers['authorization'];
34
+ const caller = await this.#verifier.callerOf(typeof header === 'string' ? header : null);
35
+ if (!caller) {
36
+ throw new common_1.UnauthorizedException();
37
+ }
38
+ if (access.kind === 'permission' && !(0, auth_contract_1.hasPermission)(caller, access.permission)) {
39
+ throw new common_1.ForbiddenException();
40
+ }
41
+ request[auth_tokens_1.REQUEST_CALLER] = caller;
42
+ return true;
43
+ }
44
+ };
45
+ exports.AuthGuard = AuthGuard;
46
+ exports.AuthGuard = AuthGuard = tslib_1.__decorate([
47
+ (0, common_1.Injectable)(),
48
+ tslib_1.__param(0, (0, common_1.Inject)(auth_tokens_1.AUTH_TOKEN_VERIFIER)),
49
+ tslib_1.__metadata("design:paramtypes", [Function])
50
+ ], AuthGuard);
51
+ //# sourceMappingURL=auth.guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.guard.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/auth.guard.ts"],"names":[],"mappings":";;;;AAAA,2CAA8H;AAE9H,2DAAiE;AAEjE,qCAAyD;AACzD,+CAAoE;AASpE;;;;;;GAMG;AAEI,IAAM,SAAS,GAAf,MAAM,SAAS;IACT,SAAS,CAAwB;IAE1C,YAAyC,QAA+B;QACpE,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC9B,CAAC;IAEM,KAAK,CAAC,WAAW,CAAC,OAAyB;QAC9C,MAAM,QAAQ,GAAuB,IAAA,6BAAoB,EAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;QAChF,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,uFAAuF;YACvF,iCAAiC;YACjC,MAAM,IAAI,8BAAqB,EAAE,CAAC;QACtC,CAAC;QACD,MAAM,MAAM,GAAY,QAAQ,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,MAAM,OAAO,GAAiB,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAgB,CAAC;QAChF,MAAM,MAAM,GAAkC,OAAO,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QAC/E,MAAM,MAAM,GAAmB,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACzG,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,8BAAqB,EAAE,CAAC;QACtC,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,IAAA,6BAAa,EAAC,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,2BAAkB,EAAE,CAAC;QACnC,CAAC;QACD,OAAO,CAAC,4BAAc,CAAC,GAAG,MAAM,CAAC;QACjC,OAAO,IAAI,CAAC;IAChB,CAAC;CACJ,CAAA;AA9BY,8BAAS;oBAAT,SAAS;IADrB,IAAA,mBAAU,GAAE;IAII,mBAAA,IAAA,eAAM,EAAC,iCAAmB,CAAC,CAAA;;GAH/B,SAAS,CA8BrB"}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REQUEST_CALLER = exports.AUTH_SERVER_OPTIONS = exports.AUTH_TOKEN_VERIFIER = void 0;
4
+ /** The token check of the admin, provided by the module. */
5
+ exports.AUTH_TOKEN_VERIFIER = Symbol('rt-tools.auth.token-verifier');
6
+ /** The options the module was configured with. */
7
+ exports.AUTH_SERVER_OPTIONS = Symbol('rt-tools.auth.options');
8
+ /** The field of the request the guard puts the caller into. */
9
+ exports.REQUEST_CALLER = Symbol('rt-tools.auth.caller');
10
+ //# sourceMappingURL=auth.tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.tokens.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/auth.tokens.ts"],"names":[],"mappings":";;;AAAA,4DAA4D;AAC/C,QAAA,mBAAmB,GAAkB,MAAM,CAAC,8BAA8B,CAAC,CAAC;AAEzF,kDAAkD;AACrC,QAAA,mBAAmB,GAAkB,MAAM,CAAC,uBAAuB,CAAC,CAAC;AAElF,+DAA+D;AAClD,QAAA,cAAc,GAAkB,MAAM,CAAC,sBAAsB,CAAC,CAAC"}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.syncPermissionCatalog = syncPermissionCatalog;
4
+ const auth_contract_1 = require("@rt-tools/auth-contract");
5
+ async function answer(response, step) {
6
+ if (!response.ok) {
7
+ throw new Error(`The catalog sync stopped at ${step}: Keycloak answered ${response.status}`);
8
+ }
9
+ return (await response.json());
10
+ }
11
+ /**
12
+ * Sends the rights of an admin to Keycloak as roles of its client.
13
+ *
14
+ * Creates the roles Keycloak lacks and removes none: removing a role takes it from every person
15
+ * who holds it. The rights Keycloak holds beyond the catalog come back as `extra` for the log.
16
+ */
17
+ async function syncPermissionCatalog(options, catalog, request = fetch) {
18
+ const base = `${options.baseUrl}/realms/${options.realm}`;
19
+ const admin = `${options.baseUrl}/admin/realms/${options.realm}`;
20
+ const token = await answer(await request(`${base}/protocol/openid-connect/token`, {
21
+ method: 'POST',
22
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
23
+ body: new URLSearchParams({
24
+ grant_type: 'client_credentials',
25
+ client_id: options.syncClientId,
26
+ client_secret: options.syncClientSecret,
27
+ }),
28
+ }), 'the token');
29
+ const headers = { authorization: `Bearer ${token.access_token}`, 'content-type': 'application/json' };
30
+ const clients = await answer(await request(`${admin}/clients?clientId=${encodeURIComponent(options.clientId)}`, { headers }), 'the client');
31
+ if (clients.length !== 1) {
32
+ throw new Error(`The catalog sync stopped at the client: the realm has no client ${options.clientId}`);
33
+ }
34
+ const rolesUrl = `${admin}/clients/${clients[0].id}/roles`;
35
+ const held = new Set((await answer(await request(rolesUrl, { headers }), 'the roles')).map((role) => role.name));
36
+ const created = [];
37
+ for (const permission of catalog) {
38
+ if (held.has(permission)) {
39
+ continue;
40
+ }
41
+ const response = await request(rolesUrl, { method: 'POST', body: JSON.stringify({ name: permission }), headers });
42
+ if (!response.ok && response.status !== 409) {
43
+ throw new Error(`The catalog sync stopped at the role ${permission}: Keycloak answered ${response.status}`);
44
+ }
45
+ created.push(permission);
46
+ }
47
+ const wanted = new Set(catalog);
48
+ // Keycloak adds roles of its own to a client; only a role of the right shape counts as extra.
49
+ return { extra: [...held].filter((role) => (0, auth_contract_1.isPermission)(role) && !wanted.has(role)), created };
50
+ }
51
+ //# sourceMappingURL=catalog-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catalog-sync.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/catalog-sync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONNECT_CALLER = void 0;
4
+ exports.connectAccessEntries = connectAccessEntries;
5
+ exports.createAuthInterceptor = createAuthInterceptor;
6
+ const connect_1 = require("@connectrpc/connect");
7
+ const auth_contract_1 = require("@rt-tools/auth-contract");
8
+ /** The caller the interceptor accepted, read by a procedure from its context values. */
9
+ exports.CONNECT_CALLER = (0, connect_1.createContextKey)(null, { description: 'rt-tools.auth.caller' });
10
+ /**
11
+ * The accesses of one service as entries `typeName/MethodName`, checked against its description.
12
+ *
13
+ * Every method declares exactly one access, and a key that names no method is refused too: both
14
+ * would otherwise show only to the person who calls the method.
15
+ */
16
+ function connectAccessEntries(service, access) {
17
+ const names = new Set(service.methods.map((method) => method.localName));
18
+ const missing = [...names].filter((name) => !(name in access));
19
+ const unknown = Object.keys(access).filter((name) => !names.has(name));
20
+ if (missing.length || unknown.length) {
21
+ throw new Error(`Every method of ${service.typeName} declares exactly one access; missing: ${missing.join(', ') || 'none'}; unknown: ${unknown.join(', ') || 'none'}`);
22
+ }
23
+ return service.methods.map((method) => [
24
+ `${service.typeName}/${method.name}`,
25
+ access[method.localName],
26
+ ]);
27
+ }
28
+ /**
29
+ * The Connect interceptor of an admin server: the same token check as the NestJS guard, and the
30
+ * same two refusals — `unauthenticated` and `permission_denied` — without naming the right.
31
+ *
32
+ * A method the map does not name is refused: the map is closed by default, as the guard is.
33
+ */
34
+ function createAuthInterceptor(verifier, entries) {
35
+ const accessOf = new Map(entries);
36
+ return (next) => async (request) => {
37
+ const access = accessOf.get(`${request.method.parent.typeName}/${request.method.name}`);
38
+ if (!access) {
39
+ throw new connect_1.ConnectError('unauthenticated', connect_1.Code.Unauthenticated);
40
+ }
41
+ if (access.kind !== 'public') {
42
+ const caller = await verifier.callerOf(request.header.get('authorization'));
43
+ if (!caller) {
44
+ throw new connect_1.ConnectError('unauthenticated', connect_1.Code.Unauthenticated);
45
+ }
46
+ if (access.kind === 'permission' && !(0, auth_contract_1.hasPermission)(caller, access.permission)) {
47
+ throw new connect_1.ConnectError('permission denied', connect_1.Code.PermissionDenied);
48
+ }
49
+ request.contextValues.set(exports.CONNECT_CALLER, caller);
50
+ }
51
+ return next(request);
52
+ };
53
+ }
54
+ //# sourceMappingURL=connect-access.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect-access.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/connect-access.ts"],"names":[],"mappings":";;;AAyBA,oDAgBC;AAQD,sDAmBC;AApED,iDAAiI;AAEjI,2DAAiE;AAcjE,wFAAwF;AAC3E,QAAA,cAAc,GAA+B,IAAA,0BAAgB,EAAiB,IAAI,EAAE,EAAE,WAAW,EAAE,sBAAsB,EAAE,CAAC,CAAC;AAE1I;;;;;GAKG;AACH,SAAgB,oBAAoB,CAChC,OAA4B,EAC5B,MAAyC;IAEzC,MAAM,KAAK,GAAgB,IAAI,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAA6B,EAAU,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC;IACrH,MAAM,OAAO,GAAa,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC;IAC1F,MAAM,OAAO,GAAa,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,IAAY,EAAW,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;IAClG,IAAI,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACX,mBAAmB,OAAO,CAAC,QAAQ,0CAA0C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,cAAc,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,EAAE,CACxJ,CAAC;IACN,CAAC;IACD,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAA2C,EAAqB,EAAE,CAAC;QAC3F,GAAG,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,IAAI,EAAE;QACpC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC;KAC3B,CAAC,CAAC;AACP,CAAC;AAED;;;;;GAKG;AACH,SAAgB,qBAAqB,CAAC,QAA+B,EAAE,OAAgD;IACnH,MAAM,QAAQ,GAAyB,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;IACxD,OAAO,CAAC,IAAgC,EAAE,EAAE,CAAC,KAAK,EAAE,OAAqC,EAAE,EAAE;QACzF,MAAM,MAAM,GAAwB,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7G,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,MAAM,IAAI,sBAAY,CAAC,iBAAiB,EAAE,cAAI,CAAC,eAAe,CAAC,CAAC;QACpE,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAmB,MAAM,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC;YAC5F,IAAI,CAAC,MAAM,EAAE,CAAC;gBACV,MAAM,IAAI,sBAAY,CAAC,iBAAiB,EAAE,cAAI,CAAC,eAAe,CAAC,CAAC;YACpE,CAAC;YACD,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,IAAI,CAAC,IAAA,6BAAa,EAAC,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC5E,MAAM,IAAI,sBAAY,CAAC,mBAAmB,EAAE,cAAI,CAAC,gBAAgB,CAAC,CAAC;YACvE,CAAC;YACD,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,sBAAc,EAAE,MAAM,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC,CAAC;AACN,CAAC"}
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CurrentCaller = void 0;
4
+ exports.callerOfRequest = callerOfRequest;
5
+ const common_1 = require("@nestjs/common");
6
+ const auth_tokens_1 = require("./auth.tokens");
7
+ /** The caller the guard accepted; `undefined` on an operation open to everyone. */
8
+ function callerOfRequest(request) {
9
+ const caller = Reflect.get(request, auth_tokens_1.REQUEST_CALLER);
10
+ return caller === undefined ? undefined : caller;
11
+ }
12
+ /** A handler parameter that receives the caller the guard accepted. */
13
+ exports.CurrentCaller = (0, common_1.createParamDecorator)((_data, context) => callerOfRequest(context.switchToHttp().getRequest()));
14
+ //# sourceMappingURL=current-caller.decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"current-caller.decorator.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/current-caller.decorator.ts"],"names":[],"mappings":";;;AAOA,0CAGC;AAVD,2CAAwE;AAIxE,+CAA+C;AAE/C,mFAAmF;AACnF,SAAgB,eAAe,CAAC,OAAe;IAC3C,MAAM,MAAM,GAAY,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,4BAAc,CAAC,CAAC;IAC7D,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,MAAkB,CAAC;AAClE,CAAC;AAED,uEAAuE;AAC1D,QAAA,aAAa,GAA6B,IAAA,6BAAoB,EACvE,CAAC,KAAc,EAAE,OAAyB,EAAuB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAU,CAAC,CACnI,CAAC"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TEST_CLIENT = exports.TEST_ISSUER = void 0;
4
+ exports.testRealm = testRealm;
5
+ exports.personClaims = personClaims;
6
+ const jose_1 = require("jose");
7
+ /** The realm of the tests. */
8
+ exports.TEST_ISSUER = 'http://localhost:58080/realms/rt';
9
+ exports.TEST_CLIENT = 'orders-admin';
10
+ async function testRealm() {
11
+ const own = await (0, jose_1.generateKeyPair)('RS256');
12
+ const foreign = await (0, jose_1.generateKeyPair)('RS256');
13
+ const jwk = { ...(await (0, jose_1.exportJWK)(own.publicKey)), kid: 'own', alg: 'RS256', use: 'sig' };
14
+ return {
15
+ keys: (0, jose_1.createLocalJWKSet)({ keys: [jwk] }),
16
+ sign: async (claims, options = {}) => new jose_1.SignJWT(claims)
17
+ .setProtectedHeader({ alg: 'RS256', kid: 'own' })
18
+ .setIssuer(options.issuer ?? exports.TEST_ISSUER)
19
+ .setIssuedAt()
20
+ .setExpirationTime(options.expiresIn ?? '5m')
21
+ .sign(options.foreignKey ? foreign.privateKey : own.privateKey),
22
+ };
23
+ }
24
+ /** The claims of a person with the given roles in the test client, issued to it. */
25
+ function personClaims(roles, azp = exports.TEST_CLIENT) {
26
+ return { sub: 'p-1', email: 'anna@example.com', resource_access: { [exports.TEST_CLIENT]: { roles } }, azp };
27
+ }
28
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../../../../projects/auth-server/src/lib/testing/keys.ts"],"names":[],"mappings":";;;AAYA,8BAiBC;AAGD,oCAEC;AAlCD,+BAAoG;AAEpG,8BAA8B;AACjB,QAAA,WAAW,GAAW,kCAAkC,CAAC;AACzD,QAAA,WAAW,GAAW,cAAc,CAAC;AAQ3C,KAAK,UAAU,SAAS;IAC3B,MAAM,GAAG,GAAkB,MAAM,IAAA,sBAAe,EAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAkB,MAAM,IAAA,sBAAe,EAAC,OAAO,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAQ,EAAE,GAAG,CAAC,MAAM,IAAA,gBAAS,EAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;IAC/F,OAAO;QACH,IAAI,EAAE,IAAA,wBAAiB,EAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;QACxC,IAAI,EAAE,KAAK,EACP,MAA+B,EAC/B,UAAyE,EAAE,EAC5D,EAAE,CACjB,IAAI,cAAO,CAAC,MAAM,CAAC;aACd,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;aAChD,SAAS,CAAC,OAAO,CAAC,MAAM,IAAI,mBAAW,CAAC;aACxC,WAAW,EAAE;aACb,iBAAiB,CAAC,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC;aAC5C,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC;KAC1E,CAAC;AACN,CAAC;AAED,oFAAoF;AACpF,SAAgB,YAAY,CAAC,KAAwB,EAAE,MAAc,mBAAW;IAC5E,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,EAAE,CAAC,mBAAW,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC;AACzG,CAAC"}
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KeycloakTokenVerifier = void 0;
4
+ exports.bearerToken = bearerToken;
5
+ const jose_1 = require("jose");
6
+ const auth_contract_1 = require("@rt-tools/auth-contract");
7
+ const BEARER = /^Bearer ([A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+)$/;
8
+ /** The token of the `Authorization` header, or `null` when the header is absent or of another form. */
9
+ function bearerToken(authorization) {
10
+ const match = BEARER.exec(authorization?.trim() ?? '');
11
+ return match ? match[1] : null;
12
+ }
13
+ function resourceAccessOf(value) {
14
+ if (typeof value !== 'object' || value === null) {
15
+ return undefined;
16
+ }
17
+ const access = {};
18
+ for (const [clientId, entry] of Object.entries(value)) {
19
+ const roles = typeof entry === 'object' && entry !== null ? Reflect.get(entry, 'roles') : undefined;
20
+ access[clientId] = { roles: Array.isArray(roles) ? roles.filter((role) => typeof role === 'string') : [] };
21
+ }
22
+ return access;
23
+ }
24
+ /** The claims the caller is read from, taken from a verified payload by their shape. */
25
+ function claimsOf(payload) {
26
+ return {
27
+ sub: String(payload.sub),
28
+ email: typeof payload['email'] === 'string' ? payload['email'] : undefined,
29
+ email_verified: payload['email_verified'] === true,
30
+ name: typeof payload['name'] === 'string' ? payload['name'] : undefined,
31
+ preferred_username: typeof payload['preferred_username'] === 'string' ? payload['preferred_username'] : undefined,
32
+ resource_access: resourceAccessOf(payload['resource_access']),
33
+ };
34
+ }
35
+ /**
36
+ * The token check of one admin, shared by the NestJS guard and the Connect interceptor.
37
+ *
38
+ * A token is accepted when a key of the realm signed it, the realm issued it, its term holds and
39
+ * it was issued to the client of this admin (`azp`). Every other case answers `null` and nothing
40
+ * more: the reason of a refusal is not told to the caller, so the answers do not show which tokens
41
+ * come close.
42
+ *
43
+ * The keys are fetched from the key set address of the realm and cached by `jose`; a key the
44
+ * realm rotates in is picked up without a restart.
45
+ */
46
+ class KeycloakTokenVerifier {
47
+ #options;
48
+ #keys;
49
+ constructor(options, keys) {
50
+ this.#options = options;
51
+ this.#keys = keys ?? (0, jose_1.createRemoteJWKSet)(new URL(`${options.issuer}/protocol/openid-connect/certs`));
52
+ }
53
+ /** The caller of the `Authorization` header, or `null` when the token is not accepted. */
54
+ async callerOf(authorization) {
55
+ const token = bearerToken(authorization);
56
+ if (!token) {
57
+ return null;
58
+ }
59
+ try {
60
+ const { payload } = await (0, jose_1.jwtVerify)(token, this.#keys, { issuer: this.#options.issuer });
61
+ if (payload['azp'] !== this.#options.clientId || typeof payload.sub !== 'string') {
62
+ return null;
63
+ }
64
+ return (0, auth_contract_1.callerFromClaims)(claimsOf(payload), this.#options.clientId);
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ }
71
+ exports.KeycloakTokenVerifier = KeycloakTokenVerifier;
72
+ //# sourceMappingURL=token-verifier.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-verifier.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './lib/access.js';
2
+ export * from './lib/access-audit.js';
3
+ export * from './lib/auth-server.module.js';
4
+ export * from './lib/auth.guard.js';
5
+ export * from './lib/auth.tokens.js';
6
+ export * from './lib/catalog-sync.js';
7
+ export * from './lib/connect-access.js';
8
+ export * from './lib/current-caller.decorator.js';
9
+ export * from './lib/token-verifier.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../projects/auth-server/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mCAAmC,CAAC;AAClD,cAAc,yBAAyB,CAAC"}
package/esm/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './lib/access.js';
2
+ export * from './lib/access-audit.js';
3
+ export * from './lib/auth-server.module.js';
4
+ export * from './lib/auth.guard.js';
5
+ export * from './lib/auth.tokens.js';
6
+ export * from './lib/catalog-sync.js';
7
+ export * from './lib/connect-access.js';
8
+ export * from './lib/current-caller.decorator.js';
9
+ export * from './lib/token-verifier.js';
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../projects/auth-server/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,yBAAyB,CAAC;AACxC,cAAc,mCAAmC,CAAC;AAClD,cAAc,yBAAyB,CAAC","sourcesContent":["export * from './lib/access.js';\nexport * from './lib/access-audit.js';\nexport * from './lib/auth-server.module.js';\nexport * from './lib/auth.guard.js';\nexport * from './lib/auth.tokens.js';\nexport * from './lib/catalog-sync.js';\nexport * from './lib/connect-access.js';\nexport * from './lib/current-caller.decorator.js';\nexport * from './lib/token-verifier.js';\n"]}