Skip to content

Commit

Permalink
feat: verify sign in signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
horsefacts committed Dec 20, 2023
1 parent 4254c8c commit 5313dda
Show file tree
Hide file tree
Showing 22 changed files with 311 additions and 64 deletions.
1 change: 1 addition & 0 deletions packages/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"prepublishOnly": "yarn run build"
},
"dependencies": {
"@farcaster/hub-web": "^0.7.1",
"siwe": "^2.1.4"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/actions/app/connect.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createAppClient } from "../../clients/createAppClient";
import { jest } from "@jest/globals";
import { viem } from "../../clients/ethereum/viem";

describe("connect", () => {
const client = createAppClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

afterEach(() => {
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/actions/app/status.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createAppClient } from "../../clients/createAppClient";
import { jest } from "@jest/globals";
import { viem } from "../../clients/ethereum/viem";

describe("status", () => {
const client = createAppClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

afterEach(() => {
Expand Down
48 changes: 48 additions & 0 deletions packages/connect/src/actions/app/verifySignInMessage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createAppClient } from "../../clients/createAppClient";
import { createAuthClient } from "../../clients/createAuthClient";
import { viem } from "../../clients/ethereum/viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { ConnectError } from "../../errors";

describe("verifySignInMessage", () => {
const client = createAppClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

const authClient = createAuthClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

const account = privateKeyToAccount(generatePrivateKey());

const siweParams = {
domain: "example.com",
uri: "https://example.com/login",
version: "1",
issuedAt: "2023-10-01T00:00:00.000Z",
};

test("verifies sign in message", async () => {
const message = authClient.buildSignInMessage({
...siweParams,
address: account.address,
fid: 1234,
});

const signature = await account.signMessage({
message: message.toMessage(),
});

const errMsg = `Invalid resource: signer ${account.address} does not own fid 1234.`;
const error = new ConnectError("unauthorized", errMsg);

await expect(
client.verifySignInMessage({
message,
signature,
}),
).rejects.toStrictEqual(error);
});
});
9 changes: 6 additions & 3 deletions packages/connect/src/actions/app/verifySignInMessage.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { SiweMessage } from "siwe";
import { Client } from "../../clients/createClient";
import { SignInResponse, verify } from "../../messages/verify";

export interface VerifySignInMessageArgs {
message: string;
message: string | Partial<SiweMessage>;
signature: `0x${string}`;
}

export type VerifySignInMessageResponse = Promise<SignInResponse>;

export const verifySignInMessage = async (
_client: Client,
client: Client,
{ message, signature }: VerifySignInMessageArgs,
): VerifySignInMessageResponse => {
const result = await verify(message, signature);
const result = await verify(message, signature, {
getFid: client.ethereum.getFid,
});
if (result.isErr()) {
throw result.error;
} else {
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/actions/auth/authenticate.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { createAuthClient } from "../../clients/createAuthClient";
import { jest } from "@jest/globals";
import { viem } from "../../clients/ethereum/viem";

describe("authenticate", () => {
const client = createAuthClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

afterEach(() => {
Expand Down
31 changes: 31 additions & 0 deletions packages/connect/src/actions/auth/buildSignInMessage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createAuthClient } from "../../clients/createAuthClient";
import { viem } from "../../clients/ethereum/viem";

describe("buildSignInMessage", () => {
const client = createAuthClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

test("builds Siwe message from provided parameters", async () => {
const message = client.buildSignInMessage({
address: "0x63C378DDC446DFf1d831B9B96F7d338FE6bd4231",
uri: "https://example.com/login",
domain: "example.com",
nonce: "12345678",
fid: 1,
resources: ["https://example.com/resource"],
});

expect(message).toMatchObject({
address: "0x63C378DDC446DFf1d831B9B96F7d338FE6bd4231",
statement: "Farcaster Connect",
chainId: 10,
uri: "https://example.com/login",
domain: "example.com",
version: "1",
nonce: "12345678",
resources: ["farcaster://fid/1", "https://example.com/resource"],
});
});
});
22 changes: 22 additions & 0 deletions packages/connect/src/actions/auth/parseSignInURI.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createAuthClient } from "../../clients/createAuthClient";
import { viem } from "../../clients/ethereum/viem";

describe("parseSignInURI", () => {
const client = createAuthClient({
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
});

test("parses sign in params from protocol URI", async () => {
const { channelToken, params } = client.parseSignInURI({
uri: "farcaster://connect?channelToken=76be6229-bdf7-4ad2-930a-540fb2de1e08&nonce=ESsxs6MaFio7OvqWb&siweUri=https%3A%2F%2Fexample.com%2Flogin&domain=example.com",
});

expect(channelToken).toBe("76be6229-bdf7-4ad2-930a-540fb2de1e08");
expect(params).toStrictEqual({
domain: "example.com",
siweUri: "https://example.com/login",
nonce: "ESsxs6MaFio7OvqWb",
});
});
});
16 changes: 16 additions & 0 deletions packages/connect/src/actions/auth/parseSignInURI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Client } from "../../clients/createClient";
import { parseSignInURI as parse, ParsedSignInURI } from "../../messages/parseSignInURI";

export interface ParseSignInURIArgs {
uri: string;
}
export type ParseSignInURIResponse = ParsedSignInURI;

export const parseSignInURI = (_client: Client, { uri }: ParseSignInURIArgs): ParseSignInURIResponse => {
const result = parse(uri);
if (result.isErr()) {
throw result.error;
} else {
return result.value;
}
};
2 changes: 2 additions & 0 deletions packages/connect/src/clients/createAppClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createAppClient, AppClient } from "./createAppClient";
import { viem } from "./ethereum/viem";

describe("createAppClient", () => {
const config = {
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
};

let appClient: AppClient;
Expand Down
4 changes: 2 additions & 2 deletions packages/connect/src/clients/createAppClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import {
VerifySignInMessageArgs,
VerifySignInMessageResponse,
} from "../actions/app/verifySignInMessage";
import { Client, ClientConfig, createClient } from "./createClient";
import { Client, CreateClientArgs, createClient } from "./createClient";

export interface AppClient extends Client {
connect: (args: ConnectArgs) => ConnectResponse;
status: (args: StatusArgs) => StatusResponse;
verifySignInMessage: (args: VerifySignInMessageArgs) => VerifySignInMessageResponse;
}

export const createAppClient = (config: ClientConfig): AppClient => {
export const createAppClient = (config: CreateClientArgs): AppClient => {
const client = createClient(config);
return {
...client,
Expand Down
2 changes: 2 additions & 0 deletions packages/connect/src/clients/createAuthClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { createAuthClient, AuthClient } from "./createAuthClient";
import { viem } from "./ethereum/viem";

describe("createAuthClient", () => {
const config = {
relayURI: "https://connect.farcaster.xyz",
ethereum: viem(),
};

let authClient: AuthClient;
Expand Down
7 changes: 5 additions & 2 deletions packages/connect/src/clients/createAuthClient.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { authenticate, AuthenticateArgs, AuthenticateResponse } from "../actions/auth/authenticate";
import { parseSignInURI, ParseSignInURIArgs, ParseSignInURIResponse } from "../actions/auth/parseSignInURI";
import {
buildSignInMessage,
BuildSignInMessageArgs,
BuildSignInMessageResponse,
} from "../actions/auth/buildSignInMessage";
import { Client, ClientConfig, createClient } from "./createClient";
import { Client, CreateClientArgs, createClient } from "./createClient";

export interface AuthClient extends Client {
authenticate: (args: AuthenticateArgs) => AuthenticateResponse;
buildSignInMessage: (args: BuildSignInMessageArgs) => BuildSignInMessageResponse;
parseSignInURI: (args: ParseSignInURIArgs) => ParseSignInURIResponse;
}

export const createAuthClient = (config: ClientConfig): AuthClient => {
export const createAuthClient = (config: CreateClientArgs): AuthClient => {
const client = createClient(config);
return {
...client,
authenticate: (args: AuthenticateArgs) => authenticate(client, args),
buildSignInMessage: (args: BuildSignInMessageArgs) => buildSignInMessage(client, args),
parseSignInURI: (args: ParseSignInURIArgs) => parseSignInURI(client, args),
};
};
12 changes: 11 additions & 1 deletion packages/connect/src/clients/createClient.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { Ethereum } from "../clients/ethereum/viem";

export interface CreateClientArgs {
relayURI: string;
version?: string;
ethereum: Ethereum;
}

export interface ClientConfig {
relayURI: string;
version?: string;
}

export interface Client {
config: ClientConfig;
ethereum: Ethereum;
}

const configDefaults = {
version: "v1",
};

export const createClient = (config: ClientConfig) => {
export const createClient = ({ ethereum, ...config }: CreateClientArgs) => {
return {
config: { ...configDefaults, ...config },
ethereum: ethereum,
};
};
31 changes: 31 additions & 0 deletions packages/connect/src/clients/ethereum/viem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Hex, createPublicClient, http } from "viem";
import { optimism } from "viem/chains";
import { ID_REGISTRY_ADDRESS, idRegistryABI } from "@farcaster/hub-web";

export interface Ethereum {
getFid: (custody: Hex) => Promise<BigInt>;
}

interface ViemConfigArgs {
rpcUrl?: string;
}

export const viem = (args?: ViemConfigArgs): Ethereum => {
const publicClient = createPublicClient({
chain: optimism,
transport: http(args?.rpcUrl),
});

const getFid = async (custody: Hex): Promise<BigInt> => {
return publicClient.readContract({
address: ID_REGISTRY_ADDRESS,
abi: idRegistryABI,
functionName: "idOf",
args: [custody],
});
};

return {
getFid,
};
};
1 change: 1 addition & 0 deletions packages/connect/src/messages/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type SignInMessageParams = Partial<SiweMessage> & FarcasterResourceParams
export const build = (params: SignInMessageParams): ConnectResult<SiweMessage> => {
const { fid, ...siweParams } = params;
const resources = siweParams.resources ?? [];
siweParams.version = "1";
siweParams.statement = STATEMENT;
siweParams.chainId = CHAIN_ID;
siweParams.resources = [buildFidResource(fid), ...resources];
Expand Down
2 changes: 1 addition & 1 deletion packages/connect/src/messages/parseSignInURI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SignInMessageParams } from "./build";

export interface ParsedSignInURI {
channelToken: string;
params: Partial<SignInMessageParams>;
params: Partial<SignInMessageParams> & { siweUri?: string };
}

export const parseSignInURI = (signInUri: string): ConnectResult<ParsedSignInURI> => {
Expand Down
Loading

0 comments on commit 5313dda

Please sign in to comment.