Skip to content

Commit

Permalink
Reuse siwe authentication (#547)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre committed Jul 23, 2024
2 parents b689493 + 6709bf3 commit fc3ddf9
Show file tree
Hide file tree
Showing 29 changed files with 218 additions and 117 deletions.
6 changes: 5 additions & 1 deletion examples/taco/nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ const decryptFromBytes = async (encryptedBytes: Uint8Array) => {
domain: 'localhost',
uri: 'http://localhost:3000',
};
const authProvider = new EIP4361AuthProvider(provider, consumerSigner, siweParams);
const authProvider = new EIP4361AuthProvider(
provider,
consumerSigner,
siweParams,
);
return decrypt(
provider,
domain,
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export * from './contracts';
export * from './porter';
export * from './schemas';
export type * from './types';
export * from './utils';
export * from './web3';
export * from './schemas';

// Re-exports
export {
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const isAddress = (address: string) => {
}
};

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

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


describe('ethereum address schema', () => {
it('should accept valid ethereum address', () => {
const validAddress = '0x1234567890123456789012345678901234567890';
Expand Down
1 change: 0 additions & 1 deletion packages/taco-auth/src/auth-sig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { z } from 'zod';
import { EIP4361_AUTH_METHOD } from './auth-provider';
import { EIP4361TypedDataSchema } from './providers';


export const authSignatureSchema = z.object({
signature: z.string(),
address: EthAddressSchema,
Expand Down
4 changes: 2 additions & 2 deletions packages/taco-auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './providers';
export * from './auth-sig';
export * from './auth-provider';
export * from './auth-sig';
export * from './providers';
7 changes: 4 additions & 3 deletions packages/taco-auth/src/providers/eip4361.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ const isSiweMessage = (message: string): boolean => {
}
};

export const EIP4361TypedDataSchema = z.string()
export const EIP4361TypedDataSchema = z
.string()
.refine(isSiweMessage, { message: 'Invalid SIWE message' });

export type EIP4361AuthProviderParams = {
domain: string;
uri: string;
}
};

const ERR_MISSING_SIWE_PARAMETERS = 'Missing default SIWE parameters';

Expand Down Expand Up @@ -86,7 +87,7 @@ export class EIP4361AuthProvider {
nonce,
chainId,
});
const scheme = 'EIP4361';
const scheme = EIP4361_AUTH_METHOD;
const message = siweMessage.prepareMessage();
const signature = await this.signer.signMessage(message);
return { signature, address, scheme, typedData: message };
Expand Down
38 changes: 38 additions & 0 deletions packages/taco-auth/src/providers/external-eip4361.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { SiweMessage } from 'siwe';

import { EIP4361_AUTH_METHOD } from '../auth-provider';
import { AuthSignature } from '../auth-sig';

export class SingleSignOnEIP4361AuthProvider {
public static async fromExistingSiweInfo(
existingSiweMessage: string,
signature: string,
): Promise<SingleSignOnEIP4361AuthProvider> {
// validation
const siweMessage = new SiweMessage(existingSiweMessage);
await siweMessage.verify({ signature });
// create provider
const authProvider = new SingleSignOnEIP4361AuthProvider(
siweMessage.prepareMessage(),
siweMessage.address,
signature,
);
return authProvider;
}

private constructor(
private readonly existingSiweMessage: string,
private readonly address: string,
private readonly signature: string,
) {}

public async getOrCreateAuthSignature(): Promise<AuthSignature> {
const scheme = EIP4361_AUTH_METHOD;
return {
signature: this.signature,
address: this.address,
scheme,
typedData: this.existingSiweMessage,
};
}
}
1 change: 1 addition & 0 deletions packages/taco-auth/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './eip4361';
export * from './external-eip4361';
10 changes: 8 additions & 2 deletions packages/taco-auth/test/auth-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { EIP4361AuthProvider, EIP4361TypedDataSchema } from '../src';
describe('auth provider', () => {
const provider = fakeProvider(bobSecretKeyBytes);
const signer = fakeSigner(bobSecretKeyBytes);
const eip4361Provider = new EIP4361AuthProvider(provider, signer, TEST_SIWE_PARAMS);
const eip4361Provider = new EIP4361AuthProvider(
provider,
signer,
TEST_SIWE_PARAMS,
);

it('creates a new SIWE message', async () => {
const typedSignature = await eip4361Provider.getOrCreateAuthSignature();
Expand Down Expand Up @@ -42,6 +46,8 @@ describe('auth provider', () => {
it('rejects an invalid EIP4361 message', async () => {
const typedSignature = await eip4361Provider.getOrCreateAuthSignature();
typedSignature.typedData = 'invalid-typed-data';
expect(() => EIP4361TypedDataSchema.parse(typedSignature.typedData)).toThrow();
expect(() =>
EIP4361TypedDataSchema.parse(typedSignature.typedData),
).toThrow();
});
});
20 changes: 11 additions & 9 deletions packages/taco-auth/test/auth-sig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest';

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


const eip4361AuthSignature = {
'signature': 'fake-signature',
'address': '0x0000000000000000000000000000000000000000',
'scheme': 'EIP4361',
'typedData': 'localhost wants you to sign in with your Ethereum account:\n0x0000000000000000000000000000000000000000\n\nlocalhost wants you to sign in with your Ethereum account: 0x0000000000000000000000000000000000000000\n\nURI: http://localhost:3000\nVersion: 1\nChain ID: 1234\nNonce: 5ixAg1odyfDnrbfGa\nIssued At: 2024-07-01T10:32:39.631Z',
signature: 'fake-signature',
address: '0x0000000000000000000000000000000000000000',
scheme: 'EIP4361',
typedData:
'localhost wants you to sign in with your Ethereum account:\n0x0000000000000000000000000000000000000000\n\nlocalhost wants you to sign in with your Ethereum account: 0x0000000000000000000000000000000000000000\n\nURI: http://localhost:3000\nVersion: 1\nChain ID: 1234\nNonce: 5ixAg1odyfDnrbfGa\nIssued At: 2024-07-01T10:32:39.631Z',
};

describe('auth signature', () => {
Expand All @@ -16,9 +16,11 @@ describe('auth signature', () => {
});

it('rejects an EIP4361 auth signature with missing fields', async () => {
expect(() => authSignatureSchema.parse({
...eip4361AuthSignature,
'signature': undefined,
})).toThrow();
expect(() =>
authSignatureSchema.parse({
...eip4361AuthSignature,
signature: undefined,
}),
).toThrow();
});
});
5 changes: 4 additions & 1 deletion packages/taco/examples/conditions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ const ownsNFT = new conditions.predefined.erc721.ERC721Ownership({
parameters: [3591],
chain: ChainId.SEPOLIA,
});
console.assert(ownsNFT.requiresAuthentication(), 'ERC721Ownership requires authentication');
console.assert(
ownsNFT.requiresAuthentication(),
'ERC721Ownership requires authentication',
);

const hasAtLeastTwoNFTs = new conditions.predefined.erc721.ERC721Balance({
contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
Expand Down
8 changes: 6 additions & 2 deletions packages/taco/examples/encrypt-decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ethers } from 'ethers';
import {
conditions,
decrypt,
domains, EIP4361AuthProvider,
domains,
EIP4361AuthProvider,
encrypt,
getPorterUri,
initialize,
Expand Down Expand Up @@ -45,7 +46,10 @@ const run = async () => {

// @ts-ignore
const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
const authProvider = new EIP4361AuthProvider(web3Provider, web3Provider.getSigner());
const authProvider = new EIP4361AuthProvider(
web3Provider,
web3Provider.getSigner(),
);
const decryptedMessage = await decrypt(
web3Provider,
domains.TESTNET,
Expand Down
4 changes: 2 additions & 2 deletions packages/taco/src/conditions/condition-expr.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Conditions as CoreConditions } from '@nucypher/nucypher-core';
import { toJSON } from '@nucypher/shared';
import {AuthProviders} from "@nucypher/taco-auth";
import { AuthProviders } from '@nucypher/taco-auth';
import { SemVer } from 'semver';

import { Condition } from './condition';
import { ConditionFactory } from './condition-factory';
import { ConditionContext, CustomContextParam} from './context';
import { ConditionContext, CustomContextParam } from './context';

const ERR_VERSION = (provided: string, current: string) =>
`Version provided, ${provided}, is incompatible with current version, ${current}`;
Expand Down
2 changes: 1 addition & 1 deletion packages/taco/src/conditions/const.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChainId } from '@nucypher/shared';
import { USER_ADDRESS_PARAM_DEFAULT } from "@nucypher/taco-auth";
import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';

export const USER_ADDRESS_PARAM_EXTERNAL_EIP4361 =
':userAddressExternalEIP4361';
Expand Down
75 changes: 50 additions & 25 deletions packages/taco/src/conditions/context/context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import { ThresholdMessageKit } from '@nucypher/nucypher-core';
import { toJSON } from '@nucypher/shared';
import { AUTH_METHOD_FOR_PARAM, AuthProviders, AuthSignature } from '@nucypher/taco-auth';
import {
AUTH_METHOD_FOR_PARAM,
AuthProviders,
AuthSignature,
} from '@nucypher/taco-auth';

import { CoreConditions, CoreContext } from '../../types';
import { CompoundConditionType } from '../compound-condition';
import { Condition, ConditionProps } from '../condition';
import { ConditionExpression } from '../condition-expr';
import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP, RESERVED_CONTEXT_PARAMS, USER_ADDRESS_PARAMS } from '../const';

import {
CONTEXT_PARAM_PREFIX,
CONTEXT_PARAM_REGEXP,
RESERVED_CONTEXT_PARAMS,
USER_ADDRESS_PARAMS,
} from '../const';

export type CustomContextParam = string | number | boolean | AuthSignature;
export type ContextParam = CustomContextParam | AuthSignature;
Expand Down Expand Up @@ -36,7 +44,8 @@ export class ConditionContext {
const condProps = condition.toObj();
this.validateContextParameters();
this.validateCoreConditions(condProps);
this.requestedParameters = ConditionContext.findContextParameters(condProps);
this.requestedParameters =
ConditionContext.findContextParameters(condProps);
this.validateAuthProviders(this.requestedParameters);
}

Expand All @@ -57,7 +66,9 @@ export class ConditionContext {
new CoreConditions(toJSON(condObject));
}

private validateNoMissingContextParameters(parameters: Record<string, ContextParam>) {
private validateNoMissingContextParameters(
parameters: Record<string, ContextParam>,
) {
// Ok, so at this point we should have all the parameters we need
// If we don't, we have a problem and we should throw
const missingParameters = Array.from(this.requestedParameters).filter(
Expand All @@ -70,7 +81,8 @@ export class ConditionContext {
// We may also have some parameters that are not used
const unknownParameters = Object.keys(parameters).filter(
(key) =>
!this.requestedParameters.has(key) && !RESERVED_CONTEXT_PARAMS.includes(key),
!this.requestedParameters.has(key) &&
!RESERVED_CONTEXT_PARAMS.includes(key),
);
if (unknownParameters.length > 0) {
throw new Error(ERR_UNKNOWN_CONTEXT_PARAMS(unknownParameters));
Expand All @@ -80,7 +92,8 @@ export class ConditionContext {
private async fillContextParameters(
requestedParameters: Set<string>,
): Promise<Record<string, ContextParam>> {
const parameters = await this.fillAuthContextParameters(requestedParameters);
const parameters =
await this.fillAuthContextParameters(requestedParameters);
for (const key in this.customParameters) {
parameters[key] = this.customParameters[key];
}
Expand Down Expand Up @@ -108,16 +121,20 @@ export class ConditionContext {
}
}

private async fillAuthContextParameters(requestedParameters: Set<string>): Promise<Record<string, ContextParam>> {
const entries = await Promise.all([...requestedParameters]
.map(param => [param, AUTH_METHOD_FOR_PARAM[param]])
.filter(([, authMethod]) => !!authMethod)
.map(async ([param, authMethod]) => {
const maybeAuthProvider = this.authProviders[authMethod];
// TODO: Throw here instead of validating in the constructor?
// TODO: Hide getOrCreateAuthSignature behind a more generic interface
return [param, await maybeAuthProvider!.getOrCreateAuthSignature()];
}));
private async fillAuthContextParameters(
requestedParameters: Set<string>,
): Promise<Record<string, ContextParam>> {
const entries = await Promise.all(
[...requestedParameters]
.map((param) => [param, AUTH_METHOD_FOR_PARAM[param]])
.filter(([, authMethod]) => !!authMethod)
.map(async ([param, authMethod]) => {
const maybeAuthProvider = this.authProviders[authMethod];
// TODO: Throw here instead of validating in the constructor?
// TODO: Hide getOrCreateAuthSignature behind a more generic interface
return [param, await maybeAuthProvider!.getOrCreateAuthSignature()];
}),
);
return Object.fromEntries(entries);
}

Expand Down Expand Up @@ -156,9 +173,7 @@ export class ConditionContext {
// If it's a compound condition, check operands
if (condition.conditionType === CompoundConditionType) {
for (const key in condition.operands) {
const innerParams = this.findContextParameters(
condition.operands[key],
);
const innerParams = this.findContextParameters(condition.operands[key]);
for (const param of innerParams) {
requestedParameters.add(param);
}
Expand All @@ -178,8 +193,12 @@ export class ConditionContext {
return new CoreContext(asJson);
}

public toContextParameters = async (): Promise<Record<string, ContextParam>> => {
const parameters = await this.fillContextParameters(this.requestedParameters);
public toContextParameters = async (): Promise<
Record<string, ContextParam>
> => {
const parameters = await this.fillContextParameters(
this.requestedParameters,
);
this.validateNoMissingContextParameters(parameters);
return parameters;
};
Expand All @@ -196,8 +215,14 @@ export class ConditionContext {
);
}

public static requestedContextParameters(messageKit: ThresholdMessageKit): Set<string> {
const conditionExpr = ConditionExpression.fromCoreConditions(messageKit.acp.conditions);
return ConditionContext.findContextParameters(conditionExpr.condition.toObj());
public static requestedContextParameters(
messageKit: ThresholdMessageKit,
): Set<string> {
const conditionExpr = ConditionExpression.fromCoreConditions(
messageKit.acp.conditions,
);
return ConditionContext.findContextParameters(
conditionExpr.condition.toObj(),
);
}
}
3 changes: 1 addition & 2 deletions packages/taco/src/conditions/predefined/erc20.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {USER_ADDRESS_PARAM_DEFAULT} from "@nucypher/taco-auth";
import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';

import {
ContractCondition,
ContractConditionProps,
ContractConditionType,
} from '../base/contract';


type ERC20BalanceFields = 'contractAddress' | 'chain' | 'returnValueTest';

const ERC20BalanceDefaults: Omit<ContractConditionProps, ERC20BalanceFields> = {
Expand Down
Loading

0 comments on commit fc3ddf9

Please sign in to comment.