From 3a23cf1bf93f7d88f4d2ac612d4422c14657a59b Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 29 Oct 2025 12:28:32 -0400 Subject: [PATCH 1/8] update config.json Signed-off-by: Alex Matson --- core/wallet-store/src/config/schema.ts | 3 + wallet-gateway/test/config.json | 147 ++++++++++++------------- 2 files changed, 74 insertions(+), 76 deletions(-) diff --git a/core/wallet-store/src/config/schema.ts b/core/wallet-store/src/config/schema.ts index d3092716..38048757 100644 --- a/core/wallet-store/src/config/schema.ts +++ b/core/wallet-store/src/config/schema.ts @@ -4,6 +4,8 @@ import { authSchema } from '@canton-network/core-wallet-auth' import { z } from 'zod' +export const idpSchema = z.object({}) + export const ledgerApiSchema = z.object({ baseUrl: z.string().url(), }) @@ -35,6 +37,7 @@ export const storeConfigSchema = z.object({ database: z.string(), }), ]), + idps: z.array(idpSchema), networks: z.array(networkSchema), }) diff --git a/wallet-gateway/test/config.json b/wallet-gateway/test/config.json index 28fc3f29..66bf8c6b 100644 --- a/wallet-gateway/test/config.json +++ b/wallet-gateway/test/config.json @@ -10,120 +10,115 @@ "type": "sqlite", "database": "store.sqlite" }, - "networks": [ + "idps": [ { - "name": "Local (password IDP)", - "chainId": "canton:local-password", - "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", - "description": "Unimplemented Password Auth", - "ledgerApi": { - "baseUrl": "https://test" - }, - "auth": { - "identityProviderId": "idp1", - "type": "password", - "issuer": "http://127.0.0.1:8889", - "configUrl": "http://127.0.0.1:8889/.well-known/openid-configuration", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "tokenUrl": "tokenUrl", - "grantType": "password", - "scope": "openid", - "clientId": "wk-service-account", - "admin": { - "clientId": "participant_admin", - "clientSecret": "admin-client-secret" - } + "id": "local-mock-oauth", + "type": "oauth", + "issuer": "http://127.0.0.1:8889", + "configUrl": "http://127.0.0.1:8889/.well-known/openid-configuration" + }, + { + "id": "idp-self-signed", + "type": "self_signed", + "issuer": "unsafe-auth", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", + "scope": "openid daml_ledger_api offline_access", + "clientId": "operator", + "clientSecret": "unsafe", + "admin": { + "clientId": "participant_admin", + "clientSecret": "admin-client-secret" } }, { + "id": "idp-devnet-auth0", + "type": "oauth", + "issuer": "https://canton-registry-app-dev-1.eu.auth0.com/", + "configUrl": "https://canton-registry-app-dev-1.eu.auth0.com/.well-known/openid-configuration" + } + ], + "networks": [ + { + "id": "canton:local-oauth", "name": "Local (OAuth IDP)", - "chainId": "canton:local-oauth", - "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", "description": "Mock OAuth IDP", - "ledgerApi": { - "baseUrl": "http://127.0.0.1:5003" - }, + "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", + "identityProviderId": "local-mock-oauth", "auth": { - "identityProviderId": "idp2", - "type": "implicit", - "issuer": "http://127.0.0.1:8889", - "configUrl": "http://127.0.0.1:8889/.well-known/openid-configuration", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "scope": "openid daml_ledger_api offline_access", - "clientId": "operator", + "user": { + "method": "pkce", + "clientId": "operator", + "scope": "openid daml_ledger_api offline_access", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424" + }, "admin": { "clientId": "participant_admin", "clientSecret": "admin-client-secret" } + }, + "ledgerApi": { + "baseUrl": "http://127.0.0.1:5003" } }, { + "id": "canton:local-oauth-client-credentials", "name": "Local (OAuth IDP - Client Credentials)", - "chainId": "canton:local-oauth-client-credentials", - "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", "description": "Mock OAuth IDP (Client Credentials)", - "ledgerApi": { - "baseUrl": "http://127.0.0.1:5003" - }, + "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", + "identityProviderId": "local-mock-oauth", "auth": { - "identityProviderId": "idp3", - "type": "client_credentials", - "issuer": "http://127.0.0.1:8889", - "configUrl": "http://127.0.0.1:8889/.well-known/openid-configuration", - "tokenUrl": "http://127.0.0.1:8889/oauth/token", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "scope": "openid daml_ledger_api offline_access", - "clientId": "operator", - "clientSecret": "your-client-secret", + "user": { + "method": "client_credentials", + "clientId": "operator", + "clientSecret": "your-client-secret", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", + "scope": "openid daml_ledger_api offline_access" + }, "admin": { "clientId": "participant_admin", "clientSecret": "admin-client-secret" } + }, + "ledgerApi": { + "baseUrl": "http://127.0.0.1:5003" } }, { + "id": "canton:local-self-signed", "name": "Local (Self signed)", - "chainId": "canton:local-self-signed", - "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", "description": "Mock OAuth IDP", + "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", + "identityProviderId": "idp-self-signed", + "auth": { + "user": { + "method": "self_signed" + }, + "admin": {} + }, "ledgerApi": { "baseUrl": "http://127.0.0.1:5003" - }, - "auth": { - "identityProviderId": "idp-self-signed", - "type": "self_signed", - "issuer": "unsafe-auth", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "scope": "openid daml_ledger_api offline_access", - "clientId": "operator", - "clientSecret": "unsafe", - "admin": { - "clientId": "participant_admin", - "clientSecret": "admin-client-secret" - } } }, { + "id": "canton:devnet-auth0", "name": "Devnet (Auth0)", - "chainId": "canton:devnet-auth0", - "synchronizerId": "global-domain::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a", "description": "devnet configuration pointing to CNU's lab-operator", - "ledgerApi": { - "baseUrl": "https://lab-operator.utility.cnu.devnet.da-int.net/api/json-api" - }, + "synchronizerId": "global-domain::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a", + "identityProviderId": "idp-devnet-auth0", "auth": { - "identityProviderId": "idp4", - "type": "implicit", - "issuer": "https://canton-registry-app-dev-1.eu.auth0.com/", - "configUrl": "https://canton-registry-app-dev-1.eu.auth0.com/.well-known/openid-configuration", - "tokenUrl": "https://canton-registry-app-dev-1.eu.auth0.com/oauth/token", - "audience": "https://canton.network.global", - "scope": "daml_ledger_api", - "clientId": "EQrKrlT5Z2B3F6TXDepQMGC4YdfnlLLR", + "user": { + "method": "pkce", + "scope": "daml_ledger_api", + "audience": "https://canton.network.global", + "clientId": "EQrKrlT5Z2B3F6TXDepQMGC4YdfnlLLR" + }, "admin": { "clientId": "uHh5IA2hQWc78HHEPDJTmZm6GYhJbfev", "clientSecret": "GET_FROM_AUTH0" } + }, + "ledgerApi": { + "baseUrl": "https://lab-operator.utility.cnu.devnet.da-int.net/api/json-api" } } ] From f20da82e47cf3dc2bb9d36be02090dc92fc9e072 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 29 Oct 2025 14:53:36 -0400 Subject: [PATCH 2/8] remove password auth type Signed-off-by: Alex Matson --- api-specs/openrpc-user-api.json | 8 -- core/rpc-generator/src/components/client.ts | 2 +- core/wallet-auth/src/config/schema.ts | 15 ---- core/wallet-store-inmemory/src/Store.test.ts | 8 +- core/wallet-store-sql/src/schema.ts | 39 ---------- core/wallet-store-sql/src/store-sql.test.ts | 15 ++-- core/wallet-store/src/config/schema.ts | 7 +- .../src/components/NetworkForm.stories.ts | 4 +- .../src/components/NetworkForm.ts | 74 ------------------- .../src/components/NetworkTable.stories.ts | 4 +- core/wallet-user-rpc-client/package.json | 2 +- core/wallet-user-rpc-client/src/index.ts | 4 - core/wallet-user-rpc-client/src/openrpc.json | 8 -- .../remote/src/config/Config.test.ts | 5 +- .../remote/src/user-api/controller.ts | 16 ---- .../remote/src/user-api/rpc-gen/typings.ts | 4 - .../remote/src/web/frontend/networks/index.ts | 17 ----- 17 files changed, 20 insertions(+), 212 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index 6519f7ab..559072be 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -471,14 +471,6 @@ "title": "identityProviderId", "type": "string" }, - "tokenUrl": { - "title": "tokenUrl", - "type": "string" - }, - "grantType": { - "title": "grantType", - "type": "string" - }, "scope": { "title": "scope", "type": "string" diff --git a/core/rpc-generator/src/components/client.ts b/core/rpc-generator/src/components/client.ts index e2c70098..e1c174ca 100644 --- a/core/rpc-generator/src/components/client.ts +++ b/core/rpc-generator/src/components/client.ts @@ -103,7 +103,7 @@ const hooks: IHooks = { versionMap.get(component.name) ?? openrpcDocument.info.version, }) - execSync(`yarn prettier --write ${dest}/**/*`) + execSync(`yarn prettier --write ${dest}/src/**/*`) return await writeFile(packagePath, updatedPkg) } if (component.language === 'rust') { diff --git a/core/wallet-auth/src/config/schema.ts b/core/wallet-auth/src/config/schema.ts index 4c2cf367..5198710f 100644 --- a/core/wallet-auth/src/config/schema.ts +++ b/core/wallet-auth/src/config/schema.ts @@ -8,19 +8,6 @@ const credentials = z.object({ clientSecret: z.string(), }) -const passwordAuthSchema = z.object({ - identityProviderId: z.string(), - type: z.literal('password'), - issuer: z.string(), - configUrl: z.string(), - audience: z.string(), - tokenUrl: z.string(), - grantType: z.string(), - scope: z.string(), - clientId: z.string(), - admin: z.optional(credentials), -}) - const implicitAuthSchema = z.object({ identityProviderId: z.string(), type: z.literal('implicit'), @@ -56,7 +43,6 @@ const selfSignedAuthSchema = z.object({ }) export const authSchema = z.discriminatedUnion('type', [ - passwordAuthSchema, implicitAuthSchema, clientCredentialAuthSchema, selfSignedAuthSchema, @@ -64,7 +50,6 @@ export const authSchema = z.discriminatedUnion('type', [ export type Auth = z.infer export type ImplicitAuth = z.infer -export type PasswordAuth = z.infer export type Credentials = z.infer export type ClientCredentialAuth = z.infer export type SelfSignedAuth = z.infer diff --git a/core/wallet-store-inmemory/src/Store.test.ts b/core/wallet-store-inmemory/src/Store.test.ts index 19646357..4a0213c2 100644 --- a/core/wallet-store-inmemory/src/Store.test.ts +++ b/core/wallet-store-inmemory/src/Store.test.ts @@ -11,7 +11,7 @@ import { LedgerApi, Network, } from '@canton-network/core-wallet-store' -import { AuthContext, PasswordAuth } from '@canton-network/core-wallet-auth' +import { AuthContext, ImplicitAuth } from '@canton-network/core-wallet-auth' import { pino, Logger } from 'pino' import { sink } from 'pino-test' @@ -147,13 +147,11 @@ implementations.forEach(([name, StoreImpl]) => { const ledgerApi: LedgerApi = { baseUrl: 'http://api', } - const auth: PasswordAuth = { + const auth: ImplicitAuth = { identityProviderId: 'idp1', - type: 'password', + type: 'implicit', issuer: 'http://auth', configUrl: 'http://auth/.well-known/openid-configuration', - tokenUrl: 'http://auth', - grantType: 'password', clientId: 'cid', scope: 'scope', audience: 'aud', diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index 5a268607..8a77bdfb 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -20,8 +20,6 @@ interface IdpTable { issuer: string configUrl: string audience: string - tokenUrl: string - grantType: string scope: string clientId: string clientSecret: string @@ -74,22 +72,6 @@ export interface DB { export const toAuth = (table: IdpTable): Auth => { switch (table.type) { - case 'password': - return { - identityProviderId: table.identityProviderId, - type: table.type, - issuer: table.issuer, - configUrl: table.configUrl, - audience: table.audience, - tokenUrl: table.tokenUrl || '', - grantType: table.grantType || '', - scope: table.scope, - clientId: table.clientId, - admin: { - clientId: table.adminClientId, - clientSecret: table.adminClientSecret, - }, - } case 'implicit': return { identityProviderId: table.identityProviderId, @@ -140,21 +122,6 @@ export const toAuth = (table: IdpTable): Auth => { export const fromAuth = (auth: Auth): IdpTable => { switch (auth.type) { - case 'password': - return { - identityProviderId: auth.identityProviderId, - type: auth.type, - issuer: auth.issuer, - configUrl: auth.configUrl, - audience: auth.audience, - tokenUrl: auth.tokenUrl, - grantType: auth.grantType, - scope: auth.scope, - clientId: auth.clientId, - clientSecret: '', - adminClientId: auth.admin?.clientId || '', - adminClientSecret: auth.admin?.clientSecret || '', - } case 'implicit': return { identityProviderId: auth.identityProviderId, @@ -162,8 +129,6 @@ export const fromAuth = (auth: Auth): IdpTable => { issuer: auth.issuer, configUrl: auth.configUrl, audience: auth.audience, - tokenUrl: '', - grantType: '', scope: auth.scope, clientId: auth.clientId, clientSecret: '', @@ -177,8 +142,6 @@ export const fromAuth = (auth: Auth): IdpTable => { issuer: auth.issuer, configUrl: auth.configUrl, audience: auth.audience, - tokenUrl: '', - grantType: '', scope: auth.scope, clientId: auth.clientId, clientSecret: auth.clientSecret, @@ -192,8 +155,6 @@ export const fromAuth = (auth: Auth): IdpTable => { issuer: auth.issuer, configUrl: '', audience: auth.audience, - tokenUrl: '', - grantType: '', scope: auth.scope, clientId: auth.clientId, clientSecret: auth.clientSecret, diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 66c3205a..4bed8518 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -3,7 +3,7 @@ import { describe, expect, test } from '@jest/globals' -import { AuthContext, PasswordAuth } from '@canton-network/core-wallet-auth' +import { AuthContext, ImplicitAuth } from '@canton-network/core-wallet-auth' import { LedgerApi, Network, @@ -26,6 +26,7 @@ const storeConfig = { connection: { type: 'memory' as const, }, + idps: [], networks: [], } @@ -40,13 +41,11 @@ const implementations: Array<[string, StoreCtor]> = [['StoreSql', StoreSql]] const ledgerApi: LedgerApi = { baseUrl: 'http://api', } -const auth: PasswordAuth = { +const auth: ImplicitAuth = { identityProviderId: 'idp1', - type: 'password', + type: 'implicit', issuer: 'http://auth', configUrl: 'http://auth/.well-known/openid-configuration', - tokenUrl: 'http://auth', - grantType: 'password', clientId: 'cid', scope: 'scope', audience: 'aud', @@ -92,13 +91,11 @@ implementations.forEach(([name, StoreImpl]) => { }) test('should filter wallets', async () => { - const auth2: PasswordAuth = { + const auth2: ImplicitAuth = { identityProviderId: 'idp2', - type: 'password', + type: 'implicit', issuer: 'http://auth', configUrl: 'http://auth/.well-known/openid-configuration', - tokenUrl: 'http://auth', - grantType: 'password', clientId: 'cid', scope: 'scope', audience: 'aud', diff --git a/core/wallet-store/src/config/schema.ts b/core/wallet-store/src/config/schema.ts index 38048757..4416d967 100644 --- a/core/wallet-store/src/config/schema.ts +++ b/core/wallet-store/src/config/schema.ts @@ -4,7 +4,12 @@ import { authSchema } from '@canton-network/core-wallet-auth' import { z } from 'zod' -export const idpSchema = z.object({}) +export const idpSchema = z.discriminatedUnion('type', [ + z.object({ + id: z.string(), + type: z.enum(['oauth', 'self-signed']), + }), +]) export const ledgerApiSchema = z.object({ baseUrl: z.string().url(), diff --git a/core/wallet-ui-components/src/components/NetworkForm.stories.ts b/core/wallet-ui-components/src/components/NetworkForm.stories.ts index e18bb9a9..08be77bb 100644 --- a/core/wallet-ui-components/src/components/NetworkForm.stories.ts +++ b/core/wallet-ui-components/src/components/NetworkForm.stories.ts @@ -69,13 +69,11 @@ const sampleNetworkPassword: Network = { }, auth: { identityProviderId: 'idp1', - type: 'password', + type: 'implicit', issuer: 'http://127.0.0.1:8889', configUrl: 'http://127.0.0.1:8889/.well-known/openid-configuration', audience: 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', - tokenUrl: 'tokenUrl', - grantType: 'password', scope: 'openid', clientId: 'wk-service-account', admin: { diff --git a/core/wallet-ui-components/src/components/NetworkForm.ts b/core/wallet-ui-components/src/components/NetworkForm.ts index 4f9ccd04..f517b957 100644 --- a/core/wallet-ui-components/src/components/NetworkForm.ts +++ b/core/wallet-ui-components/src/components/NetworkForm.ts @@ -9,7 +9,6 @@ import { NetworkInputChangedEvent } from './NetworkFormInput.js' import { Credentials, ImplicitAuth, - PasswordAuth, SelfSignedAuth, } from '@canton-network/core-wallet-auth' @@ -39,7 +38,6 @@ type LedgerApiKeys = keyof Network['ledgerApi'] type CommonAuth = Exclude type AdminAuth = keyof Credentials -type PasswordAuthKeys = Exclude @customElement('network-form') export class NetworkForm extends BaseElement { @@ -82,20 +80,6 @@ export class NetworkForm extends BaseElement { scope: auth.scope || '', admin: auth.admin, } - } else if (this.authType === 'password') { - const auth = network.auth as PasswordAuth - network.auth = { - type: 'password', - identityProviderId: auth.identityProviderId || '', - configUrl: auth.configUrl || '', - clientId: auth.clientId || '', - issuer: auth.issuer || '', - audience: auth.audience || '', - scope: auth.scope || '', - tokenUrl: auth.tokenUrl || '', - grantType: auth.grantType || '', - admin: network.auth?.admin, - } } else if (this.authType === 'self_signed') { const auth = network.auth as SelfSignedAuth network.auth = { @@ -168,29 +152,6 @@ export class NetworkForm extends BaseElement { } } - setPasswordAuth(field: PasswordAuthKeys) { - return (ev: NetworkInputChangedEvent) => { - if (this.network.auth.type !== 'password') { - return - } - - if (!this.network.auth) { - this.network.auth = { - type: 'password', - clientId: '', - identityProviderId: '', - issuer: '', - configUrl: '', - audience: '', - tokenUrl: '', - grantType: '', - scope: '', - } - } - this.network.auth[field] = ev.value - } - } - renderAuthForm() { const commonFields = html` - - ${adminFields} - ` } else if (this.authType === 'self_signed') { let auth = this.network.auth if (auth.type !== 'self_signed') { diff --git a/core/wallet-ui-components/src/components/NetworkTable.stories.ts b/core/wallet-ui-components/src/components/NetworkTable.stories.ts index 8d5ca7b1..1265d635 100644 --- a/core/wallet-ui-components/src/components/NetworkTable.stories.ts +++ b/core/wallet-ui-components/src/components/NetworkTable.stories.ts @@ -25,13 +25,11 @@ const networks: Network[] = [ }, auth: { identityProviderId: 'idp1', - type: 'password', + type: 'implicit', issuer: 'http://127.0.0.1:8889', configUrl: 'http://127.0.0.1:8889/.well-known/openid-configuration', audience: 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', - tokenUrl: 'tokenUrl', - grantType: 'password', scope: 'openid', clientId: 'wk-service-account', admin: { diff --git a/core/wallet-user-rpc-client/package.json b/core/wallet-user-rpc-client/package.json index 9837a513..876b8f43 100644 --- a/core/wallet-user-rpc-client/package.json +++ b/core/wallet-user-rpc-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-user-rpc-client", - "version": "0.11.0", + "version": "0.0.0", "type": "module", "description": "TypeScript client generated by OpenRPC", "repository": "github:hyperledger-labs/splice-wallet-kernel", diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index 7c6771f1..706416f2 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -29,8 +29,6 @@ export type SynchronizerId = string export type ChainId = string export type Type = string export type IdentityProviderId = string -export type TokenUrl = string -export type GrantType = string export type Scope = string export type ClientId = string export type ClientSecret = string @@ -50,8 +48,6 @@ export interface Admin { export interface Auth { authType?: Type identityProviderId: IdentityProviderId - tokenUrl?: TokenUrl - grantType?: GrantType scope?: Scope clientId?: ClientId clientSecret?: ClientSecret diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 6519f7ab..559072be 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -471,14 +471,6 @@ "title": "identityProviderId", "type": "string" }, - "tokenUrl": { - "title": "tokenUrl", - "type": "string" - }, - "grantType": { - "title": "grantType", - "type": "string" - }, "scope": { "title": "scope", "type": "string" diff --git a/wallet-gateway/remote/src/config/Config.test.ts b/wallet-gateway/remote/src/config/Config.test.ts index e777b382..d398f285 100644 --- a/wallet-gateway/remote/src/config/Config.test.ts +++ b/wallet-gateway/remote/src/config/Config.test.ts @@ -10,10 +10,7 @@ test('config from json file', async () => { expect(resp.store.networks[0].ledgerApi.baseUrl).toBe('https://test') expect(resp.store.networks[0].auth.clientId).toBe('wk-service-account') expect(resp.store.networks[0].auth.scope).toBe('openid') - expect(resp.store.networks[0].auth.type).toBe('password') - if (resp.store.networks[0].auth.type === 'password') { - expect(resp.store.networks[0].auth.tokenUrl).toBe('tokenUrl') - } + expect(resp.store.networks[0].auth.type).toBe('implicit') expect(resp.store.networks[1].auth.type).toBe('implicit') if (resp.store.networks[1].auth.type === 'implicit') { expect(resp.store.networks[1].auth.audience).toBe( diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 466b7a9e..eac813f1 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -74,22 +74,6 @@ export const userController = ( clientSecret: network.auth.admin?.clientSecret ?? '', }, } - } else if (network.auth.type === 'password') { - auth = { - type: 'password', - identityProviderId: network.auth.identityProviderId, - issuer: network.auth.issuer ?? '', - configUrl: network.auth.configUrl ?? '', - tokenUrl: network.auth.tokenUrl ?? '', - grantType: network.auth.grantType ?? '', - scope: network.auth.scope ?? '', - clientId: network.auth.clientId ?? '', - audience: network.auth.audience ?? '', - admin: { - clientId: network.auth.admin?.clientId ?? '', - clientSecret: network.auth.admin?.clientSecret ?? '', - }, - } } else if (network.auth.type === 'client_credentials') { auth = { type: 'client_credentials', diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts index 03bd7a8c..48c29e5e 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts @@ -29,8 +29,6 @@ export type SynchronizerId = string export type ChainId = string export type Type = string export type IdentityProviderId = string -export type TokenUrl = string -export type GrantType = string export type Scope = string export type ClientId = string export type ClientSecret = string @@ -50,8 +48,6 @@ export interface Admin { export interface Auth { authType?: Type identityProviderId: IdentityProviderId - tokenUrl?: TokenUrl - grantType?: GrantType scope?: Scope clientId?: ClientId clientSecret?: ClientSecret diff --git a/wallet-gateway/remote/src/web/frontend/networks/index.ts b/wallet-gateway/remote/src/web/frontend/networks/index.ts index b040a8ff..62d953cf 100644 --- a/wallet-gateway/remote/src/web/frontend/networks/index.ts +++ b/wallet-gateway/remote/src/web/frontend/networks/index.ts @@ -253,23 +253,6 @@ export class UserUiNetworks extends LitElement { }, } break - case 'password': - auth = { - type: 'password', - identityProviderId: e.network.auth.identityProviderId, - issuer: e.network.auth.issuer ?? '', - configUrl: e.network.auth.configUrl ?? '', - audience: e.network.auth.audience ?? '', - tokenUrl: e.network.auth.tokenUrl ?? '', - grantType: e.network.auth.grantType ?? '', - scope: e.network.auth.scope ?? '', - clientId: e.network.auth.clientId ?? '', - admin: { - clientId: e.network.auth.admin?.clientId ?? '', - clientSecret: e.network.auth.admin?.clientSecret ?? '', - }, - } - break case 'self_signed': auth = { type: 'self_signed', From 08e215652323989e5de476a6443a37e52e3ad97f Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 29 Oct 2025 15:14:00 -0400 Subject: [PATCH 3/8] start renaming chainId Signed-off-by: Alex Matson --- api-specs/openrpc-dapp-api.json | 8 +-- api-specs/openrpc-dapp-remote-api.json | 16 ++--- core/wallet-auth/src/config/schema.ts | 36 ++++------- core/wallet-store/src/config/schema.ts | 15 ++++- wallet-gateway/test/config.json | 83 ++++++++++++-------------- 5 files changed, 73 insertions(+), 85 deletions(-) diff --git a/api-specs/openrpc-dapp-api.json b/api-specs/openrpc-dapp-api.json index f657d195..fd5db8ff 100644 --- a/api-specs/openrpc-dapp-api.json +++ b/api-specs/openrpc-dapp-api.json @@ -38,7 +38,7 @@ "required": ["status", "sessionToken"] } }, - "description": "Ensures ledger connectivity and returns the connected network information along with the user access token. ChainId should follow CAIP-2 and represent the synchronizerId." + "description": "Ensures ledger connectivity and returns the connected network information along with the user access token." }, { "name": "disconnect", @@ -449,10 +449,10 @@ "type": "boolean", "description": "Whether or not a connection to a network is established." }, - "chainId": { - "title": "chainId", + "networkId": { + "title": "networkId", "type": "string", - "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." + "description": "A CAIP-2 compliant network ID, e.g. 'canton:da-mainnet'." } }, "required": ["kernel", "isConnected"] diff --git a/api-specs/openrpc-dapp-remote-api.json b/api-specs/openrpc-dapp-remote-api.json index 54a0c278..259bc12a 100644 --- a/api-specs/openrpc-dapp-remote-api.json +++ b/api-specs/openrpc-dapp-remote-api.json @@ -38,7 +38,7 @@ "required": ["status", "sessionToken"] } }, - "description": "Ensures ledger connectivity and returns the connected network information along with the user access token. ChainId should follow CAIP-2 and represent the synchronizerId." + "description": "Ensures ledger connectivity and returns the connected network information along with the user access token. NetworkId should follow CAIP-2 and represent the synchronizerId." }, { "name": "disconnect", @@ -184,10 +184,10 @@ "kernel": { "$ref": "#/components/schemas/KernelInfo" }, - "chainId": { - "title": "chainId", + "networkId": { + "title": "networkId", "type": "string", - "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." + "description": "A CAIP-2 compliant network ID, e.g. 'canton:da-mainnet'." }, "sessionToken": { "title": "sessionToken", @@ -195,7 +195,7 @@ "description": "JWT authentication token (if applicable)." } }, - "required": ["kernel", "chainId"] + "required": ["kernel", "networkId"] } }, "description": "Informs when the user connects to a network." @@ -483,10 +483,10 @@ "type": "boolean", "description": "Whether or not a connection to a network is established." }, - "chainId": { - "title": "chainId", + "networkId": { + "title": "networkId", "type": "string", - "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." + "description": "A CAIP-2 compliant network ID, e.g. 'canton:da-mainnet'." } }, "required": ["kernel", "isConnected"] diff --git a/core/wallet-auth/src/config/schema.ts b/core/wallet-auth/src/config/schema.ts index 5198710f..65c3a090 100644 --- a/core/wallet-auth/src/config/schema.ts +++ b/core/wallet-auth/src/config/schema.ts @@ -3,53 +3,37 @@ import { z } from 'zod' -const credentials = z.object({ - clientId: z.string(), - clientSecret: z.string(), -}) - -const implicitAuthSchema = z.object({ - identityProviderId: z.string(), - type: z.literal('implicit'), - issuer: z.string(), - configUrl: z.string(), +const pkceAuthSchema = z.object({ + method: z.literal('pkce'), audience: z.string(), scope: z.string(), clientId: z.string(), - admin: z.optional(credentials), }) -const clientCredentialAuthSchema = z.object({ - identityProviderId: z.string(), - type: z.literal('client_credentials'), - issuer: z.string(), - configUrl: z.string(), +const clientCredentialsAuthSchema = z.object({ + method: z.literal('client_credentials'), audience: z.string(), scope: z.string(), clientId: z.string(), clientSecret: z.string(), - admin: z.optional(credentials), }) const selfSignedAuthSchema = z.object({ - identityProviderId: z.string(), - type: z.literal('self_signed'), + method: z.literal('self_signed'), issuer: z.string(), audience: z.string(), scope: z.string(), clientId: z.string(), clientSecret: z.string(), - admin: z.optional(credentials), }) -export const authSchema = z.discriminatedUnion('type', [ - implicitAuthSchema, - clientCredentialAuthSchema, +export const authSchema = z.discriminatedUnion('method', [ + pkceAuthSchema, + clientCredentialsAuthSchema, selfSignedAuthSchema, ]) export type Auth = z.infer -export type ImplicitAuth = z.infer -export type Credentials = z.infer -export type ClientCredentialAuth = z.infer +export type PkceAuth = z.infer +export type ClientCredentialsAuth = z.infer export type SelfSignedAuth = z.infer diff --git a/core/wallet-store/src/config/schema.ts b/core/wallet-store/src/config/schema.ts index 4416d967..f2e69a02 100644 --- a/core/wallet-store/src/config/schema.ts +++ b/core/wallet-store/src/config/schema.ts @@ -7,7 +7,14 @@ import { z } from 'zod' export const idpSchema = z.discriminatedUnion('type', [ z.object({ id: z.string(), - type: z.enum(['oauth', 'self-signed']), + type: z.literal('self_signed'), + issuer: z.string(), + }), + z.object({ + id: z.string(), + type: z.literal('oauth'), + issuer: z.string(), + configUrl: z.string().url(), }), ]) @@ -16,12 +23,14 @@ export const ledgerApiSchema = z.object({ }) export const networkSchema = z.object({ + id: z.string(), name: z.string(), - chainId: z.string(), - synchronizerId: z.string(), description: z.string(), + synchronizerId: z.string(), + identityProviderId: z.string(), ledgerApi: ledgerApiSchema, auth: authSchema, + adminAuth: authSchema.optional(), }) export const storeConfigSchema = z.object({ diff --git a/wallet-gateway/test/config.json b/wallet-gateway/test/config.json index 66bf8c6b..0db418bc 100644 --- a/wallet-gateway/test/config.json +++ b/wallet-gateway/test/config.json @@ -20,15 +20,7 @@ { "id": "idp-self-signed", "type": "self_signed", - "issuer": "unsafe-auth", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "scope": "openid daml_ledger_api offline_access", - "clientId": "operator", - "clientSecret": "unsafe", - "admin": { - "clientId": "participant_admin", - "clientSecret": "admin-client-secret" - } + "issuer": "unsafe-auth" }, { "id": "idp-devnet-auth0", @@ -45,16 +37,15 @@ "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", "identityProviderId": "local-mock-oauth", "auth": { - "user": { - "method": "pkce", - "clientId": "operator", - "scope": "openid daml_ledger_api offline_access", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424" - }, - "admin": { - "clientId": "participant_admin", - "clientSecret": "admin-client-secret" - } + "method": "pkce", + "clientId": "operator", + "scope": "openid daml_ledger_api offline_access", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424" + }, + "adminAuth": { + "type": "client_credentials", + "clientId": "participant_admin", + "clientSecret": "admin-client-secret" }, "ledgerApi": { "baseUrl": "http://127.0.0.1:5003" @@ -67,17 +58,16 @@ "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", "identityProviderId": "local-mock-oauth", "auth": { - "user": { - "method": "client_credentials", - "clientId": "operator", - "clientSecret": "your-client-secret", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "scope": "openid daml_ledger_api offline_access" - }, - "admin": { - "clientId": "participant_admin", - "clientSecret": "admin-client-secret" - } + "method": "client_credentials", + "clientId": "operator", + "clientSecret": "your-client-secret", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", + "scope": "openid daml_ledger_api offline_access" + }, + "adminAuth": { + "method": "client_credentials", + "clientId": "participant_admin", + "clientSecret": "admin-client-secret" }, "ledgerApi": { "baseUrl": "http://127.0.0.1:5003" @@ -90,10 +80,16 @@ "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", "identityProviderId": "idp-self-signed", "auth": { - "user": { - "method": "self_signed" - }, - "admin": {} + "method": "self_signed", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", + "scope": "openid daml_ledger_api offline_access", + "clientId": "operator", + "clientSecret": "unsafe" + }, + "adminAuth": { + "method": "self_signed", + "clientId": "participant_admin", + "clientSecret": "admin-client-secret" }, "ledgerApi": { "baseUrl": "http://127.0.0.1:5003" @@ -106,16 +102,15 @@ "synchronizerId": "global-domain::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a", "identityProviderId": "idp-devnet-auth0", "auth": { - "user": { - "method": "pkce", - "scope": "daml_ledger_api", - "audience": "https://canton.network.global", - "clientId": "EQrKrlT5Z2B3F6TXDepQMGC4YdfnlLLR" - }, - "admin": { - "clientId": "uHh5IA2hQWc78HHEPDJTmZm6GYhJbfev", - "clientSecret": "GET_FROM_AUTH0" - } + "method": "pkce", + "scope": "daml_ledger_api", + "audience": "https://canton.network.global", + "clientId": "EQrKrlT5Z2B3F6TXDepQMGC4YdfnlLLR" + }, + "adminAuth": { + "method": "client_credentials", + "clientId": "uHh5IA2hQWc78HHEPDJTmZm6GYhJbfev", + "clientSecret": "GET_FROM_AUTH0" }, "ledgerApi": { "baseUrl": "https://lab-operator.utility.cnu.devnet.da-int.net/api/json-api" From 821a4f1292d8ba6848f28d2a3696c51c22132e88 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Thu, 30 Oct 2025 10:25:15 -0400 Subject: [PATCH 4/8] changes Signed-off-by: Alex Matson --- core/wallet-auth/src/config/schema.ts | 8 +++---- .../src/migrations/001-init.ts | 2 -- wallet-gateway/test/config.json | 22 +++++++++---------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/core/wallet-auth/src/config/schema.ts b/core/wallet-auth/src/config/schema.ts index 65c3a090..f942c9ca 100644 --- a/core/wallet-auth/src/config/schema.ts +++ b/core/wallet-auth/src/config/schema.ts @@ -3,8 +3,8 @@ import { z } from 'zod' -const pkceAuthSchema = z.object({ - method: z.literal('pkce'), +const authorizationCodeAuthSchema = z.object({ + method: z.literal('authorization_code'), audience: z.string(), scope: z.string(), clientId: z.string(), @@ -28,12 +28,12 @@ const selfSignedAuthSchema = z.object({ }) export const authSchema = z.discriminatedUnion('method', [ - pkceAuthSchema, + authorizationCodeAuthSchema, clientCredentialsAuthSchema, selfSignedAuthSchema, ]) export type Auth = z.infer -export type PkceAuth = z.infer +export type AuthorizationCodeAuth = z.infer export type ClientCredentialsAuth = z.infer export type SelfSignedAuth = z.infer diff --git a/core/wallet-store-sql/src/migrations/001-init.ts b/core/wallet-store-sql/src/migrations/001-init.ts index 23559d55..fb9ddeaa 100644 --- a/core/wallet-store-sql/src/migrations/001-init.ts +++ b/core/wallet-store-sql/src/migrations/001-init.ts @@ -14,8 +14,6 @@ export async function up(db: Kysely): Promise { .addColumn('issuer', 'text', (col) => col.notNull()) .addColumn('config_url', 'text', (col) => col.notNull()) .addColumn('audience', 'text', (col) => col.notNull()) - .addColumn('token_url', 'text') - .addColumn('grant_type', 'text') .addColumn('scope', 'text', (col) => col.notNull()) .addColumn('client_id', 'text', (col) => col.notNull()) .addColumn('client_secret', 'text') diff --git a/wallet-gateway/test/config.json b/wallet-gateway/test/config.json index 0db418bc..85fa0879 100644 --- a/wallet-gateway/test/config.json +++ b/wallet-gateway/test/config.json @@ -12,21 +12,21 @@ }, "idps": [ { - "id": "local-mock-oauth", + "id": "idp-mock-oauth", "type": "oauth", "issuer": "http://127.0.0.1:8889", "configUrl": "http://127.0.0.1:8889/.well-known/openid-configuration" }, - { - "id": "idp-self-signed", - "type": "self_signed", - "issuer": "unsafe-auth" - }, { "id": "idp-devnet-auth0", "type": "oauth", "issuer": "https://canton-registry-app-dev-1.eu.auth0.com/", "configUrl": "https://canton-registry-app-dev-1.eu.auth0.com/.well-known/openid-configuration" + }, + { + "id": "idp-self-signed", + "type": "self_signed", + "issuer": "unsafe-auth" } ], "networks": [ @@ -35,15 +35,15 @@ "name": "Local (OAuth IDP)", "description": "Mock OAuth IDP", "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", - "identityProviderId": "local-mock-oauth", + "identityProviderId": "idp-mock-oauth", "auth": { - "method": "pkce", + "method": "authorization_code", "clientId": "operator", "scope": "openid daml_ledger_api offline_access", "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424" }, "adminAuth": { - "type": "client_credentials", + "method": "client_credentials", "clientId": "participant_admin", "clientSecret": "admin-client-secret" }, @@ -56,7 +56,7 @@ "name": "Local (OAuth IDP - Client Credentials)", "description": "Mock OAuth IDP (Client Credentials)", "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", - "identityProviderId": "local-mock-oauth", + "identityProviderId": "idp-mock-oauth", "auth": { "method": "client_credentials", "clientId": "operator", @@ -102,7 +102,7 @@ "synchronizerId": "global-domain::1220be58c29e65de40bf273be1dc2b266d43a9a002ea5b18955aeef7aac881bb471a", "identityProviderId": "idp-devnet-auth0", "auth": { - "method": "pkce", + "method": "authorization_code", "scope": "daml_ledger_api", "audience": "https://canton.network.global", "clientId": "EQrKrlT5Z2B3F6TXDepQMGC4YdfnlLLR" From b8a8d26e2c6ec7ab71ce936ea5f77410959b1dfc Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 5 Nov 2025 10:13:29 -0500 Subject: [PATCH 5/8] compiles Signed-off-by: Alex Matson --- api-specs/openrpc-user-api.json | 45 +- .../src/auth-token-provider-self-signed.ts | 13 +- core/wallet-auth/src/auth-token-provider.ts | 76 ++-- core/wallet-auth/src/config/schema.ts | 16 + core/wallet-dapp-rpc-client/src/openrpc.json | 2 +- core/wallet-store-inmemory/src/Store.test.ts | 22 +- .../src/StoreInternal.ts | 46 +++ .../src/migrations/001-init.ts | 12 +- core/wallet-store-sql/src/schema.ts | 129 ++---- core/wallet-store-sql/src/store-sql.test.ts | 21 +- core/wallet-store-sql/src/store-sql.ts | 106 +++-- core/wallet-store/src/Store.ts | 8 + core/wallet-store/src/config/schema.ts | 16 +- .../src/components/NetworkCard.ts | 2 +- .../src/components/NetworkForm.stories.ts | 58 +-- .../src/components/NetworkForm.ts | 386 +++++++++--------- .../src/components/NetworkTable.stories.ts | 18 +- core/wallet-user-rpc-client/src/index.ts | 22 +- core/wallet-user-rpc-client/src/openrpc.json | 45 +- .../remote/src/auth/jwt-auth-service.ts | 10 +- .../src/auth/jwt-unsafe-auth-service.ts | 9 +- .../remote/src/config/Config.test.ts | 6 +- .../remote/src/config/ConfigUtils.ts | 91 ++++- .../ledger/party-allocation-service.test.ts | 14 +- .../remote/src/user-api/controller.ts | 64 +-- .../remote/src/user-api/rpc-gen/typings.ts | 22 +- .../remote/src/web/frontend/networks/index.ts | 76 ++-- 27 files changed, 707 insertions(+), 628 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index 37644178..3747dbbd 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -439,9 +439,17 @@ "type": "string", "description": "Synchronizer ID" }, + "identityProviderId": { + "title": "identityProviderId", + "type": "string", + "description": "Identity Provider ID" + }, "auth": { "$ref": "#/components/schemas/Auth" }, + "adminAuth": { + "$ref": "#/components/schemas/Auth" + }, "ledgerApi": { "title": "ledgerApi", "type": "string", @@ -453,6 +461,7 @@ "name", "description", "synchronizerId", + "identityProviderId", "auth", "ledgerApi" ], @@ -461,14 +470,10 @@ "Auth": { "title": "auth", "type": "object", - "description": "Represents the type of auth (implicit or password) for a specified network", + "description": "Represents the type of auth for a specified network", "properties": { - "authType": { - "title": "type", - "type": "string" - }, - "identityProviderId": { - "title": "identityProviderId", + "method": { + "title": "method", "type": "string" }, "scope": { @@ -487,36 +492,12 @@ "title": "issuer", "type": "string" }, - "configUrl": { - "title": "configUrl", - "type": "string" - }, "audience": { "title": "audience", "type": "string" - }, - "admin": { - "title": "admin", - "type": "object", - "properties": { - "clientId": { - "title": "clientId", - "type": "string" - }, - "clientSecret": { - "title": "clientSecret", - "type": "string" - } - }, - "required": ["clientId", "clientSecret"] } }, - "required": [ - "type", - "identityProviderId", - "issuer", - "configUrl" - ] + "required": ["method", "issuer"] }, "Wallet": { "title": "Wallet", diff --git a/core/wallet-auth/src/auth-token-provider-self-signed.ts b/core/wallet-auth/src/auth-token-provider-self-signed.ts index 3e416449..2c0a804d 100644 --- a/core/wallet-auth/src/auth-token-provider-self-signed.ts +++ b/core/wallet-auth/src/auth-token-provider-self-signed.ts @@ -9,6 +9,7 @@ import { SignJWT } from 'jose' export class AuthTokenProviderSelfSigned implements AccessTokenProvider { constructor( private auth: SelfSignedAuth, + private authAdmin: SelfSignedAuth, private logger: Logger, private expirySeconds: number = 3600 ) {} @@ -30,18 +31,18 @@ export class AuthTokenProviderSelfSigned implements AccessTokenProvider { async getAdminAccessToken(): Promise { this.logger.debug('Fetching self-signed admin auth token') - if (!this.auth.admin) { + if (!this.authAdmin) { throw new Error('Admin credentials are not configured') } return AuthTokenProviderSelfSigned.fetchToken( this.logger, { - clientId: this.auth.admin.clientId, - clientSecret: this.auth.admin.clientSecret, - scope: this.auth.scope, - audience: this.auth.audience, + clientId: this.authAdmin.clientId, + clientSecret: this.authAdmin.clientSecret, + scope: this.authAdmin.scope, + audience: this.authAdmin.audience, }, - this.auth.issuer, + this.authAdmin.issuer, this.expirySeconds ) } diff --git a/core/wallet-auth/src/auth-token-provider.ts b/core/wallet-auth/src/auth-token-provider.ts index 88ecd254..84410228 100644 --- a/core/wallet-auth/src/auth-token-provider.ts +++ b/core/wallet-auth/src/auth-token-provider.ts @@ -3,61 +3,85 @@ import { Logger } from '@canton-network/core-types' import { AccessTokenProvider } from './auth-service.js' -import { Auth } from './config/schema.js' +import { Auth, Idp, SelfSignedAuth } from './config/schema.js' import { AuthTokenProviderSelfSigned } from './auth-token-provider-self-signed.js' import { clientCredentialsService } from './client-credentials-service.js' export class AuthTokenProvider implements AccessTokenProvider { constructor( + private idp: Idp, private auth: Auth, + private adminAuth: Auth, private logger: Logger ) {} async getUserAccessToken(): Promise { this.logger.debug('Fetching user auth token') - if (this.auth.type === 'self_signed') + if (this.auth.method === 'self_signed') return new AuthTokenProviderSelfSigned( this.auth, + this.adminAuth as SelfSignedAuth, this.logger ).getUserAccessToken() - if (this.auth.type === 'client_credentials') - return clientCredentialsService( - this.auth.configUrl, - this.logger - ).fetchToken({ - clientId: this.auth.clientId, - clientSecret: this.auth.clientSecret, - scope: this.auth.scope, - audience: this.auth.audience, - }) + if (this.auth.method === 'client_credentials') { + if (this.idp.type === 'oauth') + return clientCredentialsService( + this.idp.configUrl, + this.logger + ).fetchToken({ + clientId: this.auth.clientId, + clientSecret: this.auth.clientSecret, + scope: this.auth.scope, + audience: this.auth.audience, + }) + else { + throw new Error( + `IDP type ${this.idp.type} not supported for client_credentials auth` + ) + } + } throw new Error( - `Auth type ${this.auth.type} not supported for user access token` + `Auth method ${this.auth.method} not supported for user access token` ) } async getAdminAccessToken(): Promise { this.logger.debug('Fetching admin auth token') - if (this.auth.type === 'self_signed') + if (this.adminAuth.method === 'self_signed') return new AuthTokenProviderSelfSigned( - this.auth, + this.auth as SelfSignedAuth, + this.adminAuth as SelfSignedAuth, this.logger ).getAdminAccessToken() - if (!this.auth.admin) { + if (!this.adminAuth) { + throw new Error( + `No admin credentials configured for auth type ${this.auth.method}` + ) + } + + if (this.adminAuth.method === 'client_credentials') { + if (this.idp.type === 'oauth') + return clientCredentialsService( + this.idp.configUrl, + this.logger + ).fetchToken({ + clientId: this.adminAuth.clientId, + clientSecret: this.adminAuth.clientSecret, + scope: this.adminAuth.scope, + audience: this.adminAuth.audience, + }) + else { + throw new Error( + `IDP type ${this.idp.type} not supported for client_credentials auth` + ) + } + } else { throw new Error( - `No admin credentials configured for auth type ${this.auth.type}` + `Auth method ${this.auth.method} not supported for admin access token` ) } - return clientCredentialsService( - this.auth.configUrl, - this.logger - ).fetchToken({ - clientId: this.auth.admin.clientId, - clientSecret: this.auth.admin.clientSecret, - scope: this.auth.scope, - audience: this.auth.audience, - }) } } diff --git a/core/wallet-auth/src/config/schema.ts b/core/wallet-auth/src/config/schema.ts index f942c9ca..411b1b5c 100644 --- a/core/wallet-auth/src/config/schema.ts +++ b/core/wallet-auth/src/config/schema.ts @@ -37,3 +37,19 @@ export type Auth = z.infer export type AuthorizationCodeAuth = z.infer export type ClientCredentialsAuth = z.infer export type SelfSignedAuth = z.infer + +export const idpSchema = z.discriminatedUnion('type', [ + z.object({ + id: z.string(), + type: z.literal('self_signed'), + issuer: z.string(), + }), + z.object({ + id: z.string(), + type: z.literal('oauth'), + issuer: z.string(), + configUrl: z.string().url(), + }), +]) + +export type Idp = z.infer diff --git a/core/wallet-dapp-rpc-client/src/openrpc.json b/core/wallet-dapp-rpc-client/src/openrpc.json index b194a202..8126a999 100644 --- a/core/wallet-dapp-rpc-client/src/openrpc.json +++ b/core/wallet-dapp-rpc-client/src/openrpc.json @@ -452,7 +452,7 @@ "networkId": { "title": "networkId", "type": "string", - "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." + "description": "A CAIP-2 compliant network ID, e.g. 'canton:da-mainnet'." } }, "required": ["kernel", "isConnected"] diff --git a/core/wallet-store-inmemory/src/Store.test.ts b/core/wallet-store-inmemory/src/Store.test.ts index 3adb88a4..4238a4e6 100644 --- a/core/wallet-store-inmemory/src/Store.test.ts +++ b/core/wallet-store-inmemory/src/Store.test.ts @@ -11,7 +11,11 @@ import { LedgerApi, Network, } from '@canton-network/core-wallet-store' -import { AuthContext, ImplicitAuth } from '@canton-network/core-wallet-auth' +import { + AuthContext, + AuthorizationCodeAuth, + Idp, +} from '@canton-network/core-wallet-auth' import { pino, Logger } from 'pino' import { sink } from 'pino-test' @@ -21,6 +25,7 @@ const authContextMock: AuthContext = { } const storeConfig: StoreInternalConfig = { + idps: [], networks: [], } @@ -144,14 +149,17 @@ implementations.forEach(([name, StoreImpl]) => { }) test('should add, list, get, update, and remove networks', async () => { + const idp: Idp = { + id: 'idp1', + type: 'oauth' as const, + issuer: 'http://auth', + configUrl: 'http://auth/.well-known/openid-configuration', + } const ledgerApi: LedgerApi = { baseUrl: 'http://api', } - const auth: ImplicitAuth = { - identityProviderId: 'idp1', - type: 'implicit', - issuer: 'http://auth', - configUrl: 'http://auth/.well-known/openid-configuration', + const auth: AuthorizationCodeAuth = { + method: 'authorization_code', clientId: 'cid', scope: 'scope', audience: 'aud', @@ -161,9 +169,11 @@ implementations.forEach(([name, StoreImpl]) => { name: 'testnet', synchronizerId: 'sync1::fingerprint', description: 'Test Network', + identityProviderId: 'idp1', ledgerApi, auth, } + await store.updateIdp(idp) await store.updateNetwork(network) const listed = await store.listNetworks() expect(listed).toHaveLength(1) diff --git a/core/wallet-store-inmemory/src/StoreInternal.ts b/core/wallet-store-inmemory/src/StoreInternal.ts index c10d4ec7..fe46a056 100644 --- a/core/wallet-store-inmemory/src/StoreInternal.ts +++ b/core/wallet-store-inmemory/src/StoreInternal.ts @@ -8,6 +8,7 @@ import { AuthAware, assertConnected, AccessTokenProvider, + Idp, } from '@canton-network/core-wallet-auth' import { Store, @@ -30,6 +31,7 @@ interface UserStorage { } export interface StoreInternalConfig { + idps: Array networks: Array } @@ -250,6 +252,50 @@ export class StoreInternal implements Store, AuthAware { this.updateStorage(storage) } + // IDP methods + async getIdp(idpId: string): Promise { + this.assertConnected() + const idps = await this.listIdps() + const idp = idps.find((i) => i.id === idpId) + if (!idp) { + throw new Error(`IdP "${idpId}" not found`) + } + return idp + } + + async listIdps(): Promise> { + this.assertConnected() + return this.systemStorage.idps + } + + async addIdp(idp: Idp): Promise { + this.assertConnected() + const existingIdp = await this.listIdps() + + if (existingIdp.find((i) => i.id === idp.id)) { + throw new Error(`IdP "${idp.id}" already exists`) + } + + this.systemStorage.idps.push(idp) + } + + async updateIdp(idp: Idp): Promise { + this.assertConnected() + const existingIdps = await this.listIdps() + const index = existingIdps.findIndex((i) => i.id === idp.id) + if (index === -1) { + throw new Error(`IdP "${idp.id}" not found`) + } + this.systemStorage.idps[index] = idp + } + + async removeIdp(idpId: string): Promise { + this.assertConnected() + this.systemStorage.idps = this.systemStorage.idps.filter( + (i) => i.id !== idpId + ) + } + // Network methods async getNetwork(networkId: string): Promise { this.assertConnected() diff --git a/core/wallet-store-sql/src/migrations/001-init.ts b/core/wallet-store-sql/src/migrations/001-init.ts index c05e6dbc..85b8c9e4 100644 --- a/core/wallet-store-sql/src/migrations/001-init.ts +++ b/core/wallet-store-sql/src/migrations/001-init.ts @@ -13,12 +13,12 @@ export async function up(db: Kysely): Promise { .addColumn('type', 'text', (col) => col.notNull()) .addColumn('issuer', 'text', (col) => col.notNull()) .addColumn('config_url', 'text', (col) => col.notNull()) - .addColumn('audience', 'text', (col) => col.notNull()) - .addColumn('scope', 'text', (col) => col.notNull()) - .addColumn('client_id', 'text', (col) => col.notNull()) - .addColumn('client_secret', 'text') - .addColumn('admin_client_id', 'text') - .addColumn('admin_client_secret', 'text') + // .addColumn('audience', 'text', (col) => col.notNull()) + // .addColumn('scope', 'text', (col) => col.notNull()) + // .addColumn('client_id', 'text', (col) => col.notNull()) + // .addColumn('client_secret', 'text') + // .addColumn('admin_client_id', 'text') + // .addColumn('admin_client_secret', 'text') .execute() // --- networks --- diff --git a/core/wallet-store-sql/src/schema.ts b/core/wallet-store-sql/src/schema.ts index 0ef12a5a..c74a49da 100644 --- a/core/wallet-store-sql/src/schema.ts +++ b/core/wallet-store-sql/src/schema.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Auth, UserId } from '@canton-network/core-wallet-auth' +import { authSchema, Idp, UserId } from '@canton-network/core-wallet-auth' import { Wallet, Transaction, @@ -15,16 +15,10 @@ interface MigrationTable { } interface IdpTable { - identityProviderId: string - type: string + id: string + type: 'oauth' | 'self_signed' issuer: string - configUrl: string - audience: string - scope: string - clientId: string - clientSecret: string - adminClientId: string - adminClientSecret: string + configUrl: string | undefined } interface NetworkTable { @@ -33,8 +27,11 @@ interface NetworkTable { synchronizerId: string description: string ledgerApiBaseUrl: string - userId: UserId | undefined // global if undefined identityProviderId: string + userId: UserId | undefined // global if undefined + + auth: string // json stringified + adminAuth: string | undefined // json stringified } interface WalletTable { @@ -70,116 +67,62 @@ export interface DB { sessions: SessionTable } -export const toAuth = (table: IdpTable): Auth => { +export const toIdp = (table: IdpTable): Idp => { switch (table.type) { - case 'implicit': - return { - identityProviderId: table.identityProviderId, - type: table.type, - issuer: table.issuer, - configUrl: table.configUrl, - audience: table.audience, - scope: table.scope, - clientId: table.clientId, - admin: { - clientId: table.adminClientId, - clientSecret: table.adminClientSecret, - }, + case 'oauth': { + if (!table.configUrl) { + throw new Error(`Missing configUrl for oauth IdP: ${table.id}`) } - case 'client_credentials': + return { - identityProviderId: table.identityProviderId, + id: table.id, type: table.type, issuer: table.issuer, configUrl: table.configUrl, - audience: table.audience, - scope: table.scope, - clientId: table.clientId, - clientSecret: table.clientSecret, - admin: { - clientId: table.adminClientId, - clientSecret: table.adminClientSecret, - }, } + } case 'self_signed': return { - identityProviderId: table.identityProviderId, + id: table.id, type: table.type, issuer: table.issuer, - audience: table.audience, - scope: table.scope, - clientId: table.clientId, - clientSecret: table.clientSecret, - admin: { - clientId: table.adminClientId, - clientSecret: table.adminClientSecret, - }, } - default: - throw new Error(`Unknown auth type: ${table.type}`) } } -export const fromAuth = (auth: Auth): IdpTable => { - switch (auth.type) { - case 'implicit': - return { - identityProviderId: auth.identityProviderId, - type: auth.type, - issuer: auth.issuer, - configUrl: auth.configUrl, - audience: auth.audience, - scope: auth.scope, - clientId: auth.clientId, - clientSecret: '', - adminClientId: auth.admin?.clientId || '', - adminClientSecret: auth.admin?.clientSecret || '', - } - case 'client_credentials': +export const fromIdp = (idp: Idp): IdpTable => { + switch (idp.type) { + case 'oauth': return { - identityProviderId: auth.identityProviderId, - type: auth.type, - issuer: auth.issuer, - configUrl: auth.configUrl, - audience: auth.audience, - scope: auth.scope, - clientId: auth.clientId, - clientSecret: auth.clientSecret, - adminClientId: auth.admin?.clientId || '', - adminClientSecret: auth.admin?.clientSecret || '', + id: idp.id, + type: idp.type, + issuer: idp.issuer, + configUrl: idp.configUrl, } case 'self_signed': return { - identityProviderId: auth.identityProviderId, - type: auth.type, - issuer: auth.issuer, - configUrl: '', - audience: auth.audience, - scope: auth.scope, - clientId: auth.clientId, - clientSecret: auth.clientSecret, - adminClientId: auth.admin?.clientId || '', - adminClientSecret: auth.admin?.clientSecret || '', + id: idp.id, + type: idp.type, + issuer: idp.issuer, + configUrl: undefined, } } } -export const toNetwork = ( - table: NetworkTable, - authTable?: IdpTable -): Network => { - if (!authTable) { - throw new Error(`Missing auth table for network: ${table.name}`) - } +export const toNetwork = (table: NetworkTable): Network => { return { name: table.name, id: table.id, synchronizerId: table.synchronizerId, + identityProviderId: table.identityProviderId, description: table.description, ledgerApi: { baseUrl: table.ledgerApiBaseUrl, }, - auth: toAuth(authTable), + auth: authSchema.parse(JSON.parse(table.auth)), + adminAuth: table.adminAuth + ? authSchema.parse(JSON.parse(table.adminAuth)) + : undefined, } } @@ -194,7 +137,11 @@ export const fromNetwork = ( description: network.description, ledgerApiBaseUrl: network.ledgerApi.baseUrl, userId: userId, - identityProviderId: network.auth.identityProviderId, + identityProviderId: network.identityProviderId, + auth: JSON.stringify(network.auth), + adminAuth: network.adminAuth + ? JSON.stringify(network.adminAuth) + : undefined, } } diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 20e96cc0..28cc18f6 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -3,7 +3,10 @@ import { describe, expect, test } from '@jest/globals' -import { AuthContext, ImplicitAuth } from '@canton-network/core-wallet-auth' +import { + AuthContext, + AuthorizationCodeAuth, +} from '@canton-network/core-wallet-auth' import { LedgerApi, Network, @@ -41,11 +44,8 @@ const implementations: Array<[string, StoreCtor]> = [['StoreSql', StoreSql]] const ledgerApi: LedgerApi = { baseUrl: 'http://api', } -const auth: ImplicitAuth = { - identityProviderId: 'idp1', - type: 'implicit', - issuer: 'http://auth', - configUrl: 'http://auth/.well-known/openid-configuration', +const auth: AuthorizationCodeAuth = { + method: 'authorization_code', clientId: 'cid', scope: 'scope', audience: 'aud', @@ -54,6 +54,7 @@ const network: Network = { name: 'testnet', id: 'network1', synchronizerId: 'sync1::fingerprint', + identityProviderId: 'idp1', description: 'Test Network', ledgerApi, auth, @@ -91,11 +92,8 @@ implementations.forEach(([name, StoreImpl]) => { }) test('should filter wallets', async () => { - const auth2: ImplicitAuth = { - identityProviderId: 'idp2', - type: 'implicit', - issuer: 'http://auth', - configUrl: 'http://auth/.well-known/openid-configuration', + const auth2: AuthorizationCodeAuth = { + method: 'authorization_code', clientId: 'cid', scope: 'scope', audience: 'aud', @@ -104,6 +102,7 @@ implementations.forEach(([name, StoreImpl]) => { name: 'testnet', id: 'network2', synchronizerId: 'sync1::fingerprint', + identityProviderId: 'idp2', description: 'Test Network', ledgerApi, auth: auth2, diff --git a/core/wallet-store-sql/src/store-sql.ts b/core/wallet-store-sql/src/store-sql.ts index 414679ff..1f19d1cd 100644 --- a/core/wallet-store-sql/src/store-sql.ts +++ b/core/wallet-store-sql/src/store-sql.ts @@ -7,6 +7,7 @@ import { UserId, AuthAware, assertConnected, + Idp, } from '@canton-network/core-wallet-auth' import { Store as BaseStore, @@ -22,10 +23,11 @@ import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely' import Database from 'better-sqlite3' import { DB, - fromAuth, + fromIdp, fromNetwork, fromTransaction, fromWallet, + toIdp, toNetwork, toTransaction, toWallet, @@ -183,6 +185,69 @@ export class StoreSql implements BaseStore, AuthAware { .execute() } + // IDP methods + + async getIdp(idpId: string): Promise { + this.assertConnected() + + const idps = await this.listIdps() + if (!idps) throw new Error('No IDPs available') + + const idp = idps.find((n) => n.id === idpId) + if (!idp) throw new Error(`IDP "${idpId}" not found`) + return idp + } + + async listIdps(): Promise> { + // All IDPs are global for now -- TO-DO: user-specific IDPs + const query = this.db.selectFrom('idps').selectAll() + + const idps = await query.execute() + return idps.map((table) => toIdp(table)) + } + + async updateIdp(idp: Idp): Promise { + // todo: check and compare userid of existing idp + await this.db.transaction().execute(async (trx) => { + const idpEntry = fromIdp(idp) + this.logger.info(idpEntry, 'Updating idp table') + await trx + .updateTable('idps') + .set(idpEntry) + .where('id', '=', idp.id) + .execute() + }) + } + + async addIdp(idp: Idp): Promise { + await this.db.transaction().execute(async (trx) => { + const idpAlreadyExists = await trx + .selectFrom('idps') + .selectAll() + .where('id', '=', idp.id) + .executeTakeFirst() + if (idpAlreadyExists) { + throw new Error(`IDP ${idp.id} already exists`) + } else { + await trx.insertInto('idps').values(fromIdp(idp)).execute() + } + }) + } + + async removeIdp(idpId: string): Promise { + await this.db.transaction().execute(async (trx) => { + const idp = await trx + .selectFrom('idps') + .selectAll() + .where('id', '=', idpId) + .executeTakeFirst() + if (!idp) { + throw new Error(`IDP ${idpId} does not exists`) + } + await trx.deleteFrom('idps').where('id', '=', idpId).execute() + }) + } + // Network methods async getNetwork(networkId: string): Promise { this.assertConnected() @@ -229,16 +294,14 @@ export class StoreSql implements BaseStore, AuthAware { } const networks = await query.execute() - const idps = await this.db.selectFrom('idps').selectAll().execute() - const idpMap = new Map(idps.map((idp) => [idp.identityProviderId, idp])) - return networks.map((table) => - toNetwork(table, idpMap.get(table.identityProviderId)) - ) + return networks.map((table) => toNetwork(table)) } async updateNetwork(network: Network): Promise { const userId = this.assertConnected() // todo: check and compare userid of existing network + // todo: check and compare idpId of existing network + await this.db.transaction().execute(async (trx) => { const networkEntry = fromNetwork(network, userId) this.logger.info(networkEntry, 'Updating network table') @@ -247,23 +310,22 @@ export class StoreSql implements BaseStore, AuthAware { .set(networkEntry) .where('id', '=', network.id) .execute() - - const authEntry = fromAuth(network.auth) - this.logger.info(authEntry, 'Updating auth table') - await trx - .updateTable('idps') - .set(authEntry) - .where( - 'identityProviderId', - '=', - network.auth.identityProviderId - ) - .execute() }) } async addNetwork(network: Network): Promise { const userId = this.authContext?.userId + const idps = await this.listIdps() + const networkIdp = idps.find( + (idp) => idp.id === network.identityProviderId + ) + + if (!networkIdp) { + throw new Error( + `Identity provider "${network.identityProviderId}" not found` + ) + } + await this.db.transaction().execute(async (trx) => { const networkAlreadyExists = await trx .selectFrom('networks') @@ -273,10 +335,6 @@ export class StoreSql implements BaseStore, AuthAware { if (networkAlreadyExists) { throw new Error(`Network ${network.id} already exists`) } else { - await trx - .insertInto('idps') - .values(fromAuth(network.auth)) - .execute() await trx .insertInto('networks') .values(fromNetwork(network, userId)) @@ -305,10 +363,6 @@ export class StoreSql implements BaseStore, AuthAware { .deleteFrom('networks') .where('id', '=', networkId) .execute() - await trx - .deleteFrom('idps') - .where('identityProviderId', '=', network.identityProviderId) - .execute() }) } diff --git a/core/wallet-store/src/Store.ts b/core/wallet-store/src/Store.ts index 2c14f27f..3e914218 100644 --- a/core/wallet-store/src/Store.ts +++ b/core/wallet-store/src/Store.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // Account +import { Idp } from '@canton-network/core-wallet-auth' import { Network } from './config/schema' export enum AddressType { @@ -67,6 +68,13 @@ export interface Store { setSession(session: Session): Promise removeSession(): Promise + // IDP methods + getIdp(idpId: string): Promise + listIdps(): Promise> + updateIdp(idp: Idp): Promise + addIdp(idp: Idp): Promise + removeIdp(idpId: string): Promise + // Network methods getNetwork(networkId: string): Promise getCurrentNetwork(): Promise diff --git a/core/wallet-store/src/config/schema.ts b/core/wallet-store/src/config/schema.ts index f2e69a02..77c5e720 100644 --- a/core/wallet-store/src/config/schema.ts +++ b/core/wallet-store/src/config/schema.ts @@ -1,23 +1,9 @@ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { authSchema } from '@canton-network/core-wallet-auth' +import { authSchema, idpSchema } from '@canton-network/core-wallet-auth' import { z } from 'zod' -export const idpSchema = z.discriminatedUnion('type', [ - z.object({ - id: z.string(), - type: z.literal('self_signed'), - issuer: z.string(), - }), - z.object({ - id: z.string(), - type: z.literal('oauth'), - issuer: z.string(), - configUrl: z.string().url(), - }), -]) - export const ledgerApiSchema = z.object({ baseUrl: z.string().url(), }) diff --git a/core/wallet-ui-components/src/components/NetworkCard.ts b/core/wallet-ui-components/src/components/NetworkCard.ts index 567a23a5..95a5f0a8 100644 --- a/core/wallet-ui-components/src/components/NetworkCard.ts +++ b/core/wallet-ui-components/src/components/NetworkCard.ts @@ -69,7 +69,7 @@ export class NetworkCard extends BaseElement {
ID: ${this.network.id}
- Auth: ${this.network.auth.type}
+ Auth: ${this.network.auth.method}
Synchronizer: ${this.network.synchronizerId}
diff --git a/core/wallet-ui-components/src/components/NetworkForm.stories.ts b/core/wallet-ui-components/src/components/NetworkForm.stories.ts index 28145400..6f2da265 100644 --- a/core/wallet-ui-components/src/components/NetworkForm.stories.ts +++ b/core/wallet-ui-components/src/components/NetworkForm.stories.ts @@ -5,6 +5,7 @@ import type { Meta, StoryObj } from '@storybook/web-components-vite' import { html } from 'lit' import { Network } from '@canton-network/core-wallet-store' +import { NetworkEditSaveEvent } from './NetworkForm' const meta: Meta = { title: 'NetworkForm', @@ -12,7 +13,8 @@ const meta: Meta = { export default meta -function onSaved() { +function onSaved(e: NetworkEditSaveEvent) { + console.log('saved!', { network: e.network }) document.getElementById('output')!.textContent = 'saved successfully!' } @@ -23,71 +25,69 @@ export const Default: StoryObj = { }, } -const sampleNetworkImplicit: Network = { - name: 'Local (OAuth IDP)', - id: 'canton:local-oauth', +const sampleNetworkAuthorizationCode: Network = { + name: 'Local (password IDP)', + id: 'canton:local-password', synchronizerId: 'wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd', - description: 'Mock OAuth IDP', + description: 'Unimplemented Password Auth', + identityProviderId: 'idp1', ledgerApi: { - baseUrl: 'http://127.0.0.1:5003', + baseUrl: 'https://test', }, auth: { - identityProviderId: 'idp2', - type: 'implicit', - issuer: 'http://127.0.0.1:8889', - configUrl: 'http://127.0.0.1:8889/.well-known/openid-configuration', + method: 'authorization_code', audience: 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', scope: 'openid daml_ledger_api offline_access', - clientId: 'operator', - admin: { - clientId: 'participant_admin', - clientSecret: 'admin-client-secret', - }, + clientId: 'wk-service-account', + }, + adminAuth: { + method: 'client_credentials', + clientId: 'participant_admin', + clientSecret: 'admin-client-secret', + scope: 'daml_ledger_api', + audience: + 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', }, } -export const PopulatedImplicitAuth: StoryObj = { +export const PopulatedAuthorizationCode: StoryObj = { render: () => { return html`
` }, } -const sampleNetworkPassword: Network = { +const sampleNetworkSelfSigned: Network = { name: 'Local (password IDP)', id: 'canton:local-password', synchronizerId: 'wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd', description: 'Unimplemented Password Auth', + identityProviderId: 'idp2', ledgerApi: { baseUrl: 'https://test', }, auth: { - identityProviderId: 'idp1', - type: 'implicit', - issuer: 'http://127.0.0.1:8889', - configUrl: 'http://127.0.0.1:8889/.well-known/openid-configuration', + method: 'self_signed', + issuer: 'unsafe-issuer', audience: 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', - scope: 'openid', + scope: 'openid daml_ledger_api offline_access', clientId: 'wk-service-account', - admin: { - clientId: 'participant_admin', - clientSecret: 'admin-client-secret', - }, + clientSecret: 'unsafe', }, } -export const PopulatedPasswordAuth: StoryObj = { +export const PopulatedSelfSigned: StoryObj = { render: () => { return html`
` }, diff --git a/core/wallet-ui-components/src/components/NetworkForm.ts b/core/wallet-ui-components/src/components/NetworkForm.ts index 75a59d62..fa1896f9 100644 --- a/core/wallet-ui-components/src/components/NetworkForm.ts +++ b/core/wallet-ui-components/src/components/NetworkForm.ts @@ -7,8 +7,8 @@ import { customElement, property, state } from 'lit/decorators.js' import { BaseElement } from '../internal/BaseElement.js' import { NetworkInputChangedEvent } from './NetworkFormInput.js' import { - Credentials, - ImplicitAuth, + AuthorizationCodeAuth, + ClientCredentialsAuth, SelfSignedAuth, } from '@canton-network/core-wallet-auth' @@ -33,19 +33,13 @@ export class NetworkEditSaveEvent extends Event { } } -type NetworkKeys = Exclude -type LedgerApiKeys = keyof Network['ledgerApi'] - -type CommonAuth = Exclude -type AdminAuth = keyof Credentials - @customElement('network-form') export class NetworkForm extends BaseElement { - @property({ type: Object }) network: Network = { + @property({ type: Object }) + accessor network: Network = { ledgerApi: {}, auth: {}, } as Network - @property({ type: String }) authType: string = 'implicit' @state() private _error = '' @@ -53,48 +47,6 @@ export class NetworkForm extends BaseElement { connectedCallback(): void { super.connectedCallback() - if (this.network.auth.type) { - this.authType = this.network.auth.type - } - } - - onAuthTypeChange(e: Event) { - const select = e.target as HTMLSelectElement - this.authType = select.value - - this.updateAuthStructure() - } - - updateAuthStructure() { - const network = { ...this.network } - - if (this.authType === 'implicit') { - const auth = network.auth as ImplicitAuth - network.auth = { - type: 'implicit', - identityProviderId: auth.identityProviderId || '', - configUrl: auth.configUrl || '', - clientId: auth.clientId || '', - issuer: auth.issuer || '', - audience: auth.audience || '', - scope: auth.scope || '', - admin: auth.admin, - } - } else if (this.authType === 'self_signed') { - const auth = network.auth as SelfSignedAuth - network.auth = { - type: 'self_signed', - identityProviderId: auth.identityProviderId || '', - issuer: auth.issuer || '', - audience: auth.audience || '', - scope: auth.scope || '', - clientId: auth.clientId || '', - clientSecret: auth.clientSecret || '', - admin: auth.admin, - } - } - - this.network = network } handleSubmit(e: Event) { @@ -112,204 +64,254 @@ export class NetworkForm extends BaseElement { } } - setNetwork(field: NetworkKeys) { - return (ev: NetworkInputChangedEvent) => { - this.network[field] = ev.value - } - } - - setLedgerApi(field: LedgerApiKeys) { - return (ev: NetworkInputChangedEvent) => { - if (!this.network.ledgerApi) { - this.network.ledgerApi = { - baseUrl: '', - } - } - this.network.ledgerApi[field] = ev.value + renderAuthForm(authObj: Network['auth']) { + if (typeof authObj.method === 'undefined') { + // Use Object.assign to ensure that we are modifying the same object reference in memory, + // so that changes are reflected in the parent `this.network.auth` (or `this.network.adminAuth`) + Object.assign(authObj, { + method: 'authorization_code', + clientId: '', + audience: '', + scope: '', + } satisfies AuthorizationCodeAuth) } - } - - setAuth(field: CommonAuth) { - return (ev: NetworkInputChangedEvent) => { - this.network.auth[field] = ev.value - } - } - setAuthConfigUrl() { - return (ev: NetworkInputChangedEvent) => { - if (this.network.auth.type === 'self_signed') { - return - } - this.network.auth['configUrl'] = ev.value - } - } - - setAdminAuth(field: AdminAuth) { - return (ev: NetworkInputChangedEvent) => { - if (this.network.auth.admin) { - this.network.auth.admin[field] = ev.value - } - } - } - - renderAuthForm() { const commonFields = html` - - +
+ + +
+ - { + authObj.clientId = e.value + }} > { + authObj.audience = e.value + }} > { + authObj.scope = e.value + }} > ` - const adminFields = html` -
-
- Admin auth fields (optional) -
+ if (authObj.method === 'authorization_code') { + return html`${commonFields}` + } else if (authObj.method === 'client_credentials') { + return html`${commonFields} { + ;(authObj as ClientCredentialsAuth).clientSecret = + e.value + }} + >` + } else if (authObj.method === 'self_signed') { + return html`${commonFields} + { + ;(authObj as SelfSignedAuth).issuer = e.value + }} > { + ;(authObj as SelfSignedAuth).audience = e.value + }} > -
- ` - - if (this.authType === 'implicit') { - let auth = this.network.auth - if (auth.type !== 'implicit') { - auth = { - type: 'implicit', - identityProviderId: '', - configUrl: '', - clientId: '', - issuer: '', - audience: '', - scope: '', - } - this.network.auth = auth - } - - return html`${commonFields}${adminFields}` - } else if (this.authType === 'self_signed') { - let auth = this.network.auth - if (auth.type !== 'self_signed') { - auth = { - type: 'self_signed', - identityProviderId: '', - issuer: '', - audience: '', - scope: '', - clientId: '', - clientSecret: '', - } - this.network.auth = auth - } - return html`${commonFields}${adminFields}` + { + ;(authObj as SelfSignedAuth).clientSecret = e.value + }} + >` } else { - throw new Error(`Unsupported auth type: ${this.authType}`) + throw new Error( + `Unsupported auth method: ${JSON.stringify(authObj)}` + ) } } render() { return html`
+ { + this.network.id = e.value + }} + > + { + this.network.name = e.value + }} > { + this.network.description = e.value + }} > { + this.network.synchronizerId = e.value + }} > { + this.network.identityProviderId = e.value + }} > { + this.network.ledgerApi.baseUrl = e.value + }} > -
-
- - +
+

+ 🔐 Configure User Auth +

+
+ ${this.renderAuthForm(this.network.auth)}
-
-
- Configuring ${this.authType} auth -
- ${this.renderAuthForm()} +
+

+ 🔐 Configure Admin Auth (optional) +

+
+ ${typeof this.network.adminAuth === 'undefined' + ? html`` + : html` + ${this.renderAuthForm(this.network.adminAuth)}`} +
${this._error}
diff --git a/core/wallet-ui-components/src/components/NetworkTable.stories.ts b/core/wallet-ui-components/src/components/NetworkTable.stories.ts index 93ec3064..77d958ce 100644 --- a/core/wallet-ui-components/src/components/NetworkTable.stories.ts +++ b/core/wallet-ui-components/src/components/NetworkTable.stories.ts @@ -20,22 +20,24 @@ const networks: Network[] = [ synchronizerId: 'wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd', description: 'Unimplemented Password Auth', + identityProviderId: 'idp1', ledgerApi: { baseUrl: 'https://test', }, auth: { - identityProviderId: 'idp1', - type: 'implicit', - issuer: 'http://127.0.0.1:8889', - configUrl: 'http://127.0.0.1:8889/.well-known/openid-configuration', + method: 'authorization_code', audience: 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', scope: 'openid', clientId: 'wk-service-account', - admin: { - clientId: 'participant_admin', - clientSecret: 'admin-client-secret', - }, + }, + adminAuth: { + method: 'client_credentials', + audience: + 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424', + scope: 'daml_ledger_api', + clientId: 'participant_admin', + clientSecret: 'admin-client-secret', }, }, ] diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index 346e212a..eee484c2 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -27,34 +27,30 @@ export type Description = string * */ export type SynchronizerId = string -export type Type = string +/** + * + * Identity Provider ID + * + */ export type IdentityProviderId = string +export type Method = string export type Scope = string export type ClientId = string export type ClientSecret = string export type Issuer = string -export type ConfigUrl = string export type Audience = string -export interface Admin { - clientId: ClientId - clientSecret: ClientSecret - [k: string]: any -} /** * - * Represents the type of auth (implicit or password) for a specified network + * Represents the type of auth for a specified network * */ export interface Auth { - authType?: Type - identityProviderId: IdentityProviderId + method: Method scope?: Scope clientId?: ClientId clientSecret?: ClientSecret issuer: Issuer - configUrl: ConfigUrl audience?: Audience - admin?: Admin [k: string]: any } /** @@ -73,7 +69,9 @@ export interface Network { name: Name description: Description synchronizerId: SynchronizerId + identityProviderId: IdentityProviderId auth: Auth + adminAuth?: Auth ledgerApi: LedgerApi } /** diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 37644178..3747dbbd 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -439,9 +439,17 @@ "type": "string", "description": "Synchronizer ID" }, + "identityProviderId": { + "title": "identityProviderId", + "type": "string", + "description": "Identity Provider ID" + }, "auth": { "$ref": "#/components/schemas/Auth" }, + "adminAuth": { + "$ref": "#/components/schemas/Auth" + }, "ledgerApi": { "title": "ledgerApi", "type": "string", @@ -453,6 +461,7 @@ "name", "description", "synchronizerId", + "identityProviderId", "auth", "ledgerApi" ], @@ -461,14 +470,10 @@ "Auth": { "title": "auth", "type": "object", - "description": "Represents the type of auth (implicit or password) for a specified network", + "description": "Represents the type of auth for a specified network", "properties": { - "authType": { - "title": "type", - "type": "string" - }, - "identityProviderId": { - "title": "identityProviderId", + "method": { + "title": "method", "type": "string" }, "scope": { @@ -487,36 +492,12 @@ "title": "issuer", "type": "string" }, - "configUrl": { - "title": "configUrl", - "type": "string" - }, "audience": { "title": "audience", "type": "string" - }, - "admin": { - "title": "admin", - "type": "object", - "properties": { - "clientId": { - "title": "clientId", - "type": "string" - }, - "clientSecret": { - "title": "clientSecret", - "type": "string" - } - }, - "required": ["clientId", "clientSecret"] } }, - "required": [ - "type", - "identityProviderId", - "issuer", - "configUrl" - ] + "required": ["method", "issuer"] }, "Wallet": { "title": "Wallet", diff --git a/wallet-gateway/remote/src/auth/jwt-auth-service.ts b/wallet-gateway/remote/src/auth/jwt-auth-service.ts index 2dcde2fd..3e61c7e3 100644 --- a/wallet-gateway/remote/src/auth/jwt-auth-service.ts +++ b/wallet-gateway/remote/src/auth/jwt-auth-service.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { Auth, AuthService } from '@canton-network/core-wallet-auth' +import { AuthService } from '@canton-network/core-wallet-auth' import { Store } from '@canton-network/core-wallet-store' import { createRemoteJWKSet, decodeJwt, jwtVerify } from 'jose' import { Logger } from 'pino' @@ -29,11 +29,9 @@ export const jwtAuthService = (store: Store, logger: Logger): AuthService => ({ return undefined } - // TODO: change once IDP is decoupled from networks - const networks = await store.listNetworks() - const idp: Auth | undefined = networks.find( - (n) => n.auth.issuer === iss - )?.auth + const idps = await store.listIdps() + const idp = idps.find((i) => i.issuer === iss) + if (!idp) { logger.warn(`No identity provider found for issuer: ${iss}`) return undefined diff --git a/wallet-gateway/remote/src/auth/jwt-unsafe-auth-service.ts b/wallet-gateway/remote/src/auth/jwt-unsafe-auth-service.ts index b62e6925..8d68d6c1 100644 --- a/wallet-gateway/remote/src/auth/jwt-unsafe-auth-service.ts +++ b/wallet-gateway/remote/src/auth/jwt-unsafe-auth-service.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { AuthService } from '@canton-network/core-wallet-auth' -import { Auth } from '@canton-network/core-wallet-auth' import { Store } from '@canton-network/core-wallet-store' import { decodeJwt } from 'jose' import { Logger } from 'pino' @@ -36,11 +35,9 @@ export const jwtAuthService = (store: Store, logger: Logger): AuthService => ({ return undefined } - // TODO: change once IDP is decoupled from networks - const networks = await store.listNetworks() - const idp: Auth | undefined = networks.find( - (n) => n.auth.issuer === iss - )?.auth + const idps = await store.listIdps() + const idp = idps.find((i) => i.issuer === iss) + if (!idp) { logger.warn(`No identity provider found for issuer: ${iss}`) return undefined diff --git a/wallet-gateway/remote/src/config/Config.test.ts b/wallet-gateway/remote/src/config/Config.test.ts index d398f285..b8fa1b38 100644 --- a/wallet-gateway/remote/src/config/Config.test.ts +++ b/wallet-gateway/remote/src/config/Config.test.ts @@ -10,9 +10,9 @@ test('config from json file', async () => { expect(resp.store.networks[0].ledgerApi.baseUrl).toBe('https://test') expect(resp.store.networks[0].auth.clientId).toBe('wk-service-account') expect(resp.store.networks[0].auth.scope).toBe('openid') - expect(resp.store.networks[0].auth.type).toBe('implicit') - expect(resp.store.networks[1].auth.type).toBe('implicit') - if (resp.store.networks[1].auth.type === 'implicit') { + expect(resp.store.networks[0].auth.method).toBe('authorization_code') + expect(resp.store.networks[1].auth.method).toBe('authorization_code') + if (resp.store.networks[1].auth.method === 'authorization_code') { expect(resp.store.networks[1].auth.audience).toBe( 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424' ) diff --git a/wallet-gateway/remote/src/config/ConfigUtils.ts b/wallet-gateway/remote/src/config/ConfigUtils.ts index dfa83ef5..857a0244 100644 --- a/wallet-gateway/remote/src/config/ConfigUtils.ts +++ b/wallet-gateway/remote/src/config/ConfigUtils.ts @@ -7,11 +7,100 @@ import { Config, configSchema } from './Config.js' export class ConfigUtils { static loadConfigFile(filePath: string): Config { if (existsSync(filePath)) { - return configSchema.parse( + const config = configSchema.parse( JSON.parse(readFileSync(filePath, 'utf-8')) ) + + /** + * Perform extra config validation beyond schema validation. + * We want to enforce the following constraints: + * + * 1. IDP IDs are unique + * 2. Network IDs are unique + * 3. Each Network's identityProviderId maps to an existing IDP (in config) + * 4. Each Network's auth method is compatible with its IDP type + */ + const duplicateIdpId = hasDuplicateElement( + config.store.idps.map((idp) => idp.id) + ) + if (duplicateIdpId) { + throw new Error( + `Non-unique IDP IDs found in config file: ${duplicateIdpId}` + ) + } + + const duplicateNetworkId = hasDuplicateElement( + config.store.networks.map((network) => network.id) + ) + if (duplicateNetworkId) { + throw new Error( + `Non-unique Network IDs found in config file: ${duplicateNetworkId}` + ) + } + + const invalidMapping = validateNetworkToIdpMapping(config) + if (invalidMapping) { + throw new Error( + `Network ${invalidMapping.networkId} references unknown Identity Provider ID ${invalidMapping.idpId}` + ) + } + + const invalidAuthMethod = validateNetworkAuthMethods(config) + if (invalidAuthMethod) { + throw new Error( + `Network ${invalidAuthMethod.networkId} has invalid auth method ${invalidAuthMethod.invalidAuthMethod} for its Identity Provider` + ) + } + + return config } else { throw new Error("Supplied file path doesn't exist " + filePath) } } } + +function hasDuplicateElement(list: string[]): string | undefined { + let duplicate: string | undefined + list.forEach((item, i) => { + if (list.indexOf(item) !== i && duplicate === undefined) { + duplicate = item + } + }) + return duplicate +} + +function validateNetworkToIdpMapping( + config: Config +): { networkId: string; idpId: string } | undefined { + for (const network of config.store.networks) { + const idp = config.store.idps.find( + (idp) => idp.id === network.identityProviderId + ) + + if (typeof idp === 'undefined') { + return { networkId: network.id, idpId: network.identityProviderId } + } + } +} + +const SUPPORTED_IDP_METHODS = { + self_signed: ['self_signed'], + oauth: ['authorization_code', 'client_credentials'], +} + +function validateNetworkAuthMethods( + config: Config +): { networkId: string; invalidAuthMethod: string } | undefined { + for (const network of config.store.networks) { + const idp = config.store.idps.find( + (idp) => idp.id === network.identityProviderId + )! + + if (!SUPPORTED_IDP_METHODS[idp.type].includes(network.auth.method)) { + return { + networkId: network.id, + invalidAuthMethod: network.auth.method, + } + } + } +} diff --git a/wallet-gateway/remote/src/ledger/party-allocation-service.test.ts b/wallet-gateway/remote/src/ledger/party-allocation-service.test.ts index 0c7d2008..028a5bc9 100644 --- a/wallet-gateway/remote/src/ledger/party-allocation-service.test.ts +++ b/wallet-gateway/remote/src/ledger/party-allocation-service.test.ts @@ -52,18 +52,22 @@ describe('PartyAllocationService', () => { id: 'network-id', synchronizerId: 'sync-id', description: 'desc', + identityProviderId: 'idp', ledgerApi: { baseUrl: 'http://ledger', }, auth: { - identityProviderId: 'idp', - type: 'implicit', - issuer: 'http://idp', - configUrl: 'http://idp/.well-known/openid-configuration', + method: 'authorization_code', audience: 'aud', scope: 'scope', clientId: 'cid', - admin: { clientId: 'cid', clientSecret: 'secret' }, + }, + adminAuth: { + method: 'client_credentials', + audience: 'aud', + scope: 'scope', + clientId: 'cid', + clientSecret: 'secret', }, } diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index ff7ce42d..f5a7eebf 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -22,8 +22,8 @@ import { NotificationService } from '../notification/NotificationService.js' import { AccessTokenProvider, assertConnected, - Auth, AuthContext, + authSchema, AuthTokenProvider, } from '@canton-network/core-wallet-auth' import { KernelInfo } from '../config/Config.js' @@ -59,60 +59,19 @@ export const userController = ( baseUrl: network.ledgerApi ?? '', } - let auth: Auth - if (network.auth.type === 'implicit') { - auth = { - type: 'implicit', - identityProviderId: network.auth.identityProviderId, - issuer: network.auth.issuer ?? '', - configUrl: network.auth.configUrl ?? '', - audience: network.auth.audience ?? '', - scope: network.auth.scope ?? '', - clientId: network.auth.clientId ?? '', - admin: { - clientId: network.auth.admin?.clientId ?? '', - clientSecret: network.auth.admin?.clientSecret ?? '', - }, - } - } else if (network.auth.type === 'client_credentials') { - auth = { - type: 'client_credentials', - identityProviderId: network.auth.identityProviderId, - issuer: network.auth.issuer ?? '', - configUrl: network.auth.configUrl ?? '', - audience: network.auth.audience ?? '', - scope: network.auth.scope ?? '', - clientId: network.auth.clientId ?? '', - clientSecret: network.auth.clientSecret ?? '', - admin: { - clientId: network.auth.admin?.clientId ?? '', - clientSecret: network.auth.admin?.clientSecret ?? '', - }, - } - } else if (network.auth.type === 'self_signed') { - auth = { - type: 'self_signed', - identityProviderId: network.auth.identityProviderId, - issuer: network.auth.issuer ?? '', - audience: network.auth.audience ?? '', - scope: network.auth.scope ?? '', - clientId: network.auth.clientId ?? '', - clientSecret: network.auth.clientSecret ?? '', - admin: { - clientId: network.auth.admin?.clientId ?? '', - clientSecret: network.auth.admin?.clientSecret ?? '', - }, - } - } else { - throw new Error(`Unsupported auth type: ${network.auth.type}`) - } + const auth = authSchema.parse(network.auth) + const adminAuth = network.adminAuth + ? authSchema.parse(network.adminAuth) + : undefined const newNetwork: Network = { name: network.name, id: network.id, description: network.description, synchronizerId: network.synchronizerId, + identityProviderId: network.identityProviderId, auth, + adminAuth, ledgerApi, } @@ -148,7 +107,14 @@ export const userController = ( throw new Error('No network session found') } - const tokenProvider = new AuthTokenProvider(network.auth, logger) + const idp = await store.getIdp(network.identityProviderId) + + const tokenProvider = new AuthTokenProvider( + idp, + network.auth, + network.adminAuth, + logger + ) const partyAllocator = new PartyAllocationService( network.synchronizerId, tokenProvider, diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts index c2508857..387fe1e4 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts @@ -27,34 +27,30 @@ export type Description = string * */ export type SynchronizerId = string -export type Type = string +/** + * + * Identity Provider ID + * + */ export type IdentityProviderId = string +export type Method = string export type Scope = string export type ClientId = string export type ClientSecret = string export type Issuer = string -export type ConfigUrl = string export type Audience = string -export interface Admin { - clientId: ClientId - clientSecret: ClientSecret - [k: string]: any -} /** * - * Represents the type of auth (implicit or password) for a specified network + * Represents the type of auth for a specified network * */ export interface Auth { - authType?: Type - identityProviderId: IdentityProviderId + method: Method scope?: Scope clientId?: ClientId clientSecret?: ClientSecret issuer: Issuer - configUrl: ConfigUrl audience?: Audience - admin?: Admin [k: string]: any } /** @@ -73,7 +69,9 @@ export interface Network { name: Name description: Description synchronizerId: SynchronizerId + identityProviderId: IdentityProviderId auth: Auth + adminAuth?: Auth ledgerApi: LedgerApi } /** diff --git a/wallet-gateway/remote/src/web/frontend/networks/index.ts b/wallet-gateway/remote/src/web/frontend/networks/index.ts index 3136fa34..108f3516 100644 --- a/wallet-gateway/remote/src/web/frontend/networks/index.ts +++ b/wallet-gateway/remote/src/web/frontend/networks/index.ts @@ -5,7 +5,7 @@ import '@canton-network/core-wallet-ui-components' import { LitElement, html, css } from 'lit' import { customElement, state } from 'lit/decorators.js' import { - Auth, + Auth as ApiAuth, Network, RemoveNetworkParams, Session, @@ -20,6 +20,7 @@ import { NetworkCardDeleteEvent, NetworkEditSaveEvent, } from '@canton-network/core-wallet-ui-components' +import { Auth } from '@canton-network/core-wallet-auth' @customElement('user-ui-networks') export class UserUiNetworks extends LitElement { @@ -216,64 +217,35 @@ export class UserUiNetworks extends LitElement { } } + private toApiAuth(auth?: Auth): ApiAuth { + if (auth) { + return { + method: auth.method, + audience: auth.audience ?? '', + scope: auth.scope ?? '', + clientId: auth.clientId ?? '', + issuer: (auth as ApiAuth).issuer ?? '', + clientSecret: (auth as ApiAuth).clientSecret ?? '', + } + } else { + return { + method: 'implicit', + issuer: '', + } + } + } + private handleSubmit = async (e: NetworkEditSaveEvent) => { e.preventDefault() - let auth: Auth - switch (e.network.auth.type) { - case 'implicit': - auth = { - type: 'implicit', - identityProviderId: e.network.auth.identityProviderId, - issuer: e.network.auth.issuer ?? '', - configUrl: e.network.auth.configUrl ?? '', - audience: e.network.auth.audience ?? '', - scope: e.network.auth.scope ?? '', - clientId: e.network.auth.clientId ?? '', - admin: { - clientId: e.network.auth.admin?.clientId ?? '', - clientSecret: e.network.auth.admin?.clientSecret ?? '', - }, - } - break - case 'client_credentials': - auth = { - type: 'client_credentials', - identityProviderId: e.network.auth.identityProviderId, - issuer: e.network.auth.issuer ?? '', - configUrl: e.network.auth.configUrl ?? '', - audience: e.network.auth.audience ?? '', - scope: e.network.auth.scope ?? '', - clientId: e.network.auth.clientId ?? '', - clientSecret: e.network.auth.clientSecret ?? '', - admin: { - clientId: e.network.auth.admin?.clientId ?? '', - clientSecret: e.network.auth.admin?.clientSecret ?? '', - }, - } - break - case 'self_signed': - auth = { - type: 'self_signed', - identityProviderId: e.network.auth.identityProviderId, - issuer: e.network.auth.issuer ?? '', - audience: e.network.auth.audience ?? '', - scope: e.network.auth.scope ?? '', - clientId: e.network.auth.clientId ?? '', - clientSecret: e.network.auth.clientSecret ?? '', - admin: { - clientId: e.network.auth.admin?.clientId ?? '', - clientSecret: e.network.auth.admin?.clientSecret ?? '', - }, - configUrl: '', - } - break - } + const auth = this.toApiAuth(e.network.auth) + const adminAuth = this.toApiAuth(e.network.adminAuth) const network: Network = { ...e.network, - auth: auth, ledgerApi: e.network.ledgerApi.baseUrl, + auth, + adminAuth, } try { From c883c70874ed8276453440efd5bad48163e9d388 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 5 Nov 2025 10:49:11 -0500 Subject: [PATCH 6/8] fix unit tests Signed-off-by: Alex Matson --- core/wallet-store-inmemory/src/Store.test.ts | 1 + .../src/migrations/001-init.ts | 14 +++---- core/wallet-store-sql/src/store-sql.test.ts | 19 +++++++++ .../remote/src/config/Config.test.ts | 41 ++++++++++++++++--- .../remote/src/user-api/server.test.ts | 11 +++-- wallet-gateway/test/config.json | 10 +++++ 6 files changed, 75 insertions(+), 21 deletions(-) diff --git a/core/wallet-store-inmemory/src/Store.test.ts b/core/wallet-store-inmemory/src/Store.test.ts index 4238a4e6..7e4be299 100644 --- a/core/wallet-store-inmemory/src/Store.test.ts +++ b/core/wallet-store-inmemory/src/Store.test.ts @@ -173,6 +173,7 @@ implementations.forEach(([name, StoreImpl]) => { ledgerApi, auth, } + await store.addIdp(idp) await store.updateIdp(idp) await store.updateNetwork(network) const listed = await store.listNetworks() diff --git a/core/wallet-store-sql/src/migrations/001-init.ts b/core/wallet-store-sql/src/migrations/001-init.ts index 85b8c9e4..c23b7800 100644 --- a/core/wallet-store-sql/src/migrations/001-init.ts +++ b/core/wallet-store-sql/src/migrations/001-init.ts @@ -9,16 +9,10 @@ export async function up(db: Kysely): Promise { await db.schema .createTable('idps') .ifNotExists() - .addColumn('identity_provider_id', 'text', (col) => col.primaryKey()) + .addColumn('id', 'text', (col) => col.primaryKey()) .addColumn('type', 'text', (col) => col.notNull()) .addColumn('issuer', 'text', (col) => col.notNull()) - .addColumn('config_url', 'text', (col) => col.notNull()) - // .addColumn('audience', 'text', (col) => col.notNull()) - // .addColumn('scope', 'text', (col) => col.notNull()) - // .addColumn('client_id', 'text', (col) => col.notNull()) - // .addColumn('client_secret', 'text') - // .addColumn('admin_client_id', 'text') - // .addColumn('admin_client_secret', 'text') + .addColumn('config_url', 'text') .execute() // --- networks --- @@ -32,8 +26,10 @@ export async function up(db: Kysely): Promise { .addColumn('ledger_api_base_url', 'text', (col) => col.notNull()) .addColumn('user_id', 'text') // optional/global if null .addColumn('identity_provider_id', 'text', (col) => - col.references('idps.identity_provider_id').onDelete('cascade') + col.references('idps.id').onDelete('cascade') ) + .addColumn('auth', 'jsonb', (col) => col.notNull()) + .addColumn('admin_auth', 'jsonb') .execute() // --- wallets --- diff --git a/core/wallet-store-sql/src/store-sql.test.ts b/core/wallet-store-sql/src/store-sql.test.ts index 28cc18f6..645f592b 100644 --- a/core/wallet-store-sql/src/store-sql.test.ts +++ b/core/wallet-store-sql/src/store-sql.test.ts @@ -6,6 +6,7 @@ import { describe, expect, test } from '@jest/globals' import { AuthContext, AuthorizationCodeAuth, + Idp, } from '@canton-network/core-wallet-auth' import { LedgerApi, @@ -50,6 +51,18 @@ const auth: AuthorizationCodeAuth = { scope: 'scope', audience: 'aud', } +const idp: Idp = { + id: 'idp1', + issuer: 'http://idp1', + type: 'oauth', + configUrl: 'http://idp-config', +} +const idp2: Idp = { + id: 'idp2', + type: 'self_signed', + issuer: 'http://idp2', +} + const network: Network = { name: 'testnet', id: 'network1', @@ -85,6 +98,7 @@ implementations.forEach(([name, StoreImpl]) => { networkId: 'network1', } const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) await store.addNetwork(network) await store.addWallet(wallet) const wallets = await store.getWallets() @@ -135,6 +149,8 @@ implementations.forEach(([name, StoreImpl]) => { networkId: 'network2', } const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) + await store.addIdp(idp2) await store.addNetwork(network) await store.addNetwork(network2) await store.addWallet(wallet1) @@ -178,6 +194,7 @@ implementations.forEach(([name, StoreImpl]) => { networkId: 'network1', } const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) await store.addNetwork(network) await store.addWallet(wallet1) await store.addWallet(wallet2) @@ -189,6 +206,7 @@ implementations.forEach(([name, StoreImpl]) => { test('should set and get session', async () => { const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) await store.addNetwork(network) const session: Session = { network: 'network1', @@ -207,6 +225,7 @@ implementations.forEach(([name, StoreImpl]) => { test('should add, list, get, update, and remove networks', async () => { const store = new StoreImpl(db, pino(sink()), authContextMock) + await store.addIdp(idp) await store.addNetwork(network) const listed = await store.listNetworks() diff --git a/wallet-gateway/remote/src/config/Config.test.ts b/wallet-gateway/remote/src/config/Config.test.ts index b8fa1b38..86bca27b 100644 --- a/wallet-gateway/remote/src/config/Config.test.ts +++ b/wallet-gateway/remote/src/config/Config.test.ts @@ -6,15 +6,44 @@ import { ConfigUtils } from './ConfigUtils.js' test('config from json file', async () => { const resp = ConfigUtils.loadConfigFile('../test/config.json') - expect(resp.store.networks[0].name).toBe('Local (password IDP)') - expect(resp.store.networks[0].ledgerApi.baseUrl).toBe('https://test') - expect(resp.store.networks[0].auth.clientId).toBe('wk-service-account') - expect(resp.store.networks[0].auth.scope).toBe('openid') + expect(resp.store.networks[0].name).toBe('Local (OAuth IDP)') + expect(resp.store.networks[0].ledgerApi.baseUrl).toBe( + 'http://127.0.0.1:5003' + ) + expect(resp.store.networks[0].auth.clientId).toBe('operator') + expect(resp.store.networks[0].auth.scope).toBe( + 'openid daml_ledger_api offline_access' + ) expect(resp.store.networks[0].auth.method).toBe('authorization_code') - expect(resp.store.networks[1].auth.method).toBe('authorization_code') - if (resp.store.networks[1].auth.method === 'authorization_code') { + expect(resp.store.networks[1].auth.method).toBe('client_credentials') + if (resp.store.networks[1].auth.method === 'client_credentials') { expect(resp.store.networks[1].auth.audience).toBe( 'https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424' ) } }) + +/** + * "id": "canton:local-oauth", + "name": "Local (OAuth IDP)", + "description": "Mock OAuth IDP", + "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", + "identityProviderId": "idp-mock-oauth", + "auth": { + "method": "authorization_code", + "clientId": "operator", + "scope": "operator", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424" + }, + "adminAuth": { + "method": "client_credentials", + "scope": "daml_ledger_api", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", + "clientId": "participant_admin", + "clientSecret": "admin-client-secret" + }, + "ledgerApi": { + "baseUrl": "http://127.0.0.1:5003" + } + + */ diff --git a/wallet-gateway/remote/src/user-api/server.test.ts b/wallet-gateway/remote/src/user-api/server.test.ts index 74bd401f..f4936706 100644 --- a/wallet-gateway/remote/src/user-api/server.test.ts +++ b/wallet-gateway/remote/src/user-api/server.test.ts @@ -50,10 +50,9 @@ test('call listNetworks rpc', async () => { const json = await response.body.result expect(response.statusCode).toBe(200) - expect(json.networks.length).toBe(5) - expect(json.networks[0].name).toBe('Local (password IDP)') - expect(json.networks[1].name).toBe('Local (OAuth IDP)') - expect(json.networks[2].name).toBe('Local (OAuth IDP - Client Credentials)') - expect(json.networks[3].name).toBe('Local (Self signed)') - expect(json.networks[4].name).toBe('Devnet (Auth0)') + expect(json.networks.length).toBe(4) + expect(json.networks[0].name).toBe('Local (OAuth IDP)') + expect(json.networks[1].name).toBe('Local (OAuth IDP - Client Credentials)') + expect(json.networks[2].name).toBe('Local (Self signed)') + expect(json.networks[3].name).toBe('Devnet (Auth0)') }) diff --git a/wallet-gateway/test/config.json b/wallet-gateway/test/config.json index 85fa0879..b775e692 100644 --- a/wallet-gateway/test/config.json +++ b/wallet-gateway/test/config.json @@ -44,6 +44,8 @@ }, "adminAuth": { "method": "client_credentials", + "scope": "daml_ledger_api", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", "clientId": "participant_admin", "clientSecret": "admin-client-secret" }, @@ -66,6 +68,8 @@ }, "adminAuth": { "method": "client_credentials", + "scope": "daml_ledger_api", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", "clientId": "participant_admin", "clientSecret": "admin-client-secret" }, @@ -81,6 +85,7 @@ "identityProviderId": "idp-self-signed", "auth": { "method": "self_signed", + "issuer": "self-signed", "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", "scope": "openid daml_ledger_api offline_access", "clientId": "operator", @@ -88,6 +93,9 @@ }, "adminAuth": { "method": "self_signed", + "issuer": "self-signed", + "scope": "daml_ledger_api", + "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", "clientId": "participant_admin", "clientSecret": "admin-client-secret" }, @@ -109,6 +117,8 @@ }, "adminAuth": { "method": "client_credentials", + "scope": "daml_ledger_api", + "audience": "https://canton.network.global", "clientId": "uHh5IA2hQWc78HHEPDJTmZm6GYhJbfev", "clientSecret": "GET_FROM_AUTH0" }, From 6ba88f6b76b1b83da3eacfac0a36924d1dfa3cd6 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 5 Nov 2025 11:11:41 -0500 Subject: [PATCH 7/8] cleanup Signed-off-by: Alex Matson --- api-specs/openrpc-dapp-api.json | 2 +- api-specs/openrpc-user-api.json | 2 +- .../remote/src/config/Config.test.ts | 25 ------------------- 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/api-specs/openrpc-dapp-api.json b/api-specs/openrpc-dapp-api.json index 8126a999..b194a202 100644 --- a/api-specs/openrpc-dapp-api.json +++ b/api-specs/openrpc-dapp-api.json @@ -452,7 +452,7 @@ "networkId": { "title": "networkId", "type": "string", - "description": "A CAIP-2 compliant network ID, e.g. 'canton:da-mainnet'." + "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." } }, "required": ["kernel", "isConnected"] diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index 3747dbbd..e40dc7d2 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -497,7 +497,7 @@ "type": "string" } }, - "required": ["method", "issuer"] + "required": ["method", "audience", "scope", "clientId"] }, "Wallet": { "title": "Wallet", diff --git a/wallet-gateway/remote/src/config/Config.test.ts b/wallet-gateway/remote/src/config/Config.test.ts index 86bca27b..40fcff22 100644 --- a/wallet-gateway/remote/src/config/Config.test.ts +++ b/wallet-gateway/remote/src/config/Config.test.ts @@ -22,28 +22,3 @@ test('config from json file', async () => { ) } }) - -/** - * "id": "canton:local-oauth", - "name": "Local (OAuth IDP)", - "description": "Mock OAuth IDP", - "synchronizerId": "wallet::1220e7b23ea52eb5c672fb0b1cdbc916922ffed3dd7676c223a605664315e2d43edd", - "identityProviderId": "idp-mock-oauth", - "auth": { - "method": "authorization_code", - "clientId": "operator", - "scope": "operator", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424" - }, - "adminAuth": { - "method": "client_credentials", - "scope": "daml_ledger_api", - "audience": "https://daml.com/jwt/aud/participant/participant1::1220d44fc1c3ba0b5bdf7b956ee71bc94ebe2d23258dc268fdf0824fbaeff2c61424", - "clientId": "participant_admin", - "clientSecret": "admin-client-secret" - }, - "ledgerApi": { - "baseUrl": "http://127.0.0.1:5003" - } - - */ From e046136207c5d02ecc81e9f756eb5a02ca59d128 Mon Sep 17 00:00:00 2001 From: Alex Matson Date: Wed, 5 Nov 2025 11:56:31 -0500 Subject: [PATCH 8/8] fix e2e test Signed-off-by: Alex Matson --- api-specs/openrpc-user-api.json | 93 ++++++++++++++---- core/wallet-dapp-rpc-client/src/openrpc.json | 2 +- core/wallet-store-sql/src/bootstrap.ts | 5 + core/wallet-user-rpc-client/package.json | 2 +- core/wallet-user-rpc-client/src/index.ts | 83 ++++++++++++---- core/wallet-user-rpc-client/src/openrpc.json | 95 ++++++++++++++----- example/tests/example.spec.ts | 2 +- .../remote/src/user-api/controller.ts | 5 +- .../remote/src/user-api/rpc-gen/index.ts | 9 +- .../remote/src/user-api/rpc-gen/typings.ts | 56 ++++++++--- .../remote/src/web/frontend/login/login.ts | 46 ++++++--- .../remote/src/web/frontend/networks/index.ts | 35 +++---- wallet-gateway/remote/tsconfig.frontend.json | 2 +- 13 files changed, 323 insertions(+), 112 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index e40dc7d2..1049a03c 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -58,6 +58,48 @@ }, "description": "Removes a new network configuration (similar to EIP-3085)." }, + { + "name": "listNetworks", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "ListNetworksResult", + "type": "object", + "properties": { + "networks": { + "title": "networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/Network" + } + } + }, + "required": ["networks"] + } + } + }, + { + "name": "listIdps", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "ListIdpsResult", + "type": "object", + "properties": { + "idps": { + "title": "idps", + "type": "array", + "items": { + "$ref": "#/components/schemas/Idp" + } + } + }, + "required": ["idps"] + } + } + }, { "name": "createWallet", "params": [ @@ -335,27 +377,6 @@ }, "description": "Executes a signed transaction." }, - { - "name": "listNetworks", - "params": [], - "result": { - "name": "result", - "schema": { - "title": "ListNetworksResult", - "type": "object", - "properties": { - "networks": { - "title": "networks", - "type": "array", - "items": { - "$ref": "#/components/schemas/Network" - } - } - }, - "required": ["networks"] - } - } - }, { "name": "addSession", "description": "Adds a network session.", @@ -467,10 +488,40 @@ ], "additionalProperties": false }, + "Idp": { + "title": "Idp", + "type": "object", + "description": "Structure representing the Identity Providers", + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "ID of the identity provider" + }, + "type": { + "title": "type", + "type": "string", + "description": "Type of identity provider (OAuth2 or Self-Signed)" + }, + "issuer": { + "title": "issuer", + "type": "string", + "description": "Issuer of identity provider" + }, + "configUrl": { + "title": "configUrl", + "type": "string", + "description": "URL to fetch the identity provider configuration" + } + }, + "required": ["id", "type", "issuer"], + "additionalProperties": false + }, "Auth": { "title": "auth", "type": "object", "description": "Represents the type of auth for a specified network", + "additionalProperties": false, "properties": { "method": { "title": "method", diff --git a/core/wallet-dapp-rpc-client/src/openrpc.json b/core/wallet-dapp-rpc-client/src/openrpc.json index 8126a999..b194a202 100644 --- a/core/wallet-dapp-rpc-client/src/openrpc.json +++ b/core/wallet-dapp-rpc-client/src/openrpc.json @@ -452,7 +452,7 @@ "networkId": { "title": "networkId", "type": "string", - "description": "A CAIP-2 compliant network ID, e.g. 'canton:da-mainnet'." + "description": "A CAIP-2 compliant chain ID, e.g. 'canton:da-mainnet'." } }, "required": ["kernel", "isConnected"] diff --git a/core/wallet-store-sql/src/bootstrap.ts b/core/wallet-store-sql/src/bootstrap.ts index 0b76a183..eb806e22 100644 --- a/core/wallet-store-sql/src/bootstrap.ts +++ b/core/wallet-store-sql/src/bootstrap.ts @@ -13,6 +13,11 @@ export async function bootstrap( logger: Logger ): Promise { const store = new StoreSql(db, logger) + + // Load all IDPs from config into the store + await Promise.all(config.idps.map((idp) => store.addIdp(idp))) + + // Load all networks from config into the store await Promise.all( config.networks.map((network) => store.addNetwork(network)) ) diff --git a/core/wallet-user-rpc-client/package.json b/core/wallet-user-rpc-client/package.json index 876b8f43..9837a513 100644 --- a/core/wallet-user-rpc-client/package.json +++ b/core/wallet-user-rpc-client/package.json @@ -1,6 +1,6 @@ { "name": "@canton-network/core-wallet-user-rpc-client", - "version": "0.0.0", + "version": "0.11.0", "type": "module", "description": "TypeScript client generated by OpenRPC", "repository": "github:hyperledger-labs/splice-wallet-kernel", diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index eee484c2..ce15bb09 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -46,12 +46,11 @@ export type Audience = string */ export interface Auth { method: Method - scope?: Scope - clientId?: ClientId + scope: Scope + clientId: ClientId clientSecret?: ClientSecret - issuer: Issuer - audience?: Audience - [k: string]: any + issuer?: Issuer + audience: Audience } /** * @@ -131,6 +130,37 @@ export type PreparedTransactionHash = string export type CommandId = string export type Signature = string export type SignedBy = string +export type Networks = Network[] +/** + * + * ID of the identity provider + * + */ +export type Id = string +/** + * + * Type of identity provider (OAuth2 or Self-Signed) + * + */ +export type Type = string +/** + * + * URL to fetch the identity provider configuration + * + */ +export type ConfigUrl = string +/** + * + * Structure representing the Identity Providers + * + */ +export interface Idp { + id: Id + type: Type + issuer: Issuer + configUrl?: ConfigUrl +} +export type Idps = Idp[] /** * * The party hint and name of the wallet. @@ -166,7 +196,6 @@ export interface Wallet { } export type Added = Wallet[] export type Removed = Wallet[] -export type Networks = Network[] /** * * The access token for the session. @@ -236,6 +265,14 @@ export interface AddSessionParams { * */ export type Null = null +export interface ListNetworksResult { + networks: Networks + [k: string]: any +} +export interface ListIdpsResult { + idps: Idps + [k: string]: any +} export interface CreateWalletResult { wallet: Wallet [k: string]: any @@ -268,10 +305,6 @@ export interface SignResult { export interface ExecuteResult { [key: string]: any } -export interface ListNetworksResult { - networks: Networks - [k: string]: any -} /** * * Structure representing the connected network session @@ -294,6 +327,8 @@ export interface ListSessionsResult { export type AddNetwork = (params: AddNetworkParams) => Promise export type RemoveNetwork = (params: RemoveNetworkParams) => Promise +export type ListNetworks = () => Promise +export type ListIdps = () => Promise export type CreateWallet = ( params: CreateWalletParams ) => Promise @@ -307,7 +342,6 @@ export type ListWallets = ( export type SyncWallets = () => Promise export type Sign = (params: SignParams) => Promise export type Execute = (params: ExecuteParams) => Promise -export type ListNetworks = () => Promise export type AddSession = (params: AddSessionParams) => Promise export type ListSessions = () => Promise @@ -336,6 +370,24 @@ export class SpliceWalletJSONRPCUserAPI { ...params: Parameters ): ReturnType + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'listNetworks', + ...params: Parameters + ): ReturnType + + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'listIdps', + ...params: Parameters + ): ReturnType + /** * */ @@ -399,15 +451,6 @@ export class SpliceWalletJSONRPCUserAPI { ...params: Parameters ): ReturnType - /** - * - */ - // tslint:disable-next-line:max-line-length - public async request( - method: 'listNetworks', - ...params: Parameters - ): ReturnType - /** * */ diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 3747dbbd..1049a03c 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -58,6 +58,48 @@ }, "description": "Removes a new network configuration (similar to EIP-3085)." }, + { + "name": "listNetworks", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "ListNetworksResult", + "type": "object", + "properties": { + "networks": { + "title": "networks", + "type": "array", + "items": { + "$ref": "#/components/schemas/Network" + } + } + }, + "required": ["networks"] + } + } + }, + { + "name": "listIdps", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "ListIdpsResult", + "type": "object", + "properties": { + "idps": { + "title": "idps", + "type": "array", + "items": { + "$ref": "#/components/schemas/Idp" + } + } + }, + "required": ["idps"] + } + } + }, { "name": "createWallet", "params": [ @@ -335,27 +377,6 @@ }, "description": "Executes a signed transaction." }, - { - "name": "listNetworks", - "params": [], - "result": { - "name": "result", - "schema": { - "title": "ListNetworksResult", - "type": "object", - "properties": { - "networks": { - "title": "networks", - "type": "array", - "items": { - "$ref": "#/components/schemas/Network" - } - } - }, - "required": ["networks"] - } - } - }, { "name": "addSession", "description": "Adds a network session.", @@ -467,10 +488,40 @@ ], "additionalProperties": false }, + "Idp": { + "title": "Idp", + "type": "object", + "description": "Structure representing the Identity Providers", + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "ID of the identity provider" + }, + "type": { + "title": "type", + "type": "string", + "description": "Type of identity provider (OAuth2 or Self-Signed)" + }, + "issuer": { + "title": "issuer", + "type": "string", + "description": "Issuer of identity provider" + }, + "configUrl": { + "title": "configUrl", + "type": "string", + "description": "URL to fetch the identity provider configuration" + } + }, + "required": ["id", "type", "issuer"], + "additionalProperties": false + }, "Auth": { "title": "auth", "type": "object", "description": "Represents the type of auth for a specified network", + "additionalProperties": false, "properties": { "method": { "title": "method", @@ -497,7 +548,7 @@ "type": "string" } }, - "required": ["method", "issuer"] + "required": ["method", "audience", "scope", "clientId"] }, "Wallet": { "title": "Wallet", diff --git a/example/tests/example.spec.ts b/example/tests/example.spec.ts index a8a5044b..1b201be1 100644 --- a/example/tests/example.spec.ts +++ b/example/tests/example.spec.ts @@ -35,7 +35,7 @@ test('dApp: execute externally signed tx', async ({ page: dappPage }) => { ]) try { - await wkPage.locator('#network').selectOption('1') + await wkPage.locator('#network').selectOption('0') await wkPage.getByRole('button', { name: 'Connect' }).click() await expect(dappPage.getByText('Loading...')).toHaveCount(0) diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index f5a7eebf..2e430864 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -89,6 +89,9 @@ export const userController = ( await store.removeNetwork(params.networkName) return null }, + listNetworks: async () => + Promise.resolve({ networks: await store.listNetworks() }), + listIdps: async () => Promise.resolve({ idps: await store.listIdps() }), createWallet: async (params: { primary?: boolean partyHint: string @@ -399,8 +402,6 @@ export const userController = ( ) } }, - listNetworks: async () => - Promise.resolve({ networks: await store.listNetworks() }), addSession: async function ( params: AddSessionParams ): Promise { diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/index.ts b/wallet-gateway/remote/src/user-api/rpc-gen/index.ts index a76d1dd4..4a7370a8 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/index.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/index.ts @@ -3,6 +3,8 @@ import { AddNetwork } from './typings.js' import { RemoveNetwork } from './typings.js' +import { ListNetworks } from './typings.js' +import { ListIdps } from './typings.js' import { CreateWallet } from './typings.js' import { SetPrimaryWallet } from './typings.js' import { RemoveWallet } from './typings.js' @@ -10,13 +12,14 @@ import { ListWallets } from './typings.js' import { SyncWallets } from './typings.js' import { Sign } from './typings.js' import { Execute } from './typings.js' -import { ListNetworks } from './typings.js' import { AddSession } from './typings.js' import { ListSessions } from './typings.js' export type Methods = { addNetwork: AddNetwork removeNetwork: RemoveNetwork + listNetworks: ListNetworks + listIdps: ListIdps createWallet: CreateWallet setPrimaryWallet: SetPrimaryWallet removeWallet: RemoveWallet @@ -24,7 +27,6 @@ export type Methods = { syncWallets: SyncWallets sign: Sign execute: Execute - listNetworks: ListNetworks addSession: AddSession listSessions: ListSessions } @@ -33,6 +35,8 @@ function buildController(methods: Methods) { return { addNetwork: methods.addNetwork, removeNetwork: methods.removeNetwork, + listNetworks: methods.listNetworks, + listIdps: methods.listIdps, createWallet: methods.createWallet, setPrimaryWallet: methods.setPrimaryWallet, removeWallet: methods.removeWallet, @@ -40,7 +44,6 @@ function buildController(methods: Methods) { syncWallets: methods.syncWallets, sign: methods.sign, execute: methods.execute, - listNetworks: methods.listNetworks, addSession: methods.addSession, listSessions: methods.listSessions, } diff --git a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts index 387fe1e4..71549dca 100644 --- a/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts +++ b/wallet-gateway/remote/src/user-api/rpc-gen/typings.ts @@ -46,12 +46,11 @@ export type Audience = string */ export interface Auth { method: Method - scope?: Scope - clientId?: ClientId + scope: Scope + clientId: ClientId clientSecret?: ClientSecret - issuer: Issuer - audience?: Audience - [k: string]: any + issuer?: Issuer + audience: Audience } /** * @@ -131,6 +130,37 @@ export type PreparedTransactionHash = string export type CommandId = string export type Signature = string export type SignedBy = string +export type Networks = Network[] +/** + * + * ID of the identity provider + * + */ +export type Id = string +/** + * + * Type of identity provider (OAuth2 or Self-Signed) + * + */ +export type Type = string +/** + * + * URL to fetch the identity provider configuration + * + */ +export type ConfigUrl = string +/** + * + * Structure representing the Identity Providers + * + */ +export interface Idp { + id: Id + type: Type + issuer: Issuer + configUrl?: ConfigUrl +} +export type Idps = Idp[] /** * * The party hint and name of the wallet. @@ -166,7 +196,6 @@ export interface Wallet { } export type Added = Wallet[] export type Removed = Wallet[] -export type Networks = Network[] /** * * The access token for the session. @@ -236,6 +265,14 @@ export interface AddSessionParams { * */ export type Null = null +export interface ListNetworksResult { + networks: Networks + [k: string]: any +} +export interface ListIdpsResult { + idps: Idps + [k: string]: any +} export interface CreateWalletResult { wallet: Wallet [k: string]: any @@ -268,10 +305,6 @@ export interface SignResult { export interface ExecuteResult { [key: string]: any } -export interface ListNetworksResult { - networks: Networks - [k: string]: any -} /** * * Structure representing the connected network session @@ -294,6 +327,8 @@ export interface ListSessionsResult { export type AddNetwork = (params: AddNetworkParams) => Promise export type RemoveNetwork = (params: RemoveNetworkParams) => Promise +export type ListNetworks = () => Promise +export type ListIdps = () => Promise export type CreateWallet = ( params: CreateWalletParams ) => Promise @@ -307,6 +342,5 @@ export type ListWallets = ( export type SyncWallets = () => Promise export type Sign = (params: SignParams) => Promise export type Execute = (params: ExecuteParams) => Promise -export type ListNetworks = () => Promise export type AddSession = (params: AddSessionParams) => Promise export type ListSessions = () => Promise diff --git a/wallet-gateway/remote/src/web/frontend/login/login.ts b/wallet-gateway/remote/src/web/frontend/login/login.ts index 9388e008..b3fb2b1e 100644 --- a/wallet-gateway/remote/src/web/frontend/login/login.ts +++ b/wallet-gateway/remote/src/web/frontend/login/login.ts @@ -6,7 +6,7 @@ import { customElement, state } from 'lit/decorators.js' import '@canton-network/core-wallet-ui-components' import { createUserClient } from '../rpc-client' -import { Network } from '@canton-network/core-wallet-user-rpc-client' +import { Idp, Network } from '@canton-network/core-wallet-user-rpc-client' import { stateManager } from '../state-manager' import '../index' import { WalletEvent } from '@canton-network/core-types' @@ -18,7 +18,10 @@ import { @customElement('user-ui-login') export class LoginUI extends LitElement { @state() - accessor idps: Network[] = [] + accessor networks: Network[] = [] + + @state() + accessor idps: Idp[] = [] @state() accessor selectedNetwork: Network | null = null @@ -169,7 +172,7 @@ export class LoginUI extends LitElement { private handleChange(e: Event) { const index = parseInt((e.target as HTMLSelectElement).value) - this.selectedNetwork = this.idps[index] ?? null + this.selectedNetwork = this.networks[index] ?? null this.message = null } @@ -179,9 +182,16 @@ export class LoginUI extends LitElement { return response.networks } + private async loadIdps() { + const userClient = createUserClient(stateManager.accessToken.get()) + const response = await userClient.request('listIdps') + return response.idps + } + async connectedCallback() { super.connectedCallback() - this.idps = await this.loadNetworks() + this.networks = await this.loadNetworks() + this.idps = await this.loadIdps() } private async handleConnectToIDP() { @@ -196,15 +206,28 @@ export class LoginUI extends LitElement { stateManager.networkId.set(this.selectedNetwork.id) const redirectUri = `${window.origin}/callback/` - if (this.selectedNetwork.auth.type === 'implicit') { + if (this.selectedNetwork.auth.method === 'authorization_code') { + const idp = this.idps.find( + (idp) => idp.id === this.selectedNetwork?.identityProviderId + ) + + console.log('Found IDP:', idp) + + if (!idp || !idp.configUrl) { + this.messageType = 'error' + this.message = + 'Identity provider misconfigured for this network.' + return + } + this.messageType = 'info' this.message = `Redirecting to ${this.selectedNetwork.name}...` const auth = this.selectedNetwork.auth - const config = await fetch(auth.configUrl).then((res) => res.json()) + const config = await fetch(idp.configUrl).then((res) => res.json()) const statePayload = { - configUrl: auth.configUrl, + configUrl: idp.configUrl, clientId: auth.clientId, audience: auth.audience, } @@ -223,7 +246,7 @@ export class LoginUI extends LitElement { setTimeout(() => { window.location.href = `${config.authorization_endpoint}?${params.toString()}` }, 400) - } else if (this.selectedNetwork.auth.type === 'self_signed') { + } else if (this.selectedNetwork.auth.method === 'self_signed') { await this.selfSign({ clientId: this.selectedNetwork.auth.clientId || '', clientSecret: this.selectedNetwork.auth.clientSecret || '', @@ -276,13 +299,12 @@ export class LoginUI extends LitElement {