@rt-tools/auth-server 0.1.0 → 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.
- package/README.md +15 -0
- package/cjs/index.js +1 -0
- package/cjs/index.js.map +1 -1
- package/cjs/lib/auth-server.module.js +1 -1
- package/cjs/lib/auth-server.module.js.map +1 -1
- package/cjs/lib/env-options.js +77 -0
- package/cjs/lib/env-options.js.map +1 -0
- package/cjs/lib/token-verifier.js +8 -3
- package/cjs/lib/token-verifier.js.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/lib/auth-server.module.js +1 -1
- package/esm/lib/auth-server.module.js.map +1 -1
- package/esm/lib/env-options.d.ts +44 -0
- package/esm/lib/env-options.d.ts.map +1 -0
- package/esm/lib/env-options.js +71 -0
- package/esm/lib/env-options.js.map +1 -0
- package/esm/lib/token-verifier.d.ts +13 -1
- package/esm/lib/token-verifier.d.ts.map +1 -1
- package/esm/lib/token-verifier.js +8 -3
- package/esm/lib/token-verifier.js.map +1 -1
- package/package.json +1 -6
package/README.md
CHANGED
|
@@ -30,6 +30,21 @@ export class AppModule {}
|
|
|
30
30
|
The module puts a guard on every route. At start it checks that every operation declares exactly
|
|
31
31
|
one access. When `sync` is given, it also sends the catalog to Keycloak.
|
|
32
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
|
+
|
|
33
48
|
## Access
|
|
34
49
|
|
|
35
50
|
```ts
|
package/cjs/index.js
CHANGED
|
@@ -9,5 +9,6 @@ tslib_1.__exportStar(require("./lib/auth.tokens.js"), exports);
|
|
|
9
9
|
tslib_1.__exportStar(require("./lib/catalog-sync.js"), exports);
|
|
10
10
|
tslib_1.__exportStar(require("./lib/connect-access.js"), exports);
|
|
11
11
|
tslib_1.__exportStar(require("./lib/current-caller.decorator.js"), exports);
|
|
12
|
+
tslib_1.__exportStar(require("./lib/env-options.js"), exports);
|
|
12
13
|
tslib_1.__exportStar(require("./lib/token-verifier.js"), exports);
|
|
13
14
|
//# sourceMappingURL=index.js.map
|
package/cjs/index.js.map
CHANGED
|
@@ -1 +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"}
|
|
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"}
|
|
@@ -60,7 +60,7 @@ let AuthServerModule = AuthServerModule_1 = class AuthServerModule {
|
|
|
60
60
|
inject: [core_1.DiscoveryService],
|
|
61
61
|
},
|
|
62
62
|
],
|
|
63
|
-
exports: [auth_tokens_1.AUTH_TOKEN_VERIFIER],
|
|
63
|
+
exports: [auth_tokens_1.AUTH_SERVER_OPTIONS, auth_tokens_1.AUTH_TOKEN_VERIFIER],
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
};
|
|
@@ -1 +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;
|
|
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,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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"}
|
|
@@ -36,7 +36,8 @@ function claimsOf(payload) {
|
|
|
36
36
|
* The token check of one admin, shared by the NestJS guard and the Connect interceptor.
|
|
37
37
|
*
|
|
38
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
|
|
39
|
+
* it was issued to the client of this admin or to a service client it names (`azp`). Every other
|
|
40
|
+
* case answers `null` and nothing
|
|
40
41
|
* more: the reason of a refusal is not told to the caller, so the answers do not show which tokens
|
|
41
42
|
* come close.
|
|
42
43
|
*
|
|
@@ -48,7 +49,7 @@ class KeycloakTokenVerifier {
|
|
|
48
49
|
#keys;
|
|
49
50
|
constructor(options, keys) {
|
|
50
51
|
this.#options = options;
|
|
51
|
-
this.#keys = keys ?? (0, jose_1.createRemoteJWKSet)(new URL(`${options.issuer}/protocol/openid-connect/certs`));
|
|
52
|
+
this.#keys = keys ?? (0, jose_1.createRemoteJWKSet)(new URL(options.keysUrl ?? `${options.issuer}/protocol/openid-connect/certs`));
|
|
52
53
|
}
|
|
53
54
|
/** The caller of the `Authorization` header, or `null` when the token is not accepted. */
|
|
54
55
|
async callerOf(authorization) {
|
|
@@ -58,7 +59,7 @@ class KeycloakTokenVerifier {
|
|
|
58
59
|
}
|
|
59
60
|
try {
|
|
60
61
|
const { payload } = await (0, jose_1.jwtVerify)(token, this.#keys, { issuer: this.#options.issuer });
|
|
61
|
-
if (payload['azp']
|
|
62
|
+
if (!this.#accepts(payload['azp']) || typeof payload.sub !== 'string') {
|
|
62
63
|
return null;
|
|
63
64
|
}
|
|
64
65
|
return (0, auth_contract_1.callerFromClaims)(claimsOf(payload), this.#options.clientId);
|
|
@@ -67,6 +68,10 @@ class KeycloakTokenVerifier {
|
|
|
67
68
|
return null;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
71
|
+
/** Whether a token issued to this client is accepted: the client of this admin or a service client it names. */
|
|
72
|
+
#accepts(azp) {
|
|
73
|
+
return azp === this.#options.clientId || (typeof azp === 'string' && (this.#options.serviceClients ?? []).includes(azp));
|
|
74
|
+
}
|
|
70
75
|
}
|
|
71
76
|
exports.KeycloakTokenVerifier = KeycloakTokenVerifier;
|
|
72
77
|
//# sourceMappingURL=token-verifier.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-verifier.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"token-verifier.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":"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"}
|
package/esm/index.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export * from './lib/auth.tokens.js';
|
|
|
6
6
|
export * from './lib/catalog-sync.js';
|
|
7
7
|
export * from './lib/connect-access.js';
|
|
8
8
|
export * from './lib/current-caller.decorator.js';
|
|
9
|
+
export * from './lib/env-options.js';
|
|
9
10
|
export * from './lib/token-verifier.js';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/esm/index.d.ts.map
CHANGED
|
@@ -1 +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"}
|
|
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,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC"}
|
package/esm/index.js
CHANGED
|
@@ -6,5 +6,6 @@ export * from './lib/auth.tokens.js';
|
|
|
6
6
|
export * from './lib/catalog-sync.js';
|
|
7
7
|
export * from './lib/connect-access.js';
|
|
8
8
|
export * from './lib/current-caller.decorator.js';
|
|
9
|
+
export * from './lib/env-options.js';
|
|
9
10
|
export * from './lib/token-verifier.js';
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
package/esm/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
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,sBAAsB,CAAC;AACrC,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/env-options.js';\nexport * from './lib/token-verifier.js';\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-server.module.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/auth-server.module.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAiB,MAAM,EAAE,MAAM,EAA0B,MAAM,gBAAgB,CAAC;AACvF,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAK5E,OAAO,EAAE,gBAAgB,EAAoB,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtF,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,EAA2C,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAChG,OAAO,EAAsB,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAU7E;;;;;GAKG;AACH,MAAM,OAAO,cAAc;IACd,UAAU,CAAmB;IAC7B,QAAQ,CAAqB;IAC7B,OAAO,GAAW,IAAI,MAAM,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,gBAAgB,CAAC,WAAW,CAAC,CAAC;QAChE,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;QACnC,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACrB,MAAM,MAAM,GAAuB,MAAM,qBAAqB,CAC1D,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;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,eAAe,CAAC;YAC1B,SAAS,EAAE;gBACP,EAAE,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,OAAO,EAAE;gBACnD,EAAE,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,IAAI,qBAAqB,CAAC,OAAO,CAAC,EAAE;gBAC9E,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,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,gBAAgB,CAAC;iBAC7B;aACJ;YACD,OAAO,EAAE,CAAC,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"auth-server.module.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/auth-server.module.ts"],"names":[],"mappings":"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","sourcesContent":["import { DynamicModule, Logger, Module, OnApplicationBootstrap } from '@nestjs/common';\nimport { APP_GUARD, DiscoveryModule, DiscoveryService } from '@nestjs/core';\nimport { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';\n\nimport { TPermission } from '@rt-tools/auth-contract';\n\nimport { accessAuditError, TControllerClass, undeclaredAccess } from './access-audit';\nimport { AuthGuard } from './auth.guard';\nimport { AUTH_SERVER_OPTIONS, AUTH_TOKEN_VERIFIER } from './auth.tokens';\nimport { ICatalogSyncOptions, ICatalogSyncResult, syncPermissionCatalog } from './catalog-sync';\nimport { ITokenCheckOptions, KeycloakTokenVerifier } from './token-verifier';\n\n/** What an admin server tells the module. */\nexport interface IAuthServerOptions extends ITokenCheckOptions {\n /** Every right of this admin — `definePermissions(…)` of the contract. */\n readonly catalog: readonly TPermission[];\n /** Where the catalog is sent at start; without it nothing is sent. */\n readonly sync?: Omit<ICatalogSyncOptions, 'clientId'>;\n}\n\n/**\n * At start: every operation declares exactly one access, and the catalog reaches Keycloak.\n *\n * An operation without a declaration or with two stops the start and is named; the catalog sync\n * creates the roles Keycloak lacks and names the extra ones in the log.\n */\nexport class AuthStartCheck implements OnApplicationBootstrap {\n readonly #discovery: DiscoveryService;\n readonly #options: IAuthServerOptions;\n readonly #logger: Logger = new Logger('AuthServer');\n\n constructor(discovery: DiscoveryService, options: IAuthServerOptions) {\n this.#discovery = discovery;\n this.#options = options;\n }\n\n public async onApplicationBootstrap(): Promise<void> {\n const controllers: TControllerClass[] = this.#discovery\n .getControllers()\n .map((wrapper: InstanceWrapper): unknown => wrapper.metatype)\n .filter((metatype: unknown): metatype is TControllerClass => typeof metatype === 'function');\n const faults: readonly string[] = undeclaredAccess(controllers);\n if (faults.length) {\n throw accessAuditError(faults);\n }\n if (this.#options.sync) {\n const result: ICatalogSyncResult = await syncPermissionCatalog(\n { ...this.#options.sync, clientId: this.#options.clientId },\n this.#options.catalog\n );\n this.#logger.log(`catalog: ${this.#options.catalog.length} rights, created ${result.created.length}`);\n if (result.extra.length) {\n this.#logger.warn(`Keycloak holds rights the catalog lacks: ${result.extra.join(', ')}`);\n }\n }\n }\n}\n\n/** The entry module of an admin server: the token check, the guard and the start check. */\n@Module({})\nexport class AuthServerModule {\n public static forRoot(options: IAuthServerOptions): DynamicModule {\n return {\n module: AuthServerModule,\n global: true,\n imports: [DiscoveryModule],\n providers: [\n { provide: AUTH_SERVER_OPTIONS, useValue: options },\n { provide: AUTH_TOKEN_VERIFIER, useValue: new KeycloakTokenVerifier(options) },\n { provide: APP_GUARD, useClass: AuthGuard },\n {\n provide: AuthStartCheck,\n useFactory: (discovery: DiscoveryService): AuthStartCheck => new AuthStartCheck(discovery, options),\n inject: [DiscoveryService],\n },\n ],\n exports: [AUTH_SERVER_OPTIONS, AUTH_TOKEN_VERIFIER],\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TPermission } from '@rt-tools/auth-contract';
|
|
2
|
+
import type { IAuthServerOptions } from './auth-server.module';
|
|
3
|
+
/** The environment variables the entry module is set up by; `process.env` fits it as it is. */
|
|
4
|
+
export interface IAuthEnv {
|
|
5
|
+
readonly AUTH_ISSUER?: string;
|
|
6
|
+
readonly AUTH_CLIENT_ID?: string;
|
|
7
|
+
readonly AUTH_SYNC_SECRET?: string;
|
|
8
|
+
readonly AUTH_SYNC_CLIENT_ID?: string;
|
|
9
|
+
readonly AUTH_KEYS_URL?: string;
|
|
10
|
+
readonly AUTH_SERVICE_CLIENTS?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The variables without which a server checks no token. */
|
|
13
|
+
export declare const AUTH_ENV_VARIABLES: readonly (keyof IAuthEnv)[];
|
|
14
|
+
/** What the browser part of an admin needs to sign a person in: the same realm and client as the server. */
|
|
15
|
+
export interface IAuthClientSettings {
|
|
16
|
+
/** The Keycloak address, for example `https://sso.example.com`. */
|
|
17
|
+
readonly url: string;
|
|
18
|
+
readonly realm: string;
|
|
19
|
+
readonly clientId: string;
|
|
20
|
+
}
|
|
21
|
+
/** The Keycloak address and the realm of an issuer, or `null` when it is not the address of a realm. */
|
|
22
|
+
export declare function realmOfIssuer(issuer: string): {
|
|
23
|
+
readonly url: string;
|
|
24
|
+
readonly realm: string;
|
|
25
|
+
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* The settings the browser part signs in with, taken from the options of the server. The admin
|
|
28
|
+
* asks them at start instead of carrying its own copy: a copy built into the page drifts from the
|
|
29
|
+
* server at the first move of Keycloak to another address.
|
|
30
|
+
*/
|
|
31
|
+
export declare function clientSettingsOf(options: IAuthServerOptions): IAuthClientSettings;
|
|
32
|
+
/**
|
|
33
|
+
* The options of the entry module from the environment of the server.
|
|
34
|
+
*
|
|
35
|
+
* The realm and the client belong to the stand, not to the code: a missing one stops the start by
|
|
36
|
+
* name instead of checking tokens against a guess. The catalog goes to Keycloak only when the
|
|
37
|
+
* environment holds the secret of the sync client (`AUTH_SYNC_SECRET`); `AUTH_SYNC_CLIENT_ID`
|
|
38
|
+
* names that client when it is not `rt-catalog-sync`. Without the secret the realm file of the
|
|
39
|
+
* stand declares the roles itself. `AUTH_KEYS_URL` names where the keys are read when the server
|
|
40
|
+
* reaches Keycloak by another address than the browser. `AUTH_SERVICE_CLIENTS` names, by commas, the
|
|
41
|
+
* service clients whose tokens the server accepts besides the client of the admin.
|
|
42
|
+
*/
|
|
43
|
+
export declare function authOptionsFromEnv(env: IAuthEnv, catalog: readonly TPermission[]): IAuthServerOptions;
|
|
44
|
+
//# sourceMappingURL=env-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-options.d.ts","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/env-options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,+FAA+F;AAC/F,MAAM,WAAW,QAAQ;IACrB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC1C;AAED,4DAA4D;AAC5D,eAAO,MAAM,kBAAkB,EAAE,SAAS,CAAC,MAAM,QAAQ,CAAC,EAAsC,CAAC;AAQjG,4GAA4G;AAC5G,MAAM,WAAW,mBAAmB;IAChC,mEAAmE;IACnE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC7B;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAGrG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,mBAAmB,CAMjF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,WAAW,EAAE,GAAG,kBAAkB,CAmCrG"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** The variables without which a server checks no token. */
|
|
2
|
+
export const AUTH_ENV_VARIABLES = ['AUTH_ISSUER', 'AUTH_CLIENT_ID'];
|
|
3
|
+
/** The service client of the realm allowed to create the roles of a client, unless named otherwise. */
|
|
4
|
+
const SYNC_CLIENT_ID = 'rt-catalog-sync';
|
|
5
|
+
/** The Keycloak address and the realm name, taken from the issuer: `https://sso.example.com/realms/rt`. */
|
|
6
|
+
const ISSUER = /^(https?:\/\/.+)\/realms\/([^/]+)\/?$/;
|
|
7
|
+
/** The Keycloak address and the realm of an issuer, or `null` when it is not the address of a realm. */
|
|
8
|
+
export function realmOfIssuer(issuer) {
|
|
9
|
+
const match = ISSUER.exec(issuer);
|
|
10
|
+
return match ? { url: match[1], realm: match[2] } : null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The settings the browser part signs in with, taken from the options of the server. The admin
|
|
14
|
+
* asks them at start instead of carrying its own copy: a copy built into the page drifts from the
|
|
15
|
+
* server at the first move of Keycloak to another address.
|
|
16
|
+
*/
|
|
17
|
+
export function clientSettingsOf(options) {
|
|
18
|
+
const realm = realmOfIssuer(options.issuer);
|
|
19
|
+
if (!realm) {
|
|
20
|
+
throw new Error(`The issuer is not the address of a Keycloak realm: ${options.issuer}`);
|
|
21
|
+
}
|
|
22
|
+
return { ...realm, clientId: options.clientId };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The options of the entry module from the environment of the server.
|
|
26
|
+
*
|
|
27
|
+
* The realm and the client belong to the stand, not to the code: a missing one stops the start by
|
|
28
|
+
* name instead of checking tokens against a guess. The catalog goes to Keycloak only when the
|
|
29
|
+
* environment holds the secret of the sync client (`AUTH_SYNC_SECRET`); `AUTH_SYNC_CLIENT_ID`
|
|
30
|
+
* names that client when it is not `rt-catalog-sync`. Without the secret the realm file of the
|
|
31
|
+
* stand declares the roles itself. `AUTH_KEYS_URL` names where the keys are read when the server
|
|
32
|
+
* reaches Keycloak by another address than the browser. `AUTH_SERVICE_CLIENTS` names, by commas, the
|
|
33
|
+
* service clients whose tokens the server accepts besides the client of the admin.
|
|
34
|
+
*/
|
|
35
|
+
export function authOptionsFromEnv(env, catalog) {
|
|
36
|
+
const missing = AUTH_ENV_VARIABLES.filter((name) => !env[name]);
|
|
37
|
+
if (missing.length) {
|
|
38
|
+
throw new Error(`The server needs ${missing.join(' and ')} in the environment`);
|
|
39
|
+
}
|
|
40
|
+
const issuer = String(env.AUTH_ISSUER);
|
|
41
|
+
const clientId = String(env.AUTH_CLIENT_ID);
|
|
42
|
+
const secret = env.AUTH_SYNC_SECRET;
|
|
43
|
+
const services = (env.AUTH_SERVICE_CLIENTS ?? '')
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((name) => name.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
const keys = {
|
|
48
|
+
...(env.AUTH_KEYS_URL ? { keysUrl: env.AUTH_KEYS_URL } : {}),
|
|
49
|
+
...(services.length ? { serviceClients: services } : {}),
|
|
50
|
+
};
|
|
51
|
+
if (!secret) {
|
|
52
|
+
return { issuer, clientId, catalog, ...keys };
|
|
53
|
+
}
|
|
54
|
+
const realm = realmOfIssuer(issuer);
|
|
55
|
+
if (!realm) {
|
|
56
|
+
throw new Error(`AUTH_ISSUER is not the address of a Keycloak realm: ${issuer}`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
issuer,
|
|
60
|
+
clientId,
|
|
61
|
+
catalog,
|
|
62
|
+
...keys,
|
|
63
|
+
sync: {
|
|
64
|
+
baseUrl: realm.url,
|
|
65
|
+
realm: realm.realm,
|
|
66
|
+
syncClientId: env.AUTH_SYNC_CLIENT_ID || SYNC_CLIENT_ID,
|
|
67
|
+
syncClientSecret: secret,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# 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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","sourcesContent":["import { TPermission } from '@rt-tools/auth-contract';\n\nimport type { IAuthServerOptions } from './auth-server.module';\n\n/** The environment variables the entry module is set up by; `process.env` fits it as it is. */\nexport interface IAuthEnv {\n readonly AUTH_ISSUER?: string;\n readonly AUTH_CLIENT_ID?: string;\n readonly AUTH_SYNC_SECRET?: string;\n readonly AUTH_SYNC_CLIENT_ID?: string;\n readonly AUTH_KEYS_URL?: string;\n readonly AUTH_SERVICE_CLIENTS?: string;\n}\n\n/** The variables without which a server checks no token. */\nexport const AUTH_ENV_VARIABLES: readonly (keyof IAuthEnv)[] = ['AUTH_ISSUER', 'AUTH_CLIENT_ID'];\n\n/** The service client of the realm allowed to create the roles of a client, unless named otherwise. */\nconst SYNC_CLIENT_ID: string = 'rt-catalog-sync';\n\n/** The Keycloak address and the realm name, taken from the issuer: `https://sso.example.com/realms/rt`. */\nconst ISSUER: RegExp = /^(https?:\\/\\/.+)\\/realms\\/([^/]+)\\/?$/;\n\n/** What the browser part of an admin needs to sign a person in: the same realm and client as the server. */\nexport interface IAuthClientSettings {\n /** The Keycloak address, for example `https://sso.example.com`. */\n readonly url: string;\n readonly realm: string;\n readonly clientId: string;\n}\n\n/** The Keycloak address and the realm of an issuer, or `null` when it is not the address of a realm. */\nexport function realmOfIssuer(issuer: string): { readonly url: string; readonly realm: string } | null {\n const match: RegExpExecArray | null = ISSUER.exec(issuer);\n return match ? { url: match[1], realm: match[2] } : null;\n}\n\n/**\n * The settings the browser part signs in with, taken from the options of the server. The admin\n * asks them at start instead of carrying its own copy: a copy built into the page drifts from the\n * server at the first move of Keycloak to another address.\n */\nexport function clientSettingsOf(options: IAuthServerOptions): IAuthClientSettings {\n const realm: { readonly url: string; readonly realm: string } | null = realmOfIssuer(options.issuer);\n if (!realm) {\n throw new Error(`The issuer is not the address of a Keycloak realm: ${options.issuer}`);\n }\n return { ...realm, clientId: options.clientId };\n}\n\n/**\n * The options of the entry module from the environment of the server.\n *\n * The realm and the client belong to the stand, not to the code: a missing one stops the start by\n * name instead of checking tokens against a guess. The catalog goes to Keycloak only when the\n * environment holds the secret of the sync client (`AUTH_SYNC_SECRET`); `AUTH_SYNC_CLIENT_ID`\n * names that client when it is not `rt-catalog-sync`. Without the secret the realm file of the\n * stand declares the roles itself. `AUTH_KEYS_URL` names where the keys are read when the server\n * reaches Keycloak by another address than the browser. `AUTH_SERVICE_CLIENTS` names, by commas, the\n * service clients whose tokens the server accepts besides the client of the admin.\n */\nexport function authOptionsFromEnv(env: IAuthEnv, catalog: readonly TPermission[]): IAuthServerOptions {\n const missing: readonly string[] = AUTH_ENV_VARIABLES.filter((name: keyof IAuthEnv): boolean => !env[name]);\n if (missing.length) {\n throw new Error(`The server needs ${missing.join(' and ')} in the environment`);\n }\n const issuer: string = String(env.AUTH_ISSUER);\n const clientId: string = String(env.AUTH_CLIENT_ID);\n const secret: string | undefined = env.AUTH_SYNC_SECRET;\n const services: readonly string[] = (env.AUTH_SERVICE_CLIENTS ?? '')\n .split(',')\n .map((name: string): string => name.trim())\n .filter(Boolean);\n const keys: { readonly keysUrl?: string; readonly serviceClients?: readonly string[] } = {\n ...(env.AUTH_KEYS_URL ? { keysUrl: env.AUTH_KEYS_URL } : {}),\n ...(services.length ? { serviceClients: services } : {}),\n };\n if (!secret) {\n return { issuer, clientId, catalog, ...keys };\n }\n const realm: { readonly url: string; readonly realm: string } | null = realmOfIssuer(issuer);\n if (!realm) {\n throw new Error(`AUTH_ISSUER is not the address of a Keycloak realm: ${issuer}`);\n }\n return {\n issuer,\n clientId,\n catalog,\n ...keys,\n sync: {\n baseUrl: realm.url,\n realm: realm.realm,\n syncClientId: env.AUTH_SYNC_CLIENT_ID || SYNC_CLIENT_ID,\n syncClientSecret: secret,\n },\n };\n}\n"]}
|
|
@@ -6,6 +6,17 @@ export interface ITokenCheckOptions {
|
|
|
6
6
|
readonly issuer: string;
|
|
7
7
|
/** The client of this admin in the realm. A token issued to another client is refused. */
|
|
8
8
|
readonly clientId: string;
|
|
9
|
+
/**
|
|
10
|
+
* The service clients whose tokens this admin accepts too: commands that call it without a
|
|
11
|
+
* person. Their rights are the roles of `clientId` on their service account, the same place the
|
|
12
|
+
* rights of a person are read from.
|
|
13
|
+
*/
|
|
14
|
+
readonly serviceClients?: readonly string[];
|
|
15
|
+
/**
|
|
16
|
+
* Where the server reads the keys of the realm, when it reaches Keycloak by another address than
|
|
17
|
+
* the browser does: an inner network, a container. By default — the key set of the issuer.
|
|
18
|
+
*/
|
|
19
|
+
readonly keysUrl?: string;
|
|
9
20
|
}
|
|
10
21
|
/** The token of the `Authorization` header, or `null` when the header is absent or of another form. */
|
|
11
22
|
export declare function bearerToken(authorization: string | null | undefined): string | null;
|
|
@@ -13,7 +24,8 @@ export declare function bearerToken(authorization: string | null | undefined): s
|
|
|
13
24
|
* The token check of one admin, shared by the NestJS guard and the Connect interceptor.
|
|
14
25
|
*
|
|
15
26
|
* A token is accepted when a key of the realm signed it, the realm issued it, its term holds and
|
|
16
|
-
* it was issued to the client of this admin (`azp`). Every other
|
|
27
|
+
* it was issued to the client of this admin or to a service client it names (`azp`). Every other
|
|
28
|
+
* case answers `null` and nothing
|
|
17
29
|
* more: the reason of a refusal is not told to the caller, so the answers do not show which tokens
|
|
18
30
|
* come close.
|
|
19
31
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-verifier.d.ts","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,eAAe,EAAa,MAAM,MAAM,CAAC;AAElF,OAAO,EAAoB,OAAO,EAAmB,MAAM,yBAAyB,CAAC;AAErF,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC7B;AAID,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAGnF;AA0BD
|
|
1
|
+
{"version":3,"file":"token-verifier.d.ts","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,eAAe,EAAa,MAAM,MAAM,CAAC;AAElF,OAAO,EAAoB,OAAO,EAAmB,MAAM,yBAAyB,CAAC;AAErF,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC7B;AAID,uGAAuG;AACvG,wBAAgB,WAAW,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAGnF;AA0BD;;;;;;;;;;;GAWG;AACH,qBAAa,qBAAqB;;gBAIlB,OAAO,EAAE,kBAAkB,EAAE,IAAI,CAAC,EAAE,eAAe;IAK/D,0FAA0F;IAC7E,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;CAoB3F"}
|
|
@@ -32,7 +32,8 @@ function claimsOf(payload) {
|
|
|
32
32
|
* The token check of one admin, shared by the NestJS guard and the Connect interceptor.
|
|
33
33
|
*
|
|
34
34
|
* A token is accepted when a key of the realm signed it, the realm issued it, its term holds and
|
|
35
|
-
* it was issued to the client of this admin (`azp`). Every other
|
|
35
|
+
* it was issued to the client of this admin or to a service client it names (`azp`). Every other
|
|
36
|
+
* case answers `null` and nothing
|
|
36
37
|
* more: the reason of a refusal is not told to the caller, so the answers do not show which tokens
|
|
37
38
|
* come close.
|
|
38
39
|
*
|
|
@@ -44,7 +45,7 @@ export class KeycloakTokenVerifier {
|
|
|
44
45
|
#keys;
|
|
45
46
|
constructor(options, keys) {
|
|
46
47
|
this.#options = options;
|
|
47
|
-
this.#keys = keys ?? createRemoteJWKSet(new URL(`${options.issuer}/protocol/openid-connect/certs`));
|
|
48
|
+
this.#keys = keys ?? createRemoteJWKSet(new URL(options.keysUrl ?? `${options.issuer}/protocol/openid-connect/certs`));
|
|
48
49
|
}
|
|
49
50
|
/** The caller of the `Authorization` header, or `null` when the token is not accepted. */
|
|
50
51
|
async callerOf(authorization) {
|
|
@@ -54,7 +55,7 @@ export class KeycloakTokenVerifier {
|
|
|
54
55
|
}
|
|
55
56
|
try {
|
|
56
57
|
const { payload } = await jwtVerify(token, this.#keys, { issuer: this.#options.issuer });
|
|
57
|
-
if (payload['azp']
|
|
58
|
+
if (!this.#accepts(payload['azp']) || typeof payload.sub !== 'string') {
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
return callerFromClaims(claimsOf(payload), this.#options.clientId);
|
|
@@ -63,5 +64,9 @@ export class KeycloakTokenVerifier {
|
|
|
63
64
|
return null;
|
|
64
65
|
}
|
|
65
66
|
}
|
|
67
|
+
/** Whether a token issued to this client is accepted: the client of this admin or a service client it names. */
|
|
68
|
+
#accepts(azp) {
|
|
69
|
+
return azp === this.#options.clientId || (typeof azp === 'string' && (this.#options.serviceClients ?? []).includes(azp));
|
|
70
|
+
}
|
|
66
71
|
}
|
|
67
72
|
//# sourceMappingURL=token-verifier.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-verifier.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA+B,SAAS,EAAE,MAAM,MAAM,CAAC;AAElF,OAAO,EAAE,gBAAgB,EAA4B,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"token-verifier.js","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/token-verifier.ts"],"names":[],"mappings":"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","sourcesContent":["import { createRemoteJWKSet, JWTPayload, JWTVerifyGetKey, jwtVerify } from 'jose';\n\nimport { callerFromClaims, ICaller, IKeycloakClaims } from '@rt-tools/auth-contract';\n\n/** Where the tokens of an admin come from: the realm and the client of this admin. */\nexport interface ITokenCheckOptions {\n /** The realm issuer, for example `https://sso.example.com/realms/rt`. */\n readonly issuer: string;\n /** The client of this admin in the realm. A token issued to another client is refused. */\n readonly clientId: string;\n /**\n * The service clients whose tokens this admin accepts too: commands that call it without a\n * person. Their rights are the roles of `clientId` on their service account, the same place the\n * rights of a person are read from.\n */\n readonly serviceClients?: readonly string[];\n /**\n * Where the server reads the keys of the realm, when it reaches Keycloak by another address than\n * the browser does: an inner network, a container. By default — the key set of the issuer.\n */\n readonly keysUrl?: string;\n}\n\nconst BEARER: RegExp = /^Bearer ([A-Za-z0-9\\-_]+\\.[A-Za-z0-9\\-_]+\\.[A-Za-z0-9\\-_]+)$/;\n\n/** The token of the `Authorization` header, or `null` when the header is absent or of another form. */\nexport function bearerToken(authorization: string | null | undefined): string | null {\n const match: RegExpExecArray | null = BEARER.exec(authorization?.trim() ?? '');\n return match ? match[1] : null;\n}\n\nfunction resourceAccessOf(value: unknown): IKeycloakClaims['resource_access'] {\n if (typeof value !== 'object' || value === null) {\n return undefined;\n }\n const access: Record<string, { roles?: readonly string[] }> = {};\n for (const [clientId, entry] of Object.entries(value)) {\n const roles: unknown = typeof entry === 'object' && entry !== null ? Reflect.get(entry, 'roles') : undefined;\n access[clientId] = { roles: Array.isArray(roles) ? roles.filter((role: unknown): role is string => typeof role === 'string') : [] };\n }\n return access;\n}\n\n/** The claims the caller is read from, taken from a verified payload by their shape. */\nfunction claimsOf(payload: JWTPayload): IKeycloakClaims {\n return {\n sub: String(payload.sub),\n email: typeof payload['email'] === 'string' ? payload['email'] : undefined,\n email_verified: payload['email_verified'] === true,\n name: typeof payload['name'] === 'string' ? payload['name'] : undefined,\n preferred_username: typeof payload['preferred_username'] === 'string' ? payload['preferred_username'] : undefined,\n resource_access: resourceAccessOf(payload['resource_access']),\n };\n}\n\n/**\n * The token check of one admin, shared by the NestJS guard and the Connect interceptor.\n *\n * A token is accepted when a key of the realm signed it, the realm issued it, its term holds and\n * it was issued to the client of this admin or to a service client it names (`azp`). Every other\n * case answers `null` and nothing\n * more: the reason of a refusal is not told to the caller, so the answers do not show which tokens\n * come close.\n *\n * The keys are fetched from the key set address of the realm and cached by `jose`; a key the\n * realm rotates in is picked up without a restart.\n */\nexport class KeycloakTokenVerifier {\n readonly #options: ITokenCheckOptions;\n readonly #keys: JWTVerifyGetKey;\n\n constructor(options: ITokenCheckOptions, keys?: JWTVerifyGetKey) {\n this.#options = options;\n this.#keys = keys ?? createRemoteJWKSet(new URL(options.keysUrl ?? `${options.issuer}/protocol/openid-connect/certs`));\n }\n\n /** The caller of the `Authorization` header, or `null` when the token is not accepted. */\n public async callerOf(authorization: string | null | undefined): Promise<ICaller | null> {\n const token: string | null = bearerToken(authorization);\n if (!token) {\n return null;\n }\n try {\n const { payload } = await jwtVerify(token, this.#keys, { issuer: this.#options.issuer });\n if (!this.#accepts(payload['azp']) || typeof payload.sub !== 'string') {\n return null;\n }\n return callerFromClaims(claimsOf(payload), this.#options.clientId);\n } catch {\n return null;\n }\n }\n\n /** Whether a token issued to this client is accepted: the client of this admin or a service client it names. */\n #accepts(azp: unknown): boolean {\n return azp === this.#options.clientId || (typeof azp === 'string' && (this.#options.serviceClients ?? []).includes(azp));\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rt-tools/auth-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Keycloak token check, access declarations and the rights catalog sync for NestJS controllers and Connect procedures",
|
|
5
5
|
"author": "RT Team",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -32,11 +32,6 @@
|
|
|
32
32
|
"reflect-metadata": "^0.2.0",
|
|
33
33
|
"rxjs": "^7.8.0"
|
|
34
34
|
},
|
|
35
|
-
"peerDependenciesMeta": {
|
|
36
|
-
"@connectrpc/connect": {
|
|
37
|
-
"optional": true
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
35
|
"devDependencies": {
|
|
41
36
|
"@connectrpc/connect": "2.2.0"
|
|
42
37
|
},
|