From 467959ee92a538f4812637029c468c588765bfc0 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Wed, 3 Jul 2024 13:50:19 +0100 Subject: [PATCH] solution: auth api for tokens --- api-definitions | 2 +- packages/core/src/index.ts | 8 +- packages/core/src/signature.ts | 10 +-- packages/core/src/typesAuth.ts | 75 ++++++++++++++++++- packages/core/src/typesCommon.ts | 2 + packages/node/src/EmeraldApi.ts | 5 ++ .../src/__integration-tests__/auth.test.ts | 8 ++ packages/node/src/credentials.ts | 4 +- packages/node/src/wrapped/Auth.ts | 23 +++++- packages/node/src/wrapped/Factory.ts | 3 + packages/web/src/EmeraldApi.ts | 5 ++ .../src/__integration-tests__/auth.test.ts | 13 ++++ packages/web/src/credentials.ts | 4 +- packages/web/src/wrapped/AuthClient.ts | 45 +++++++++++ packages/web/src/wrapped/Factory.ts | 3 + 15 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 packages/web/src/__integration-tests__/auth.test.ts create mode 100644 packages/web/src/wrapped/AuthClient.ts diff --git a/api-definitions b/api-definitions index 0ca84c7..eae9026 160000 --- a/api-definitions +++ b/api-definitions @@ -1 +1 @@ -Subproject commit 0ca84c76689f11f53674bd9bf6cc6310edfe5b70 +Subproject commit eae90263ae3b22f8c73b7051cf3bf672a4116efe diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e6b95bc..40387ca 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,9 +29,12 @@ export { RefreshToken, AuthRequest, AuthResponse, AuthResponseOk, AuthResponseFail, isAuthResponseFail, isAuthResponseOk, - BaseAuthClient, + OrganizationId, ProjectId, TokenId, + CredentialsClient, RefreshRequest, - ConvertAuth + ConvertAuth, + ListTokensRequest, ListTokensResponse, TokenDetails, + WhoIAmResponse, IAmAuthenticated, IAMUnauthenticated, } from './typesAuth'; export { AddressBalance, @@ -62,6 +65,7 @@ export { SingleAddress, isAsset, isErc20Asset, + UUID, } from './typesCommon'; export { MessageFactory } from './typesConvert'; export { diff --git a/packages/core/src/signature.ts b/packages/core/src/signature.ts index b396fe1..1307af8 100644 --- a/packages/core/src/signature.ts +++ b/packages/core/src/signature.ts @@ -1,5 +1,5 @@ import { - BaseAuthClient, + CredentialsClient, RefreshToken, SecretToken, AuthRequest, @@ -143,7 +143,7 @@ export class NoSigner implements Signer { * @see JwtAuthProvider */ export class StandardSigner implements Signer { - private readonly client: BaseAuthClient; + private readonly client: CredentialsClient; private readonly secretToken: SecretToken; private readonly agents: string[]; @@ -153,7 +153,7 @@ export class StandardSigner implements Signer { private listener: AuthenticationListener | undefined; private authenticationStatus = AuthenticationStatus.AUTHENTICATING; - constructor(client: BaseAuthClient, secretToken: SecretToken, agents: string[]) { + constructor(client: CredentialsClient, secretToken: SecretToken, agents: string[]) { this.client = client; this.secretToken = secretToken; this.agents = agents; @@ -251,12 +251,12 @@ export class StandardSigner implements Signer { } class JwtAuthProvider implements EmeraldAuthenticator { - private readonly client: BaseAuthClient; + private readonly client: CredentialsClient; private readonly agents: string[]; private readonly secretToken: SecretToken; private refreshToken: RefreshToken | undefined; - constructor(client: BaseAuthClient, secretToken: SecretToken, agents: string[]) { + constructor(client: CredentialsClient, secretToken: SecretToken, agents: string[]) { this.client = client; this.secretToken = secretToken; if (this.agents == null || this.agents.length == 0) { diff --git a/packages/core/src/typesAuth.ts b/packages/core/src/typesAuth.ts index e783716..c7afc0e 100644 --- a/packages/core/src/typesAuth.ts +++ b/packages/core/src/typesAuth.ts @@ -1,8 +1,13 @@ import {MessageFactory} from "./typesConvert"; import * as auth_pb from "./generated/auth_pb"; +import {UUID} from "./typesCommon"; export type AuthCapability = 'JWT_RS256'; +export type TokenId = UUID; +export type OrganizationId = UUID; +export type ProjectId = UUID; + export type SecretToken = string; const SecretTokenRegex = new RegExp('^emrld_[0-9a-zA-Z]{38}$'); @@ -46,6 +51,33 @@ export function isAuthResponseFail(res: AuthResponse): res is AuthResponseFail { return res.status != 0; } +export type IAmAuthenticated = { + authenticated: true, + tokenId: TokenId, +} + +export type IAMUnauthenticated = { + authenticated: false, +} + +export type WhoIAmResponse = IAmAuthenticated | IAMUnauthenticated; + +export type ListTokensRequest = { + organizationId: OrganizationId, + projectId?: ProjectId, +} + +export type ListTokensResponse = { + tokens: TokenDetails[], +} + +export type TokenDetails = { + organizationId: OrganizationId, + projectId: ProjectId, + tokenId: TokenId, + createdAt: Date, +} + export class ConvertAuth { private readonly factory: MessageFactory; @@ -87,9 +119,50 @@ export class ConvertAuth { } } } + + public whoIAmResponse(res: auth_pb.WhoAmIResponse): WhoIAmResponse { + if (res.getIsAuthenticated()) { + return { + authenticated: true, + tokenId: res.getTokenId(), + } + } else { + return { + authenticated: false, + } + } + } + + public listTokensRequest(req: ListTokensRequest): auth_pb.ListTokensRequest { + const result: auth_pb.ListTokensRequest = this.factory('auth_pb.ListTokensRequest'); + + result.setOrganizationId(req.organizationId); + if (req.projectId) { + result.setProjectId(req.projectId); + } + + return result; + } + + public listTokensResponse(res: auth_pb.ListTokensResponse): ListTokensResponse { + return { + tokens: res.getTokensList().map((token) => { + return { + organizationId: token.getOrganizationId(), + projectId: token.getProjectId(), + tokenId: token.getTokenId(), + createdAt: new Date(token.getCreationDate()), + } + }), + } + } + } -export interface BaseAuthClient { +/** + * A subset of the Auth API used to get credentials + */ +export interface CredentialsClient { authenticate(req: AuthRequest): Promise; refresh(req: RefreshRequest): Promise; } \ No newline at end of file diff --git a/packages/core/src/typesCommon.ts b/packages/core/src/typesCommon.ts index e9f50ed..bcc82b3 100644 --- a/packages/core/src/typesCommon.ts +++ b/packages/core/src/typesCommon.ts @@ -43,6 +43,8 @@ export type XpubAddress = string | DetailedXpubAddress; export type MultiAddress = SingleAddress[]; export type AnyAddress = SingleAddress | MultiAddress | XpubAddress; +export type UUID = string; + export interface DetailedXpubAddress { xpub: string; start?: number; diff --git a/packages/node/src/EmeraldApi.ts b/packages/node/src/EmeraldApi.ts index 092bbf4..e9c71f2 100644 --- a/packages/node/src/EmeraldApi.ts +++ b/packages/node/src/EmeraldApi.ts @@ -7,6 +7,7 @@ import { MonitoringClient } from './wrapped/MonitoringClient'; import { TokenClient } from './wrapped/TokenClient'; import { TransactionClient } from './wrapped/TransactionClient'; import {SecretToken} from "@emeraldpay/api/lib/typesAuth"; +import {AuthClient} from "./wrapped/Auth"; export class EmeraldApi { private readonly agents: string[] = ['test-client/0.0.0']; @@ -58,4 +59,8 @@ export class EmeraldApi { transaction(): TransactionClient { return new TransactionClient(this.hostname, this.credentials, this.agents); } + + auth(): AuthClient { + return new AuthClient(this.hostname, this.credentials, this.agents); + } } diff --git a/packages/node/src/__integration-tests__/auth.test.ts b/packages/node/src/__integration-tests__/auth.test.ts index 69b5863..8b4a987 100644 --- a/packages/node/src/__integration-tests__/auth.test.ts +++ b/packages/node/src/__integration-tests__/auth.test.ts @@ -16,6 +16,14 @@ describe('Auth', () => { expect(balance).toBeDefined(); }); + test('is authenticated', async () => { + const client = EmeraldApi.devApi().auth(); + + const resp = await client.whoIAm() + + expect(resp.authenticated).toBeTruthy(); + }); + test('terminate connection after timeout', async () => { const client = EmeraldApi.localApi().blockchain(); diff --git a/packages/node/src/credentials.ts b/packages/node/src/credentials.ts index 7e442b4..fe793c8 100644 --- a/packages/node/src/credentials.ts +++ b/packages/node/src/credentials.ts @@ -11,7 +11,7 @@ import { import { AuthRequest as BaseAuthRequest, AuthResponse as BaseAuthResponse, - BaseAuthClient, + CredentialsClient, ConvertAuth, RefreshRequest as BaseRefreshRequest } from "@emeraldpay/api"; @@ -33,7 +33,7 @@ export function emeraldCredentials(url: string, agents: string[], secretToken: S /// ------------- Internal implementation details ------------- /// -class NodeAuthClient implements BaseAuthClient { +class NodeAuthClient implements CredentialsClient { private readonly client: AuthClient; private readonly convert = new ConvertAuth(classFactory); diff --git a/packages/node/src/wrapped/Auth.ts b/packages/node/src/wrapped/Auth.ts index c87ea97..ffa55a1 100644 --- a/packages/node/src/wrapped/Auth.ts +++ b/packages/node/src/wrapped/Auth.ts @@ -1,4 +1,11 @@ -import { ConnectionListener, publishToPromise, readOnce } from '@emeraldpay/api'; +import { + ConnectionListener, ConvertAuth, + ListTokensRequest, + ListTokensResponse, + publishToPromise, + readOnce, + WhoIAmResponse +} from '@emeraldpay/api'; import { ChannelCredentials } from '@grpc/grpc-js'; import { NativeChannel, callSingle } from '../channel'; import { AuthClient as ProtoAuthClient } from '../generated/auth_grpc_pb'; @@ -6,7 +13,9 @@ import { AuthRequest as ProtoAuthRequest, AuthResponse as ProtoAuthResponse, RefreshRequest as ProtoRefreshRequest, + WhoAmIRequest as ProtoWhoAmIRequest, } from '../generated/auth_pb'; +import {classFactory} from "./Factory"; // eslint-disable-next-line @typescript-eslint/no-var-requires const { version: clientVersion } = require('../../package.json'); @@ -16,6 +25,8 @@ export class AuthClient { readonly credentials: ChannelCredentials; readonly retries: number; + private readonly convert = new ConvertAuth(classFactory); + constructor(address: string, credentials: ChannelCredentials, agents: string[], retries = 3) { const agent = [...agents, `emerald-client-node/${clientVersion}`].join(' '); @@ -39,4 +50,14 @@ export class AuthClient { return publishToPromise(readOnce(this.channel, call, request, this.retries)); } + whoIAm(): Promise { + const call = callSingle(this.client.whoAmI.bind(this.client), this.convert.whoIAmResponse); + return publishToPromise(readOnce(this.channel, call, new ProtoWhoAmIRequest(), this.retries)); + } + + listTokens(req: ListTokensRequest): Promise { + const request = this.convert.listTokensRequest(req); + const call = callSingle(this.client.listTokens.bind(this.client), this.convert.listTokensResponse); + return publishToPromise(readOnce(this.channel, call, request, this.retries)); + } } diff --git a/packages/node/src/wrapped/Factory.ts b/packages/node/src/wrapped/Factory.ts index 0bbee4d..0caaca8 100644 --- a/packages/node/src/wrapped/Factory.ts +++ b/packages/node/src/wrapped/Factory.ts @@ -6,6 +6,7 @@ import * as market_pb from '../generated/market_pb'; import * as token_message_pb from '../generated/token.message_pb'; import * as transaction_message_pb from '../generated/transaction.message_pb'; import * as auth_message_pb from '../generated/auth_pb'; +import * as auth_pb from "@emeraldpay/api-web/src/generated/auth_pb"; export const classFactory: MessageFactory = (id: string) => { switch (id) { @@ -31,6 +32,8 @@ export const classFactory: MessageFactory = (id: string) => { return new auth_message_pb.AuthRequest(); case 'auth_pb.AuthResponse': return new auth_message_pb.AuthResponse(); + case 'auth_pb.ListTokensRequest': + return new auth_pb.ListTokensRequest(); // Address case 'address_message_pb.DescribeRequest': return new address_message_pb.DescribeRequest(); diff --git a/packages/web/src/EmeraldApi.ts b/packages/web/src/EmeraldApi.ts index 840d26a..e16ed9c 100644 --- a/packages/web/src/EmeraldApi.ts +++ b/packages/web/src/EmeraldApi.ts @@ -5,6 +5,7 @@ import {BlockchainClient} from "./wrapped/BlockchainClient"; import {InsightsClient} from "./wrapped/InsightsClient"; import {MarketClient} from "./wrapped/MarketClient"; import {SierraStatClient} from "./wrapped/SierraStatClient"; +import {AuthClient} from "./wrapped/AuthClient"; export class EmeraldApi { private readonly hostname: string; @@ -46,4 +47,8 @@ export class EmeraldApi { return new SierraStatClient(this.hostname, this.channel, this.credentials); } + get auth(): AuthClient { + return new AuthClient(this.hostname, this.channel, this.credentials); + } + } diff --git a/packages/web/src/__integration-tests__/auth.test.ts b/packages/web/src/__integration-tests__/auth.test.ts new file mode 100644 index 0000000..51906b3 --- /dev/null +++ b/packages/web/src/__integration-tests__/auth.test.ts @@ -0,0 +1,13 @@ +import { EmeraldApi } from '../EmeraldApi'; + +describe('Auth', () => { + + test('is authenticated', async () => { + const client = EmeraldApi.devApi().auth; + + const resp = await client.whoIAm() + + expect(resp.authenticated).toBeTruthy(); + }); + +}); diff --git a/packages/web/src/credentials.ts b/packages/web/src/credentials.ts index d5fee1c..971219c 100644 --- a/packages/web/src/credentials.ts +++ b/packages/web/src/credentials.ts @@ -4,7 +4,7 @@ import {AuthClient} from "./generated/AuthServiceClientPb"; import { AuthRequest as BaseAuthRequest, AuthResponse as BaseAuthResponse, - BaseAuthClient, + CredentialsClient, ConvertAuth, RefreshRequest as BaseRefreshRequest } from "@emeraldpay/api"; import {classFactory} from "./wrapped/Factory"; @@ -35,7 +35,7 @@ class WebHeaders implements Headers { } } -class WebAuthClient implements BaseAuthClient { +class WebAuthClient implements CredentialsClient { private readonly client: AuthClient; private readonly convert = new ConvertAuth(classFactory); diff --git a/packages/web/src/wrapped/AuthClient.ts b/packages/web/src/wrapped/AuthClient.ts new file mode 100644 index 0000000..f96aaf1 --- /dev/null +++ b/packages/web/src/wrapped/AuthClient.ts @@ -0,0 +1,45 @@ +import {AuthRequest, AuthResponse, + CredentialsClient, + ConvertAuth, + ListTokensRequest, + ListTokensResponse, + RefreshRequest, + WhoIAmResponse, + publishToPromise, + readOnce +} from "@emeraldpay/api"; +import {callPromise, WebChannel} from "../channel"; +import * as auth_rpc from '../generated/AuthServiceClientPb'; +import * as auth_pb from "../generated/auth_pb"; +import {classFactory} from "./Factory"; +import {CredentialsContext} from "../credentials"; + +export class AuthClient { + readonly client: auth_rpc.AuthClient; + readonly channel: WebChannel; + readonly retries: number; + + private readonly convert = new ConvertAuth(classFactory); + + constructor(hostname: string, channel: WebChannel, credentials: CredentialsContext, retries = 3) { + this.client = new auth_rpc.AuthClient(hostname, null, credentials.options); + this.channel = channel; + this.retries = retries; + } + + whoIAm(): Promise { + const mapper = this.convert.whoIAmResponse; + + const call = callPromise(this.client.whoAmI.bind(this.client), mapper); + return publishToPromise(readOnce(this.channel, call, new auth_pb.WhoAmIRequest(), this.retries)); + } + + listTokens(req: ListTokensRequest): Promise { + const request = this.convert.listTokensRequest(req); + const mapper = this.convert.listTokensResponse; + + const call = callPromise(this.client.listTokens.bind(this.client), mapper); + return publishToPromise(readOnce(this.channel, call, request, this.retries)); + } + +} diff --git a/packages/web/src/wrapped/Factory.ts b/packages/web/src/wrapped/Factory.ts index 199afa2..dfeaa2d 100644 --- a/packages/web/src/wrapped/Factory.ts +++ b/packages/web/src/wrapped/Factory.ts @@ -37,6 +37,9 @@ export const classFactory: MessageFactory = (id: string) => { if (id == 'auth_pb.AuthResponse') { return new auth_pb.AuthResponse(); } + if (id == 'auth_pb.ListTokensRequest') { + return new auth_pb.ListTokensRequest(); + } // Blockchain if (id == "blockchain_pb.NativeCallRequest") { return new blockchain_pb.NativeCallRequest();