Skip to content

Commit

Permalink
feat: only allow adding wallets with signature
Browse files Browse the repository at this point in the history
  • Loading branch information
iamacook committed Feb 13, 2025
1 parent 7c65cf5 commit 60d4ab6
Show file tree
Hide file tree
Showing 7 changed files with 621 additions and 26 deletions.
71 changes: 71 additions & 0 deletions src/domain/siwe/entities/siwe-message.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { AddressSchema } from '@/validation/entities/schemas/address.schema';
import { parseSiweMessage } from 'viem/siwe';
import { z } from 'zod';

/**
* viem provides both parseSiweMessage (used here) and validatedSiweMessage
* functions but the former returns a Partial<SiweMessage> and the latter
* does not validate issuedAt as of writing this.
*
* @see https://github.com/wevm/viem/blob/main/src/utils/siwe/parseSiweMessage.ts
* @see https://github.com/wevm/viem/blob/main/src/utils/siwe/validateSiweMessage.ts
*
* We define our own schema to parse, validate and refine the message to ensure
* compliance with EIP-4361 according to our requirements, with custom error
* messages and strict types.
*
* @see https://eips.ethereum.org/EIPS/eip-4361
*/
export const SiweMessageSchema = z
.string()
.transform(parseSiweMessage)
.pipe(
// We only validate primitives as parseSiweMessage ensures compliance,
// e.g. scheme, domain and uri should be RFC 3986 compliant.
z.object({
scheme: z.string().optional(),
domain: z.string(),
address: AddressSchema,
statement: z.string().optional(),
uri: z.string(),
version: z.literal('1'),
chainId: z.coerce.number(),
nonce: z.string(),
issuedAt: z.coerce.date(),
expirationTime: z.coerce.date().optional(),
notBefore: z.coerce.date().optional(),
requestId: z.string().optional(),
resources: z.array(z.string()).optional(),
}),
)
.superRefine((message, ctx) => {
/**
* According to the spec., we should also compare the scheme, domain and uri
* of the message against the request but as our API is often used either
* locally or across environments, those checks would fail.
*/
const now = new Date();

if (!message.issuedAt || message.issuedAt > now) {
ctx.addIssue({
code: 'custom',
message: 'Message yet issued',
});
}

if (message.expirationTime && message.expirationTime <= now) {
ctx.addIssue({
code: 'custom',
message: 'Message has expired',
});
}

if (message.notBefore && message.notBefore > now) {
ctx.addIssue({
code: 'custom',
message: 'Message yet valid',
});
}

return z.NEVER;
});
6 changes: 6 additions & 0 deletions src/domain/siwe/siwe.repository.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { SiweApiModule } from '@/datasources/siwe-api/siwe-api.module';
import { BlockchainApiManagerModule } from '@/domain/interfaces/blockchain-api.manager.interface';
import { SiweRepository } from '@/domain/siwe/siwe.repository';
import { Module } from '@nestjs/common';
import type { SiweMessage } from 'viem/siwe';

export const ISiweRepository = Symbol('ISiweRepository');

export interface ISiweRepository {
generateNonce(): Promise<{ nonce: string }>;

getValidatedSiweMessage(args: {
message: string;
signature: `0x${string}`;
}): Promise<SiweMessage>;

getMaxValidityDate(): Date;

isValidMessage(args: {
Expand Down
34 changes: 33 additions & 1 deletion src/domain/siwe/siwe.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { ISiweApi } from '@/domain/interfaces/siwe-api.interface';
import { IConfigurationService } from '@/config/configuration.service.interface';
import { ISiweRepository } from '@/domain/siwe/siwe.repository.interface';
import { LoggingService, ILoggingService } from '@/logging/logging.interface';
import { Inject, Injectable } from '@nestjs/common';
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { verifyMessage } from 'viem';
import {
generateSiweNonce,
parseSiweMessage,
SiweMessage,
validateSiweMessage,
} from 'viem/siwe';
import { IBlockchainApiManager } from '@/domain/interfaces/blockchain-api.manager.interface';
import { SiweMessageSchema } from '@/domain/siwe/entities/siwe-message.entity';

@Injectable()
export class SiweRepository implements ISiweRepository {
Expand Down Expand Up @@ -45,6 +47,36 @@ export class SiweRepository implements ISiweRepository {
};
}

async getValidatedSiweMessage(args: {
message: string;
signature: `0x${string}`;
}): Promise<SiweMessage> {
const result = SiweMessageSchema.safeParse(args.message);
if (!result.success) {
throw new UnauthorizedException('Invalid message');
}

const cachedNonce = await this.siweApi.getNonce(result.data.nonce);
if (!cachedNonce) {
throw new UnauthorizedException('Invalid nonce');
}

await this.siweApi.clearNonce(result.data.nonce);

const isValidSignature = await verifyMessage({
message: args.message,
signature: args.signature,
address: result.data.address,
});

if (!isValidSignature) {
throw new UnauthorizedException('Invalid signature');
}

return result.data;
}

// TODO: Modify the following to use the above getValidatedSiweMessage instead
/**
* Verifies the validity of a signed message:
*
Expand Down
Loading

0 comments on commit 60d4ab6

Please sign in to comment.