Skip to content

Commit

Permalink
Merge pull request #43 from emeraldpay/feature/temp_auth
Browse files Browse the repository at this point in the history
Feature/temp auth
  • Loading branch information
splix authored Sep 11, 2024
2 parents b4e896b + 1877dae commit 7f5ce57
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 9 deletions.
2 changes: 1 addition & 1 deletion api-definitions
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export {
ConvertAuth,
ListTokensRequest, ListTokensResponse, TokenDetails,
WhoIAmResponse, IAmAuthenticated, IAMUnauthenticated,
IssueTokenRequest, IssuedTokenResponse,
} from './typesAuth';
export {
AddressBalance,
Expand Down
70 changes: 69 additions & 1 deletion packages/core/src/typesAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ export type TokenId = UUID;
export type OrganizationId = UUID;
export type ProjectId = UUID;

/**
* A token to authenticate with API. Format: emrld_ + 38 random alphanumeric characters.
* When it's a one-time auth token, it starts with "emrld_temp_"
*/
export type SecretToken = string;
const SecretTokenRegex = new RegExp('^emrld_[0-9a-zA-Z]{38}$');

const SecretTokenRegex = new RegExp('^emrld_(temp_)?[0-9a-zA-Z]{38}$');

export function isSecretToken(token: string): token is SecretToken {
return SecretTokenRegex.test(token);
Expand Down Expand Up @@ -78,6 +83,43 @@ export type TokenDetails = {
createdAt: Date,
}

export type IssueTokenRequest = {
/**
* Type of the token to issue
* - "temp" - one-time token
*/
type: "temp",

/**
* The scopes to be used for the token. Cannot be larger that the current authenticated scopes.
*/
scopes?: string[],

/**
* The user id associated with the token, i.e. who will use the token.
* There are restrictions who can set this. In short the token issuer must be in control of the user / impersonate the user.
*/
userId?: string,

/**
* A timestamp when it expires.
* For a temp one-time token, by default, it's 1 Day and cannot be more than 30 days.
*/
expireAt?: Date,
}

export type IssuedTokenResponse = {
/**
* The issued token
*/
secret: SecretToken,

/**
* When the token expires
*/
expiresAt: Date,
}

export class ConvertAuth {
private readonly factory: MessageFactory;

Expand Down Expand Up @@ -157,6 +199,32 @@ export class ConvertAuth {
}
}

public issueTokenRequest(req: IssueTokenRequest): auth_pb.IssueTokenRequest {
const result: auth_pb.IssueTokenRequest = this.factory('auth_pb.IssueTokenRequest');
if (req.type == "temp") {
result.setType(auth_pb.IssueTokenRequest.TokenType.TEMP);
}
if (req.scopes) {
result.setScopesList(req.scopes);
}
if (req.userId) {
result.setUserId(req.userId);
}
if (req.expireAt) {
result.setExpireAt(req.expireAt.getTime());
}
return result;
}

public issuedTokenResponse(res: auth_pb.IssuedTokenResponse): IssuedTokenResponse {
return {
secret: res.getAuthSecret(),

// when backed returns 0, it means "never expires" (though it almost always has some actual date).
// Let's set 10 years from now here, to avoid unnecessary null checks
expiresAt: res.getExpiresAt() > 0 ? new Date(res.getExpiresAt()) : new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000),
}
}
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/node/src/EmeraldApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export class EmeraldApi {
this.hostname = hostname;
}

static devApi(credentials?: ChannelCredentials): EmeraldApi {
static devApi(token?: SecretToken | undefined, credentials?: ChannelCredentials): EmeraldApi {
// a dev token with access only from the internal network
let devToken = 'emrld_8ntrHbZN67DF8TWKgCMO1I9nSaMG0cpoMhj3GP';
let devToken = token ?? 'emrld_8ntrHbZN67DF8TWKgCMO1I9nSaMG0cpoMhj3GP';
return new EmeraldApi('api.emeraldpay.dev:443', devToken, credentials);
}

Expand Down
22 changes: 20 additions & 2 deletions packages/node/src/__integration-tests__/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Blockchain, AuthDetails, JwtSignature, EmeraldAuthenticator, TokenStatus} from '@emeraldpay/api';
import {Blockchain, AuthDetails, JwtSignature, EmeraldAuthenticator, TokenStatus, isSecretToken} from '@emeraldpay/api';
import { emeraldCredentials } from '../credentials';
import { EmeraldApi } from '../EmeraldApi';

Expand All @@ -16,6 +16,24 @@ describe('Auth', () => {
expect(balance).toBeDefined();
});

test('auth with temp token', async () => {
const client = EmeraldApi.devApi().auth();

const token = await client.issueToken({
type: 'temp',
});

expect(token).toBeDefined();
expect(token.secret).toBeDefined();
expect(token.expiresAt).toBeDefined();
expect(isSecretToken(token.secret)).toBeTruthy();

const client2 = EmeraldApi.devApi(token.secret).auth();

const me = await client2.whoIAm();
expect(me.authenticated).toBeTruthy();
});

test('is authenticated', async () => {
const client = EmeraldApi.devApi().auth();

Expand Down Expand Up @@ -99,7 +117,7 @@ describe('Auth', () => {
credentials.setAuthentication(new FakeAuthentication());
credentials.setListener((...statuses) => ([, tokenStatus] = statuses));

const api = EmeraldApi.devApi(credentials.getChannelCredentials());
const api = EmeraldApi.devApi(null, credentials.getChannelCredentials());

const blockchainClient = api.blockchain();
const marketClient = api.market();
Expand Down
9 changes: 8 additions & 1 deletion packages/node/src/wrapped/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
ConnectionListener, ConvertAuth,
ConnectionListener, ConvertAuth, IssuedTokenResponse, IssueTokenRequest,
ListTokensRequest,
ListTokensResponse,
publishToPromise,
Expand Down Expand Up @@ -60,4 +60,11 @@ export class AuthClient {
const call = callSingle(this.client.listTokens.bind(this.client), this.convert.listTokensResponse);
return publishToPromise(readOnce(this.channel, call, request, this.retries));
}

issueToken(req: IssueTokenRequest): Promise<IssuedTokenResponse> {
const request = this.convert.issueTokenRequest(req);
const call = callSingle(this.client.issueToken.bind(this.client), this.convert.issuedTokenResponse);
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 @@ -33,6 +33,8 @@ export const classFactory: MessageFactory = (id: string) => {
return new auth_message_pb.AuthResponse();
case 'auth_pb.ListTokensRequest':
return new auth_message_pb.ListTokensRequest();
case 'auth_pb.IssueTokenRequest':
return new auth_message_pb.IssueTokenRequest();
// Address
case 'address_message_pb.DescribeRequest':
return new address_message_pb.DescribeRequest();
Expand Down
12 changes: 10 additions & 2 deletions packages/web/src/wrapped/AuthClient.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {AuthRequest, AuthResponse,
import {
AuthRequest, AuthResponse,
CredentialsClient,
ConvertAuth,
ListTokensRequest,
ListTokensResponse,
RefreshRequest,
WhoIAmResponse,
publishToPromise,
readOnce
readOnce, IssueTokenRequest, IssuedTokenResponse
} 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";
import {callSingle} from "@emeraldpay/api-node/lib/channel";

export class AuthClient {
readonly client: auth_rpc.AuthClient;
Expand Down Expand Up @@ -42,4 +44,10 @@ export class AuthClient {
return publishToPromise(readOnce(this.channel, call, request, this.retries));
}

issueToken(req: IssueTokenRequest): Promise<IssuedTokenResponse> {
const request = this.convert.issueTokenRequest(req);
const call = callSingle(this.client.issueToken.bind(this.client), this.convert.issuedTokenResponse);
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 @@ -40,6 +40,9 @@ export const classFactory: MessageFactory = (id: string) => {
if (id == 'auth_pb.ListTokensRequest') {
return new auth_pb.ListTokensRequest();
}
if (id == 'auth_pb.IssueTokenRequest') {
return new auth_pb.IssueTokenRequest();
}
// Blockchain
if (id == "blockchain_pb.NativeCallRequest") {
return new blockchain_pb.NativeCallRequest();
Expand Down

0 comments on commit 7f5ce57

Please sign in to comment.