Skip to content

Commit

Permalink
Require login domain for validation (#181)
Browse files Browse the repository at this point in the history
# Problem

Login domain was not being validated.

## Solution

Added the domain check.
  • Loading branch information
wilwade authored Oct 4, 2024
1 parent 1c8c458 commit 9ad33a4
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 76 deletions.
22 changes: 14 additions & 8 deletions libraries/js/src/mocks/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,38 @@ import {
serializeItemActionsPayloadHex,
} from '../util.js';

function generateLoginMessage(account: string, issued: Date, expires: Date) {
return `localhost wants you to sign in with your Frequency account:\n${account}\n\n\n\nURI: https://testnet.frequencyaccess.com/signin/confirm\nNonce: N6rLwqyz34oUxJEXJ\nIssued At: ${issued.toISOString()}\nExpiration Time: ${expires.toISOString()}`;
function generateLoginMessage(account: string, issued: Date, expires: Date, domain: string) {
return `${domain} wants you to sign in with your Frequency account:\n${account}\n\n\n\nURI: https://testnet.frequencyaccess.com/signin/confirm\nNonce: N6rLwqyz34oUxJEXJ\nIssued At: ${issued.toISOString()}\nExpiration Time: ${expires.toISOString()}`;
}

// Setup now so that it is consistent for the entire test run
const now = Date.now();

const loginMessageGood = () =>
generateLoginMessage(ExampleUserKey.public, new Date(now - 24 * 60 * 60 * 1000), new Date(now + 24 * 60 * 60 * 1000));
const loginMessageGood = (domain: string) =>
generateLoginMessage(
ExampleUserKey.public,
new Date(now - 24 * 60 * 60 * 1000),
new Date(now + 24 * 60 * 60 * 1000),
domain
);

const loginMessageExpired = () =>
generateLoginMessage(
ExampleUserKey.public,
new Date(now - 2 * 24 * 60 * 60 * 1000),
new Date(now - 24 * 60 * 60 * 1000)
new Date(now - 24 * 60 * 60 * 1000),
'localhost'
);

export const ExamplePayloadLoginGood = (): SiwfResponsePayloadLogin => ({
export const ExamplePayloadLoginGood = (domain: string): SiwfResponsePayloadLogin => ({
signature: {
algo: 'Sr25519',
encoding: 'base16',
encodedValue: u8aToHex(ExampleUserKey.keyPair().sign(loginMessageGood())),
encodedValue: u8aToHex(ExampleUserKey.keyPair().sign(loginMessageGood(domain))),
},
type: 'login',
payload: {
message: loginMessageGood(),
message: loginMessageGood(domain),
},
});

Expand Down
143 changes: 99 additions & 44 deletions libraries/js/src/payloads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,111 +22,166 @@ describe('validatePayloads', () => {
describe('Login Related Payloads', () => {
it('Can verify a Generated Login Payload', async () => {
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginGood()],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginGood('localhost')],
},
'localhost'
)
).resolves.toBeUndefined();
});

it('Will fail to verify a Login Payload with the wrong domain', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginGood('badhost')],
},
'localhost'
)
).rejects.toThrowError('Message does not match expected domain. Message: badhost Expected: localhost');

await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginGood('localhost')],
},
'betterhost'
)
).rejects.toThrowError('Message does not match expected domain. Message: localhost Expected: betterhost');
});

it('Can verify a Static Login Payload', async () => {
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginStatic],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginStatic],
},
'localhost'
)
).resolves.toBeUndefined();
});
});

it('Can verify a ClaimHandle', async () => {
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadClaimHandle()],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadClaimHandle()],
},
'localhost'
)
).resolves.toBeUndefined();
});

it('Can fail a ClaimHandle with wrong key', async () => {
const upk = { ...ExampleUserPublicKey };
upk.encodedValue = ExampleProviderKey.public;
await expect(
validatePayloads({
userPublicKey: upk,
payloads: [ExamplePayloadClaimHandle()],
})
validatePayloads(
{
userPublicKey: upk,
payloads: [ExamplePayloadClaimHandle()],
},
'localhost'
)
).rejects.toThrowError('Payload signature failed');
});

it('Can verify a Create MSA', async () => {
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadCreateSponsoredAccount()],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadCreateSponsoredAccount()],
},
'localhost'
)
).resolves.toBeUndefined();
});

it('Can fail a bad Create MSA with a bad signature', async () => {
const payload = ExamplePayloadCreateSponsoredAccount();
payload.signature.encodedValue += 'ff';
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
},
'localhost'
)
).rejects.toThrowError('Payload signature failed');
});

it('Can verify a Add Delegation', async () => {
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadGrantDelegation()],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadGrantDelegation()],
},
'localhost'
)
).resolves.toBeUndefined();
});

it('Can fail a bad Add Delegation with a wrong payload', async () => {
const payload = ExamplePayloadGrantDelegation();
payload.payload.authorizedMsaId = 100000;
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
},
'localhost'
)
).rejects.toThrowError('Payload signature failed');
});

it('Can verify an Add Items', async () => {
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadPublicGraphKey()],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadPublicGraphKey()],
},
'localhost'
)
).resolves.toBeUndefined();
});

it('Can fail with a wrong Add Items payload', async () => {
const payload = ExamplePayloadPublicGraphKey();
payload.payload.schemaId = 1111;
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
},
'localhost'
)
).rejects.toThrowError('Payload signature failed');
});

it('Can fail with a wrong payload', async () => {
const payload = ExamplePayloadPublicGraphKey();
(payload.payload as any) = {};
await expect(
validatePayloads({
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
})
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [payload],
},
'localhost'
)
).rejects.toThrowError('Unknown or Bad Payload: itemActions');
});
});
16 changes: 11 additions & 5 deletions libraries/js/src/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@ function expect(test: boolean, errorMessage: string) {
if (!test) throw new Error(errorMessage);
}

function validateLoginPayload(payload: SiwfResponsePayloadLogin, userPublicKey: SiwfPublicKey): void {
function validateLoginPayload(
payload: SiwfResponsePayloadLogin,
userPublicKey: SiwfPublicKey,
loginMsgDomain: string
): void {
// Check that the userPublicKey signed the message
const signedMessage = payload.payload.message;
const verifyResult = signatureVerify(signedMessage, payload.signature.encodedValue, userPublicKey.encodedValue);
Expand All @@ -67,8 +71,10 @@ function validateLoginPayload(payload: SiwfResponsePayloadLogin, userPublicKey:

// Validate the message contents
const msg = parseMessage(signedMessage);
// TODO: Get the domain of the callback URL and get it passed through
//expect(msg.domain === domain, `Message does not match expected domain. Message: ${msg.domain} Expected: ${domain}`);
expect(
msg.domain === loginMsgDomain,
`Message does not match expected domain. Message: ${msg.domain} Expected: ${loginMsgDomain}`
);
expect(
msg.address === userPublicKey.encodedValue,
`Message does not match expected user public key value. Message: ${msg.address}`
Expand All @@ -85,13 +91,13 @@ function validateSignature(key: string, signature: string, message: string) {
expect(verifyResult.isValid, 'Payload signature failed');
}

export async function validatePayloads(response: SiwfResponse): Promise<void> {
export async function validatePayloads(response: SiwfResponse, loginMsgDomain: string): Promise<void> {
// Wait for the WASM to load
await cryptoWaitReady();
response.payloads.every((payload) => {
switch (true) {
case isPayloadLogin(payload):
return validateLoginPayload(payload, response.userPublicKey);
return validateLoginPayload(payload, response.userPublicKey, loginMsgDomain);
case isPayloadAddProvider(payload):
return validateSignature(
response.userPublicKey.encodedValue,
Expand Down
16 changes: 9 additions & 7 deletions libraries/js/src/response.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('getLoginResult', () => {
text: () => Promise.resolve('MOCK'),
} as any);

await expect(getLoginResult('fakeAuthCode')).to.resolves.toMatchObject(example);
await expect(getLoginResult('fakeAuthCode', { loginMsgDomain: 'localhost' })).to.resolves.toMatchObject(example);
});

it('Can get and validate a New User', async () => {
Expand All @@ -30,7 +30,7 @@ describe('getLoginResult', () => {
text: () => Promise.resolve('MOCK'),
} as any);

await expect(getLoginResult('fakeAuthCode')).to.resolves.toMatchObject(example);
await expect(getLoginResult('fakeAuthCode', { loginMsgDomain: 'localhost' })).to.resolves.toMatchObject(example);
});

it('Can get and validate a New Provider', async () => {
Expand All @@ -41,7 +41,7 @@ describe('getLoginResult', () => {
text: () => Promise.resolve('MOCK'),
} as any);

await expect(getLoginResult('fakeAuthCode')).to.resolves.toMatchObject(example);
await expect(getLoginResult('fakeAuthCode', { loginMsgDomain: 'localhost' })).to.resolves.toMatchObject(example);
});
});

Expand All @@ -62,23 +62,25 @@ describe('hasChainSubmissions', () => {
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);
await expect(validateSiwfResponse(base64url(JSON.stringify(example)), 'localhost')).to.resolves.toMatchObject(
example
);
});

it('can handle an object value', async () => {
const example = await ExampleLogin();
await expect(validateSiwfResponse(example)).to.resolves.toMatchObject(example);
await expect(validateSiwfResponse(example, 'localhost')).to.resolves.toMatchObject(example);
});

it('throws on a null value', async () => {
await expect(validateSiwfResponse(null)).to.rejects.toThrowError(
await expect(validateSiwfResponse(null, 'localhost')).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(
await expect(validateSiwfResponse(value, 'localhost')).to.rejects.toThrowError(
'Response failed to correctly parse or invalid content: {"foo":"bad"}'
);
});
Expand Down
Loading

0 comments on commit 9ad33a4

Please sign in to comment.