Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Commit

Permalink
feat(auth): validate user-provided auth signature
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-roslaniec committed Jul 1, 2024
1 parent 4eea8e7 commit 85cd025
Show file tree
Hide file tree
Showing 21 changed files with 244 additions and 117 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
4 changes: 4 additions & 0 deletions packages/shared/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { z } from 'zod';

export const ETH_ADDRESS_REGEXP = new RegExp('^0x[a-fA-F0-9]{40}$');
export const EthAddressSchema = z.string().regex(ETH_ADDRESS_REGEXP);
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,27 +1,22 @@
import {EIP4361AuthProvider, EIP4361TypedData} from './providers/eip4361';
import {EIP712AuthProvider, EIP712TypedData} from './providers/eip712';
import { AuthSignature } from './auth-sig';
import { EIP4361AuthProvider, EIP712AuthProvider } from './providers';

export const EIP712_AUTH_METHOD = 'EIP712';
export const EIP4361_AUTH_METHOD = 'EIP4361';


export interface AuthProvider {
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;
}

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 @@ -72,15 +81,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 @@ -90,7 +99,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 @@ -131,7 +140,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';
31 changes: 23 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,24 @@ 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 LocalStorage.fromJson(asJson);
}

public setAuthSignature(key: string, authSignature: AuthSignature): void {
const asJson = LocalStorage.toJson(authSignature);
this.storage.setItem(key, asJson);
}

public static fromJson(json: string): AuthSignature {
return authSignatureSchema.parse(JSON.parse(json));
}

setItem(key: string, value: string): void {
this.storage.setItem(key, value);
public static toJson(signature: AuthSignature): string {
return JSON.stringify(signature);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
bobSecretKeyBytes,
fakeProvider,
fakeSigner, TEST_SIWE_PARAMS,
fakeSigner,
TEST_SIWE_PARAMS,
} from '@nucypher/test-utils';
import { SiweMessage } from 'siwe';
import { describe, expect, it } from 'vitest';
Expand All @@ -12,7 +13,7 @@ import {
EIP712TypedData,
} from '../src';

describe('taco authorization', () => {
describe('auth provider', () => {
it('creates a new EIP-712 message', async () => {
const provider = fakeProvider(bobSecretKeyBytes);
const signer = fakeSigner(bobSecretKeyBytes);
Expand All @@ -36,7 +37,7 @@ describe('taco authorization', () => {
expect(typedData.message.blockNumber).toEqual(
await provider.getBlockNumber(),
);
expect(typedData.message).toHaveProperty('blockHash');
expect(typedData.message['blockHash']).toBeDefined();
});

it('creates a new SIWE message', async () => {
Expand Down
Loading

0 comments on commit 85cd025

Please sign in to comment.