@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.
- package/README.md +92 -2
- package/cjs/index.js +14 -0
- package/cjs/index.js.map +1 -0
- package/cjs/lib/access-audit.js +39 -0
- package/cjs/lib/access-audit.js.map +1 -0
- package/cjs/lib/access.js +36 -0
- package/cjs/lib/access.js.map +1 -0
- package/cjs/lib/auth-server.module.js +71 -0
- package/cjs/lib/auth-server.module.js.map +1 -0
- package/cjs/lib/auth.guard.js +51 -0
- package/cjs/lib/auth.guard.js.map +1 -0
- package/cjs/lib/auth.tokens.js +10 -0
- package/cjs/lib/auth.tokens.js.map +1 -0
- package/cjs/lib/catalog-sync.js +51 -0
- package/cjs/lib/catalog-sync.js.map +1 -0
- package/cjs/lib/connect-access.js +54 -0
- package/cjs/lib/connect-access.js.map +1 -0
- package/cjs/lib/current-caller.decorator.js +14 -0
- package/cjs/lib/current-caller.decorator.js.map +1 -0
- package/cjs/lib/env-options.js +77 -0
- package/cjs/lib/env-options.js.map +1 -0
- package/cjs/lib/testing/keys.js +28 -0
- package/cjs/lib/testing/keys.js.map +1 -0
- package/cjs/lib/token-verifier.js +77 -0
- package/cjs/lib/token-verifier.js.map +1 -0
- package/cjs/package.json +3 -0
- package/esm/index.d.ts +11 -0
- package/esm/index.d.ts.map +1 -0
- package/esm/index.js +11 -0
- package/esm/index.js.map +1 -0
- package/esm/lib/access-audit.d.ts +13 -0
- package/esm/lib/access-audit.d.ts.map +1 -0
- package/esm/lib/access-audit.js +35 -0
- package/esm/lib/access-audit.js.map +1 -0
- package/esm/lib/access.d.ts +31 -0
- package/esm/lib/access.d.ts.map +1 -0
- package/esm/lib/access.js +29 -0
- package/esm/lib/access.js.map +1 -0
- package/esm/lib/auth-server.module.d.ts +28 -0
- package/esm/lib/auth-server.module.d.ts.map +1 -0
- package/esm/lib/auth-server.module.js +67 -0
- package/esm/lib/auth-server.module.js.map +1 -0
- package/esm/lib/auth.guard.d.ts +15 -0
- package/esm/lib/auth.guard.d.ts.map +1 -0
- package/esm/lib/auth.guard.js +48 -0
- package/esm/lib/auth.guard.js.map +1 -0
- package/esm/lib/auth.tokens.d.ts +7 -0
- package/esm/lib/auth.tokens.d.ts.map +1 -0
- package/esm/lib/auth.tokens.js +7 -0
- package/esm/lib/auth.tokens.js.map +1 -0
- package/esm/lib/catalog-sync.d.ts +27 -0
- package/esm/lib/catalog-sync.d.ts.map +1 -0
- package/esm/lib/catalog-sync.js +48 -0
- package/esm/lib/catalog-sync.js.map +1 -0
- package/esm/lib/connect-access.d.ts +35 -0
- package/esm/lib/connect-access.d.ts.map +1 -0
- package/esm/lib/connect-access.js +49 -0
- package/esm/lib/connect-access.js.map +1 -0
- package/esm/lib/current-caller.decorator.d.ts +6 -0
- package/esm/lib/current-caller.decorator.d.ts.map +1 -0
- package/esm/lib/current-caller.decorator.js +10 -0
- package/esm/lib/current-caller.decorator.js.map +1 -0
- 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/testing/keys.d.ts +17 -0
- package/esm/lib/testing/keys.d.ts.map +1 -0
- package/esm/lib/testing/keys.js +23 -0
- package/esm/lib/testing/keys.js.map +1 -0
- package/esm/lib/token-verifier.d.ts +41 -0
- package/esm/lib/token-verifier.d.ts.map +1 -0
- package/esm/lib/token-verifier.js +72 -0
- package/esm/lib/token-verifier.js.map +1 -0
- package/esm/package.json +3 -0
- package/package.json +48 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect-access.d.ts","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/connect-access.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,UAAU,EAAE,WAAW,EAA+B,MAAM,qBAAqB,CAAC;AAEjI,OAAO,EAAiB,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,mFAAmF;AACnF,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACtF;AAED,iFAAiF;AACjF,MAAM,MAAM,qBAAqB,CAAC,OAAO,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,IAAI;IAAE,QAAQ,EAAE,MAAM,IAAI,MAAM,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO;CAAE,CAAC;AAE3I,wFAAwF;AACxF,eAAO,MAAM,cAAc,EAAE,UAAU,CAAC,OAAO,GAAG,IAAI,CAAmF,CAAC;AAE1I;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,mBAAmB,EAC5B,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC1C,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAa9B;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,qBAAqB,EAAE,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,GAAG,WAAW,CAmBpI"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Code, ConnectError, createContextKey } from '@connectrpc/connect';
|
|
2
|
+
import { hasPermission } from '@rt-tools/auth-contract';
|
|
3
|
+
/** The caller the interceptor accepted, read by a procedure from its context values. */
|
|
4
|
+
export const CONNECT_CALLER = createContextKey(null, { description: 'rt-tools.auth.caller' });
|
|
5
|
+
/**
|
|
6
|
+
* The accesses of one service as entries `typeName/MethodName`, checked against its description.
|
|
7
|
+
*
|
|
8
|
+
* Every method declares exactly one access, and a key that names no method is refused too: both
|
|
9
|
+
* would otherwise show only to the person who calls the method.
|
|
10
|
+
*/
|
|
11
|
+
export function connectAccessEntries(service, access) {
|
|
12
|
+
const names = new Set(service.methods.map((method) => method.localName));
|
|
13
|
+
const missing = [...names].filter((name) => !(name in access));
|
|
14
|
+
const unknown = Object.keys(access).filter((name) => !names.has(name));
|
|
15
|
+
if (missing.length || unknown.length) {
|
|
16
|
+
throw new Error(`Every method of ${service.typeName} declares exactly one access; missing: ${missing.join(', ') || 'none'}; unknown: ${unknown.join(', ') || 'none'}`);
|
|
17
|
+
}
|
|
18
|
+
return service.methods.map((method) => [
|
|
19
|
+
`${service.typeName}/${method.name}`,
|
|
20
|
+
access[method.localName],
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The Connect interceptor of an admin server: the same token check as the NestJS guard, and the
|
|
25
|
+
* same two refusals — `unauthenticated` and `permission_denied` — without naming the right.
|
|
26
|
+
*
|
|
27
|
+
* A method the map does not name is refused: the map is closed by default, as the guard is.
|
|
28
|
+
*/
|
|
29
|
+
export function createAuthInterceptor(verifier, entries) {
|
|
30
|
+
const accessOf = new Map(entries);
|
|
31
|
+
return (next) => async (request) => {
|
|
32
|
+
const access = accessOf.get(`${request.method.parent.typeName}/${request.method.name}`);
|
|
33
|
+
if (!access) {
|
|
34
|
+
throw new ConnectError('unauthenticated', Code.Unauthenticated);
|
|
35
|
+
}
|
|
36
|
+
if (access.kind !== 'public') {
|
|
37
|
+
const caller = await verifier.callerOf(request.header.get('authorization'));
|
|
38
|
+
if (!caller) {
|
|
39
|
+
throw new ConnectError('unauthenticated', Code.Unauthenticated);
|
|
40
|
+
}
|
|
41
|
+
if (access.kind === 'permission' && !hasPermission(caller, access.permission)) {
|
|
42
|
+
throw new ConnectError('permission denied', Code.PermissionDenied);
|
|
43
|
+
}
|
|
44
|
+
request.contextValues.set(CONNECT_CALLER, caller);
|
|
45
|
+
}
|
|
46
|
+
return next(request);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# 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":"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","sourcesContent":["import { Code, ConnectError, createContextKey, ContextKey, Interceptor, UnaryRequest, StreamRequest } from '@connectrpc/connect';\n\nimport { hasPermission, ICaller } from '@rt-tools/auth-contract';\n\nimport { TAccess } from './access';\nimport { KeycloakTokenVerifier } from './token-verifier';\n\n/** The part of a Connect service description the access map is checked against. */\nexport interface IConnectServiceLike {\n readonly typeName: string;\n readonly methods: readonly { readonly name: string; readonly localName: string }[];\n}\n\n/** The access of every method of one service, keyed by the local method name. */\nexport type TConnectServiceAccess<SERVICE extends { readonly method: object }> = { readonly [METHOD in keyof SERVICE['method']]: TAccess };\n\n/** The caller the interceptor accepted, read by a procedure from its context values. */\nexport const CONNECT_CALLER: ContextKey<ICaller | null> = createContextKey<ICaller | null>(null, { description: 'rt-tools.auth.caller' });\n\n/**\n * The accesses of one service as entries `typeName/MethodName`, checked against its description.\n *\n * Every method declares exactly one access, and a key that names no method is refused too: both\n * would otherwise show only to the person who calls the method.\n */\nexport function connectAccessEntries(\n service: IConnectServiceLike,\n access: Readonly<Record<string, TAccess>>\n): readonly [string, TAccess][] {\n const names: Set<string> = new Set(service.methods.map((method: { localName: string }): string => method.localName));\n const missing: string[] = [...names].filter((name: string): boolean => !(name in access));\n const unknown: string[] = Object.keys(access).filter((name: string): boolean => !names.has(name));\n if (missing.length || unknown.length) {\n throw new Error(\n `Every method of ${service.typeName} declares exactly one access; missing: ${missing.join(', ') || 'none'}; unknown: ${unknown.join(', ') || 'none'}`\n );\n }\n return service.methods.map((method: { name: string; localName: string }): [string, TAccess] => [\n `${service.typeName}/${method.name}`,\n access[method.localName],\n ]);\n}\n\n/**\n * The Connect interceptor of an admin server: the same token check as the NestJS guard, and the\n * same two refusals — `unauthenticated` and `permission_denied` — without naming the right.\n *\n * A method the map does not name is refused: the map is closed by default, as the guard is.\n */\nexport function createAuthInterceptor(verifier: KeycloakTokenVerifier, entries: readonly (readonly [string, TAccess])[]): Interceptor {\n const accessOf: Map<string, TAccess> = new Map(entries);\n return (next: Parameters<Interceptor>[0]) => async (request: UnaryRequest | StreamRequest) => {\n const access: TAccess | undefined = accessOf.get(`${request.method.parent.typeName}/${request.method.name}`);\n if (!access) {\n throw new ConnectError('unauthenticated', Code.Unauthenticated);\n }\n if (access.kind !== 'public') {\n const caller: ICaller | null = await verifier.callerOf(request.header.get('authorization'));\n if (!caller) {\n throw new ConnectError('unauthenticated', Code.Unauthenticated);\n }\n if (access.kind === 'permission' && !hasPermission(caller, access.permission)) {\n throw new ConnectError('permission denied', Code.PermissionDenied);\n }\n request.contextValues.set(CONNECT_CALLER, caller);\n }\n return next(request);\n };\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ICaller } from '@rt-tools/auth-contract';
|
|
2
|
+
/** The caller the guard accepted; `undefined` on an operation open to everyone. */
|
|
3
|
+
export declare function callerOfRequest(request: object): ICaller | undefined;
|
|
4
|
+
/** A handler parameter that receives the caller the guard accepted. */
|
|
5
|
+
export declare const CurrentCaller: () => ParameterDecorator;
|
|
6
|
+
//# sourceMappingURL=current-caller.decorator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"current-caller.decorator.d.ts","sourceRoot":"","sources":["../../../../projects/auth-server/src/lib/current-caller.decorator.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAIlD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAGpE;AAED,uEAAuE;AACvE,eAAO,MAAM,aAAa,EAAE,MAAM,kBAEjC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createParamDecorator } from '@nestjs/common';
|
|
2
|
+
import { REQUEST_CALLER } from './auth.tokens';
|
|
3
|
+
/** The caller the guard accepted; `undefined` on an operation open to everyone. */
|
|
4
|
+
export function callerOfRequest(request) {
|
|
5
|
+
const caller = Reflect.get(request, REQUEST_CALLER);
|
|
6
|
+
return caller === undefined ? undefined : caller;
|
|
7
|
+
}
|
|
8
|
+
/** A handler parameter that receives the caller the guard accepted. */
|
|
9
|
+
export const CurrentCaller = createParamDecorator((_data, context) => callerOfRequest(context.switchToHttp().getRequest()));
|
|
10
|
+
//# 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":"AAAA,OAAO,EAAE,oBAAoB,EAAoB,MAAM,gBAAgB,CAAC;AAIxE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,mFAAmF;AACnF,MAAM,UAAU,eAAe,CAAC,OAAe;IAC3C,MAAM,MAAM,GAAY,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAC7D,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,MAAkB,CAAC;AAClE,CAAC;AAED,uEAAuE;AACvE,MAAM,CAAC,MAAM,aAAa,GAA6B,oBAAoB,CACvE,CAAC,KAAc,EAAE,OAAyB,EAAuB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,UAAU,EAAU,CAAC,CACnI,CAAC","sourcesContent":["import { createParamDecorator, ExecutionContext } from '@nestjs/common';\n\nimport { ICaller } from '@rt-tools/auth-contract';\n\nimport { REQUEST_CALLER } from './auth.tokens';\n\n/** The caller the guard accepted; `undefined` on an operation open to everyone. */\nexport function callerOfRequest(request: object): ICaller | undefined {\n const caller: unknown = Reflect.get(request, REQUEST_CALLER);\n return caller === undefined ? undefined : (caller as ICaller);\n}\n\n/** A handler parameter that receives the caller the guard accepted. */\nexport const CurrentCaller: () => ParameterDecorator = createParamDecorator(\n (_data: unknown, context: ExecutionContext): ICaller | undefined => callerOfRequest(context.switchToHttp().getRequest<object>())\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"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { JWTVerifyGetKey } from 'jose';
|
|
2
|
+
/** The realm of the tests. */
|
|
3
|
+
export declare const TEST_ISSUER: string;
|
|
4
|
+
export declare const TEST_CLIENT: string;
|
|
5
|
+
/** A realm key pair and the key set the verifier reads, as a realm serves it. */
|
|
6
|
+
export interface ITestRealm {
|
|
7
|
+
readonly keys: JWTVerifyGetKey;
|
|
8
|
+
sign(claims: Record<string, unknown>, options?: {
|
|
9
|
+
issuer?: string;
|
|
10
|
+
expiresIn?: string;
|
|
11
|
+
foreignKey?: boolean;
|
|
12
|
+
}): Promise<string>;
|
|
13
|
+
}
|
|
14
|
+
export declare function testRealm(): Promise<ITestRealm>;
|
|
15
|
+
/** The claims of a person with the given roles in the test client, issued to it. */
|
|
16
|
+
export declare function personClaims(roles: readonly string[], azp?: string): Record<string, unknown>;
|
|
17
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../../../../projects/auth-server/src/lib/testing/keys.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsD,eAAe,EAAW,MAAM,MAAM,CAAC;AAEpG,8BAA8B;AAC9B,eAAO,MAAM,WAAW,EAAE,MAA2C,CAAC;AACtE,eAAO,MAAM,WAAW,EAAE,MAAuB,CAAC;AAElD,iFAAiF;AACjF,MAAM,WAAW,UAAU;IACvB,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnI;AAED,wBAAsB,SAAS,IAAI,OAAO,CAAC,UAAU,CAAC,CAiBrD;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,GAAG,GAAE,MAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createLocalJWKSet, exportJWK, generateKeyPair, SignJWT } from 'jose';
|
|
2
|
+
/** The realm of the tests. */
|
|
3
|
+
export const TEST_ISSUER = 'http://localhost:58080/realms/rt';
|
|
4
|
+
export const TEST_CLIENT = 'orders-admin';
|
|
5
|
+
export async function testRealm() {
|
|
6
|
+
const own = await generateKeyPair('RS256');
|
|
7
|
+
const foreign = await generateKeyPair('RS256');
|
|
8
|
+
const jwk = { ...(await exportJWK(own.publicKey)), kid: 'own', alg: 'RS256', use: 'sig' };
|
|
9
|
+
return {
|
|
10
|
+
keys: createLocalJWKSet({ keys: [jwk] }),
|
|
11
|
+
sign: async (claims, options = {}) => new SignJWT(claims)
|
|
12
|
+
.setProtectedHeader({ alg: 'RS256', kid: 'own' })
|
|
13
|
+
.setIssuer(options.issuer ?? TEST_ISSUER)
|
|
14
|
+
.setIssuedAt()
|
|
15
|
+
.setExpirationTime(options.expiresIn ?? '5m')
|
|
16
|
+
.sign(options.foreignKey ? foreign.privateKey : own.privateKey),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The claims of a person with the given roles in the test client, issued to it. */
|
|
20
|
+
export function personClaims(roles, azp = TEST_CLIENT) {
|
|
21
|
+
return { sub: 'p-1', email: 'anna@example.com', resource_access: { [TEST_CLIENT]: { roles } }, azp };
|
|
22
|
+
}
|
|
23
|
+
//# 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":"AAAA,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,eAAe,EAAwB,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpG,8BAA8B;AAC9B,MAAM,CAAC,MAAM,WAAW,GAAW,kCAAkC,CAAC;AACtE,MAAM,CAAC,MAAM,WAAW,GAAW,cAAc,CAAC;AAQlD,MAAM,CAAC,KAAK,UAAU,SAAS;IAC3B,MAAM,GAAG,GAAkB,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC1D,MAAM,OAAO,GAAkB,MAAM,eAAe,CAAC,OAAO,CAAC,CAAC;IAC9D,MAAM,GAAG,GAAQ,EAAE,GAAG,CAAC,MAAM,SAAS,CAAC,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,iBAAiB,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;QACxC,IAAI,EAAE,KAAK,EACP,MAA+B,EAC/B,UAAyE,EAAE,EAC5D,EAAE,CACjB,IAAI,OAAO,CAAC,MAAM,CAAC;aACd,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;aAChD,SAAS,CAAC,OAAO,CAAC,MAAM,IAAI,WAAW,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,MAAM,UAAU,YAAY,CAAC,KAAwB,EAAE,MAAc,WAAW;IAC5E,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC;AACzG,CAAC","sourcesContent":["import { createLocalJWKSet, exportJWK, generateKeyPair, JWK, JWTVerifyGetKey, SignJWT } from 'jose';\n\n/** The realm of the tests. */\nexport const TEST_ISSUER: string = 'http://localhost:58080/realms/rt';\nexport const TEST_CLIENT: string = 'orders-admin';\n\n/** A realm key pair and the key set the verifier reads, as a realm serves it. */\nexport interface ITestRealm {\n readonly keys: JWTVerifyGetKey;\n sign(claims: Record<string, unknown>, options?: { issuer?: string; expiresIn?: string; foreignKey?: boolean }): Promise<string>;\n}\n\nexport async function testRealm(): Promise<ITestRealm> {\n const own: CryptoKeyPair = await generateKeyPair('RS256');\n const foreign: CryptoKeyPair = await generateKeyPair('RS256');\n const jwk: JWK = { ...(await exportJWK(own.publicKey)), kid: 'own', alg: 'RS256', use: 'sig' };\n return {\n keys: createLocalJWKSet({ keys: [jwk] }),\n sign: async (\n claims: Record<string, unknown>,\n options: { issuer?: string; expiresIn?: string; foreignKey?: boolean } = {}\n ): Promise<string> =>\n new SignJWT(claims)\n .setProtectedHeader({ alg: 'RS256', kid: 'own' })\n .setIssuer(options.issuer ?? TEST_ISSUER)\n .setIssuedAt()\n .setExpirationTime(options.expiresIn ?? '5m')\n .sign(options.foreignKey ? foreign.privateKey : own.privateKey),\n };\n}\n\n/** The claims of a person with the given roles in the test client, issued to it. */\nexport function personClaims(roles: readonly string[], azp: string = TEST_CLIENT): Record<string, unknown> {\n return { sub: 'p-1', email: 'anna@example.com', resource_access: { [TEST_CLIENT]: { roles } }, azp };\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { JWTVerifyGetKey } from 'jose';
|
|
2
|
+
import { ICaller } from '@rt-tools/auth-contract';
|
|
3
|
+
/** Where the tokens of an admin come from: the realm and the client of this admin. */
|
|
4
|
+
export interface ITokenCheckOptions {
|
|
5
|
+
/** The realm issuer, for example `https://sso.example.com/realms/rt`. */
|
|
6
|
+
readonly issuer: string;
|
|
7
|
+
/** The client of this admin in the realm. A token issued to another client is refused. */
|
|
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;
|
|
20
|
+
}
|
|
21
|
+
/** The token of the `Authorization` header, or `null` when the header is absent or of another form. */
|
|
22
|
+
export declare function bearerToken(authorization: string | null | undefined): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* The token check of one admin, shared by the NestJS guard and the Connect interceptor.
|
|
25
|
+
*
|
|
26
|
+
* A token is accepted when a key of the realm signed it, the realm issued it, its term holds and
|
|
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
|
|
29
|
+
* more: the reason of a refusal is not told to the caller, so the answers do not show which tokens
|
|
30
|
+
* come close.
|
|
31
|
+
*
|
|
32
|
+
* The keys are fetched from the key set address of the realm and cached by `jose`; a key the
|
|
33
|
+
* realm rotates in is picked up without a restart.
|
|
34
|
+
*/
|
|
35
|
+
export declare class KeycloakTokenVerifier {
|
|
36
|
+
#private;
|
|
37
|
+
constructor(options: ITokenCheckOptions, keys?: JWTVerifyGetKey);
|
|
38
|
+
/** The caller of the `Authorization` header, or `null` when the token is not accepted. */
|
|
39
|
+
callerOf(authorization: string | null | undefined): Promise<ICaller | null>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=token-verifier.d.ts.map
|
|
@@ -0,0 +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;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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
+
import { callerFromClaims } from '@rt-tools/auth-contract';
|
|
3
|
+
const BEARER = /^Bearer ([A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+)$/;
|
|
4
|
+
/** The token of the `Authorization` header, or `null` when the header is absent or of another form. */
|
|
5
|
+
export function bearerToken(authorization) {
|
|
6
|
+
const match = BEARER.exec(authorization?.trim() ?? '');
|
|
7
|
+
return match ? match[1] : null;
|
|
8
|
+
}
|
|
9
|
+
function resourceAccessOf(value) {
|
|
10
|
+
if (typeof value !== 'object' || value === null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const access = {};
|
|
14
|
+
for (const [clientId, entry] of Object.entries(value)) {
|
|
15
|
+
const roles = typeof entry === 'object' && entry !== null ? Reflect.get(entry, 'roles') : undefined;
|
|
16
|
+
access[clientId] = { roles: Array.isArray(roles) ? roles.filter((role) => typeof role === 'string') : [] };
|
|
17
|
+
}
|
|
18
|
+
return access;
|
|
19
|
+
}
|
|
20
|
+
/** The claims the caller is read from, taken from a verified payload by their shape. */
|
|
21
|
+
function claimsOf(payload) {
|
|
22
|
+
return {
|
|
23
|
+
sub: String(payload.sub),
|
|
24
|
+
email: typeof payload['email'] === 'string' ? payload['email'] : undefined,
|
|
25
|
+
email_verified: payload['email_verified'] === true,
|
|
26
|
+
name: typeof payload['name'] === 'string' ? payload['name'] : undefined,
|
|
27
|
+
preferred_username: typeof payload['preferred_username'] === 'string' ? payload['preferred_username'] : undefined,
|
|
28
|
+
resource_access: resourceAccessOf(payload['resource_access']),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The token check of one admin, shared by the NestJS guard and the Connect interceptor.
|
|
33
|
+
*
|
|
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 or to a service client it names (`azp`). Every other
|
|
36
|
+
* case answers `null` and nothing
|
|
37
|
+
* more: the reason of a refusal is not told to the caller, so the answers do not show which tokens
|
|
38
|
+
* come close.
|
|
39
|
+
*
|
|
40
|
+
* The keys are fetched from the key set address of the realm and cached by `jose`; a key the
|
|
41
|
+
* realm rotates in is picked up without a restart.
|
|
42
|
+
*/
|
|
43
|
+
export class KeycloakTokenVerifier {
|
|
44
|
+
#options;
|
|
45
|
+
#keys;
|
|
46
|
+
constructor(options, keys) {
|
|
47
|
+
this.#options = options;
|
|
48
|
+
this.#keys = keys ?? createRemoteJWKSet(new URL(options.keysUrl ?? `${options.issuer}/protocol/openid-connect/certs`));
|
|
49
|
+
}
|
|
50
|
+
/** The caller of the `Authorization` header, or `null` when the token is not accepted. */
|
|
51
|
+
async callerOf(authorization) {
|
|
52
|
+
const token = bearerToken(authorization);
|
|
53
|
+
if (!token) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const { payload } = await jwtVerify(token, this.#keys, { issuer: this.#options.issuer });
|
|
58
|
+
if (!this.#accepts(payload['azp']) || typeof payload.sub !== 'string') {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return callerFromClaims(claimsOf(payload), this.#options.clientId);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
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
|
+
}
|
|
71
|
+
}
|
|
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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","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/esm/package.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rt-tools/auth-server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Keycloak token check, access declarations and the rights catalog sync for NestJS controllers and Connect procedures",
|
|
5
|
+
"author": "RT Team",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/nickmdf/rt-tools.git",
|
|
10
|
+
"directory": "projects/auth-server"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/nickmdf/rt-tools/tree/main/projects/auth-server",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nickmdf/rt-tools/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"nestjs",
|
|
18
|
+
"connect",
|
|
19
|
+
"keycloak",
|
|
20
|
+
"oidc",
|
|
21
|
+
"permissions"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@rt-tools/auth-contract": "^0.1.0",
|
|
25
|
+
"jose": "^6.2.12",
|
|
26
|
+
"tslib": "^2.3.0"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@nestjs/common": "^11.0.0",
|
|
30
|
+
"@nestjs/core": "^11.0.0",
|
|
31
|
+
"@connectrpc/connect": "^2.0.0",
|
|
32
|
+
"reflect-metadata": "^0.2.0",
|
|
33
|
+
"rxjs": "^7.8.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@connectrpc/connect": "2.2.0"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"main": "./cjs/index.js",
|
|
40
|
+
"types": "./esm/index.d.ts",
|
|
41
|
+
"module": "./esm/index.js",
|
|
42
|
+
"exports": {
|
|
43
|
+
"./package.json": "./package.json",
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./esm/index.d.ts",
|
|
46
|
+
"import": "./esm/index.js",
|
|
47
|
+
"require": "./cjs/index.js"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|