Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

solution: auth api for tokens #42

Merged
merged 2 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
}
}
2 changes: 2 additions & 0 deletions packages/node/src/wrapped/Factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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_message_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
Loading