@rt-tools/auth-server 0.0.0-stage → 0.2.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 (76) hide show
  1. package/README.md +92 -2
  2. package/cjs/index.js +14 -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/env-options.js +77 -0
  21. package/cjs/lib/env-options.js.map +1 -0
  22. package/cjs/lib/testing/keys.js +28 -0
  23. package/cjs/lib/testing/keys.js.map +1 -0
  24. package/cjs/lib/token-verifier.js +77 -0
  25. package/cjs/lib/token-verifier.js.map +1 -0
  26. package/cjs/package.json +3 -0
  27. package/esm/index.d.ts +11 -0
  28. package/esm/index.d.ts.map +1 -0
  29. package/esm/index.js +11 -0
  30. package/esm/index.js.map +1 -0
  31. package/esm/lib/access-audit.d.ts +13 -0
  32. package/esm/lib/access-audit.d.ts.map +1 -0
  33. package/esm/lib/access-audit.js +35 -0
  34. package/esm/lib/access-audit.js.map +1 -0
  35. package/esm/lib/access.d.ts +31 -0
  36. package/esm/lib/access.d.ts.map +1 -0
  37. package/esm/lib/access.js +29 -0
  38. package/esm/lib/access.js.map +1 -0
  39. package/esm/lib/auth-server.module.d.ts +28 -0
  40. package/esm/lib/auth-server.module.d.ts.map +1 -0
  41. package/esm/lib/auth-server.module.js +67 -0
  42. package/esm/lib/auth-server.module.js.map +1 -0
  43. package/esm/lib/auth.guard.d.ts +15 -0
  44. package/esm/lib/auth.guard.d.ts.map +1 -0
  45. package/esm/lib/auth.guard.js +48 -0
  46. package/esm/lib/auth.guard.js.map +1 -0
  47. package/esm/lib/auth.tokens.d.ts +7 -0
  48. package/esm/lib/auth.tokens.d.ts.map +1 -0
  49. package/esm/lib/auth.tokens.js +7 -0
  50. package/esm/lib/auth.tokens.js.map +1 -0
  51. package/esm/lib/catalog-sync.d.ts +27 -0
  52. package/esm/lib/catalog-sync.d.ts.map +1 -0
  53. package/esm/lib/catalog-sync.js +48 -0
  54. package/esm/lib/catalog-sync.js.map +1 -0
  55. package/esm/lib/connect-access.d.ts +35 -0
  56. package/esm/lib/connect-access.d.ts.map +1 -0
  57. package/esm/lib/connect-access.js +49 -0
  58. package/esm/lib/connect-access.js.map +1 -0
  59. package/esm/lib/current-caller.decorator.d.ts +6 -0
  60. package/esm/lib/current-caller.decorator.d.ts.map +1 -0
  61. package/esm/lib/current-caller.decorator.js +10 -0
  62. package/esm/lib/current-caller.decorator.js.map +1 -0
  63. package/esm/lib/env-options.d.ts +44 -0
  64. package/esm/lib/env-options.d.ts.map +1 -0
  65. package/esm/lib/env-options.js +71 -0
  66. package/esm/lib/env-options.js.map +1 -0
  67. package/esm/lib/testing/keys.d.ts +17 -0
  68. package/esm/lib/testing/keys.d.ts.map +1 -0
  69. package/esm/lib/testing/keys.js +23 -0
  70. package/esm/lib/testing/keys.js.map +1 -0
  71. package/esm/lib/token-verifier.d.ts +41 -0
  72. package/esm/lib/token-verifier.d.ts.map +1 -0
  73. package/esm/lib/token-verifier.js +72 -0
  74. package/esm/lib/token-verifier.js.map +1 -0
  75. package/esm/package.json +3 -0
  76. package/package.json +48 -4
package/README.md CHANGED
@@ -1,3 +1,93 @@
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
+ The same options are read from the environment by `authOptionsFromEnv`:
34
+
35
+ ```ts
36
+ AuthServerModule.forRoot(authOptionsFromEnv(process.env, RIGHTS));
37
+ ```
38
+
39
+ `AUTH_ISSUER` and `AUTH_CLIENT_ID` are required, and a missing one stops the start by name.
40
+ `AUTH_SYNC_SECRET` turns the catalog sync on; the Keycloak address and the realm are taken from
41
+ the issuer, and `AUTH_SYNC_CLIENT_ID` names the sync client when it is not `rt-catalog-sync`.
42
+ `AUTH_KEYS_URL` names where the keys of the realm are read when the server reaches Keycloak by
43
+ another address than the browser does — from a container or an inner network.
44
+ `AUTH_SERVICE_CLIENTS` is a comma list of service clients whose tokens the server accepts with the
45
+ rights of `AUTH_CLIENT_ID`. Such a client has a service account holding the roles of that client;
46
+ a command without a person signs in by it.
47
+
48
+ ## Access
49
+
50
+ ```ts
51
+ @Get()
52
+ @PermittedOperation('orders:read')
53
+ public list(@CurrentCaller() caller: ICaller): Order[] { … }
54
+ ```
55
+
56
+ | Decorator | The operation is open to |
57
+ | --------------------------------- | ----------------------------- |
58
+ | `@OpenOperation()` | everyone, without a token |
59
+ | `@SignedInOperation()` | anyone with an accepted token |
60
+ | `@PermittedOperation(permission)` | a caller with the right |
61
+
62
+ An operation with no declaration or with two stops the start and is named. A call without an
63
+ accepted token gets 401, a call without the right gets 403. Neither names the right.
64
+
65
+ A token is accepted when the realm signed it by its key and named itself the issuer. Its term must
66
+ hold, and it must be issued to the client of this admin (`azp`). The keys are fetched from the
67
+ realm and cached.
68
+
69
+ ## Connect
70
+
71
+ ```ts
72
+ const entries = connectAccessEntries(OrdersService, {
73
+ listOrders: { kind: 'permission', permission: 'orders:read' },
74
+ health: { kind: 'public' },
75
+ });
76
+ const interceptor = createAuthInterceptor(app.get(AUTH_TOKEN_VERIFIER), entries);
77
+ ```
78
+
79
+ The map names every method of the service exactly once, otherwise `connectAccessEntries` throws.
80
+ The refusals are `unauthenticated` and `permission_denied`. A procedure reads the caller with
81
+ `context.values.get(CONNECT_CALLER)`.
82
+
83
+ ## The catalog sync
84
+
85
+ `syncPermissionCatalog` creates the roles the client of the admin lacks and removes none. Removing
86
+ a role takes it from every person who holds it. The rights Keycloak holds beyond the catalog are
87
+ named in the log. The sync client needs a service account with `view-clients` and `manage-clients`.
88
+
89
+ ## Connecting it to an application
90
+
91
+ The order of the whole connection is in the guide of the module.
92
+ It covers Keycloak, the server, the admin, the entry screens and the move of people:
93
+ [docs/auth-integration.md](https://github.com/Eyhenij/rt-tools/blob/main/docs/auth-integration.md).
package/cjs/index.js ADDED
@@ -0,0 +1,14 @@
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/env-options.js"), exports);
13
+ tslib_1.__exportStar(require("./lib/token-verifier.js"), exports);
14
+ //# 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,+DAAqC;AACrC,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_SERVER_OPTIONS, 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,EAAE,iCAAmB,CAAC;SACtD,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,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AUTH_ENV_VARIABLES = void 0;
4
+ exports.realmOfIssuer = realmOfIssuer;
5
+ exports.clientSettingsOf = clientSettingsOf;
6
+ exports.authOptionsFromEnv = authOptionsFromEnv;
7
+ /** The variables without which a server checks no token. */
8
+ exports.AUTH_ENV_VARIABLES = ['AUTH_ISSUER', 'AUTH_CLIENT_ID'];
9
+ /** The service client of the realm allowed to create the roles of a client, unless named otherwise. */
10
+ const SYNC_CLIENT_ID = 'rt-catalog-sync';
11
+ /** The Keycloak address and the realm name, taken from the issuer: `https://sso.example.com/realms/rt`. */
12
+ const ISSUER = /^(https?:\/\/.+)\/realms\/([^/]+)\/?$/;
13
+ /** The Keycloak address and the realm of an issuer, or `null` when it is not the address of a realm. */
14
+ function realmOfIssuer(issuer) {
15
+ const match = ISSUER.exec(issuer);
16
+ return match ? { url: match[1], realm: match[2] } : null;
17
+ }
18
+ /**
19
+ * The settings the browser part signs in with, taken from the options of the server. The admin
20
+ * asks them at start instead of carrying its own copy: a copy built into the page drifts from the
21
+ * server at the first move of Keycloak to another address.
22
+ */
23
+ function clientSettingsOf(options) {
24
+ const realm = realmOfIssuer(options.issuer);
25
+ if (!realm) {
26
+ throw new Error(`The issuer is not the address of a Keycloak realm: ${options.issuer}`);
27
+ }
28
+ return { ...realm, clientId: options.clientId };
29
+ }
30
+ /**
31
+ * The options of the entry module from the environment of the server.
32
+ *
33
+ * The realm and the client belong to the stand, not to the code: a missing one stops the start by
34
+ * name instead of checking tokens against a guess. The catalog goes to Keycloak only when the
35
+ * environment holds the secret of the sync client (`AUTH_SYNC_SECRET`); `AUTH_SYNC_CLIENT_ID`
36
+ * names that client when it is not `rt-catalog-sync`. Without the secret the realm file of the
37
+ * stand declares the roles itself. `AUTH_KEYS_URL` names where the keys are read when the server
38
+ * reaches Keycloak by another address than the browser. `AUTH_SERVICE_CLIENTS` names, by commas, the
39
+ * service clients whose tokens the server accepts besides the client of the admin.
40
+ */
41
+ function authOptionsFromEnv(env, catalog) {
42
+ const missing = exports.AUTH_ENV_VARIABLES.filter((name) => !env[name]);
43
+ if (missing.length) {
44
+ throw new Error(`The server needs ${missing.join(' and ')} in the environment`);
45
+ }
46
+ const issuer = String(env.AUTH_ISSUER);
47
+ const clientId = String(env.AUTH_CLIENT_ID);
48
+ const secret = env.AUTH_SYNC_SECRET;
49
+ const services = (env.AUTH_SERVICE_CLIENTS ?? '')
50
+ .split(',')
51
+ .map((name) => name.trim())
52
+ .filter(Boolean);
53
+ const keys = {
54
+ ...(env.AUTH_KEYS_URL ? { keysUrl: env.AUTH_KEYS_URL } : {}),
55
+ ...(services.length ? { serviceClients: services } : {}),
56
+ };
57
+ if (!secret) {
58
+ return { issuer, clientId, catalog, ...keys };
59
+ }
60
+ const realm = realmOfIssuer(issuer);
61
+ if (!realm) {
62
+ throw new Error(`AUTH_ISSUER is not the address of a Keycloak realm: ${issuer}`);
63
+ }
64
+ return {
65
+ issuer,
66
+ clientId,
67
+ catalog,
68
+ ...keys,
69
+ sync: {
70
+ baseUrl: realm.url,
71
+ realm: realm.realm,
72
+ syncClientId: env.AUTH_SYNC_CLIENT_ID || SYNC_CLIENT_ID,
73
+ syncClientSecret: secret,
74
+ },
75
+ };
76
+ }
77
+ //# sourceMappingURL=env-options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env-options.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/env-options.ts"],"names":[],"mappings":"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"}
@@ -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"}