Skip to content

Commit

Permalink
solution: auth api for tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
splix committed Jul 3, 2024
1 parent 1ef80a3 commit 467959e
Show file tree
Hide file tree
Showing 15 changed files with 196 additions and 14 deletions.
2 changes: 1 addition & 1 deletion api-definitions
8 changes: 6 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -62,6 +65,7 @@ export {
SingleAddress,
isAsset,
isErc20Asset,
UUID,
} from './typesCommon';
export { MessageFactory } from './typesConvert';
export {
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/signature.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
BaseAuthClient,
CredentialsClient,
RefreshToken,
SecretToken,
AuthRequest,
Expand Down Expand Up @@ -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[];

Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
75 changes: 74 additions & 1 deletion packages/core/src/typesAuth.ts
Original file line number Diff line number Diff line change
@@ -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}$');

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<AuthResponse>;
refresh(req: RefreshRequest): Promise<AuthResponse>;
}
2 changes: 2 additions & 0 deletions packages/core/src/typesCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions packages/node/src/EmeraldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -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);
}
}
8 changes: 8 additions & 0 deletions packages/node/src/__integration-tests__/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
4 changes: 2 additions & 2 deletions packages/node/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import {
AuthRequest as BaseAuthRequest,
AuthResponse as BaseAuthResponse,
BaseAuthClient,
CredentialsClient,
ConvertAuth,
RefreshRequest as BaseRefreshRequest
} from "@emeraldpay/api";
Expand All @@ -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);

Expand Down
23 changes: 22 additions & 1 deletion packages/node/src/wrapped/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
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';
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');

Expand All @@ -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(' ');

Expand All @@ -39,4 +50,14 @@ export class AuthClient {
return publishToPromise(readOnce(this.channel, call, request, this.retries));
}

whoIAm(): Promise<WhoIAmResponse> {
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<ListTokensResponse> {
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));
}
}
3 changes: 3 additions & 0 deletions packages/node/src/wrapped/Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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();
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/EmeraldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

}
13 changes: 13 additions & 0 deletions packages/web/src/__integration-tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});

});
4 changes: 2 additions & 2 deletions packages/web/src/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);

Expand Down
45 changes: 45 additions & 0 deletions packages/web/src/wrapped/AuthClient.ts
Original file line number Diff line number Diff line change
@@ -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<WhoIAmResponse> {
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<ListTokensResponse> {
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));
}

}
3 changes: 3 additions & 0 deletions packages/web/src/wrapped/Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit 467959e

Please sign in to comment.