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

Reuse siwe authentication #547

Merged
merged 5 commits into from
Jul 23, 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
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
Loading