Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Domain only validation doesn't work for custom schemes #205

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/src/Actions/Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ No submission to the chain is required, but the application *must* validate the
The message signed follows [CAIP-122: Sign in With X](https://chainagnostic.org/CAIPs/caip-122) specification which is derived from [EIP-4361: Sign-In with Ethereum](https://eips.ethereum.org/EIPS/eip-4361).

#### Example Message with Placeholders
```

```text
{{domain}} wants you to sign in with your Frequency account:
frequency:{{chainReference}}:{{ss58Address}}

Expand All @@ -107,7 +108,10 @@ Chain ID: frequency:{{chainReference}}
Issued At: {{issued-at}}
```

Inside the message, `{{domain}}` is the domain of the application requesting the sign-in. `{{domain}}` should match the domain contained in the `URI` field.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Domain clarification


#### Validation Steps

1. Perform an Sr25519 signature verification using:
- `userPublicKey`: The signing key
- `payload.message`: The signed message parsing `\n` into `LF` line breaks
Expand Down
2 changes: 1 addition & 1 deletion docs/src/DataStructures/Request.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
```json
{
"signedRequest": "eyJyZXF1ZXN0ZWRTaWduYXR1cmVzIjp7InB1YmxpY0tleSI6eyJlbmNvZGVkVmFsdWUiOiJmNmNMNHdxMUhVTngxMVRjdmRBQk5mOVVOWFhveUg0N21WVXdUNTl0elNGUlc4eURIIiwiZW5jb2RpbmciOiJiYXNlNTgiLCJmb3JtYXQiOiJzczU4IiwidHlwZSI6IlNyMjU1MTkifSwic2lnbmF0dXJlIjp7ImFsZ28iOiJTUjI1NTE5IiwiZW5jb2RpbmciOiJiYXNlMTYiLCJlbmNvZGVkVmFsdWUiOiIweDNlMTdhYzM3Yzk3ZWE3M2E3YzM1ZjBjYTJkZTcxYmY3MmE5NjlkYjhiNjQyYzU3ZTI2N2Q4N2Q1OTA3ZGM4MzVmYTJjODI4MTdlODA2YTQ5NGIyY2E5Y2U5MjJmNDM1NDY4M2U4YzAxMzY5NTNlMGZlNWExODJkMzU0NjQ2Yzg4In0sInBheWxvYWQiOnsiY2FsbGJhY2siOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJwZXJtaXNzaW9ucyI6WzUsNyw4LDksMTBdfX0sInJlcXVlc3RlZENyZWRlbnRpYWxzIjpbeyJ0eXBlIjoiVmVyaWZpZWRHcmFwaEtleUNyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFtZHZteGQ1NHp2ZTVraWZ5Y2dzZHRvYWhzNWVjZjRoYWwydHMzZWV4a2dvY3ljNW9jYTJ5Il19LHsiYW55T2YiOlt7InR5cGUiOiJWZXJpZmllZEVtYWlsQWRkcmVzc0NyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFlNHFvY3poZnRpY2k0ZHpmdmZiZWw3Zm80aDRzcjVncmNvM29vdnd5azZ5NHluZjQ0dHNpIl19LHsidHlwZSI6IlZlcmlmaWVkUGhvbmVOdW1iZXJDcmVkZW50aWFsIiwiaGFzaCI6WyJiY2lxanNwbmJ3cGMzd2p4NGZld2NlazVkYXlzZGpwYmY1eGppbXo1d251NXVqN2UzdnUydXducSJdfV19XX0",
"signedRequest": "eyJyZXF1ZXN0ZWRTaWduYXR1cmVzIjp7InB1YmxpY0tleSI6eyJlbmNvZGVkVmFsdWUiOiJmNmNMNHdxMUhVTngxMVRjdmRBQk5mOVVOWFhveUg0N21WVXdUNTl0elNGUlc4eURIIiwiZW5jb2RpbmciOiJiYXNlNTgiLCJmb3JtYXQiOiJzczU4IiwidHlwZSI6IlNyMjU1MTkifSwic2lnbmF0dXJlIjp7ImFsZ28iOiJTUjI1NTE5IiwiZW5jb2RpbmciOiJiYXNlMTYiLCJlbmNvZGVkVmFsdWUiOiIweDk2MGYxOTVkYzFmOTFiZjcxYzBiMzUyMzE1MGFlMzc0NzFiZWRlMDdhMDAzOTA5NjQ3Y2NmMDQwYWNkNWNkMDRlYTQ4NzBiZDEyNGNhZmEyZGViNTliMGUzNzhjYjE5ZmJjNmFmNjAxYjc1NTU5ZmFhYjdiNzY4ZGU4MWEwOTgzIn0sInBheWxvYWQiOnsiY2FsbGJhY2siOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJwZXJtaXNzaW9ucyI6WzUsNyw4LDksMTBdfX0sInJlcXVlc3RlZENyZWRlbnRpYWxzIjpbeyJ0eXBlIjoiVmVyaWZpZWRHcmFwaEtleUNyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFtZHZteGQ1NHp2ZTVraWZ5Y2dzZHRvYWhzNWVjZjRoYWwydHMzZWV4a2dvY3ljNW9jYTJ5Il19LHsiYW55T2YiOlt7InR5cGUiOiJWZXJpZmllZEVtYWlsQWRkcmVzc0NyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFlNHFvY3poZnRpY2k0ZHpmdmZiZWw3Zm80aDRzcjVncmNvM29vdnd5azZ5NHluZjQ0dHNpIl19LHsidHlwZSI6IlZlcmlmaWVkUGhvbmVOdW1iZXJDcmVkZW50aWFsIiwiaGFzaCI6WyJiY2lxanNwbmJ3cGMzd2p4NGZld2NlazVkYXlzZGpwYmY1eGppbXo1d251NXVqN2UzdnUydXducSJdfV19XX0",
"mode": "dark"
}
```
2 changes: 1 addition & 1 deletion docs/src/DataStructures/RequestUrl.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
```json
"https://testnet.frequencyaccess.com/siwa/start?signedRequest=eyJyZXF1ZXN0ZWRTaWduYXR1cmVzIjp7InB1YmxpY0tleSI6eyJlbmNvZGVkVmFsdWUiOiJmNmNMNHdxMUhVTngxMVRjdmRBQk5mOVVOWFhveUg0N21WVXdUNTl0elNGUlc4eURIIiwiZW5jb2RpbmciOiJiYXNlNTgiLCJmb3JtYXQiOiJzczU4IiwidHlwZSI6IlNyMjU1MTkifSwic2lnbmF0dXJlIjp7ImFsZ28iOiJTUjI1NTE5IiwiZW5jb2RpbmciOiJiYXNlMTYiLCJlbmNvZGVkVmFsdWUiOiIweDNlMTdhYzM3Yzk3ZWE3M2E3YzM1ZjBjYTJkZTcxYmY3MmE5NjlkYjhiNjQyYzU3ZTI2N2Q4N2Q1OTA3ZGM4MzVmYTJjODI4MTdlODA2YTQ5NGIyY2E5Y2U5MjJmNDM1NDY4M2U4YzAxMzY5NTNlMGZlNWExODJkMzU0NjQ2Yzg4In0sInBheWxvYWQiOnsiY2FsbGJhY2siOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJwZXJtaXNzaW9ucyI6WzUsNyw4LDksMTBdfX0sInJlcXVlc3RlZENyZWRlbnRpYWxzIjpbeyJ0eXBlIjoiVmVyaWZpZWRHcmFwaEtleUNyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFtZHZteGQ1NHp2ZTVraWZ5Y2dzZHRvYWhzNWVjZjRoYWwydHMzZWV4a2dvY3ljNW9jYTJ5Il19LHsiYW55T2YiOlt7InR5cGUiOiJWZXJpZmllZEVtYWlsQWRkcmVzc0NyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFlNHFvY3poZnRpY2k0ZHpmdmZiZWw3Zm80aDRzcjVncmNvM29vdnd5azZ5NHluZjQ0dHNpIl19LHsidHlwZSI6IlZlcmlmaWVkUGhvbmVOdW1iZXJDcmVkZW50aWFsIiwiaGFzaCI6WyJiY2lxanNwbmJ3cGMzd2p4NGZld2NlazVkYXlzZGpwYmY1eGppbXo1d251NXVqN2UzdnUydXducSJdfV19XX0&mode=dark"
"https://testnet.frequencyaccess.com/siwa/start?signedRequest=eyJyZXF1ZXN0ZWRTaWduYXR1cmVzIjp7InB1YmxpY0tleSI6eyJlbmNvZGVkVmFsdWUiOiJmNmNMNHdxMUhVTngxMVRjdmRBQk5mOVVOWFhveUg0N21WVXdUNTl0elNGUlc4eURIIiwiZW5jb2RpbmciOiJiYXNlNTgiLCJmb3JtYXQiOiJzczU4IiwidHlwZSI6IlNyMjU1MTkifSwic2lnbmF0dXJlIjp7ImFsZ28iOiJTUjI1NTE5IiwiZW5jb2RpbmciOiJiYXNlMTYiLCJlbmNvZGVkVmFsdWUiOiIweDk2MGYxOTVkYzFmOTFiZjcxYzBiMzUyMzE1MGFlMzc0NzFiZWRlMDdhMDAzOTA5NjQ3Y2NmMDQwYWNkNWNkMDRlYTQ4NzBiZDEyNGNhZmEyZGViNTliMGUzNzhjYjE5ZmJjNmFmNjAxYjc1NTU5ZmFhYjdiNzY4ZGU4MWEwOTgzIn0sInBheWxvYWQiOnsiY2FsbGJhY2siOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJwZXJtaXNzaW9ucyI6WzUsNyw4LDksMTBdfX0sInJlcXVlc3RlZENyZWRlbnRpYWxzIjpbeyJ0eXBlIjoiVmVyaWZpZWRHcmFwaEtleUNyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFtZHZteGQ1NHp2ZTVraWZ5Y2dzZHRvYWhzNWVjZjRoYWwydHMzZWV4a2dvY3ljNW9jYTJ5Il19LHsiYW55T2YiOlt7InR5cGUiOiJWZXJpZmllZEVtYWlsQWRkcmVzc0NyZWRlbnRpYWwiLCJoYXNoIjpbImJjaXFlNHFvY3poZnRpY2k0ZHpmdmZiZWw3Zm80aDRzcjVncmNvM29vdnd5azZ5NHluZjQ0dHNpIl19LHsidHlwZSI6IlZlcmlmaWVkUGhvbmVOdW1iZXJDcmVkZW50aWFsIiwiaGFzaCI6WyJiY2lxanNwbmJ3cGMzd2p4NGZld2NlazVkYXlzZGpwYmY1eGppbXo1d251NXVqN2UzdnUydXducSJdfV19XX0&mode=dark"
```
4 changes: 2 additions & 2 deletions docs/src/DataStructures/Response-LoginOnly.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
"signature": {
"algo": "SR25519",
"encoding": "base16",
"encodedValue": "0x84a4e03344b07d64087ebdf47b2c8c679aa7de78179689988992609f1b83c34f6086c7de99ef41c5325cce64d148624e716c605d355f22d1281f6d23f546f584"
"encodedValue": "0xe261698297111834e68b4152bf1f89819e886b6528f6fff45715f7781d0f1e7dc4007ccfed1e85b8c603c0fea2f7abf22bfe6336869ad21f11a09a114452c680"
},
"type": "login",
"payload": {
"message": "localhost wants you to sign in with your Frequency account:\nf6akufkq9Lex6rT8RCEDRuoZQRgo5pWiRzeo81nmKNGWGNJdJ\n\n\n\nURI: https://testnet.frequencyaccess.com/signin/confirm\nNonce: N6rLwqyz34oUxJEXJ\nIssued At: 2024-03-05T23:18:03.041Z\nExpiration Time: 2060-03-05T23:23:03.041Z"
"message": "your-app.com wants you to sign in with your Frequency account:\nf6akufkq9Lex6rT8RCEDRuoZQRgo5pWiRzeo81nmKNGWGNJdJ\n\n\n\nURI: https://your-app.com/signin/callback\nNonce: N6rLwqyz34oUxJEXJ\nIssued At: 2024-10-29T19:17:27.077Z\nExpiration Time: 2060-03-05T23:23:03.041Z"
}
}
],
Expand Down
42 changes: 32 additions & 10 deletions libraries/js/src/mocks/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,52 @@ import {
serializeItemActionsPayloadHex,
} from '../util.js';

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()}`;
function generateLoginMessage(account: string, issued: Date, expires: Date, uri: URL) {
return `${uri.hostname} wants you to sign in with your Frequency account:\n${account}\n\n\n\nURI: ${uri}\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 = (domain: string) =>
const loginMessageUrl = (url: string) =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows to build test cases with different urls

generateLoginMessage(
ExampleUserKey.public,
new Date(now - 24 * 60 * 60 * 1000),
new Date(now + 24 * 60 * 60 * 1000),
domain
new URL(url)
);

const loginMessageGood = () => loginMessageUrl('https://your-app.com/signin/callback');

const loginMessageExpired = () =>
generateLoginMessage(
ExampleUserKey.public,
new Date(now - 2 * 24 * 60 * 60 * 1000),
new Date(now - 24 * 60 * 60 * 1000),
'localhost'
new URL('https://your-app.com/signin/callback')
);

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

export const ExamplePayloadLoginGood = (): SiwfResponsePayloadLogin => ({
signature: {
algo: 'SR25519',
encoding: 'base16',
encodedValue: u8aToHex(ExampleUserKey.keyPair().sign(loginMessageGood(domain))),
encodedValue: u8aToHex(ExampleUserKey.keyPair().sign(loginMessageGood())),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domain will always match the URI in the test cases

},
type: 'login',
payload: {
message: loginMessageGood(domain),
message: loginMessageGood(),
},
});

Expand All @@ -64,12 +78,20 @@ export const ExamplePayloadLoginStatic: SiwfResponsePayloadLogin = {
algo: 'SR25519',
encoding: 'base16',
encodedValue:
'0x84a4e03344b07d64087ebdf47b2c8c679aa7de78179689988992609f1b83c34f6086c7de99ef41c5325cce64d148624e716c605d355f22d1281f6d23f546f584',
'0xe261698297111834e68b4152bf1f89819e886b6528f6fff45715f7781d0f1e7dc4007ccfed1e85b8c603c0fea2f7abf22bfe6336869ad21f11a09a114452c680',
},
type: 'login',
payload: {
message:
'localhost wants you to sign in with your Frequency account:\nf6akufkq9Lex6rT8RCEDRuoZQRgo5pWiRzeo81nmKNGWGNJdJ\n\n\n\nURI: https://testnet.frequencyaccess.com/signin/confirm\nNonce: N6rLwqyz34oUxJEXJ\nIssued At: 2024-03-05T23:18:03.041Z\nExpiration Time: 2060-03-05T23:23:03.041Z',
'your-app.com wants you to sign in with your Frequency account:\n' +
'f6akufkq9Lex6rT8RCEDRuoZQRgo5pWiRzeo81nmKNGWGNJdJ\n' +
'\n' +
'\n' +
'\n' +
'URI: https://your-app.com/signin/callback\n' +
'Nonce: N6rLwqyz34oUxJEXJ\n' +
'Issued At: 2024-10-29T19:17:27.077Z\n' +
'Expiration Time: 2060-03-05T23:23:03.041Z',
},
};

Expand Down
144 changes: 137 additions & 7 deletions libraries/js/src/payloads.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ExamplePayloadGrantDelegation,
ExamplePayloadLoginGood,
ExamplePayloadLoginStatic,
ExamplePayloadLoginUrl,
ExamplePayloadPublicGraphKey,
} from './mocks/payloads.js';
import { ExampleUserPublicKey } from './mocks/index.js';
Expand Down Expand Up @@ -62,9 +63,57 @@ Issued At: 2024-10-10T18:40:37.344099626Z`,
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginGood('localhost')],
payloads: [ExamplePayloadLoginGood()],
},
'localhost'
'your-app.com'
)
).resolves.toBeUndefined();
});

it('Can verify a Generated Login Payload: app scheme', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('example://login')],
},
'example://login'
)
).resolves.toBeUndefined();
});

it('Can verify a Generated Login Payload: https://example.com/login', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('https://example.com/login')],
},
'https://example.com/login'
)
).resolves.toBeUndefined();
});

it('Can verify a Generated Login Payload: www.example.com/login', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('http://www.example.com/login')],
},
'www.example.com/login'
)
).resolves.toBeUndefined();
});

it('Can verify a Generated Login Payload: localhost:3030/login/path', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('localhost:3030/login/path')],
},
'localhost:3030/login/path'
)
).resolves.toBeUndefined();
});
Expand All @@ -74,21 +123,102 @@ Issued At: 2024-10-10T18:40:37.344099626Z`,
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginGood('badhost')],
payloads: [ExamplePayloadLoginUrl('http://badhost')],
},
'localhost'
)
).rejects.toThrowError('Message does not match expected domain. Message: badhost Expected: localhost');
).rejects.toThrowError('Message does not match expected domain. Domain: badhost Expected: localhost');

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

it('Will fail to verify a Generated Login Payload with an incorrect app scheme', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('notexample://login')],
},
'example://login'
)
).rejects.toThrowError(
'Message does not match expected domain. Domain scheme mismatch. Scheme: notexample Expected: example'
);
});

it('Will fail to verify a Generated Login Payload with an incorrect https scheme', async () => {
// Check the scheme
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('http://example.com/login')],
},
'https://example.com/login'
)
).rejects.toThrowError(
'Message does not match expected domain. Domain scheme mismatch. Scheme: http Expected: https'
);

// Check the domain
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('https://www.examples.com/login')],
},
'https://www.example.com/login'
)
).rejects.toThrowError(
'Message does not match expected domain. Domain: www.examples.com Expected: www.example.com'
);

// Check the path
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('https://www.example.com/logins')],
},
'https://www.example.com/login'
)
).rejects.toThrowError(
'Message does not match expected domain. Domain path mismatch. Path: logins Expected: login'
);
});

it('Will fail to verify a Generated Login Payload with an incorrect www scheme', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('http://www.examples.com/login')],
},
'www.example.com/login'
)
).rejects.toThrowError(
'Message does not match expected domain. Domain: www.examples.com Expected: www.example.com'
);
});

it('Can verify a Generated Login Payload with a query string', async () => {
await expect(
validatePayloads(
{
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginUrl('https://example.com/login?query=string')],
},
'https://example.com/login'
)
).resolves.toBeUndefined();
});

it('Can verify a Static Login Payload', async () => {
Expand All @@ -98,7 +228,7 @@ Issued At: 2024-10-10T18:40:37.344099626Z`,
userPublicKey: ExampleUserPublicKey,
payloads: [ExamplePayloadLoginStatic],
},
'localhost'
'your-app.com'
)
).resolves.toBeUndefined();
});
Expand Down
Loading