@stellar/typescript-wallet-sdk 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.MD +14 -0
- package/lib/bundle.js +5807 -1314
- package/lib/bundle.js.map +1 -1
- package/lib/bundle_browser.js +428 -7717
- package/lib/bundle_browser.js.map +1 -1
- package/lib/walletSdk/Auth/index.d.ts +2 -0
- package/lib/walletSdk/Exceptions/index.d.ts +6 -0
- package/lib/walletSdk/Types/recovery.d.ts +1 -0
- package/lib/walletSdk/Types/sep7.d.ts +1 -0
- package/package.json +1 -1
- package/src/walletSdk/Anchor/index.ts +1 -0
- package/src/walletSdk/Auth/index.ts +194 -7
- package/src/walletSdk/Exceptions/index.ts +16 -0
- package/src/walletSdk/Recovery/index.ts +1 -0
- package/src/walletSdk/Types/auth.ts +11 -5
- package/src/walletSdk/Types/recovery.ts +1 -0
- package/src/walletSdk/Types/sep7.ts +1 -0
- package/src/walletSdk/Uri/sep7Parser.ts +46 -6
- package/test/auth.test.ts +1032 -0
- package/test/customer.test.ts +2 -1
- package/test/docker/docker-compose.yml +2 -2
- package/test/e2e/browser.test.ts +0 -2
- package/test/integration/README.md +3 -8
- package/test/integration/anchorplatform.test.ts +2 -1
- package/test/sep38.test.ts +0 -2
- package/test/sep6.test.ts +9 -3
- package/test/sep7.test.ts +43 -1
- package/test/server.test.ts +19 -23
- package/test/wallet.test.ts +10 -11
- package/tsconfig.json +2 -1
|
@@ -0,0 +1,1032 @@
|
|
|
1
|
+
import { sign, decode } from "jws";
|
|
2
|
+
import {
|
|
3
|
+
Keypair,
|
|
4
|
+
Account,
|
|
5
|
+
Asset,
|
|
6
|
+
Memo,
|
|
7
|
+
MuxedAccount,
|
|
8
|
+
Networks,
|
|
9
|
+
StellarToml,
|
|
10
|
+
Transaction,
|
|
11
|
+
TransactionBuilder as SdkTransactionBuilder,
|
|
12
|
+
Operation,
|
|
13
|
+
BASE_FEE,
|
|
14
|
+
xdr as StellarXdr,
|
|
15
|
+
} from "@stellar/stellar-sdk";
|
|
16
|
+
import { randomBytes } from "crypto";
|
|
17
|
+
import axios from "axios";
|
|
18
|
+
import sinon from "sinon";
|
|
19
|
+
|
|
20
|
+
import { validateToken, Sep10 } from "../src/walletSdk/Auth";
|
|
21
|
+
import {
|
|
22
|
+
Config,
|
|
23
|
+
StellarConfiguration,
|
|
24
|
+
ApplicationConfiguration,
|
|
25
|
+
} from "../src/walletSdk";
|
|
26
|
+
import { Anchor } from "../src/walletSdk/Anchor";
|
|
27
|
+
import { SigningKeypair } from "../src/walletSdk/Horizon/Account";
|
|
28
|
+
import {
|
|
29
|
+
InvalidTokenError,
|
|
30
|
+
ExpiredTokenError,
|
|
31
|
+
ChallengeValidationFailedError,
|
|
32
|
+
NetworkPassphraseMismatchError,
|
|
33
|
+
} from "../src/walletSdk/Exceptions";
|
|
34
|
+
|
|
35
|
+
const createToken = (payload: Record<string, unknown>): string => {
|
|
36
|
+
return sign({
|
|
37
|
+
header: { alg: "HS256", typ: "JWT" },
|
|
38
|
+
payload,
|
|
39
|
+
secret: "test-secret",
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("jws.decode return structure", () => {
|
|
44
|
+
// In SEP-10, authentication happens via Stellar transaction signing, not JWT
|
|
45
|
+
// signature verification. The JWT is a bearer token issued by the anchor after
|
|
46
|
+
// the wallet proves ownership of its Stellar account. The SDK only decodes the
|
|
47
|
+
// payload to read claims (exp, iss, sub) — verifying the JWT signature
|
|
48
|
+
// client-side is not part of the SEP-10 trust model.
|
|
49
|
+
it("should expose SEP-10 claims via payload, not as top-level properties", () => {
|
|
50
|
+
const token = createToken({
|
|
51
|
+
iss: "https://anchor.example.com",
|
|
52
|
+
sub: "GABC1234",
|
|
53
|
+
iat: 1700000000,
|
|
54
|
+
exp: 1700003600,
|
|
55
|
+
client_domain: "wallet.example.com",
|
|
56
|
+
});
|
|
57
|
+
const decoded = decode(token);
|
|
58
|
+
|
|
59
|
+
expect(decoded).toHaveProperty("header");
|
|
60
|
+
expect(decoded).toHaveProperty("payload");
|
|
61
|
+
|
|
62
|
+
expect(decoded.payload.exp).toBe(1700003600);
|
|
63
|
+
expect(decoded.payload.iss).toBe("https://anchor.example.com");
|
|
64
|
+
expect(decoded.payload.sub).toBe("GABC1234");
|
|
65
|
+
expect(decoded.payload.iat).toBe(1700000000);
|
|
66
|
+
expect(decoded.payload.client_domain).toBe("wallet.example.com");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("validateToken", () => {
|
|
71
|
+
it("should accept a valid, non-expired token", () => {
|
|
72
|
+
const futureExp = Math.floor(Date.now() / 1000) + 3600;
|
|
73
|
+
const token = createToken({
|
|
74
|
+
iss: "https://anchor.example.com",
|
|
75
|
+
sub: "GABC1234",
|
|
76
|
+
iat: Math.floor(Date.now() / 1000),
|
|
77
|
+
exp: futureExp,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(() => validateToken(token)).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("should throw ExpiredTokenError for an expired token", () => {
|
|
84
|
+
const pastExp = Math.floor(Date.now() / 1000) - 3600;
|
|
85
|
+
const token = createToken({
|
|
86
|
+
iss: "https://anchor.example.com",
|
|
87
|
+
sub: "GABC1234",
|
|
88
|
+
iat: Math.floor(Date.now() / 1000) - 7200,
|
|
89
|
+
exp: pastExp,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(() => validateToken(token)).toThrow(ExpiredTokenError);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should throw ExpiredTokenError for a token with exp=1", () => {
|
|
96
|
+
const token = createToken({
|
|
97
|
+
iss: "https://anchor.example.com",
|
|
98
|
+
sub: "GABC1234",
|
|
99
|
+
exp: 1,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(() => validateToken(token)).toThrow(ExpiredTokenError);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should throw ExpiredTokenError for a token with exp=0", () => {
|
|
106
|
+
const token = createToken({
|
|
107
|
+
iss: "https://anchor.example.com",
|
|
108
|
+
sub: "GABC1234",
|
|
109
|
+
exp: 0,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(() => validateToken(token)).toThrow(ExpiredTokenError);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("should throw InvalidTokenError for a malformed token", () => {
|
|
116
|
+
expect(() => validateToken("not-a-valid-jwt")).toThrow(InvalidTokenError);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should throw InvalidTokenError for an empty string", () => {
|
|
120
|
+
expect(() => validateToken("")).toThrow(InvalidTokenError);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should accept a token without an exp claim", () => {
|
|
124
|
+
const token = createToken({
|
|
125
|
+
iss: "https://anchor.example.com",
|
|
126
|
+
sub: "GABC1234",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(() => validateToken(token)).not.toThrow();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Sep10 challenge validation", () => {
|
|
134
|
+
const homeDomain = "testanchor.stellar.org";
|
|
135
|
+
const webAuthEndpoint = "https://testanchor.stellar.org/auth";
|
|
136
|
+
const networkPassphrase = Networks.TESTNET;
|
|
137
|
+
const webAuthDomain = new URL(webAuthEndpoint).hostname;
|
|
138
|
+
const cfg = new Config({
|
|
139
|
+
stellarConfiguration: StellarConfiguration.TestNet(),
|
|
140
|
+
applicationConfiguration: new ApplicationConfiguration(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* Flexible challenge transaction builder for testing each validation check.
|
|
145
|
+
*
|
|
146
|
+
* Builds a SEP-10 challenge transaction with configurable properties so each
|
|
147
|
+
* test can violate exactly one validation rule while keeping everything else
|
|
148
|
+
* correct.
|
|
149
|
+
*/
|
|
150
|
+
const buildChallenge = ({
|
|
151
|
+
serverKeypair = Keypair.random(),
|
|
152
|
+
clientKeypair = Keypair.random(),
|
|
153
|
+
clientSource,
|
|
154
|
+
challengeHomeDomain = homeDomain,
|
|
155
|
+
sequence = "-1",
|
|
156
|
+
nonce = randomBytes(48).toString("base64"),
|
|
157
|
+
omitNonce = false,
|
|
158
|
+
memo,
|
|
159
|
+
useExplicitTimebounds = false,
|
|
160
|
+
minTime = 0,
|
|
161
|
+
maxTime = 0,
|
|
162
|
+
timeout = 300,
|
|
163
|
+
firstOpType = "manageData" as string,
|
|
164
|
+
omitFirstOpSource = false,
|
|
165
|
+
includeWebAuthDomain = true,
|
|
166
|
+
webAuthDomainValue = webAuthDomain,
|
|
167
|
+
additionalOps = [] as any[],
|
|
168
|
+
shouldSign = true,
|
|
169
|
+
}: {
|
|
170
|
+
serverKeypair?: Keypair;
|
|
171
|
+
clientKeypair?: Keypair;
|
|
172
|
+
clientSource?: string;
|
|
173
|
+
challengeHomeDomain?: string;
|
|
174
|
+
sequence?: string;
|
|
175
|
+
nonce?: string;
|
|
176
|
+
omitNonce?: boolean;
|
|
177
|
+
memo?: any;
|
|
178
|
+
useExplicitTimebounds?: boolean;
|
|
179
|
+
minTime?: number;
|
|
180
|
+
maxTime?: number;
|
|
181
|
+
timeout?: number;
|
|
182
|
+
firstOpType?: string;
|
|
183
|
+
omitFirstOpSource?: boolean;
|
|
184
|
+
includeWebAuthDomain?: boolean;
|
|
185
|
+
webAuthDomainValue?: string | null;
|
|
186
|
+
additionalOps?: any[];
|
|
187
|
+
shouldSign?: boolean;
|
|
188
|
+
} = {}) => {
|
|
189
|
+
const serverAccount = new Account(serverKeypair.publicKey(), sequence);
|
|
190
|
+
|
|
191
|
+
const builderOpts: any = {
|
|
192
|
+
fee: BASE_FEE,
|
|
193
|
+
networkPassphrase,
|
|
194
|
+
};
|
|
195
|
+
if (memo) {
|
|
196
|
+
builderOpts.memo = memo;
|
|
197
|
+
}
|
|
198
|
+
if (useExplicitTimebounds) {
|
|
199
|
+
builderOpts.timebounds = { minTime, maxTime };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const builder = new SdkTransactionBuilder(serverAccount, builderOpts);
|
|
203
|
+
|
|
204
|
+
if (firstOpType === "payment") {
|
|
205
|
+
builder.addOperation(
|
|
206
|
+
Operation.payment({
|
|
207
|
+
destination: serverKeypair.publicKey(),
|
|
208
|
+
asset: Asset.native(),
|
|
209
|
+
amount: "1",
|
|
210
|
+
...(omitFirstOpSource
|
|
211
|
+
? {}
|
|
212
|
+
: { source: clientSource ?? clientKeypair.publicKey() }),
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
215
|
+
} else if (firstOpType === "manageData") {
|
|
216
|
+
const mdOpts: any = {
|
|
217
|
+
name: `${challengeHomeDomain} auth`,
|
|
218
|
+
};
|
|
219
|
+
if (omitNonce) {
|
|
220
|
+
mdOpts.value = null;
|
|
221
|
+
} else {
|
|
222
|
+
mdOpts.value = nonce;
|
|
223
|
+
}
|
|
224
|
+
if (!omitFirstOpSource) {
|
|
225
|
+
mdOpts.source = clientSource ?? clientKeypair.publicKey();
|
|
226
|
+
}
|
|
227
|
+
builder.addOperation(Operation.manageData(mdOpts));
|
|
228
|
+
}
|
|
229
|
+
if (includeWebAuthDomain) {
|
|
230
|
+
const waOpts: any = {
|
|
231
|
+
name: "web_auth_domain",
|
|
232
|
+
source: serverAccount.accountId(),
|
|
233
|
+
};
|
|
234
|
+
if (webAuthDomainValue === null) {
|
|
235
|
+
waOpts.value = null;
|
|
236
|
+
} else {
|
|
237
|
+
waOpts.value = webAuthDomainValue;
|
|
238
|
+
}
|
|
239
|
+
builder.addOperation(Operation.manageData(waOpts));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const op of additionalOps) {
|
|
243
|
+
builder.addOperation(op);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!useExplicitTimebounds) {
|
|
247
|
+
builder.setTimeout(timeout);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const tx = builder.build();
|
|
251
|
+
if (shouldSign) {
|
|
252
|
+
tx.sign(serverKeypair);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { xdr: tx.toXDR(), serverKeypair, clientKeypair };
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const setupSep10 = ({
|
|
259
|
+
serverSigningKey,
|
|
260
|
+
challengeXdr,
|
|
261
|
+
token,
|
|
262
|
+
responseNetworkPassphrase = networkPassphrase,
|
|
263
|
+
}: {
|
|
264
|
+
serverSigningKey?: string;
|
|
265
|
+
challengeXdr: string;
|
|
266
|
+
token: string;
|
|
267
|
+
responseNetworkPassphrase?: string;
|
|
268
|
+
}) => {
|
|
269
|
+
const httpClient = axios.create();
|
|
270
|
+
sinon.stub(httpClient, "get").resolves({
|
|
271
|
+
data: {
|
|
272
|
+
transaction: challengeXdr,
|
|
273
|
+
network_passphrase: responseNetworkPassphrase,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
const postStub = sinon.stub(httpClient, "post").resolves({
|
|
277
|
+
data: { token },
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const sep10 = new Sep10({
|
|
281
|
+
cfg,
|
|
282
|
+
webAuthEndpoint,
|
|
283
|
+
homeDomain,
|
|
284
|
+
httpClient,
|
|
285
|
+
...(serverSigningKey ? { serverSigningKey } : {}),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return { sep10, postStub };
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const createJwt = (clientKeypair: Keypair): string => {
|
|
292
|
+
const now = Math.floor(Date.now() / 1000);
|
|
293
|
+
return createToken({
|
|
294
|
+
iss: webAuthEndpoint,
|
|
295
|
+
sub: clientKeypair.publicKey(),
|
|
296
|
+
iat: now,
|
|
297
|
+
exp: now + 3600,
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
afterEach(() => {
|
|
302
|
+
sinon.restore();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ============================================================
|
|
306
|
+
// WITH serverSigningKey — uses WebAuth.readChallengeTx from SDK
|
|
307
|
+
// ============================================================
|
|
308
|
+
describe("with serverSigningKey", () => {
|
|
309
|
+
const authenticateWithKey = (
|
|
310
|
+
challengeXdr: string,
|
|
311
|
+
serverPublicKey: string,
|
|
312
|
+
clientKeypair: Keypair,
|
|
313
|
+
) => {
|
|
314
|
+
const accountKp = SigningKeypair.fromSecret(clientKeypair.secret());
|
|
315
|
+
const token = createJwt(clientKeypair);
|
|
316
|
+
const { sep10, postStub } = setupSep10({
|
|
317
|
+
serverSigningKey: serverPublicKey,
|
|
318
|
+
challengeXdr,
|
|
319
|
+
token,
|
|
320
|
+
});
|
|
321
|
+
return { sep10, accountKp, postStub };
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
it("should accept a valid challenge", async () => {
|
|
325
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge();
|
|
326
|
+
const { sep10, accountKp } = authenticateWithKey(
|
|
327
|
+
xdr,
|
|
328
|
+
serverKeypair.publicKey(),
|
|
329
|
+
clientKeypair,
|
|
330
|
+
);
|
|
331
|
+
const authToken = await sep10.authenticate({ accountKp });
|
|
332
|
+
expect(authToken.account).toBe(clientKeypair.publicKey());
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should reject when signed by wrong server key", async () => {
|
|
336
|
+
const { xdr, clientKeypair } = buildChallenge();
|
|
337
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
338
|
+
xdr,
|
|
339
|
+
Keypair.random().publicKey(),
|
|
340
|
+
clientKeypair,
|
|
341
|
+
);
|
|
342
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
343
|
+
ChallengeValidationFailedError,
|
|
344
|
+
);
|
|
345
|
+
expect(postStub.notCalled).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should reject invalid XDR", async () => {
|
|
349
|
+
const clientKeypair = Keypair.random();
|
|
350
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
351
|
+
"not-valid-xdr",
|
|
352
|
+
Keypair.random().publicKey(),
|
|
353
|
+
clientKeypair,
|
|
354
|
+
);
|
|
355
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
356
|
+
ChallengeValidationFailedError,
|
|
357
|
+
);
|
|
358
|
+
expect(postStub.notCalled).toBe(true);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should reject a FeeBumpTransaction", async () => {
|
|
362
|
+
const { xdr: innerXdr, serverKeypair, clientKeypair } = buildChallenge();
|
|
363
|
+
const innerTx = new Transaction(innerXdr, networkPassphrase);
|
|
364
|
+
const feeBump = SdkTransactionBuilder.buildFeeBumpTransaction(
|
|
365
|
+
serverKeypair,
|
|
366
|
+
BASE_FEE,
|
|
367
|
+
innerTx,
|
|
368
|
+
networkPassphrase,
|
|
369
|
+
);
|
|
370
|
+
feeBump.sign(serverKeypair);
|
|
371
|
+
|
|
372
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
373
|
+
feeBump.toXDR(),
|
|
374
|
+
serverKeypair.publicKey(),
|
|
375
|
+
clientKeypair,
|
|
376
|
+
);
|
|
377
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
378
|
+
ChallengeValidationFailedError,
|
|
379
|
+
);
|
|
380
|
+
expect(postStub.notCalled).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("should reject non-zero sequence number", async () => {
|
|
384
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
385
|
+
sequence: "99",
|
|
386
|
+
});
|
|
387
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
388
|
+
xdr,
|
|
389
|
+
serverKeypair.publicKey(),
|
|
390
|
+
clientKeypair,
|
|
391
|
+
);
|
|
392
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
393
|
+
ChallengeValidationFailedError,
|
|
394
|
+
);
|
|
395
|
+
expect(postStub.notCalled).toBe(true);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it("should reject a challenge with no operations", async () => {
|
|
399
|
+
const serverKeypair = Keypair.random();
|
|
400
|
+
const clientKeypair = Keypair.random();
|
|
401
|
+
const serverAccount = new Account(serverKeypair.publicKey(), "-1");
|
|
402
|
+
const tx = new SdkTransactionBuilder(serverAccount, {
|
|
403
|
+
fee: BASE_FEE,
|
|
404
|
+
networkPassphrase,
|
|
405
|
+
})
|
|
406
|
+
.setTimeout(300)
|
|
407
|
+
.build();
|
|
408
|
+
tx.sign(serverKeypair);
|
|
409
|
+
|
|
410
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
411
|
+
tx.toXDR(),
|
|
412
|
+
serverKeypair.publicKey(),
|
|
413
|
+
clientKeypair,
|
|
414
|
+
);
|
|
415
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
416
|
+
ChallengeValidationFailedError,
|
|
417
|
+
);
|
|
418
|
+
expect(postStub.notCalled).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should reject first operation without source account", async () => {
|
|
422
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
423
|
+
omitFirstOpSource: true,
|
|
424
|
+
});
|
|
425
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
426
|
+
xdr,
|
|
427
|
+
serverKeypair.publicKey(),
|
|
428
|
+
clientKeypair,
|
|
429
|
+
);
|
|
430
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
431
|
+
ChallengeValidationFailedError,
|
|
432
|
+
);
|
|
433
|
+
expect(postStub.notCalled).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should reject memo with muxed client account", async () => {
|
|
437
|
+
const clientKeypair = Keypair.random();
|
|
438
|
+
const baseAccount = new Account(clientKeypair.publicKey(), "0");
|
|
439
|
+
const muxed = new MuxedAccount(baseAccount, "123");
|
|
440
|
+
|
|
441
|
+
const { xdr, serverKeypair } = buildChallenge({
|
|
442
|
+
clientKeypair,
|
|
443
|
+
clientSource: muxed.accountId(),
|
|
444
|
+
memo: Memo.id("456"),
|
|
445
|
+
});
|
|
446
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
447
|
+
xdr,
|
|
448
|
+
serverKeypair.publicKey(),
|
|
449
|
+
clientKeypair,
|
|
450
|
+
);
|
|
451
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
452
|
+
ChallengeValidationFailedError,
|
|
453
|
+
);
|
|
454
|
+
expect(postStub.notCalled).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("should reject non-id memo type", async () => {
|
|
458
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
459
|
+
memo: Memo.text("test"),
|
|
460
|
+
});
|
|
461
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
462
|
+
xdr,
|
|
463
|
+
serverKeypair.publicKey(),
|
|
464
|
+
clientKeypair,
|
|
465
|
+
);
|
|
466
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
467
|
+
ChallengeValidationFailedError,
|
|
468
|
+
);
|
|
469
|
+
expect(postStub.notCalled).toBe(true);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it("should reject first operation that is not manageData", async () => {
|
|
473
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
474
|
+
firstOpType: "payment",
|
|
475
|
+
});
|
|
476
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
477
|
+
xdr,
|
|
478
|
+
serverKeypair.publicKey(),
|
|
479
|
+
clientKeypair,
|
|
480
|
+
);
|
|
481
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
482
|
+
ChallengeValidationFailedError,
|
|
483
|
+
);
|
|
484
|
+
expect(postStub.notCalled).toBe(true);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("should reject infinite timebounds (maxTime=0)", async () => {
|
|
488
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
489
|
+
useExplicitTimebounds: true,
|
|
490
|
+
minTime: 0,
|
|
491
|
+
maxTime: 0,
|
|
492
|
+
});
|
|
493
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
494
|
+
xdr,
|
|
495
|
+
serverKeypair.publicKey(),
|
|
496
|
+
clientKeypair,
|
|
497
|
+
);
|
|
498
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
499
|
+
ChallengeValidationFailedError,
|
|
500
|
+
);
|
|
501
|
+
expect(postStub.notCalled).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("should reject expired timebounds", async () => {
|
|
505
|
+
const now = Math.floor(Date.now() / 1000);
|
|
506
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
507
|
+
useExplicitTimebounds: true,
|
|
508
|
+
minTime: now - 7200,
|
|
509
|
+
maxTime: now - 3600,
|
|
510
|
+
});
|
|
511
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
512
|
+
xdr,
|
|
513
|
+
serverKeypair.publicKey(),
|
|
514
|
+
clientKeypair,
|
|
515
|
+
);
|
|
516
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
517
|
+
ChallengeValidationFailedError,
|
|
518
|
+
);
|
|
519
|
+
expect(postStub.notCalled).toBe(true);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("should reject missing nonce value", async () => {
|
|
523
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
524
|
+
omitNonce: true,
|
|
525
|
+
});
|
|
526
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
527
|
+
xdr,
|
|
528
|
+
serverKeypair.publicKey(),
|
|
529
|
+
clientKeypair,
|
|
530
|
+
);
|
|
531
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
532
|
+
ChallengeValidationFailedError,
|
|
533
|
+
);
|
|
534
|
+
expect(postStub.notCalled).toBe(true);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("should reject wrong nonce length", async () => {
|
|
538
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
539
|
+
nonce: randomBytes(16).toString("base64"),
|
|
540
|
+
});
|
|
541
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
542
|
+
xdr,
|
|
543
|
+
serverKeypair.publicKey(),
|
|
544
|
+
clientKeypair,
|
|
545
|
+
);
|
|
546
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
547
|
+
ChallengeValidationFailedError,
|
|
548
|
+
);
|
|
549
|
+
expect(postStub.notCalled).toBe(true);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("should reject wrong home domain", async () => {
|
|
553
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
554
|
+
challengeHomeDomain: "evil.example.com",
|
|
555
|
+
});
|
|
556
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
557
|
+
xdr,
|
|
558
|
+
serverKeypair.publicKey(),
|
|
559
|
+
clientKeypair,
|
|
560
|
+
);
|
|
561
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
562
|
+
ChallengeValidationFailedError,
|
|
563
|
+
);
|
|
564
|
+
expect(postStub.notCalled).toBe(true);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("should reject subsequent non-manageData operation", async () => {
|
|
568
|
+
const serverKeypair = Keypair.random();
|
|
569
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
570
|
+
serverKeypair,
|
|
571
|
+
additionalOps: [
|
|
572
|
+
Operation.payment({
|
|
573
|
+
destination: serverKeypair.publicKey(),
|
|
574
|
+
asset: Asset.native(),
|
|
575
|
+
amount: "1",
|
|
576
|
+
}),
|
|
577
|
+
],
|
|
578
|
+
});
|
|
579
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
580
|
+
xdr,
|
|
581
|
+
serverKeypair.publicKey(),
|
|
582
|
+
clientKeypair,
|
|
583
|
+
);
|
|
584
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
585
|
+
ChallengeValidationFailedError,
|
|
586
|
+
);
|
|
587
|
+
expect(postStub.notCalled).toBe(true);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("should reject null web_auth_domain value", async () => {
|
|
591
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
592
|
+
webAuthDomainValue: null,
|
|
593
|
+
});
|
|
594
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
595
|
+
xdr,
|
|
596
|
+
serverKeypair.publicKey(),
|
|
597
|
+
clientKeypair,
|
|
598
|
+
);
|
|
599
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
600
|
+
ChallengeValidationFailedError,
|
|
601
|
+
);
|
|
602
|
+
expect(postStub.notCalled).toBe(true);
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("should reject mismatched web_auth_domain", async () => {
|
|
606
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge({
|
|
607
|
+
webAuthDomainValue: "evil.example.com",
|
|
608
|
+
});
|
|
609
|
+
const { sep10, accountKp, postStub } = authenticateWithKey(
|
|
610
|
+
xdr,
|
|
611
|
+
serverKeypair.publicKey(),
|
|
612
|
+
clientKeypair,
|
|
613
|
+
);
|
|
614
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
615
|
+
ChallengeValidationFailedError,
|
|
616
|
+
);
|
|
617
|
+
expect(postStub.notCalled).toBe(true);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// ============================================================
|
|
622
|
+
// WITHOUT serverSigningKey — uses local readChallengeTx
|
|
623
|
+
// ============================================================
|
|
624
|
+
describe("without serverSigningKey (local readChallengeTx)", () => {
|
|
625
|
+
const authenticateWithoutKey = (
|
|
626
|
+
challengeXdr: string,
|
|
627
|
+
clientKeypair: Keypair,
|
|
628
|
+
) => {
|
|
629
|
+
const accountKp = SigningKeypair.fromSecret(clientKeypair.secret());
|
|
630
|
+
const token = createJwt(clientKeypair);
|
|
631
|
+
const { sep10, postStub } = setupSep10({
|
|
632
|
+
challengeXdr,
|
|
633
|
+
token,
|
|
634
|
+
});
|
|
635
|
+
return { sep10, accountKp, postStub };
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
it("should accept a valid challenge", async () => {
|
|
639
|
+
const { xdr, clientKeypair } = buildChallenge();
|
|
640
|
+
const { sep10, accountKp } = authenticateWithoutKey(xdr, clientKeypair);
|
|
641
|
+
const authToken = await sep10.authenticate({ accountKp });
|
|
642
|
+
expect(authToken.account).toBe(clientKeypair.publicKey());
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("should reject invalid XDR", async () => {
|
|
646
|
+
const clientKeypair = Keypair.random();
|
|
647
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
648
|
+
"not-valid-xdr",
|
|
649
|
+
clientKeypair,
|
|
650
|
+
);
|
|
651
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
652
|
+
ChallengeValidationFailedError,
|
|
653
|
+
);
|
|
654
|
+
expect(postStub.notCalled).toBe(true);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("should reject a FeeBumpTransaction", async () => {
|
|
658
|
+
const { xdr: innerXdr, serverKeypair, clientKeypair } = buildChallenge();
|
|
659
|
+
const innerTx = new Transaction(innerXdr, networkPassphrase);
|
|
660
|
+
const feeBump = SdkTransactionBuilder.buildFeeBumpTransaction(
|
|
661
|
+
serverKeypair,
|
|
662
|
+
BASE_FEE,
|
|
663
|
+
innerTx,
|
|
664
|
+
networkPassphrase,
|
|
665
|
+
);
|
|
666
|
+
feeBump.sign(serverKeypair);
|
|
667
|
+
|
|
668
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
669
|
+
feeBump.toXDR(),
|
|
670
|
+
clientKeypair,
|
|
671
|
+
);
|
|
672
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
673
|
+
ChallengeValidationFailedError,
|
|
674
|
+
);
|
|
675
|
+
expect(postStub.notCalled).toBe(true);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it("should reject non-zero sequence number", async () => {
|
|
679
|
+
const { xdr, clientKeypair } = buildChallenge({ sequence: "99" });
|
|
680
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
681
|
+
xdr,
|
|
682
|
+
clientKeypair,
|
|
683
|
+
);
|
|
684
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
685
|
+
ChallengeValidationFailedError,
|
|
686
|
+
);
|
|
687
|
+
expect(postStub.notCalled).toBe(true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("should reject a challenge with no operations", async () => {
|
|
691
|
+
const serverKeypair = Keypair.random();
|
|
692
|
+
const clientKeypair = Keypair.random();
|
|
693
|
+
const serverAccount = new Account(serverKeypair.publicKey(), "-1");
|
|
694
|
+
const tx = new SdkTransactionBuilder(serverAccount, {
|
|
695
|
+
fee: BASE_FEE,
|
|
696
|
+
networkPassphrase,
|
|
697
|
+
})
|
|
698
|
+
.setTimeout(300)
|
|
699
|
+
.build();
|
|
700
|
+
tx.sign(serverKeypair);
|
|
701
|
+
|
|
702
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
703
|
+
tx.toXDR(),
|
|
704
|
+
clientKeypair,
|
|
705
|
+
);
|
|
706
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
707
|
+
ChallengeValidationFailedError,
|
|
708
|
+
);
|
|
709
|
+
expect(postStub.notCalled).toBe(true);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
it("should reject first operation without source account", async () => {
|
|
713
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
714
|
+
omitFirstOpSource: true,
|
|
715
|
+
});
|
|
716
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
717
|
+
xdr,
|
|
718
|
+
clientKeypair,
|
|
719
|
+
);
|
|
720
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
721
|
+
ChallengeValidationFailedError,
|
|
722
|
+
);
|
|
723
|
+
expect(postStub.notCalled).toBe(true);
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("should reject memo with muxed client account", async () => {
|
|
727
|
+
const clientKeypair = Keypair.random();
|
|
728
|
+
const baseAccount = new Account(clientKeypair.publicKey(), "0");
|
|
729
|
+
const muxed = new MuxedAccount(baseAccount, "123");
|
|
730
|
+
|
|
731
|
+
const { xdr } = buildChallenge({
|
|
732
|
+
clientKeypair,
|
|
733
|
+
clientSource: muxed.accountId(),
|
|
734
|
+
memo: Memo.id("456"),
|
|
735
|
+
});
|
|
736
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
737
|
+
xdr,
|
|
738
|
+
clientKeypair,
|
|
739
|
+
);
|
|
740
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
741
|
+
ChallengeValidationFailedError,
|
|
742
|
+
);
|
|
743
|
+
expect(postStub.notCalled).toBe(true);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("should reject non-id memo type", async () => {
|
|
747
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
748
|
+
memo: Memo.text("test"),
|
|
749
|
+
});
|
|
750
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
751
|
+
xdr,
|
|
752
|
+
clientKeypair,
|
|
753
|
+
);
|
|
754
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
755
|
+
ChallengeValidationFailedError,
|
|
756
|
+
);
|
|
757
|
+
expect(postStub.notCalled).toBe(true);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it("should reject first operation that is not manageData", async () => {
|
|
761
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
762
|
+
firstOpType: "payment",
|
|
763
|
+
});
|
|
764
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
765
|
+
xdr,
|
|
766
|
+
clientKeypair,
|
|
767
|
+
);
|
|
768
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
769
|
+
ChallengeValidationFailedError,
|
|
770
|
+
);
|
|
771
|
+
expect(postStub.notCalled).toBe(true);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it("should reject infinite timebounds (maxTime=0)", async () => {
|
|
775
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
776
|
+
useExplicitTimebounds: true,
|
|
777
|
+
minTime: 0,
|
|
778
|
+
maxTime: 0,
|
|
779
|
+
});
|
|
780
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
781
|
+
xdr,
|
|
782
|
+
clientKeypair,
|
|
783
|
+
);
|
|
784
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
785
|
+
ChallengeValidationFailedError,
|
|
786
|
+
);
|
|
787
|
+
expect(postStub.notCalled).toBe(true);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("should reject expired timebounds", async () => {
|
|
791
|
+
const now = Math.floor(Date.now() / 1000);
|
|
792
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
793
|
+
useExplicitTimebounds: true,
|
|
794
|
+
minTime: now - 7200,
|
|
795
|
+
maxTime: now - 3600,
|
|
796
|
+
});
|
|
797
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
798
|
+
xdr,
|
|
799
|
+
clientKeypair,
|
|
800
|
+
);
|
|
801
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
802
|
+
ChallengeValidationFailedError,
|
|
803
|
+
);
|
|
804
|
+
expect(postStub.notCalled).toBe(true);
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
it("should reject missing nonce value", async () => {
|
|
808
|
+
const { xdr, clientKeypair } = buildChallenge({ omitNonce: true });
|
|
809
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
810
|
+
xdr,
|
|
811
|
+
clientKeypair,
|
|
812
|
+
);
|
|
813
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
814
|
+
ChallengeValidationFailedError,
|
|
815
|
+
);
|
|
816
|
+
expect(postStub.notCalled).toBe(true);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it("should reject wrong nonce length", async () => {
|
|
820
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
821
|
+
nonce: randomBytes(16).toString("base64"),
|
|
822
|
+
});
|
|
823
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
824
|
+
xdr,
|
|
825
|
+
clientKeypair,
|
|
826
|
+
);
|
|
827
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
828
|
+
ChallengeValidationFailedError,
|
|
829
|
+
);
|
|
830
|
+
expect(postStub.notCalled).toBe(true);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("should reject wrong home domain", async () => {
|
|
834
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
835
|
+
challengeHomeDomain: "evil.example.com",
|
|
836
|
+
});
|
|
837
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
838
|
+
xdr,
|
|
839
|
+
clientKeypair,
|
|
840
|
+
);
|
|
841
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
842
|
+
ChallengeValidationFailedError,
|
|
843
|
+
);
|
|
844
|
+
expect(postStub.notCalled).toBe(true);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it("should reject subsequent non-manageData operation", async () => {
|
|
848
|
+
const serverKeypair = Keypair.random();
|
|
849
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
850
|
+
serverKeypair,
|
|
851
|
+
additionalOps: [
|
|
852
|
+
Operation.payment({
|
|
853
|
+
destination: serverKeypair.publicKey(),
|
|
854
|
+
asset: Asset.native(),
|
|
855
|
+
amount: "1",
|
|
856
|
+
}),
|
|
857
|
+
],
|
|
858
|
+
});
|
|
859
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
860
|
+
xdr,
|
|
861
|
+
clientKeypair,
|
|
862
|
+
);
|
|
863
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
864
|
+
ChallengeValidationFailedError,
|
|
865
|
+
);
|
|
866
|
+
expect(postStub.notCalled).toBe(true);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it("should reject null web_auth_domain value", async () => {
|
|
870
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
871
|
+
webAuthDomainValue: null,
|
|
872
|
+
});
|
|
873
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
874
|
+
xdr,
|
|
875
|
+
clientKeypair,
|
|
876
|
+
);
|
|
877
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
878
|
+
ChallengeValidationFailedError,
|
|
879
|
+
);
|
|
880
|
+
expect(postStub.notCalled).toBe(true);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it("should reject mismatched web_auth_domain", async () => {
|
|
884
|
+
const { xdr, clientKeypair } = buildChallenge({
|
|
885
|
+
webAuthDomainValue: "evil.example.com",
|
|
886
|
+
});
|
|
887
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
888
|
+
xdr,
|
|
889
|
+
clientKeypair,
|
|
890
|
+
);
|
|
891
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
892
|
+
ChallengeValidationFailedError,
|
|
893
|
+
);
|
|
894
|
+
expect(postStub.notCalled).toBe(true);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("should reject a challenge with missing timebounds", async () => {
|
|
898
|
+
const { xdr: txXdr, clientKeypair } = buildChallenge();
|
|
899
|
+
|
|
900
|
+
// Strip timebounds by setting preconditions to PRECOND_NONE in the XDR
|
|
901
|
+
const envelope = StellarXdr.TransactionEnvelope.fromXDR(txXdr, "base64");
|
|
902
|
+
envelope.v1().tx().cond(StellarXdr.Preconditions.precondNone());
|
|
903
|
+
const noTimeboundsXdr = envelope.toXDR().toString("base64");
|
|
904
|
+
|
|
905
|
+
const { sep10, accountKp, postStub } = authenticateWithoutKey(
|
|
906
|
+
noTimeboundsXdr,
|
|
907
|
+
clientKeypair,
|
|
908
|
+
);
|
|
909
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
910
|
+
ChallengeValidationFailedError,
|
|
911
|
+
);
|
|
912
|
+
expect(postStub.notCalled).toBe(true);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
describe("network passphrase mismatch", () => {
|
|
917
|
+
it("should reject when server returns a different network passphrase", async () => {
|
|
918
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge();
|
|
919
|
+
const accountKp = SigningKeypair.fromSecret(clientKeypair.secret());
|
|
920
|
+
const now = Math.floor(Date.now() / 1000);
|
|
921
|
+
const token = createToken({
|
|
922
|
+
iss: webAuthEndpoint,
|
|
923
|
+
sub: clientKeypair.publicKey(),
|
|
924
|
+
iat: now,
|
|
925
|
+
exp: now + 3600,
|
|
926
|
+
});
|
|
927
|
+
const { sep10, postStub } = setupSep10({
|
|
928
|
+
serverSigningKey: serverKeypair.publicKey(),
|
|
929
|
+
challengeXdr: xdr,
|
|
930
|
+
token,
|
|
931
|
+
responseNetworkPassphrase: Networks.PUBLIC,
|
|
932
|
+
});
|
|
933
|
+
await expect(sep10.authenticate({ accountKp })).rejects.toThrow(
|
|
934
|
+
NetworkPassphraseMismatchError,
|
|
935
|
+
);
|
|
936
|
+
expect(postStub.notCalled).toBe(true);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
it("should accept when server returns matching network passphrase", async () => {
|
|
940
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge();
|
|
941
|
+
const accountKp = SigningKeypair.fromSecret(clientKeypair.secret());
|
|
942
|
+
const now = Math.floor(Date.now() / 1000);
|
|
943
|
+
const token = createToken({
|
|
944
|
+
iss: webAuthEndpoint,
|
|
945
|
+
sub: clientKeypair.publicKey(),
|
|
946
|
+
iat: now,
|
|
947
|
+
exp: now + 3600,
|
|
948
|
+
});
|
|
949
|
+
const { sep10 } = setupSep10({
|
|
950
|
+
serverSigningKey: serverKeypair.publicKey(),
|
|
951
|
+
challengeXdr: xdr,
|
|
952
|
+
token,
|
|
953
|
+
responseNetworkPassphrase: networkPassphrase,
|
|
954
|
+
});
|
|
955
|
+
const authToken = await sep10.authenticate({ accountKp });
|
|
956
|
+
expect(authToken.account).toBe(clientKeypair.publicKey());
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
it("should accept when server omits network passphrase", async () => {
|
|
960
|
+
const { xdr, serverKeypair, clientKeypair } = buildChallenge();
|
|
961
|
+
const accountKp = SigningKeypair.fromSecret(clientKeypair.secret());
|
|
962
|
+
const now = Math.floor(Date.now() / 1000);
|
|
963
|
+
const token = createToken({
|
|
964
|
+
iss: webAuthEndpoint,
|
|
965
|
+
sub: clientKeypair.publicKey(),
|
|
966
|
+
iat: now,
|
|
967
|
+
exp: now + 3600,
|
|
968
|
+
});
|
|
969
|
+
const { sep10 } = setupSep10({
|
|
970
|
+
serverSigningKey: serverKeypair.publicKey(),
|
|
971
|
+
challengeXdr: xdr,
|
|
972
|
+
token,
|
|
973
|
+
responseNetworkPassphrase: undefined,
|
|
974
|
+
});
|
|
975
|
+
const authToken = await sep10.authenticate({ accountKp });
|
|
976
|
+
expect(authToken.account).toBe(clientKeypair.publicKey());
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
describe("Anchor.sep10() signing key handling", () => {
|
|
982
|
+
afterEach(() => {
|
|
983
|
+
sinon.restore();
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("should succeed when TOML has no SIGNING_KEY", async () => {
|
|
987
|
+
sinon.stub(StellarToml.Resolver, "resolve").resolves({
|
|
988
|
+
WEB_AUTH_ENDPOINT: "https://testanchor.stellar.org/auth",
|
|
989
|
+
DOCUMENTATION: {},
|
|
990
|
+
} as StellarToml.Api.StellarToml);
|
|
991
|
+
|
|
992
|
+
const cfg = new Config({
|
|
993
|
+
stellarConfiguration: StellarConfiguration.TestNet(),
|
|
994
|
+
applicationConfiguration: new ApplicationConfiguration(),
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const anchor = new Anchor({
|
|
998
|
+
cfg,
|
|
999
|
+
homeDomain: "testanchor.stellar.org",
|
|
1000
|
+
httpClient: axios.create(),
|
|
1001
|
+
language: "en",
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
const sep10 = await anchor.sep10();
|
|
1005
|
+
expect(sep10).toBeDefined();
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it("should succeed when TOML has SIGNING_KEY", async () => {
|
|
1009
|
+
const serverKeypair = Keypair.random();
|
|
1010
|
+
|
|
1011
|
+
sinon.stub(StellarToml.Resolver, "resolve").resolves({
|
|
1012
|
+
WEB_AUTH_ENDPOINT: "https://testanchor.stellar.org/auth",
|
|
1013
|
+
SIGNING_KEY: serverKeypair.publicKey(),
|
|
1014
|
+
DOCUMENTATION: {},
|
|
1015
|
+
} as StellarToml.Api.StellarToml);
|
|
1016
|
+
|
|
1017
|
+
const cfg = new Config({
|
|
1018
|
+
stellarConfiguration: StellarConfiguration.TestNet(),
|
|
1019
|
+
applicationConfiguration: new ApplicationConfiguration(),
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
const anchor = new Anchor({
|
|
1023
|
+
cfg,
|
|
1024
|
+
homeDomain: "testanchor.stellar.org",
|
|
1025
|
+
httpClient: axios.create(),
|
|
1026
|
+
language: "en",
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
const sep10 = await anchor.sep10();
|
|
1030
|
+
expect(sep10).toBeDefined();
|
|
1031
|
+
});
|
|
1032
|
+
});
|