Skip to content

Commit

Permalink
fix(cacheServerClient): log in if refresh token is expired
Browse files Browse the repository at this point in the history
Also remove SignerFactory as it is simpler to
have getPublicKeyAndIdentityToken() as a util
  • Loading branch information
jrhender committed May 8, 2021
1 parent 7c2c765 commit 557a767
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 132 deletions.
5 changes: 4 additions & 1 deletion src/cacheServerClient/ICacheServerClient.ts
@@ -1,5 +1,6 @@
import { IDIDDocument } from "@ew-did-registry/did-resolver-interface";
import { IClaimIssuance, IClaimRejection, IClaimRequest } from "../iam";
import { IPubKeyAndIdentityToken } from "../utils/getPublicKeyAndIdentityToken";
import {
Asset,
AssetHistory,
Expand All @@ -15,7 +16,9 @@ import {
} from "./cacheServerClient.types";

export interface ICacheServerClient {
login: (claim: string) => Promise<void>;
pubKeyAndIdentityToken: IPubKeyAndIdentityToken | undefined;
login: () => Promise<void>;
isAuthEnabled: () => boolean;
getRoleDefinition: ({ namespace }: { namespace: string }) => Promise<IRoleDefinition>;
getOrgDefinition: ({ namespace }: { namespace: string }) => Promise<IOrganizationDefinition>;
getAppDefinition: ({ namespace }: { namespace: string }) => Promise<IAppDefinition>;
Expand Down
39 changes: 30 additions & 9 deletions src/cacheServerClient/cacheServerClient.ts
Expand Up @@ -16,29 +16,38 @@ import { IDIDDocument } from "@ew-did-registry/did-resolver-interface";

import { ICacheServerClient } from "./ICacheServerClient";
import { isBrowser } from "../utils/isBrowser";
import { getPublicKeyAndIdentityToken, IPubKeyAndIdentityToken } from "../utils/getPublicKeyAndIdentityToken";
import { Signer } from "ethers";

export interface CacheServerClientOptions {
url: string;
cacheServerSupportsAuth?: boolean;
}
export class CacheServerClient implements ICacheServerClient {
public pubKeyAndIdentityToken: IPubKeyAndIdentityToken | undefined;
private httpClient: AxiosInstance;
private isAlreadyFetchingAccessToken = false;
private failedRequests: Array<(token?: string) => void> = [];
private authEnabled: boolean;
private isBrowser: boolean;
private refresh_token: string | undefined;
private readonly signer: Signer

constructor({ url, cacheServerSupportsAuth = true }: CacheServerClientOptions) {
constructor({ url, cacheServerSupportsAuth = true }: CacheServerClientOptions, signer: Signer) {
this.httpClient = axios.create({
baseURL: url,
withCredentials: true
});
this.httpClient.interceptors.response.use(function(response: AxiosResponse) {
this.httpClient.interceptors.response.use(function (response: AxiosResponse) {
return response;
}, this.handleUnauthorized);
this.authEnabled = cacheServerSupportsAuth;
this.isBrowser = isBrowser();
this.signer = signer;
}

isAuthEnabled() {
return this.authEnabled;
}

async handleRefreshToken() {
Expand Down Expand Up @@ -86,9 +95,13 @@ export class CacheServerClient implements ICacheServerClient {
return Promise.reject(error);
};

async login(identityToken: string) {
if (!this.authEnabled) return;
async login() {
// Simple test to check if logged in or no. TODO: have dedicated endpoint on the cache-server
// If receive unauthorized response, expect that refreshToken() will be called
await this.getRoleDefinition({ namespace: "testing.if.logged.in" });
}

private async performlogin(identityToken: string) {
const {
data: { refreshToken, token }
} = await this.httpClient.post<{ token: string; refreshToken: string }>("/login", {
Expand All @@ -99,12 +112,20 @@ export class CacheServerClient implements ICacheServerClient {
this.httpClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
this.refresh_token = refreshToken;
}
return { token, refreshToken };
}

async refreshToken() {
const { data } = await this.httpClient.get<{ token: string; refreshToken: string }>(
`/refresh_token${this.isBrowser ? "" : `?refresh_token=${this.refresh_token}`}`
);
private async refreshToken() {
let data: { token: string, refreshToken: string };
try {
({ data } = await this.httpClient.get<{ token: string; refreshToken: string }>(
`/refresh_token${this.isBrowser ? "" : `?refresh_token=${this.refresh_token}`}`
));
}
catch {
this.pubKeyAndIdentityToken = await getPublicKeyAndIdentityToken(this.signer);
data = await this.performlogin(this.pubKeyAndIdentityToken.identityToken);
}
return data;
}

Expand Down Expand Up @@ -233,7 +254,7 @@ export class CacheServerClient implements ICacheServerClient {
});
return data;
}

async getClaimsBySubject({
did,
isAccepted,
Expand Down
1 change: 1 addition & 0 deletions src/errors/ErrorMessages.ts
Expand Up @@ -18,6 +18,7 @@ export enum ERROR_MESSAGES {
WALLET_TYPE_NOT_PROVIDED = "A wallet provider type or a private key must be provided",
ENS_REGISTRY_CONTRACT_NOT_INITIALIZED = "ENS Registry contract not initialized",
PUBLIC_KEY_NOT_RECOVERED = "Public key not recovered",
UNABLE_TO_OBTAIN_PUBLIC_KEY = "Enable to obtain public key",
ORG_WITH_APPS = "You are not able to remove organization with registered apps",
ORG_WITH_ROLES = "You are not able to remove organization with registered roles",
APP_WITH_ROLES = "You are not able to remove application with registered roles",
Expand Down
2 changes: 1 addition & 1 deletion src/iam.ts
Expand Up @@ -196,7 +196,7 @@ export class IAM extends IAMBase {
connected: this.isConnected() || false,
userClosedModal: false,
didDocument: await this.getDidDocument(),
identityToken: this._didSigner?.identityToken,
identityToken: this._identityToken,
realtimeExchangeConnected: Boolean(this._natsConnection)
};
}
Expand Down
59 changes: 40 additions & 19 deletions src/iam/iam-base.ts
Expand Up @@ -23,9 +23,8 @@ import {
import difference from "lodash.difference";
import { TransactionOverrides } from "../../ethers";
import detectMetamask from "@metamask/detect-provider";
import { Owner as IdentityOwner } from "../signer/Signer";
import { Owner as IdentityOwner, Owner } from "../signer/Owner";
import { WalletProvider } from "../types/WalletProvider";
import { SignerFactory } from "../signer/SignerFactory";
import { CacheServerClient } from "../cacheServerClient/cacheServerClient";
import {
emptyAddress,
Expand All @@ -44,6 +43,7 @@ import { WalletConnectService } from "../walletconnect/WalletConnectService";
import { OfferableIdentityFactory } from "../../ethers/OfferableIdentityFactory";
import { IdentityManagerFactory } from '../../ethers/IdentityManagerFactory';
import { IdentityManager } from '../../ethers/IdentityManager';
import { getPublicKeyAndIdentityToken, IPubKeyAndIdentityToken } from "../utils/getPublicKeyAndIdentityToken";

const { hexlify, bigNumberify } = utils;
const { JsonRpcProvider } = providers;
Expand Down Expand Up @@ -91,6 +91,7 @@ export class IAMBase {
protected _signer: Signer | undefined;
protected _safeAddress: string | undefined;
protected _didSigner: IdentityOwner | undefined;
protected _identityToken: string | undefined;
protected _transactionOverrides: TransactionOverrides = {};
protected _providerType: WalletProvider | undefined;
protected _publicKey: string | undefined;
Expand Down Expand Up @@ -145,10 +146,6 @@ export class IAMBase {

if (this._runningInBrowser) {
this._providerType = localStorage.getItem(WALLET_PROVIDER) as WalletProvider;
const publicKey = localStorage.getItem(PUBLIC_KEY);
if (publicKey) {
this._publicKey = publicKey;
}
}

this._walletConnectService = new WalletConnectService(bridgeUrl, infuraId, ewKeyManagerUrl);
Expand All @@ -171,15 +168,36 @@ export class IAMBase {
await this.setupMessaging();
}
if (this._signer) {
this._didSigner = await SignerFactory.create({
provider: this._provider,
signer: this._signer,
publicKey: this._publicKey
});
this._publicKey = this._didSigner.publicKey;
this._didSigner.identityToken &&
(await this.loginToCacheServer(this._didSigner.identityToken));
await this.setAddress();
let identityToken: string | undefined;
let publicKey: string | undefined;

// Login to the cache-server may require signature of identityToken
// from which we can derive a publicKey
const fromCacheLogin = await this.loginToCacheServer();
publicKey = fromCacheLogin?.publicKey;
identityToken = fromCacheLogin?.identityToken;

// We need a pubKey to create DID document.
// So if didn't get one from cache server login, need to get in some other way.
if (!publicKey && this._runningInBrowser) {
// Check local storage.
// This is to that publicKey can be reused when refreshing the page
const savedPublicKey = localStorage.getItem(PUBLIC_KEY);
if (savedPublicKey) {
publicKey = savedPublicKey;
}
}
if (!publicKey) {
({ publicKey, identityToken } = await getPublicKeyAndIdentityToken(this._signer));
}
if (!publicKey) {
throw new Error(ERROR_MESSAGES.UNABLE_TO_OBTAIN_PUBLIC_KEY);
}
this._didSigner = new Owner(this._signer, this._provider, publicKey);
this._identityToken = identityToken;
this._publicKey = publicKey;

identityToken && await this.setAddress();
this.setDid();
await this.setDocument();
this.setClaims();
Expand Down Expand Up @@ -336,10 +354,13 @@ export class IAMBase {
this._signer = undefined;
}

private async loginToCacheServer(token: string) {
if (this._cacheClient) {
await this._cacheClient.login(token);
private async loginToCacheServer(): Promise<IPubKeyAndIdentityToken | undefined> {
if (this._signer && this._cacheClient && this._cacheClient.isAuthEnabled()) {
await this._cacheClient.login();
// Expect that if login generated pubKey&IdToken, then will be accessible as property
return this._cacheClient.pubKeyAndIdentityToken;
}
return undefined;
}

protected async setAddress() {
Expand Down Expand Up @@ -632,7 +653,7 @@ export class IAMBase {

const cacheOptions = cacheServerClientOptions[chainId];

cacheOptions.url && (this._cacheClient = new CacheServerClient(cacheOptions));
cacheOptions.url && (this._cacheClient = new CacheServerClient(cacheOptions, this._signer));

this._messagingOptions = messagingOptions[chainId];
}
Expand Down
1 change: 0 additions & 1 deletion src/signer/Signer.ts → src/signer/Owner.ts
Expand Up @@ -7,7 +7,6 @@ export class Owner extends Signer implements IdentityOwner {
private signer: Signer,
public provider: providers.Provider,
public publicKey: string,
public identityToken?: string,
public privateKey?: string
) {
super();
Expand Down
75 changes: 0 additions & 75 deletions src/signer/SignerFactory.ts

This file was deleted.

60 changes: 60 additions & 0 deletions src/utils/getPublicKeyAndIdentityToken.ts
@@ -0,0 +1,60 @@
import base64url from "base64url";
import { Signer, utils } from "ethers";
import { ERROR_MESSAGES } from "../errors";

const {
arrayify,
computeAddress,
computePublicKey,
getAddress,
hashMessage,
keccak256,
recoverPublicKey
} = utils;

export interface IPubKeyAndIdentityToken {
publicKey: string;
identityToken: string;
}

export async function getPublicKeyAndIdentityToken(signer: Signer): Promise<IPubKeyAndIdentityToken> {
if (signer) {
const header = {
alg: "ES256",
typ: "JWT"
};
const encodedHeader = base64url(JSON.stringify(header));
const address = await signer.getAddress();
const payload = {
iss: `did:ethr:${address}`,
claimData: {
blockNumber: await signer.provider?.getBlockNumber()
}
};

const encodedPayload = base64url(JSON.stringify(payload));
const token = `0x${Buffer.from(`${encodedHeader}.${encodedPayload}`).toString("hex")}`;
// arrayification is necessary for WalletConnect signatures to work. eth_sign expects message in bytes: https://docs.walletconnect.org/json-rpc-api-methods/ethereum#eth_sign
// keccak256 hash is applied for Metamask to display a coherent hex value when signing
const message = arrayify(keccak256(token));
const sig = await signer.signMessage(message);
const recoverValidatedPublicKey = (signedMessage: Uint8Array): string | undefined => {
const publicKey = recoverPublicKey(signedMessage, sig);
if (getAddress(address) === getAddress(computeAddress(publicKey))) {
return computePublicKey(publicKey, true).slice(2);
}
return undefined;
};

// Computation of the digest in order to recover the public key under the assumption
// that signature was performed as per the eth_sign spec (https://eth.wiki/json-rpc/API#eth_sign)
// In the event that the wallet isn't prefixing & hashing message as per spec, attempt recovery without digest
const digest = arrayify(hashMessage(message));
const publicKey = recoverValidatedPublicKey(digest) ?? recoverValidatedPublicKey(message);
if (publicKey) {
return { publicKey, identityToken: `${encodedHeader}.${encodedPayload}.${base64url(sig)}` };
}
throw new Error(ERROR_MESSAGES.PUBLIC_KEY_NOT_RECOVERED);
}
throw new Error(ERROR_MESSAGES.SIGNER_NOT_INITIALIZED);
}

0 comments on commit 557a767

Please sign in to comment.