Skip to content

Commit

Permalink
Re-introduce validate() and check for invalid parameters/options (#77)
Browse files Browse the repository at this point in the history
* Fix siwe-parser import

* Rename validate to validateMessage

* Null check for nonce

* Version bump

* Re introduce validate for backward compatibilty and add invalid parameters checking

* Fix null nonce before validation

* Remove empty if

* Add tests for object parsing

* Fix comments

* Fix comments
  • Loading branch information
w4ll3 authored May 12, 2022
1 parent 7a5dd34 commit eb64c77
Show file tree
Hide file tree
Showing 4 changed files with 475 additions and 24 deletions.
128 changes: 119 additions & 9 deletions packages/siwe/lib/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const parsingPositive: Object = require('../../../test/parsing_positive.json');
const parsingNegative: Object = require('../../../test/parsing_negative.json');
const parsingNegativeObjects: Object = require('../../../test/parsing_negative_objects.json');
const verificationPositive: Object = require('../../../test/verification_positive.json');
const verificationNegative: Object = require('../../../test/verification_negative.json');
const EIP1271: Object = require('../../../test/eip1271.json');
Expand All @@ -19,17 +20,28 @@ describe(`Message Generation`, () => {

test.concurrent.each(Object.entries(parsingNegative))(
'Fails to generate message: %s',
(_, test) => {
(n, test) => {
try {
new SiweMessage(test.fields);
new SiweMessage(test);
} catch (error) {
expect(Object.values(SiweErrorType).includes(error));
}
}
);

test.concurrent.each(Object.entries(parsingNegativeObjects))(
'Fails to generate message: %s',
(n, test) => {
try {
new SiweMessage(test);
} catch (error) {
expect(Object.values(SiweErrorType).includes(error));
}
}
);
});

describe(`Message verification`, () => {
describe(`Message verification without suppressExceptions`, () => {
test.concurrent.each(Object.entries(verificationPositive))(
'Verificates message successfully: %s',
async (_, test_fields) => {
Expand All @@ -45,7 +57,26 @@ describe(`Message verification`, () => {
}
);
test.concurrent.each(Object.entries(verificationNegative))(
'Fails to verify message: %s',
'Fails to verify message: %s and rejects the promise',
async (n, test_fields) => {
try {
const msg = new SiweMessage(test_fields);
await expect(msg.verify({
signature: test_fields.signature,
time: test_fields.time || test_fields.issuedAt,
domain: test_fields.domainBinding,
nonce: test_fields.matchNonce,
}).then(({ success }) => success)).rejects.toBeFalsy();
} catch (error) {
expect(Object.values(SiweErrorType).includes(error));
}
}
);
});

describe(`Message verification with suppressExceptions`, () => {
test.concurrent.each(Object.entries(verificationNegative))(
'Fails to verify message: %s but still resolves the promise',
async (n, test_fields) => {
try {
const msg = new SiweMessage(test_fields);
Expand All @@ -54,7 +85,7 @@ describe(`Message verification`, () => {
time: test_fields.time || test_fields.issuedAt,
domain: test_fields.domainBinding,
nonce: test_fields.matchNonce,
}).then(({ success }) => success)).resolves.toBeFalsy();
}, { suppressExceptions: true }).then(({ success }) => success)).resolves.toBeFalsy();
} catch (error) {
expect(Object.values(SiweErrorType).includes(error));
}
Expand All @@ -75,6 +106,18 @@ describe(`Round Trip`, () => {
);
});

describe(`Round Trip`, () => {
let wallet = Wallet.createRandom();
test.concurrent.each(Object.entries(parsingPositive))(
'Generates a Successfully Verifying message: %s',
async (_, test) => {
const msg = new SiweMessage(test.fields);
msg.address = wallet.address;
const signature = await wallet.signMessage(msg.toMessage());
await expect(msg.verify({ signature }).then(({ success }) => success)).resolves.toBeTruthy();
}
);
});

describe(`EIP1271`, () => {
test.concurrent.each(Object.entries(EIP1271))(
Expand All @@ -84,10 +127,13 @@ describe(`EIP1271`, () => {
await expect(
msg.verify({
signature: test_fields.signature,
}, new providers.InfuraProvider(1, {
projectId: process.env.INFURA_ID,
projectSecret: process.env.INFURA_SECRET,
})).then(({ success }) => success)
}, {
provider: new providers.InfuraProvider(1, {
projectId: process.env.INFURA_ID,
projectSecret: process.env.INFURA_SECRET,
},
)
}).then(({ success }) => success)
).resolves.toBeTruthy();
}
);
Expand All @@ -111,4 +157,68 @@ describe(`Unit`, () => {
});
(msg as any).validateMessage('0xdc35c7f8ba2720df052e0092556456127f00f7707eaa8e3bbff7e56774e7f2e05a093cfc9e02964c33d86e8e066e221b7d153d27e5a2e97ccd5ca7d3f2ce06cb1b');
}).toThrow());

test('Should not throw if params are valid.', async () => {
let wallet = Wallet.createRandom();
let msg = new SiweMessage({
address: wallet.address,
domain: "login.xyz",
statement: "Sign-In With Ethereum Example Statement",
uri: "https://login.xyz",
version: "1",
nonce: "bTyXgcQxn2htgkjJn",
issuedAt: "2022-01-27T17:09:38.578Z",
chainId: 1,
expirationTime: "2100-01-07T14:31:43.952Z"
});
const signature = await wallet.signMessage(msg.toMessage());
const result = await (msg as any).verify({ signature });
expect(result.success).toBeTruthy();
});

test('Should throw if params are invalid.', async () => {
let wallet = Wallet.createRandom();
let msg = new SiweMessage({
address: wallet.address,
domain: "login.xyz",
statement: "Sign-In With Ethereum Example Statement",
uri: "https://login.xyz",
version: "1",
nonce: "bTyXgcQxn2htgkjJn",
issuedAt: "2022-01-27T17:09:38.578Z",
chainId: 1,
expirationTime: "2100-01-07T14:31:43.952Z"
});
const signature = await wallet.signMessage(msg.toMessage());
let result;
try {
result = await (msg as any).verify({ signature, invalidKey: 'should throw' });
} catch (e) {
expect(e.success).toBeFalsy();
expect(e.error).toEqual(new Error('invalidKey is not a valid key for VerifyParams.'));
}
});

test('Should throw if opts are invalid.', async () => {
let wallet = Wallet.createRandom();
let msg = new SiweMessage({
address: wallet.address,
domain: "login.xyz",
statement: "Sign-In With Ethereum Example Statement",
uri: "https://login.xyz",
version: "1",
nonce: "bTyXgcQxn2htgkjJn",
issuedAt: "2022-01-27T17:09:38.578Z",
chainId: 1,
expirationTime: "2100-01-07T14:31:43.952Z"
});
const signature = await wallet.signMessage(msg.toMessage());
let result;
try {
result = await (msg as any).verify({ signature }, { suppressExceptions: true, invalidKey: 'should throw' });
} catch (e) {
expect(e.success).toBeFalsy();
expect(e.error).toEqual(new Error('invalidKey is not a valid key for VerifyOpts.'));
}
});
});
74 changes: 59 additions & 15 deletions packages/siwe/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
} from "@spruceid/siwe-parser";
import { providers, utils } from "ethers";
import * as uri from "valid-url";
import { SiweError, SiweErrorType, SiweResponse, VerifyParams } from "./types";
import { SiweError, SiweErrorType, SiweResponse, VerifyOpts, VerifyOptsKeys, VerifyParams, VerifyParamsKeys } from "./types";
import { checkContractWalletSignature, generateNonce } from "./utils";

export class SiweMessage {
Expand Down Expand Up @@ -70,6 +70,7 @@ export class SiweMessage {
this.chainId = parseInt(this.chainId);
}
}
this.nonce = this.nonce || generateNonce();
this.validateMessage();
}

Expand Down Expand Up @@ -145,7 +146,7 @@ export class SiweMessage {
}

/**
* This method parses all the fields in the object and creates a sign
* This method parses all the fields in the object and creates a messaging for signing
* message according with the type defined.
* @returns {string} Returns a message ready to be signed according with the
* type defined in the object.
Expand All @@ -167,20 +168,63 @@ export class SiweMessage {
}

/**
* Validates the integrity of the object by matching its signature.
* @deprecated
* Verifies the integrity of the object by matching its signature.
* @param signature Signature to match the address in the message.
* @param provider Ethers provider to be used for EIP-1271 validation
*/
async validate(
signature: string,
provider?: providers.Provider
) {
console.warn("validate() has been deprecated, please update your code to use verify(). validate() may be removed in future versions.");
return this.verify({ signature }, { provider, suppressExceptions: false });
}

/**
* Verifies the integrity of the object by matching its signature.
* @param params Parameters to verify the integrity of the message, signature is required.
* @returns {Promise<SiweMessage>} This object if valid.
*/
async verify(
params: VerifyParams,
provider?: providers.Provider
opts: VerifyOpts = { suppressExceptions: false },
): Promise<SiweResponse> {
return new Promise<SiweResponse>(async (resolve) => {
return new Promise<SiweResponse>(async (resolve, reject) => {

Object.keys(params).forEach((key: keyof VerifyParams) => {
if (!VerifyParamsKeys.includes(key)) {
reject({
success: false,
data: this,
error: new Error(`${key} is not a valid key for VerifyParams.`),
});
}
});

Object.keys(opts).forEach((key: keyof VerifyOpts) => {
if (!VerifyOptsKeys.includes(key)) {
reject({
success: false,
data: this,
error: new Error(`${key} is not a valid key for VerifyOpts.`),
});
}
});

const assert = (result) => {
if (opts.suppressExceptions) {
resolve(result);
} else {
reject(result);
}
};

const { signature, domain, nonce, time } = params;

/** Domain binding */
if (domain && domain !== this.domain) {
resolve({
assert({
success: false,
data: this,
error: new SiweError(
Expand All @@ -193,7 +237,7 @@ export class SiweMessage {

/** Nonce binding */
if (nonce && nonce !== this.nonce) {
resolve({
assert({
success: false,
data: this,
error: new SiweError(SiweErrorType.NONCE_MISMATCH, nonce, this.nonce),
Expand All @@ -207,7 +251,7 @@ export class SiweMessage {
if (this.expirationTime) {
const expirationDate = new Date(this.expirationTime);
if (checkTime.getTime() >= expirationDate.getTime()) {
resolve({
assert({
success: false,
data: this,
error: new SiweError(
Expand All @@ -223,11 +267,11 @@ export class SiweMessage {
if (this.notBefore) {
const notBefore = new Date(this.notBefore);
if (checkTime.getTime() < notBefore.getTime()) {
resolve({
assert({
success: false,
data: this,
error: new SiweError(
SiweErrorType.EXPIRED_MESSAGE,
SiweErrorType.NOT_YET_VALID_MESSAGE,
`${checkTime.toISOString()} >= ${notBefore.toISOString()}`,
`${checkTime.toISOString()} < ${notBefore.toISOString()}`
),
Expand All @@ -238,7 +282,7 @@ export class SiweMessage {
try {
EIP4361Message = this.prepareMessage();
} catch (e) {
resolve({
assert({
success: false,
data: this,
error: e,
Expand All @@ -259,13 +303,13 @@ export class SiweMessage {
isValid = await checkContractWalletSignature(
this,
signature,
provider
opts.provider
);
} catch (_) {
isValid = false;
} finally {
if (!isValid) {
resolve({
assert({
success: false,
data: this,
error: new SiweError(
Expand All @@ -287,7 +331,7 @@ export class SiweMessage {
}

/**
* Validates the value of this object fields.
* Validates the values of this object fields.
* @throws Throws an {ErrorType} if a field is invalid.
*/
private validateMessage(...args) {
Expand All @@ -297,7 +341,7 @@ export class SiweMessage {
}

/** `domain` check. */
if (this.domain.length === 0 || !/[^#?]*/.test(this.domain)) {
if (!this.domain || this.domain.length === 0 || !/[^#?]*/.test(this.domain)) {
throw new SiweError(SiweErrorType.INVALID_DOMAIN, `${this.domain} to be a valid domain.`);
}

Expand Down
12 changes: 12 additions & 0 deletions packages/siwe/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { providers } from "ethers";
import { SiweMessage } from "./client";

export interface VerifyParams {
Expand All @@ -14,6 +15,17 @@ export interface VerifyParams {
time?: string;
}

export const VerifyParamsKeys: Array<keyof VerifyParams> = ["signature", "domain", "nonce", "time"];

export interface VerifyOpts {
/** ethers provider to be used for EIP-1271 validation */
provider?: providers.Provider;

/** If the library should reject promises on errors, defaults to false */
suppressExceptions?: boolean;
}

export const VerifyOptsKeys: Array<keyof VerifyOpts> = ["provider", "suppressExceptions"];

/**
* Returned on verifications.
Expand Down
Loading

0 comments on commit eb64c77

Please sign in to comment.