Skip to content

Commit 5f9a57e

Browse files
committed
Strip URL scheme from login domain
Add stripUrlScheme utility and use it to normalize domains by removing http/https schemes. Apply this to createLoginMessage, generateLoginPayload, and verifyLoginPayload so payload.domain is stored and compared without a URL scheme (per EIP-4361). Update tests to cover scheme stripping and backward compatibility when verifying payloads. Also adjust biome.json formatter line ending to CRLF.
1 parent 8e9c51e commit 5f9a57e

7 files changed

Lines changed: 122 additions & 5 deletions

File tree

.changeset/hot-donkeys-itch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"thirdweb": minor
3+
---
4+
5+
Stripe URL scheme from SIWE domain across all touchpoints

packages/thirdweb/src/auth/core/create-login-message.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
2-
import { createLoginMessage } from "./create-login-message.js";
2+
import { createLoginMessage, stripUrlScheme } from "./create-login-message.js";
33

44
describe("createLoginMessage", () => {
55
beforeAll(() => {
@@ -72,4 +72,37 @@ describe("createLoginMessage", () => {
7272
Not Before: 1634567800"
7373
`);
7474
});
75+
76+
test("should strip URL scheme from domain in the message", () => {
77+
const payload = {
78+
address: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
79+
chain_id: "1",
80+
domain: "https://example.com",
81+
expiration_time: "1634567990",
82+
invalid_before: "1634567800",
83+
issued_at: "1634567890",
84+
nonce: "123456",
85+
statement: "This is a statement",
86+
uri: "https://example.com",
87+
version: "1.0",
88+
};
89+
90+
const result = createLoginMessage(payload);
91+
expect(result).toContain(
92+
"example.com wants you to sign in with your Ethereum account:",
93+
);
94+
expect(result).not.toContain("https://example.com wants you to sign in");
95+
});
96+
97+
test("stripUrlScheme should strip https scheme", () => {
98+
expect(stripUrlScheme("https://example.com")).toBe("example.com");
99+
});
100+
101+
test("stripUrlScheme should strip http scheme", () => {
102+
expect(stripUrlScheme("http://example.com")).toBe("example.com");
103+
});
104+
105+
test("stripUrlScheme should leave bare domains unchanged", () => {
106+
expect(stripUrlScheme("example.com")).toBe("example.com");
107+
});
75108
});

packages/thirdweb/src/auth/core/create-login-message.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
import type { LoginPayload } from "./types.js";
22

3+
/**
4+
* Strips the URL scheme (e.g. "https://") from a domain string.
5+
* Per EIP-4361, the domain field should be an RFC 3986 authority (host without scheme).
6+
* @internal
7+
*/
8+
export function stripUrlScheme(domain: string): string {
9+
return domain.replace(/^https?:\/\//, "");
10+
}
11+
312
/**
413
* Create an EIP-4361 & CAIP-122 compliant message to sign based on the login payload
514
* @param payload - The login payload containing the necessary information.
@@ -8,7 +17,8 @@ import type { LoginPayload } from "./types.js";
817
*/
918
export function createLoginMessage(payload: LoginPayload): string {
1019
const typeField = "Ethereum";
11-
const header = `${payload.domain} wants you to sign in with your ${typeField} account:`;
20+
const domain = stripUrlScheme(payload.domain);
21+
const header = `${domain} wants you to sign in with your ${typeField} account:`;
1222
let prefix = [header, payload.address].join("\n");
1323
prefix = [prefix, payload.statement].join("\n\n");
1424
if (payload.statement) {

packages/thirdweb/src/auth/core/generate-login-payload.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,29 @@ describe("generateLoginPayload", () => {
9494
}
9595
`);
9696
});
97+
98+
test("should strip URL scheme from domain", async () => {
99+
const options = {
100+
client: TEST_CLIENT,
101+
domain: "https://example.com",
102+
login: {
103+
nonce: {
104+
generate() {
105+
return "20cd4ddb-6857-4d36-8e44-9f6e026b8de9";
106+
},
107+
validate(uuid: string) {
108+
return uuid === "20cd4ddb-6857-4d36-8e44-9f6e026b8de9";
109+
},
110+
},
111+
},
112+
};
113+
114+
const generatePayload = generateLoginPayload(options);
115+
const result = await generatePayload({
116+
address: "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B",
117+
chainId: 1,
118+
});
119+
120+
expect(result.domain).toBe("example.com");
121+
});
97122
});

packages/thirdweb/src/auth/core/generate-login-payload.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
DEFAULT_LOGIN_STATEMENT,
44
DEFAULT_LOGIN_VERSION,
55
} from "./constants.js";
6+
import { stripUrlScheme } from "./create-login-message.js";
67
import type { AuthOptions, LoginPayload } from "./types.js";
78

89
/**
@@ -31,7 +32,7 @@ export function generateLoginPayload(options: AuthOptions) {
3132
return {
3233
address,
3334
chain_id: chainId ? chainId.toString() : undefined,
34-
domain: options.domain,
35+
domain: stripUrlScheme(options.domain),
3536
expiration_time: new Date(now + expirationTime).toISOString(),
3637
invalid_before: new Date(now - expirationTime).toISOString(),
3738
issued_at: new Date(now).toISOString(),

packages/thirdweb/src/auth/core/verify-login-payload.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,46 @@ describe("verifyLoginPayload", () => {
145145

146146
expect(verificationResult.valid).toBe(false);
147147
});
148+
149+
test("should work when domain has URL scheme (backward compat)", async () => {
150+
const options = {
151+
client: TEST_CLIENT,
152+
domain: "https://example.com",
153+
login: {
154+
nonce: {
155+
generate() {
156+
return "20cd4ddb-6857-4d36-8e44-9f6e026b8de9";
157+
},
158+
validate(uuid: string) {
159+
return uuid === "20cd4ddb-6857-4d36-8e44-9f6e026b8de9";
160+
},
161+
},
162+
payloadExpirationTimeSeconds: 3600,
163+
statement: "This is a statement",
164+
},
165+
};
166+
167+
const generatePayload = generateLoginPayload(options);
168+
const payloadToSign = await generatePayload({
169+
address: TEST_ACCOUNT_A.address,
170+
});
171+
172+
// sign the payload
173+
const signatureResult = await signLoginPayload({
174+
account: TEST_ACCOUNT_A,
175+
payload: payloadToSign,
176+
});
177+
178+
// verify the payload
179+
const verifyPayload = verifyLoginPayload(options);
180+
181+
const verificationResult = await verifyPayload(signatureResult);
182+
183+
expect(verificationResult.valid).toBe(true);
184+
if (verificationResult.valid) {
185+
expect(verificationResult.payload.address).toBe(TEST_ACCOUNT_A.address);
186+
// domain in payload should have scheme stripped
187+
expect(verificationResult.payload.domain).toBe("example.com");
188+
}
189+
});
148190
});

packages/thirdweb/src/auth/core/verify-login-payload.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { trackLogin } from "../../analytics/track/siwe.js";
22
import { getCachedChain } from "../../chains/utils.js";
33
import { verifySignature } from "../verify-signature.js";
44
import { DEFAULT_LOGIN_STATEMENT, DEFAULT_LOGIN_VERSION } from "./constants.js";
5-
import { createLoginMessage } from "./create-login-message.js";
5+
import { createLoginMessage, stripUrlScheme } from "./create-login-message.js";
66
import type { AuthOptions, LoginPayload } from "./types.js";
77

88
/**
@@ -48,7 +48,8 @@ export function verifyLoginPayload(options: AuthOptions) {
4848
signature,
4949
}: VerifyLoginPayloadParams): Promise<VerifyLoginPayloadResult> => {
5050
// check that the intended domain matches the domain of the payload
51-
if (payload.domain !== options.domain) {
51+
// normalize both sides by stripping URL scheme for backward compatibility
52+
if (stripUrlScheme(payload.domain) !== stripUrlScheme(options.domain)) {
5253
return {
5354
error: `Expected domain '${options.domain}' does not match domain on payload '${payload.domain}'`,
5455
valid: false,

0 commit comments

Comments
 (0)