@canton-network/core-wallet-auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # Wallet Auth
2
+
3
+ This workspace contains the authentication logic and interfaces used by the Splice Wallet Kernel.
4
+ It provides abstractions for authentication services, user identity management, and integration points for different identity providers (IDPs), such as password-based and OAuth2/OIDC-based authentication.
5
+
6
+ ## Installation
7
+
8
+ This package requires [NodeJS](https://nodejs.org/) v16 or higher and can be added to your project using:
9
+
10
+ ```sh
11
+ npm install @canton-network/core-wallet-auth --save
12
+ ```
13
+
14
+ or
15
+
16
+ ```sh
17
+ yarn add @canton-network/core-wallet-auth
18
+ ```
19
+
20
+ ## Interfaces
21
+
22
+ This workspace defines several core interfaces for authentication.
23
+
24
+ ### [AuthContext](./src/AuthService.ts)
25
+
26
+ Represents the authentication context for a user, including their unique user ID and the associated access token.
27
+
28
+ ### [AuthAware](./src/AuthService.ts)
29
+
30
+ Provides a pattern for classes or services that are aware of authentication context.
31
+ It exposes the current `authContext` and a method `withAuthContext` to create a new instance with a specific authentication context.
32
+ An example the application of this pattern can be seen in the [StoreInternal](../wallet-store/src/StoreInternal.ts).
33
+
34
+ ### [AuthService](./src/AuthService.ts)
35
+
36
+ Defines the contract for authentication services.
37
+ The `verifyToken` method takes an access token and returns an `AuthContext` if the token is valid, or `undefined` otherwise.
38
+
39
+ ## JWT Implementation
40
+
41
+ For JWT-based authentication, see the [`JwtAuthService`](../clients/remote/src/auth/JwtAuthService.ts) implementation.
42
+ This service verifies JWT tokens using remote JWK sets and integrates with the wallet kernel's network configuration to dynamically resolve the appropriate identity provider for each request.
43
+
44
+ It is important to note that, since the wallet kernel supports multiple identity providers (IDPs), the token issuer (iss) is used as the unique identifier for each IDP.
45
+ This component therefore collaborates with the [Store](../wallet-store/src/Store.ts), which enables lookup of the configured IDP based on the issuer value.
@@ -0,0 +1,13 @@
1
+ export type UserId = string;
2
+ export interface AuthContext {
3
+ userId: UserId;
4
+ accessToken: string;
5
+ }
6
+ export interface AuthService {
7
+ verifyToken(accessToken?: string): Promise<AuthContext | undefined>;
8
+ }
9
+ export interface AuthAware<T> {
10
+ authContext: AuthContext | undefined;
11
+ withAuthContext: (context?: AuthContext) => T;
12
+ }
13
+ //# sourceMappingURL=AuthService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthService.d.ts","sourceRoot":"","sources":["../src/AuthService.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,MAAM,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,WAAW;IACxB,WAAW,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;CACtE;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IACxB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAA;IACpC,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE,WAAW,KAAK,CAAC,CAAA;CAChD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ export type UserId = string;
2
+ export interface AuthContext {
3
+ userId: UserId;
4
+ accessToken: string;
5
+ }
6
+ export interface AuthService {
7
+ verifyToken(accessToken?: string): Promise<AuthContext | undefined>;
8
+ }
9
+ export interface AuthAware<T> {
10
+ authContext: AuthContext | undefined;
11
+ withAuthContext: (context?: AuthContext) => T;
12
+ }
13
+ //# sourceMappingURL=auth-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-service.d.ts","sourceRoot":"","sources":["../src/auth-service.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG,MAAM,CAAA;AAE3B,MAAM,WAAW,WAAW;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,WAAW;IACxB,WAAW,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAA;CACtE;AAED,MAAM,WAAW,SAAS,CAAC,CAAC;IACxB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAA;IACpC,eAAe,EAAE,CAAC,OAAO,CAAC,EAAE,WAAW,KAAK,CAAC,CAAA;CAChD"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { Logger } from '@canton-network/core-types';
2
+ export interface OIDCConfig {
3
+ token_endpoint: string;
4
+ }
5
+ export interface ClientCredentials {
6
+ clientId: string;
7
+ clientSecret: string;
8
+ scope: string | undefined;
9
+ audience: string | undefined;
10
+ }
11
+ export declare class ClientCredentialsService {
12
+ private configUrl;
13
+ private logger;
14
+ constructor(configUrl: string, logger: Logger | undefined);
15
+ /**
16
+ * Fetches the JWT token (M2M) using client credentials.
17
+ *
18
+ * @returns The JWT access token as a string.
19
+ * @throws If fetching the token fails or the response is invalid.
20
+ */
21
+ fetchToken(credentials: ClientCredentials): Promise<string>;
22
+ fetchTokenEndpoint(tokenEndpoint: string, credentials: ClientCredentials): Promise<Response>;
23
+ getOIDCConfig(url: string): Promise<OIDCConfig>;
24
+ }
25
+ export declare const clientCredentialsService: (configUrl: string, logger: Logger | undefined) => {
26
+ fetchToken: (credentials: ClientCredentials) => Promise<string>;
27
+ };
28
+ //# sourceMappingURL=client-credentials-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-credentials-service.d.ts","sourceRoot":"","sources":["../src/client-credentials-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAA;AAEnD,MAAM,WAAW,UAAU;IACvB,cAAc,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;IACzB,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAA;CAC/B;AAED,qBAAa,wBAAwB;IAE7B,OAAO,CAAC,SAAS;IACjB,OAAO,CAAC,MAAM;gBADN,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GAAG,SAAS;IAGtC;;;;;OAKG;IACG,UAAU,CAAC,WAAW,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IA2B3D,kBAAkB,CACpB,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,iBAAiB,GAC/B,OAAO,CAAC,QAAQ,CAAC;IA4Bd,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAcxD;AAED,eAAO,MAAM,wBAAwB,GACjC,WAAW,MAAM,EACjB,QAAQ,MAAM,GAAG,SAAS;8BAEM,iBAAiB;CAEnD,CAAA"}
@@ -0,0 +1,62 @@
1
+ export class ClientCredentialsService {
2
+ configUrl;
3
+ logger;
4
+ constructor(configUrl, logger) {
5
+ this.configUrl = configUrl;
6
+ this.logger = logger;
7
+ }
8
+ /**
9
+ * Fetches the JWT token (M2M) using client credentials.
10
+ *
11
+ * @returns The JWT access token as a string.
12
+ * @throws If fetching the token fails or the response is invalid.
13
+ */
14
+ async fetchToken(credentials) {
15
+ try {
16
+ const oidcConfig = await this.getOIDCConfig(this.configUrl);
17
+ this.logger?.debug({ oidcConfig }, 'Fetched OIDC config');
18
+ const res = await this.fetchTokenEndpoint(oidcConfig.token_endpoint, credentials);
19
+ const json = await res.json();
20
+ this.logger?.info({ response: json }, `Fetched admin token for clientId: ${credentials.clientId}`);
21
+ if (!json.access_token) {
22
+ throw new Error('No access_token in token endpoint response');
23
+ }
24
+ return json.access_token;
25
+ }
26
+ catch (error) {
27
+ this.logger?.error({ err: error }, 'Failed to fetch admin token');
28
+ throw error;
29
+ }
30
+ }
31
+ async fetchTokenEndpoint(tokenEndpoint, credentials) {
32
+ const params = new URLSearchParams({
33
+ grant_type: 'client_credentials',
34
+ client_id: credentials.clientId,
35
+ client_secret: credentials.clientSecret,
36
+ scope: credentials.scope ?? '',
37
+ audience: credentials.audience ?? '',
38
+ });
39
+ const res = await fetch(tokenEndpoint, {
40
+ method: 'POST',
41
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
42
+ body: params.toString(),
43
+ });
44
+ if (!res.ok) {
45
+ this.logger?.error({ status: res.status, statusText: res.statusText }, 'Token endpoint error');
46
+ throw new Error(`Token endpoint error: ${res.status} ${res.statusText}`);
47
+ }
48
+ return res;
49
+ }
50
+ async getOIDCConfig(url) {
51
+ const res = await fetch(url);
52
+ if (!res.ok) {
53
+ const text = await res.text();
54
+ this.logger?.error({ status: res.status, statusText: res.statusText, body: text }, 'Failed to fetch OIDC config');
55
+ throw new Error(`OIDC config error: ${res.status} ${res.statusText}`);
56
+ }
57
+ return res.json();
58
+ }
59
+ }
60
+ export const clientCredentialsService = (configUrl, logger) => ({
61
+ fetchToken: async (credentials) => new ClientCredentialsService(configUrl, logger).fetchToken(credentials),
62
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client-credentials.service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-credentials.service.test.d.ts","sourceRoot":"","sources":["../src/client-credentials.service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { jest } from '@jest/globals';
2
+ import { ClientCredentialsService, } from './client-credentials-service.js';
3
+ describe('ClientCredentialsService', () => {
4
+ const configUrl = 'http://idp/.well-known/openid-configuration';
5
+ const credentials = {
6
+ audience: 'aud',
7
+ scope: 'scope',
8
+ clientId: 'cid',
9
+ clientSecret: 'secret',
10
+ };
11
+ let service;
12
+ let getOIDCConfigSpy;
13
+ let fetchTokenEndpointSpy;
14
+ beforeEach(() => {
15
+ service = new ClientCredentialsService(configUrl, undefined);
16
+ getOIDCConfigSpy = jest.spyOn(service, 'getOIDCConfig');
17
+ fetchTokenEndpointSpy = jest.spyOn(service, 'fetchTokenEndpoint');
18
+ });
19
+ it('returns access_token on success', async () => {
20
+ getOIDCConfigSpy.mockResolvedValue({
21
+ token_endpoint: 'http://idp/token',
22
+ });
23
+ fetchTokenEndpointSpy.mockResolvedValue({
24
+ ok: true,
25
+ json: jest
26
+ .fn()
27
+ .mockResolvedValue({ access_token: 'jwt' }),
28
+ });
29
+ const token = await service.fetchToken(credentials);
30
+ expect(token).toBe('jwt');
31
+ });
32
+ it('throws if OIDC config fetch fails', async () => {
33
+ getOIDCConfigSpy.mockRejectedValue(new Error('config fail'));
34
+ await expect(service.fetchToken(credentials)).rejects.toThrow('config fail');
35
+ });
36
+ it('throws if token endpoint fetch fails', async () => {
37
+ getOIDCConfigSpy.mockResolvedValue({
38
+ token_endpoint: 'http://idp/token',
39
+ });
40
+ fetchTokenEndpointSpy.mockRejectedValue(new Error('token fail'));
41
+ await expect(service.fetchToken(credentials)).rejects.toThrow('token fail');
42
+ });
43
+ it('throws if access_token missing', async () => {
44
+ getOIDCConfigSpy.mockResolvedValue({
45
+ token_endpoint: 'http://idp/token',
46
+ });
47
+ fetchTokenEndpointSpy.mockResolvedValue({
48
+ ok: true,
49
+ json: jest.fn().mockResolvedValue({}),
50
+ });
51
+ await expect(service.fetchToken(credentials)).rejects.toThrow('No access_token in token endpoint response');
52
+ });
53
+ });
@@ -0,0 +1,3 @@
1
+ export * from './auth-service.js';
2
+ export * from './client-credentials-service.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA;AACjC,cAAc,iCAAiC,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './auth-service.js';
2
+ export * from './client-credentials-service.js';
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@canton-network/core-wallet-auth",
3
+ "version": "0.1.0",
4
+ "description": "Provides authentcation middleware and user management for the wallet kernel.",
5
+ "author": "Marc Juchli <marc.juchli@digitalasset.com>",
6
+ "license": "Apache-2.0",
7
+ "packageManager": "yarn@4.9.2",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "type": "module",
11
+ "scripts": {
12
+ "build": "tsc -b",
13
+ "dev": "tsc -b --watch",
14
+ "clean": "tsc -b --clean; rm -rf dist",
15
+ "test": "yarn node --experimental-vm-modules $(yarn bin jest) --passWithNoTests"
16
+ },
17
+ "devDependencies": {
18
+ "@jest/globals": "^29.0.0",
19
+ "@swc/core": "^1.11.31",
20
+ "@swc/jest": "^0.2.38",
21
+ "@types/jest": "^30.0.0",
22
+ "jest": "^30.0.0",
23
+ "ts-jest": "^29.4.0",
24
+ "ts-jest-resolver": "^2.0.1",
25
+ "typescript": "^5.8.3"
26
+ },
27
+ "dependencies": {
28
+ "@canton-network/core-types": "^0.1.0"
29
+ },
30
+ "files": [
31
+ "dist/*"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }