Skip to content

Commit 0b9a133

Browse files
authored
feat: validate against multiple login uris (#291)
# Problem Providers deploying multiple apps may want to generate login payloads for multiple domains. Login URI validation should accept an array of URIs to validate against. # Acceptance Criteria - [x] Login URI validation accepts either a single string, or an array of strings of URIs to validate against
1 parent 254ab33 commit 0b9a133

File tree

2 files changed

+61
-24
lines changed

2 files changed

+61
-24
lines changed

libraries/js/src/payloads.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,17 @@ Issued At: 2024-10-10T18:40:37.344099626Z`,
118118
).resolves.toBeUndefined();
119119
});
120120

121+
it('Can verify a payload against multiple allowable domains', async () => {
122+
await expect;
123+
validatePayloads(
124+
{
125+
userPublicKey: ExampleUserPublicKey,
126+
payloads: [ExamplePayloadLoginUrl('http://localhost:3030/login')],
127+
},
128+
['otherdomain/login/callback', 'localhost:3030/login']
129+
);
130+
});
131+
121132
it('Will fail to verify a Login Payload with the wrong domain', async () => {
122133
await expect(
123134
validatePayloads(

libraries/js/src/payloads.ts

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ interface SiwxMessage {
2525
uri: string;
2626
}
2727

28+
interface ParsedUri {
29+
scheme: string | null;
30+
domain?: string;
31+
path?: string;
32+
queryString?: string;
33+
}
34+
2835
/**
2936
* Parses a given message string and extracts various components into a SiwxMessage object.
3037
*
@@ -126,42 +133,61 @@ function expect(test: boolean, errorMessage: string) {
126133
*
127134
* @throws Will throw an error if the schemes, paths, or domains do not match.
128135
*/
129-
function validateDomainAndUri(msgUri: string, expectedUri: string) {
130-
const parseUri = (uri: string) => {
136+
function validateDomainAndUri(msgUri: string, expectedUri: string | string[]) {
137+
const parseUri = (uri: string): ParsedUri => {
131138
const [scheme, domainWithPath] = uri.includes('://') ? uri.split('://', 2) : [null, uri];
132139
const [domainAndPath, queryString] = domainWithPath.split('?', 2) ?? [domainWithPath, ''];
133140
const [domain, path] = domainAndPath?.split('/', 2) ?? [domainAndPath, ''];
134141
return { scheme, domain, path, queryString };
135142
};
136143

137144
const msgParsed = parseUri(msgUri);
138-
const expectedParsed = parseUri(expectedUri);
139145

140-
// If the expected URI has a scheme, the scheme must match
141-
if (expectedParsed.scheme) {
142-
expect(
143-
msgParsed.scheme === expectedParsed.scheme,
144-
`Message does not match expected domain. Domain scheme mismatch. Scheme: ${msgParsed.scheme} Expected: ${expectedParsed.scheme}`
145-
);
146+
const errors: string[] = [];
147+
const uri = Array.isArray(expectedUri) ? expectedUri : [expectedUri];
148+
for (const expected of uri) {
149+
const expectedParsed = parseUri(expected);
150+
const error = validateParsedDomainAndUri(msgParsed, expectedParsed);
151+
if (!error) return;
152+
errors.push(error);
146153
}
147154

148-
// If the expected URI has a path, the path must match
149-
if (expectedParsed.path) {
155+
expect(errors.length === 0, 'Message does not match any expected domain. ' + errors.join('\n'));
156+
}
157+
158+
function validateParsedDomainAndUri(msgParsed: ParsedUri, expectedParsed: ParsedUri): string | null {
159+
try {
160+
// If the expected URI has a scheme, the scheme must match
161+
if (expectedParsed.scheme) {
162+
expect(
163+
msgParsed.scheme === expectedParsed.scheme,
164+
`Message does not match expected domain. Domain scheme mismatch. Scheme: ${msgParsed.scheme} Expected: ${expectedParsed.scheme}`
165+
);
166+
}
167+
168+
// If the expected URI has a path, the path must match
169+
if (expectedParsed.path) {
170+
expect(
171+
msgParsed.path === expectedParsed.path,
172+
`Message does not match expected domain. Domain path mismatch. Path: ${msgParsed.path} Expected: ${expectedParsed.path}`
173+
);
174+
}
175+
176+
// Ignore ports in validation
177+
const msgParsedDomain = msgParsed.domain?.split(':')[0];
178+
const expectedParsedDomain = expectedParsed.domain?.split(':')[0];
179+
180+
// If the domain in the message does not match the domain in the URI, throw an error
150181
expect(
151-
msgParsed.path === expectedParsed.path,
152-
`Message does not match expected domain. Domain path mismatch. Path: ${msgParsed.path} Expected: ${expectedParsed.path}`
182+
msgParsedDomain === expectedParsedDomain,
183+
`Message does not match expected domain. Domain: ${msgParsedDomain} Expected: ${expectedParsedDomain}`
153184
);
185+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
186+
} catch (error: any) {
187+
return error.message;
154188
}
155189

156-
// Ignore ports in validation
157-
const msgParsedDomain = msgParsed.domain?.split(':')[0];
158-
const expectedParsedDomain = expectedParsed.domain?.split(':')[0];
159-
160-
// If the domain in the message does not match the domain in the URI, throw an error
161-
expect(
162-
msgParsedDomain === expectedParsedDomain,
163-
`Message does not match expected domain. Domain: ${msgParsedDomain} Expected: ${expectedParsedDomain}`
164-
);
190+
return null;
165191
}
166192

167193
/**
@@ -179,7 +205,7 @@ function validateDomainAndUri(msgUri: string, expectedUri: string) {
179205
function validateLoginPayload(
180206
payload: SiwfResponsePayloadLogin,
181207
userPublicKey: SiwfPublicKey,
182-
loginMsgUri: string
208+
loginMsgUri: string | string[]
183209
): void {
184210
// Check that the userPublicKey signed the message
185211
expect(
@@ -219,7 +245,7 @@ function validateExtrinsicPayloadSignature(key: string, signature: string, messa
219245
expect(verifySignatureMaybeWrapped(key, signature, hexToU8a(message)), 'Payload signature failed');
220246
}
221247

222-
export async function validatePayloads(response: SiwfResponse, loginMsgUri: string): Promise<void> {
248+
export async function validatePayloads(response: SiwfResponse, loginMsgUri: string | string[]): Promise<void> {
223249
// Wait for the WASM to load
224250
await cryptoWaitReady();
225251
response.payloads.every((payload) => {

0 commit comments

Comments
 (0)