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

feat(client): add auth logout method #74

Merged
merged 3 commits into from
Apr 15, 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
32 changes: 31 additions & 1 deletion src/api_client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { TxInfoReceipt } from '../core/txQuery';
import { Message, MsgData, MsgReceipt } from '../core/message';
import { kwilDecode } from '../utils/rlp';
import { BroadcastSyncType, BytesEncodingStatus, EnvironmentType } from '../core/enums';
import { AuthInfo, AuthSuccess, AuthenticatedBody } from '../core/auth';
import { AuthInfo, AuthSuccess, AuthenticatedBody, LogoutResponse } from '../core/auth';
import { AxiosResponse } from 'axios';

export default class Client extends Api {
Expand Down Expand Up @@ -101,6 +101,36 @@ export default class Client extends Api {
}
}

public async logout<T extends EnvironmentType>(): Promise<GenericResponse<LogoutResponse<T>>> {
const res = await super.get<LogoutResponse<T>>(`/logout`);
checkRes(res);

// if we are in nodejs, we need to return the cookie
if(typeof window === 'undefined') {
const cookie = res.headers['set-cookie'];
if(!cookie) {
throw new Error('No cookie receiveed from gateway. An error occured with authentication.');
}

return {
status: res.status,
// @ts-ignore
data: {
result: res.data.result,
cookie: cookie[0],
},
}
}

// if we are in the browser, we don't need to return the cookie
return {
status: res.status,
data: {
result: res.data.result,
},
}
}

public async getAccount(owner: Uint8Array): Promise<GenericResponse<Account>> {
const urlSafeB64 = base64UrlEncode(bytesToBase64(owner));
const unconfirmedNonce = this.unconfirmedNonce ? '?status=1' : '';
Expand Down
64 changes: 64 additions & 0 deletions src/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Client from '../api_client/client';
import { GenericResponse } from '../core/resreq';
import { KwilSigner } from '../core/kwilSigner';
import { AuthSuccess, AuthenticatedBody, LogoutResponse, composeAuthMsg, removeTrailingSlash, verifyAuthProperties } from '../core/auth';
import { BytesEncodingStatus, EnvironmentType } from '../core/enums';
import { objects } from '../utils/objects';
import { executeSign } from '../core/signature';
import { stringToBytes } from '../utils/serial';
import { bytesToBase64 } from '../utils/base64';

export class Auth<T extends EnvironmentType> {
private client: Client;
private kwilProvider: string;
private chainId: string;

public constructor(client: Client, kwilProvider: string, chainId: string) {
this.client = client;
this.kwilProvider = kwilProvider;
this.chainId = chainId;
}

/**
* Authenticates a user with the Kwil Gateway (KGW). This is required to execute view actions with the `@kgw(authn='true')` annotation.
*
* This method should only be used if your Kwil Network is using the Kwil Gateway.
*
* @param {KwilSigner} signer - The signer for the authentication.
* @returns A promise that resolves to the authentication success or failure.
*/
public async authenticate(signer: KwilSigner): Promise<GenericResponse<AuthSuccess<T>>> {
const authParam = await this.client.getAuthenticate();

const authProperties = objects.requireNonNil(
authParam.data,
'something went wrong retrieving auth info from KGW'
);

const domain = removeTrailingSlash(this.kwilProvider);
const version = '1';

verifyAuthProperties(authProperties, domain, version, this.chainId);

const msg = composeAuthMsg(authProperties, domain, version, this.chainId);

const signature = await executeSign(stringToBytes(msg), signer.signer, signer.signatureType);

const authBody: AuthenticatedBody<BytesEncodingStatus.BASE64_ENCODED> = {
nonce: authProperties.nonce,
sender: bytesToBase64(signer.identifier),
signature: {
signature_bytes: bytesToBase64(signature),
signature_type: signer.signatureType,
},
};

const res = await this.client.postAuthenticate(authBody);

return res;
}

public async logout(): Promise<GenericResponse<LogoutResponse<T>>> {
return await this.client.logout();
}
}
81 changes: 31 additions & 50 deletions src/client/kwil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@ import { Database, DeployBody, DropBody, SelectQuery } from '../core/database';
import { BaseTransaction, TxReceipt } from '../core/tx';
import { Account, ChainInfo, DatasetInfo } from '../core/network';
import { ActionBuilderImpl } from '../builders/action_builder';
import { base64ToBytes, bytesToBase64 } from '../utils/base64';
import { base64ToBytes } from '../utils/base64';
import { DBBuilderImpl } from '../builders/db_builder';
import { NonNil } from '../utils/types';
import { ActionBuilder, DBBuilder } from '../core/builders';
import { Cache } from '../utils/cache';
import { TxInfoReceipt } from '../core/txQuery';
import { BaseMessage, Message, MsgReceipt } from '../core/message';
import { BroadcastSyncType, BytesEncodingStatus, EnvironmentType, PayloadType } from '../core/enums';
import { hexToBytes, stringToBytes } from '../utils/serial';
import {
BroadcastSyncType,
BytesEncodingStatus,
EnvironmentType,
PayloadType,
} from '../core/enums';
import { hexToBytes } from '../utils/serial';
import { isNearPubKey, nearB58ToHex } from '../utils/keys';
import { ActionBody, ActionInput, Entries, resolveActionInputs } from '../core/action';
import { ActionBody, resolveActionInputs } from '../core/action';
import { KwilSigner } from '../core/kwilSigner';
import { objects } from '../utils/objects';
import { executeSign } from '../core/signature';
import { AuthSuccess, composeAuthMsg } from '../core/auth';
import { wrap } from './intern';
import { Funder } from '../funder/funder';
import { Auth } from '../auth/auth';

/**
* The main class for interacting with the Kwil network.
Expand All @@ -35,6 +37,7 @@ export abstract class Kwil<T extends EnvironmentType> {
//cache schemas
private schemas: Cache<GenericResponse<Database>>;
public funder: Funder<T>;
public auth: Auth<T>;

protected constructor(opts: Config) {
this.client = new Client({
Expand All @@ -57,6 +60,9 @@ export abstract class Kwil<T extends EnvironmentType> {
// create funder
this.funder = new Funder<T>(this, this.client, this.chainId);

// create authenticate
this.auth = new Auth<T>(this.client, this.kwilProvider, this.chainId);

//create a wrapped symbol of estimateCost method
wrap(this, this.client.estimateCost.bind(this.client));
}
Expand Down Expand Up @@ -203,7 +209,10 @@ export abstract class Kwil<T extends EnvironmentType> {

const transaction = await tx.buildTx();

return await this.client.broadcast(transaction, synchronous ? BroadcastSyncType.COMMIT : undefined);
return await this.client.broadcast(
transaction,
synchronous ? BroadcastSyncType.COMMIT : undefined
);
}

/**
Expand Down Expand Up @@ -232,7 +241,10 @@ export abstract class Kwil<T extends EnvironmentType> {

const transaction = await tx.buildTx();

return await this.client.broadcast(transaction, synchronous ? BroadcastSyncType.COMMIT : undefined);
return await this.client.broadcast(
transaction,
synchronous ? BroadcastSyncType.COMMIT : undefined
);
}

/**
Expand All @@ -253,15 +265,18 @@ export abstract class Kwil<T extends EnvironmentType> {
.payload({ dbid: dropBody.dbid })
.publicKey(kwilSigner.identifier)
.signer(kwilSigner.signer, kwilSigner.signatureType)
.chainId(this.chainId)
.chainId(this.chainId);

if(dropBody.nonce) {
tx = tx.nonce(dropBody.nonce);
}
if (dropBody.nonce) {
tx = tx.nonce(dropBody.nonce);
}

const transaction = await tx.buildTx();
const transaction = await tx.buildTx();

return await this.client.broadcast(transaction, synchronous ? BroadcastSyncType.COMMIT : undefined);
return await this.client.broadcast(
transaction,
synchronous ? BroadcastSyncType.COMMIT : undefined
);
}

/**
Expand Down Expand Up @@ -345,38 +360,4 @@ export abstract class Kwil<T extends EnvironmentType> {
public async ping(): Promise<GenericResponse<string>> {
return await this.client.ping();
}

/**
* Authenticates a user with the Kwil Gateway (KGW). This is required to execute mustsign view actions.
*
* This method should only be used if your Kwil Network is using the Kwil Gateway.
*
* @param {KwilSigner} signer - The signer for the authentication.
* @returns A promise that resolves to the authentication success or failure.
*/
protected async authenticate(signer: KwilSigner): Promise<GenericResponse<AuthSuccess<T>>> {
const authParam = await this.client.getAuthenticate();

const authProperties = objects.requireNonNil(
authParam.data,
'something went wrong retrieving auth info from KGW'
);

const msg = composeAuthMsg(authProperties, this.kwilProvider, '1', this.chainId);

const signature = await executeSign(stringToBytes(msg), signer.signer, signer.signatureType);

const authBody = {
nonce: authProperties.nonce,
sender: bytesToBase64(signer.identifier),
signature: {
signature_bytes: bytesToBase64(signature),
signature_type: signer.signatureType,
},
};

const res = await this.client.postAuthenticate(authBody);

return res;
}
}
10 changes: 7 additions & 3 deletions src/client/node/nodeKwil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export class NodeKwil extends Kwil<EnvironmentType.NODE> {
* Calls a Kwil node. This can be used to execute read-only ('view') actions on Kwil.
* If the action requires authentication in the Kwil Gateway, the kwilSigner should be passed. If the user is not authenticated, the user will be prompted to authenticate.
*
* @param actionBody - The body of the action to send. This should use the `ActionBody` interface.
* @param kwilSigner (optional) - Caller should be passed if the action requires authentication OR if the action uses a `@caller` contextual variable. If `@caller` is used and authentication is not required, the user will not be prompted to authenticate; however, the user's identifier will be passed as the sender.
* @param {ActionBody} actionBody - The body of the action to send. This should use the `ActionBody` interface.
* @param {KwilSigner} kwilSigner (optional) - KwilSigner should be passed if the action requires authentication OR if the action uses a `@caller` contextual variable. If `@caller` is used and authentication is not required, the user will not be prompted to authenticate; however, the user's identifier will be passed as the sender.
* @returns A promise that resolves to the receipt of the message.
*/
public async call(
Expand Down Expand Up @@ -68,11 +68,15 @@ export class NodeKwil extends Kwil<EnvironmentType.NODE> {

let res = await this.client.call(message);

// if we get a 401, we need to return the response so we can try to authenticate
if(res.status === 401) {
if (kwilSigner) {
const authRes = await this.authenticate(kwilSigner);
const authRes = await this.auth.authenticate(kwilSigner);
if(authRes.status === 200) {
// create a new client with the new cookie
this.client = new Client(this.config, authRes.data?.cookie);

// call the message again
res = await this.client.call(message);
}
} else {
Expand Down
6 changes: 3 additions & 3 deletions src/client/web/webKwil.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Config } from '../../api_client/config';
import { ActionBuilderImpl } from '../../builders/action_builder';
import { ActionBody, ActionInput, Entries } from '../../core/action';
import { AuthSuccess } from '../../core/auth';
import { EnvironmentType } from '../../core/enums';
import { KwilSigner } from '../../core/kwilSigner';
import { BaseMessage, Message, MsgReceipt } from '../../core/message';
Expand All @@ -18,7 +17,7 @@ export class WebKwil extends Kwil<EnvironmentType.BROWSER> {
* If the action requires authentication in the Kwil Gateway, the kwilSigner should be passed. If the user is not authenticated, the user will be prompted to authenticate.
*
* @param {ActionBody} actionBody - The body of the action to send. This should use the `ActionBody` interface.
* @param {KwilSigner} kwilSigner (optional) - Caller should be passed if the action requires authentication OR if the action uses a `@caller` contextual variable. If `@caller` is used and authentication is not required, the user will not be prompted to authenticate; however, the user's identifier will be passed as the sender.
* @param {KwilSigner} kwilSigner (optional) - KwilSigner should be passed if the action requires authentication OR if the action uses a `@caller` contextual variable. If `@caller` is used and authentication is not required, the user will not be prompted to authenticate; however, the user's identifier will be passed as the sender.
* @returns A promise that resolves to the receipt of the message.
*/
public async call(
Expand Down Expand Up @@ -66,9 +65,10 @@ export class WebKwil extends Kwil<EnvironmentType.BROWSER> {

let res = await this.client.call(message);

// if we get a 401 and autoAuthenticate is true, try to authenticate and call again
if(res.status === 401) {
if (kwilSigner) {
const authRes = await this.authenticate(kwilSigner);
const authRes = await this.auth.authenticate(kwilSigner);
if(authRes.status === 200) {
res = await this.client.call(message);
}
Expand Down
39 changes: 39 additions & 0 deletions src/core/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export interface AuthInfo {
statement: string;
issue_at: string;
expiration_time: string;
chain_id: string;
domain: string;
version: string;
uri: string;
}

export type AuthSuccess<T extends EnvironmentType> = T extends EnvironmentType.BROWSER ? BrowserAuthSuccess : NodeAuthSuccess;
Expand All @@ -28,6 +32,17 @@ interface NodeAuthSuccess {
cookie?: string;
}

export type LogoutResponse<T extends EnvironmentType> = T extends EnvironmentType.BROWSER ? LogoutResponseWeb : LogoutResponseNode;

interface LogoutResponseWeb {
result: string;
}

interface LogoutResponseNode {
result: string;
cookie?: string;
}

export function composeAuthMsg(
authParam: AuthInfo,
domain: string,
Expand All @@ -50,3 +65,27 @@ export function composeAuthMsg(
msg += `Expiration Time: ${authParam.expiration_time}\n`;
return msg;
}

export function removeTrailingSlash(url: string): string {
if (url.endsWith('/')) {
return url.slice(0, -1);
}
return url;
}

export function verifyAuthProperties(
authParm: AuthInfo,
domain: string,
version: string,
chainId: string,
): void {
if (authParm.domain && authParm.domain!== domain) {
throw new Error(`Domain mismatch: ${authParm.domain} !== ${domain}`);
}
if (authParm.version && authParm.version !== version) {
throw new Error(`Version mismatch: ${authParm.version} !== ${version}`);
}
if (authParm.chain_id && authParm.chain_id !== chainId) {
throw new Error(`Chain ID mismatch: ${authParm.chain_id} !== ${chainId}`);
}
}
Loading
Loading