diff --git a/docs/src/Delegations.md b/docs/src/Delegations.md index b1a19c96..6e1a815c 100644 --- a/docs/src/Delegations.md +++ b/docs/src/Delegations.md @@ -18,5 +18,5 @@ Staging-Testnet and Production-Mainnet have the same delegation Schema Ids avail | [`dsnp.reply@v2`](https://spec.dsnp.org/DSNP/Types/Reply.html) | Reply to a content | 18 | | [`dsnp.tombstone@v2`](https://spec.dsnp.org/DSNP/Types/Tombstone.html) | Mark content for deletion | 16 | | [`dsnp.update@v2`](https://spec.dsnp.org/DSNP/Types/Update.html) | Update an existing post or reply | 19 | -| [`dsnp.user-attribute-set@v1`](https://spec.dsnp.org/DSNP/Types/UserAttributeSet.html) | Create an authenticated attribute set for a DSNP User | 13 | +| [`dsnp.user-attribute-set@v2`](https://spec.dsnp.org/DSNP/Types/UserAttributeSet.html) | Create an authenticated attribute set for a DSNP User | 20 | | [`frequency.default-token-address@v1`](https://github.com/frequency-chain/schemas) | List one or more default token receiving addresses | 21 | diff --git a/libraries/js/README.md b/libraries/js/README.md index f30f51f1..4272ae21 100644 --- a/libraries/js/README.md +++ b/libraries/js/README.md @@ -19,6 +19,7 @@ See [Markdown/GitHub Docs](../../docs/src/QuickStart.md) or | `generateAuthenticationUrl` | Generates the signed request for the authentication flow | | `getLoginResult` | Fetch and extract the Result of the Login | | `hasChainSubmissions` | Checks to see if there are any chain submissions in the given result | +| `validateSiwfResponse` | Takes a response payload and validates it | | `generateSignedRequest` | Generates the signed payload for the authentication flow using a keypair | | `buildSignedRequest` | Builds the signed request for the authentication flow using the signature and public key | | `generateEncodedSignedRequest` | Generates the encoded signed payload for the authentication flow using a keypair | diff --git a/libraries/js/src/response.test.ts b/libraries/js/src/response.test.ts index 32015441..9d6fa42b 100644 --- a/libraries/js/src/response.test.ts +++ b/libraries/js/src/response.test.ts @@ -1,7 +1,8 @@ import { describe, it, vi, expect, beforeAll } from 'vitest'; -import { ExampleLogin, ExampleNewProvider, ExampleNewUser } from './mocks/index.js'; -import { getLoginResult, hasChainSubmissions } from './response.js'; import { cryptoWaitReady } from '@polkadot/util-crypto'; +import base64url from 'base64url'; +import { ExampleLogin, ExampleNewProvider, ExampleNewUser } from './mocks/index.js'; +import { getLoginResult, hasChainSubmissions, validateSiwfResponse } from './response.js'; global.fetch = vi.fn(); @@ -57,3 +58,28 @@ describe('hasChainSubmissions', () => { expect(hasChainSubmissions(loginResponse)).toBe(false); }); }); + +describe('validateSiwfResponse', () => { + it('can handle a JSON strigified base64url encoded value', async () => { + const example = await ExampleLogin(); + await expect(validateSiwfResponse(base64url(JSON.stringify(example)))).to.resolves.toMatchObject(example); + }); + + it('can handle an object value', async () => { + const example = await ExampleLogin(); + await expect(validateSiwfResponse(example)).to.resolves.toMatchObject(example); + }); + + it('throws on a null value', async () => { + await expect(validateSiwfResponse(null)).to.rejects.toThrowError( + 'Response failed to correctly parse or invalid content: null' + ); + }); + + it('throws on a bad string value', async () => { + const value = base64url(JSON.stringify({ foo: 'bad' })); + await expect(validateSiwfResponse(value)).to.rejects.toThrowError( + 'Response failed to correctly parse or invalid content: {"foo":"bad"}' + ); + }); +}); diff --git a/libraries/js/src/response.ts b/libraries/js/src/response.ts index 5561d0fd..4c318c09 100644 --- a/libraries/js/src/response.ts +++ b/libraries/js/src/response.ts @@ -1,4 +1,5 @@ import { cryptoWaitReady } from '@polkadot/util-crypto'; +import base64url from 'base64url'; import { SiwfOptions } from './types/general.js'; import { isSiwfResponse, SiwfResponse } from './types/response.js'; import { parseEndpoint } from './util.js'; @@ -17,6 +18,39 @@ export function hasChainSubmissions(result: SiwfResponse): boolean { return !!result.payloads.find((x) => !!x.endpoint); } +/** + * Validate a possible SIWF Response + * + * @param {unknown} response - A possible SIWF Response. + * + * @returns {Promise} The validated response + */ +export async function validateSiwfResponse(response: unknown): Promise { + await cryptoWaitReady(); + + let body = response; + if (typeof response === 'string') { + try { + body = JSON.parse(base64url.decode(response)); + } catch (_e) { + throw new Error(`Response failed to correctly parse: ${response}`); + } + } + + // This also validates that userPublicKey is a valid address + if (!isSiwfResponse(body)) { + throw new Error(`Response failed to correctly parse or invalid content: ${JSON.stringify(body)}`); + } + + // Validate Payloads + await validatePayloads(body); + + // Validate Credentials (if any), but trust DIDs from frequencyAccess + await validateCredentials(body.credentials, ['did:web:frequencyaccess.com', 'did:web:testnet.frequencyaccess.com']); + + return body; +} + /** * Fetch and extract the Result of the Login from Frequency Access * @@ -27,7 +61,6 @@ export function hasChainSubmissions(result: SiwfResponse): boolean { * @returns {Promise} The parsed and validated response */ export async function getLoginResult(authorizationCode: string, options?: SiwfOptions): Promise { - await cryptoWaitReady(); const endpoint = new URL( `${parseEndpoint(options?.endpoint, '/api/payload')}?authorizationCode=${authorizationCode}` ); @@ -39,16 +72,5 @@ export async function getLoginResult(authorizationCode: string, options?: SiwfOp const body = await response.json(); - // This also validates that userPublicKey is a valid address - if (!isSiwfResponse(body)) { - throw new Error(`Response failed to correctly parse or invalid content: ${await response.text()}`); - } - - // Validate Payloads - await validatePayloads(body); - - // Validate Credentials (if any), but trust DIDs from frequencyAccess - await validateCredentials(body.credentials, ['did:web:frequencyaccess.com', 'did:web:testnet.frequencyaccess.com']); - - return body; + return validateSiwfResponse(body); }