Skip to content

Commit

Permalink
Validate user-provided AuthSignature (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-roslaniec authored Jul 3, 2024
2 parents e9ce886 + 6638c5d commit b3d724d
Show file tree
Hide file tree
Showing 24 changed files with 276 additions and 126 deletions.
3 changes: 2 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
"axios": "^1.6.8",
"deep-equal": "^2.2.3",
"ethers": "*",
"qs": "^6.12.1"
"qs": "^6.12.1",
"zod": "*"
},
"devDependencies": {
"@typechain/ethers-v5": "^11.1.2",
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './porter';
export type * from './types';
export * from './utils';
export * from './web3';
export * from './schemas';

// Re-exports
export {
Expand Down
16 changes: 16 additions & 0 deletions packages/shared/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ethers } from 'ethers';
import { z } from 'zod';

export const ETH_ADDRESS_REGEXP = new RegExp('^0x[a-fA-F0-9]{40}$');

const isAddress = (address: string) => {
try {
return ethers.utils.getAddress(address);
} catch {
return false;
}
};

export const EthAddressSchema = z.string()
.regex(ETH_ADDRESS_REGEXP)
.refine(isAddress, { message: 'Invalid Ethereum address' });
16 changes: 16 additions & 0 deletions packages/shared/test/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {describe, expect, it} from 'vitest';

import { EthAddressSchema } from '../src';


describe('ethereum address schema', () => {
it('should accept valid ethereum address', () => {
const validAddress = '0x1234567890123456789012345678901234567890';
EthAddressSchema.parse(validAddress);
});

it('should reject invalid ethereum address', () => {
const invalidAddress = '0x123456789012345678901234567890123456789';
expect(() => EthAddressSchema.parse(invalidAddress)).toThrow();
});
});
3 changes: 2 additions & 1 deletion packages/taco-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
"dependencies": {
"@ethersproject/abstract-signer": "^5.7.0",
"@nucypher/shared": "workspace:*",
"siwe": "^2.3.2"
"siwe": "^2.3.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@nucypher/test-utils": "workspace:*"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {EIP4361AuthProvider, EIP4361TypedData} from './providers/eip4361';
import {EIP712AuthProvider, EIP712TypedData} from './providers/eip712';
import { AuthSignature } from './auth-sig';
import { EIP4361AuthProvider, EIP712AuthProvider } from './providers';

/**
* @deprecated Use EIP4361_AUTH_METHOD instead.
*/
export const EIP712_AUTH_METHOD = 'EIP712';
export const EIP4361_AUTH_METHOD = 'EIP4361';


export interface AuthProvider {
getOrCreateAuthSignature(): Promise<AuthSignature>;
Expand All @@ -12,19 +19,6 @@ export type AuthProviders = {
[key: string]: AuthProvider | undefined;
};

export interface AuthSignature {
signature: string;
address: string;
scheme: 'EIP712' | 'EIP4361';
typedData: EIP712TypedData | EIP4361TypedData;
}

/**
* @deprecated Use EIP4361_AUTH_METHOD instead.
*/
export const EIP712_AUTH_METHOD = 'EIP712';
export const EIP4361_AUTH_METHOD = 'EIP4361';

export const USER_ADDRESS_PARAM_DEFAULT = ':userAddress';
export const USER_ADDRESS_PARAM_EIP712 = `:userAddress${EIP712_AUTH_METHOD}`;
export const USER_ADDRESS_PARAM_EIP4361 = `:userAddress${EIP4361_AUTH_METHOD}`;
Expand Down
19 changes: 19 additions & 0 deletions packages/taco-auth/src/auth-sig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { EthAddressSchema } from '@nucypher/shared';
import { z } from 'zod';

import { EIP4361_AUTH_METHOD, EIP712_AUTH_METHOD } from './auth-provider';
import { EIP4361TypedDataSchema, EIP712TypedDataSchema } from './providers';


export const authSignatureSchema = z.object({
signature: z.string(),
address: EthAddressSchema,
scheme: z.enum([EIP712_AUTH_METHOD, EIP4361_AUTH_METHOD]),
typedData: z.union([
EIP4361TypedDataSchema,
// TODO(#536): Remove post EIP712 deprecation
EIP712TypedDataSchema,
]),
});

export type AuthSignature = z.infer<typeof authSignatureSchema>;
15 changes: 9 additions & 6 deletions packages/taco-auth/src/helper.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import {ethers} from "ethers";
import { ethers } from 'ethers';

import { EIP4361AuthProvider, EIP4361AuthProviderParams } from './providers/eip4361';
import { EIP712AuthProvider } from './providers/eip712';
import { AuthProviders, EIP4361_AUTH_METHOD, EIP712_AUTH_METHOD } from './types';
import { AuthProviders, EIP4361_AUTH_METHOD, EIP712_AUTH_METHOD } from './auth-provider';
import {
EIP4361AuthProvider,
EIP4361AuthProviderParams,
EIP712AuthProvider,
} from './providers';

export const makeAuthProviders = (
provider: ethers.providers.Provider,
signer?: ethers.Signer,
siweDefaultParams?: EIP4361AuthProviderParams
siweDefaultParams?: EIP4361AuthProviderParams,
): AuthProviders => {
return {
[EIP712_AUTH_METHOD]: signer ? new EIP712AuthProvider(provider, signer) : undefined,
[EIP4361_AUTH_METHOD]: signer ? new EIP4361AuthProvider(provider, signer, siweDefaultParams) : undefined
[EIP4361_AUTH_METHOD]: signer ? new EIP4361AuthProvider(provider, signer, siweDefaultParams) : undefined,
} as AuthProviders;
};
6 changes: 3 additions & 3 deletions packages/taco-auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './providers/eip4361';
export * from './providers/eip712';
export * from './types';
export * from './providers';
export * from './helper';
export * from './auth-sig';
export * from './auth-provider';
19 changes: 11 additions & 8 deletions packages/taco-auth/src/providers/eip4361.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ethers } from 'ethers';
import { generateNonce, SiweMessage } from 'siwe';
import { z } from 'zod';

import { EIP4361_AUTH_METHOD } from '../auth-provider';
import { AuthSignature } from '../auth-sig';
import { LocalStorage } from '../storage';
import { AuthSignature, EIP4361_AUTH_METHOD } from '../types';

export type EIP4361TypedData = string;

export const EIP4361TypedDataSchema = z.string();

export type EIP4361AuthProviderParams = {
domain: string;
Expand All @@ -29,15 +32,15 @@ export class EIP4361AuthProvider {
const address = await this.signer.getAddress();
const storageKey = `eth-${EIP4361_AUTH_METHOD}-message-${address}`;

// If we have a message in localStorage, return it
const maybeMessage = this.storage.getItem(storageKey);
if (maybeMessage) {
return JSON.parse(maybeMessage);
// If we have a signature in localStorage, return it
const maybeSignature = this.storage.getAuthSignature(storageKey);
if (maybeSignature) {
return maybeSignature;
}

// If at this point we didn't return, we need to create a new message
const authMessage = await this.createSIWEAuthMessage();
this.storage.setItem(storageKey, JSON.stringify(authMessage));
this.storage.setAuthSignature(storageKey, authMessage);
return authMessage;
}

Expand Down Expand Up @@ -79,7 +82,7 @@ export class EIP4361AuthProvider {
return {
domain: this.providerParams.domain,
uri: this.providerParams.uri,
}
};
}
throw new Error(ERR_MISSING_SIWE_PARAMETERS);
}
Expand Down
76 changes: 43 additions & 33 deletions packages/taco-auth/src/providers/eip712.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,44 @@
import type { TypedDataSigner } from '@ethersproject/abstract-signer';
import { ethers } from 'ethers';
import { utils as ethersUtils } from 'ethers/lib/ethers';
import { z } from 'zod';

import { AuthProvider } from '../auth-provider';
import { AuthSignature } from '../auth-sig';
import { LocalStorage } from '../storage';
import type { AuthProvider, AuthSignature } from '../types';

interface EIP712 {
types: {
Wallet: { name: string; type: string }[];
};
domain: {
salt: string;
chainId: number;
name: string;
version: string;
};
message: {
blockHash: string;
address: string;
blockNumber: number;
signatureText: string;
};
}

export interface EIP712TypedData extends EIP712 {
primaryType: 'Wallet';
types: {
EIP712Domain: { name: string; type: string }[];
Wallet: { name: string; type: string }[];
};
}

const typeFieldSchema = z.object({
name: z.string(),
type: z.string(),
});

const domain = z.object({
salt: z.string(),
chainId: z.number(),
name: z.string(),
version: z.string(),
});

const messageSchema = z.object({
blockHash: z.string(),
address: z.string(),
blockNumber: z.number(),
signatureText: z.string(),
});

export const EIP712TypedDataSchema = z.object({
primaryType: z.literal('Wallet'),
types: z.object({
EIP712Domain: z.array(typeFieldSchema),
Wallet: z.array(typeFieldSchema),
}),
domain: domain,
message: messageSchema,
});


export type EIP712TypedData = z.infer<typeof EIP712TypedDataSchema>;

interface ChainData {
blockHash: string;
Expand Down Expand Up @@ -79,15 +88,15 @@ export class EIP712AuthProvider implements AuthProvider {
const storageKey = `eip712-signature-${address}`;

// If we have a signature in localStorage, return it
const maybeSignature = this.storage.getItem(storageKey);
const maybeSignature = this.storage.getAuthSignature(storageKey);
if (maybeSignature) {
return JSON.parse(maybeSignature);
return maybeSignature;
}

// If at this point we didn't return, we need to create a new signature
const authMessage = await this.createAuthMessage();
this.storage.setItem(storageKey, JSON.stringify(authMessage));
return authMessage;
const authSignature = await this.createAuthMessage();
this.storage.setAuthSignature(storageKey, authSignature);
return authSignature;
}

private async createAuthMessage(): Promise<AuthSignature> {
Expand All @@ -97,7 +106,7 @@ export class EIP712AuthProvider implements AuthProvider {
const signatureText = `I'm the owner of address ${address} as of block number ${blockNumber}`;
const salt = ethersUtils.hexlify(ethersUtils.randomBytes(32));

const typedData: EIP712 = {
const typedData = {
types: {
Wallet: [
{ name: 'address', type: 'address' },
Expand Down Expand Up @@ -138,7 +147,8 @@ export class EIP712AuthProvider implements AuthProvider {

private async getChainData(): Promise<ChainData> {
const blockNumber = await this.provider.getBlockNumber();
const blockHash = (await this.provider.getBlock(blockNumber)).hash;
const block = await this.provider.getBlock(blockNumber);
const blockHash = block.hash;
const chainId = (await this.provider.getNetwork()).chainId;
return { blockNumber, blockHash, chainId };
}
Expand Down
27 changes: 2 additions & 25 deletions packages/taco-auth/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,2 @@
import { EIP4361_AUTH_METHOD, EIP712_AUTH_METHOD } from '../types';

import { EIP4361AuthProvider, EIP4361TypedData } from './eip4361';
import { EIP712AuthProvider, EIP712TypedData } from './eip712';

export interface AuthSignatureProvider {
getOrCreateAuthSignature(): Promise<AuthSignature>;
}

export type AuthProviders = {
[EIP712_AUTH_METHOD]?: EIP712AuthProvider;
[EIP4361_AUTH_METHOD]?: EIP4361AuthProvider;
// Fallback to satisfy type checking
[key: string]: AuthProvider | undefined;
};

export interface AuthSignature {
signature: string;
address: string;
scheme: 'EIP712' | 'EIP4361';
typedData: EIP712TypedData | EIP4361TypedData;
}

// Add other providers here
export type AuthProvider = AuthSignatureProvider;
export * from './eip712';
export * from './eip4361';
23 changes: 15 additions & 8 deletions packages/taco-auth/src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { AuthSignature, authSignatureSchema } from './index';

interface IStorage {
getItem(key: string): string | null;

setItem(key: string, value: string): void;
}

class BrowserStorage implements IStorage {
getItem(key: string): string | null {
public getItem(key: string): string | null {
return localStorage.getItem(key);
}

setItem(key: string, value: string): void {
public setItem(key: string, value: string): void {
localStorage.setItem(key, value);
}
}

class NodeStorage implements IStorage {
private storage: Record<string, string> = {};

getItem(key: string): string | null {
public getItem(key: string): string | null {
return this.storage[key] || null;
}

setItem(key: string, value: string): void {
public setItem(key: string, value: string): void {
this.storage[key] = value;
}
}
Expand All @@ -36,11 +38,16 @@ export class LocalStorage {
: new BrowserStorage();
}

getItem(key: string): string | null {
return this.storage.getItem(key);
public getAuthSignature(key: string): AuthSignature | null {
const asJson = this.storage.getItem(key);
if (!asJson) {
return null;
}
return authSignatureSchema.parse(JSON.parse(asJson));
}

setItem(key: string, value: string): void {
this.storage.setItem(key, value);
public setAuthSignature(key: string, authSignature: AuthSignature): void {
const asJson = JSON.stringify(authSignature);
this.storage.setItem(key, asJson);
}
}
Loading

0 comments on commit b3d724d

Please sign in to comment.