diff --git a/packages/siwe/lib/client.test.ts b/packages/siwe/lib/client.test.ts index 68609a8e..b921712e 100644 --- a/packages/siwe/lib/client.test.ts +++ b/packages/siwe/lib/client.test.ts @@ -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'); @@ -19,9 +20,20 @@ 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)); } @@ -29,7 +41,7 @@ describe(`Message Generation`, () => { ); }); -describe(`Message verification`, () => { +describe(`Message verification without suppressExceptions`, () => { test.concurrent.each(Object.entries(verificationPositive))( 'Verificates message successfully: %s', async (_, test_fields) => { @@ -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); @@ -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)); } @@ -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))( @@ -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(); } ); @@ -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.')); + } + }); }); \ No newline at end of file diff --git a/packages/siwe/lib/client.ts b/packages/siwe/lib/client.ts index 7cacd921..aa51d3a1 100644 --- a/packages/siwe/lib/client.ts +++ b/packages/siwe/lib/client.ts @@ -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 { @@ -70,6 +70,7 @@ export class SiweMessage { this.chainId = parseInt(this.chainId); } } + this.nonce = this.nonce || generateNonce(); this.validateMessage(); } @@ -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. @@ -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} This object if valid. */ async verify( params: VerifyParams, - provider?: providers.Provider + opts: VerifyOpts = { suppressExceptions: false }, ): Promise { - return new Promise(async (resolve) => { + return new Promise(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( @@ -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), @@ -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( @@ -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()}` ), @@ -238,7 +282,7 @@ export class SiweMessage { try { EIP4361Message = this.prepareMessage(); } catch (e) { - resolve({ + assert({ success: false, data: this, error: e, @@ -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( @@ -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) { @@ -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.`); } diff --git a/packages/siwe/lib/types.ts b/packages/siwe/lib/types.ts index 0115edae..9082645b 100644 --- a/packages/siwe/lib/types.ts +++ b/packages/siwe/lib/types.ts @@ -1,3 +1,4 @@ +import { providers } from "ethers"; import { SiweMessage } from "./client"; export interface VerifyParams { @@ -14,6 +15,17 @@ export interface VerifyParams { time?: string; } +export const VerifyParamsKeys: Array = ["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 = ["provider", "suppressExceptions"]; /** * Returned on verifications. diff --git a/test/parsing_negative_objects.json b/test/parsing_negative_objects.json new file mode 100644 index 00000000..04992566 --- /dev/null +++ b/test/parsing_negative_objects.json @@ -0,0 +1,285 @@ +{ + "missing domain": { + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "missing address": { + "domain": "service.org", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "missing uri": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "missing version": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "missing chainId": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "missing nonce": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "missing issuedAt": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "domain not RFC4501 authority": { + "domain": "#notrfc4501", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "address not EIP-55": { + "domain": "service.org", + "address": "0xE5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "uri is non-RFC 3986": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": ":not_a_rfc3986_valid_uri_", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "version not 1": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "3", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "not a valid chainId": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": "?", + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "nonce with less then 8 chars": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "1234567", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "non-ISO 8601 issuedAt": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "Wed Oct 05 2011 16:48:00 GMT+0200 (CEST)", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "non-ISO 8601 expirationTime": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "Wed Oct 05 2011 16:48:00 GMT+0200 (CEST)", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "non-ISO 8601 notBefore": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "Wed Oct 05 2011 16:48:00 GMT+0200 (CEST)", + "requestId": "some_id", + "resources": [ + "https://service.org/login" + ] + }, + "first resource not-RFC 3986": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + ":not_a_rfc3986_valid_uri_", + "https://service.org/login" + ] + }, + "second resource is not-RFC3986": { + "domain": "service.org", + "address": "0xe5A12547fe4E872D192E3eCecb76F2Ce1aeA4946", + "statement": "I accept the ServiceOrg Terms of Service: https://service.org/tos", + "uri": "https://service.org/login", + "version": "1", + "chainId": 1, + "nonce": "12341234", + "issuedAt": "2022-03-17T12:45:13.610Z", + "expirationTime": "2023-03-17T12:45:13.610Z", + "notBefore": "2022-03-17T12:45:13.610Z", + "requestId": "some_id", + "resources": [ + "https://service.org/login", + ":not_a_rfc3986_valid_uri_" + ] + } +} \ No newline at end of file