Skip to content

Commit 97fd86d

Browse files
chore: sync from internal
1 parent 2994b78 commit 97fd86d

14 files changed

Lines changed: 236 additions & 53 deletions

File tree

packages/auth2/src/Auth2Token.ts

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { jwtDecode } from "jwt-decode";
22

3-
type Auth2TokenClaims = {
3+
type Claims = {
44
sub: string;
55
client_id: string;
66
iss: string;
@@ -12,52 +12,76 @@ type Auth2TokenClaims = {
1212
};
1313
};
1414

15+
type A2TClaims = {
16+
exp: number;
17+
iat: number;
18+
};
19+
1520
type WalletIdentity = {
1621
id: string;
1722
derivationIndex: number;
1823
};
1924

2025
export class Auth2Token {
2126
private WALLET_URN_PREFIX = "urn:phantom:wallet:";
27+
private WALLET_TAG_URN_PREFIX = "urn:phantom:wallet-tag:";
28+
29+
private _claims: Claims;
30+
private _a2tClaims: A2TClaims;
2231

23-
private _claims: Auth2TokenClaims;
2432
private _identity?: WalletIdentity;
33+
private _walletTag?: string;
2534

2635
private constructor(accessToken: string) {
27-
const claims = jwtDecode<Auth2TokenClaims>(accessToken);
36+
this._claims = jwtDecode<Claims>(accessToken);
37+
this._a2tClaims = jwtDecode<A2TClaims>(this._claims.ext.a2t);
2838

29-
this._claims = claims;
30-
31-
const aud = claims.aud.find(aud => aud.startsWith(this.WALLET_URN_PREFIX));
32-
33-
if (!aud) {
34-
return;
39+
const walletAud = this._claims.aud.find(aud => aud.startsWith(this.WALLET_URN_PREFIX));
40+
if (walletAud) {
41+
const [id, derivationIndex] = walletAud.replace(this.WALLET_URN_PREFIX, "").split(":");
42+
this._identity = {
43+
id,
44+
derivationIndex: Number(derivationIndex),
45+
};
3546
}
3647

37-
const [id, derivationIndex] = aud.replace(this.WALLET_URN_PREFIX, "").split(":");
38-
this._identity = {
39-
id,
40-
derivationIndex: Number(derivationIndex),
41-
};
48+
const walletTagAud = this._claims.aud.find(aud => aud.startsWith(this.WALLET_TAG_URN_PREFIX));
49+
if (walletTagAud) {
50+
this._walletTag = walletTagAud.replace(this.WALLET_TAG_URN_PREFIX, "");
51+
}
4252
}
4353

4454
static fromAccessToken(accessToken: string): Auth2Token {
4555
return new this(accessToken);
4656
}
4757

48-
get sub(): Auth2TokenClaims["sub"] {
58+
get sub(): Claims["sub"] {
4959
return this._claims.sub;
5060
}
5161

52-
get clientId(): Auth2TokenClaims["client_id"] {
62+
get clientId(): Claims["client_id"] {
5363
return this._claims.client_id;
5464
}
5565

56-
get a2t(): Auth2TokenClaims["ext"]["a2t"] {
66+
get wallet(): WalletIdentity | undefined {
67+
return this._identity;
68+
}
69+
70+
get walletTag(): string | undefined {
71+
return this._walletTag;
72+
}
73+
74+
get a2t(): Claims["ext"]["a2t"] {
75+
if (this._a2tClaims.exp < Date.now() / 1_000) {
76+
throw new Auth2TokenExpiredError();
77+
}
5778
return this._claims.ext.a2t;
5879
}
80+
}
5981

60-
get wallet(): WalletIdentity | undefined {
61-
return this._identity;
82+
export class Auth2TokenExpiredError extends Error {
83+
constructor() {
84+
super("Auth2 token expired");
85+
this.name = "Auth2TokenExpiredError";
6286
}
6387
}

packages/auth2/src/__tests__/Auth2Token.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ jest.mock("jwt-decode", () => ({
33
jwtDecode: mockJwtDecode,
44
}));
55

6-
import { Auth2Token } from "../Auth2Token";
6+
import { Auth2Token, Auth2TokenExpiredError } from "../Auth2Token";
77

88
const WALLET_URN_PREFIX = "urn:phantom:wallet:";
99

@@ -54,10 +54,22 @@ describe("Auth2Token", () => {
5454
});
5555

5656
describe("a2t getter", () => {
57-
it("returns the ext.a2t claim from the token", () => {
58-
mockJwtDecode.mockReturnValueOnce(makeClaims({ a2t: "inner-auth2-tok" }));
57+
it("returns a2t when exp is in the future", () => {
58+
const validA2tClaims = { exp: Math.floor(Date.now() / 1_000) + 3600, iat: 0 };
59+
mockJwtDecode.mockReturnValueOnce(makeClaims({ a2t: "valid-a2t" })).mockReturnValueOnce(validA2tClaims);
5960

60-
expect(Auth2Token.fromAccessToken("tok").a2t).toBe("inner-auth2-tok");
61+
const token = Auth2Token.fromAccessToken("tok");
62+
63+
expect(token.a2t).toBe("valid-a2t");
64+
});
65+
66+
it("throws Auth2TokenExpiredError when a2t exp is in the past", () => {
67+
const expiredA2tClaims = { exp: Math.floor(Date.now() / 1_000) - 60, iat: 0 };
68+
mockJwtDecode.mockReturnValueOnce(makeClaims({ a2t: "expired-a2t" })).mockReturnValueOnce(expiredA2tClaims);
69+
70+
const token = Auth2Token.fromAccessToken("tok");
71+
72+
expect(() => token.a2t).toThrow(Auth2TokenExpiredError);
6173
});
6274
});
6375

packages/auth2/src/__tests__/auth2Flow.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ function makeJwt(payload: Record<string, unknown>): string {
4343
return `${encode({ alg: "HS256" })}.${encode({ aud: [], ...payload })}.sig`;
4444
}
4545

46-
const DEFAULT_ACCESS_TOKEN = makeJwt({ sub: "user-1", client_id: "test-client", ext: { a2t: "auth2-tok" } });
46+
const DEFAULT_A2T = makeJwt({ exp: Math.floor(Date.now() / 1_000) + 3600, iat: Math.floor(Date.now() / 1_000) });
47+
const DEFAULT_ACCESS_TOKEN = makeJwt({ sub: "user-1", client_id: "test-client", ext: { a2t: DEFAULT_A2T } });
4748

4849
const MOCK_RAW_PUB = new Uint8Array([0x04, ...Array(64).fill(0x01)]);
4950

@@ -582,7 +583,7 @@ describe("completeAuth2Exchange()", () => {
582583
mockExchangeAuthCode.mockResolvedValueOnce({
583584
accessToken: makeJwt({
584585
sub: "user-1",
585-
ext: { a2t: "auth2-tok" },
586+
ext: { a2t: DEFAULT_A2T },
586587
aud: ["urn:phantom:wallet:wallet-from-token:2"],
587588
}),
588589
idType: "Bearer",
@@ -608,7 +609,7 @@ describe("completeAuth2Exchange()", () => {
608609
mockExchangeAuthCode.mockResolvedValueOnce({
609610
accessToken: makeJwt({
610611
sub: "user-1",
611-
ext: { a2t: "auth2-tok" },
612+
ext: { a2t: DEFAULT_A2T },
612613
aud: ["urn:phantom:wallet:wallet-from-token:3"],
613614
}),
614615
idType: "Bearer",
@@ -837,7 +838,7 @@ describe("_getOrMigrateWallet()", () => {
837838
function makeToken(overrides: Partial<{ client_id: string; walletUrn: string }> = {}) {
838839
const aud = overrides.walletUrn ? [overrides.walletUrn] : [];
839840
return Auth2Token.fromAccessToken(
840-
makeJwt({ sub: "user-1", client_id: overrides.client_id ?? "test-client", ext: { a2t: "t" }, aud }),
841+
makeJwt({ sub: "user-1", client_id: overrides.client_id ?? "test-client", ext: { a2t: DEFAULT_A2T }, aud }),
841842
);
842843
}
843844

packages/auth2/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { Auth2KmsRpcClient, type Auth2KmsClientOptions } from "./Auth2KmsRpcClient";
22
export { Auth2Stamper, type Auth2StamperRefreshConfig } from "./Auth2Stamper";
33
export type { Auth2StamperStorage, Auth2StamperStoredRecord } from "./Auth2StamperStorage";
4+
export { Auth2TokenExpiredError } from "./Auth2Token";
45
export { exchangeAuthCode, refreshToken } from "./tokenExchange";
56
export type { Auth2StamperWithKeyManagement, Auth2AuthProviderOptions } from "./types";
67
export {

packages/browser-sdk/src/providers/embedded/adapters/Auth2Stamper.test.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ function makeJwt(payload: Record<string, unknown>): string {
3030
return `${encode({ alg: "HS256" })}.${encode({ aud: [], ...payload })}.sig`;
3131
}
3232

33+
function makeA2tJwt(): string {
34+
const now = Math.floor(Date.now() / 1000);
35+
return makeJwt({ exp: now + 3600, iat: now });
36+
}
37+
3338
beforeEach(() => {
3439
Object.defineProperty(globalThis.crypto, "subtle", {
3540
value: mockSubtle,
@@ -131,7 +136,7 @@ describe("Auth2Stamper", () => {
131136
const stamper1 = new Auth2Stamper(new IndexedDBAuth2StamperStorage(dbName));
132137
await stamper1.init();
133138
await stamper1.setTokens({
134-
accessToken: makeJwt({ sub: "u", ext: { a2t: "persisted-token" } }),
139+
accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }),
135140
idType: "Bearer",
136141
});
137142

@@ -149,7 +154,7 @@ describe("Auth2Stamper", () => {
149154
const stamper = makeStamper();
150155
await stamper.init();
151156

152-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "my-token" } }), idType: "Bearer" });
157+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
153158

154159
await expect(stamper.stamp({ type: "OIDC", data: Buffer.from("payload") })).resolves.toBeTruthy();
155160
});
@@ -158,8 +163,9 @@ describe("Auth2Stamper", () => {
158163
const dbName = `persist-db-${Date.now()}`;
159164
const stamper = new Auth2Stamper(new IndexedDBAuth2StamperStorage(dbName));
160165
await stamper.init();
166+
const a2tJwt = makeA2tJwt();
161167
await stamper.setTokens({
162-
accessToken: makeJwt({ sub: "u", ext: { a2t: "auto-connect-token" } }),
168+
accessToken: makeJwt({ sub: "u", ext: { a2t: a2tJwt } }),
163169
idType: "Bearer",
164170
});
165171

@@ -170,14 +176,14 @@ describe("Auth2Stamper", () => {
170176
const decoded = JSON.parse(Buffer.from(stampStr, "base64url").toString("utf-8")) as {
171177
idToken: string;
172178
};
173-
expect(decoded.idToken).toBe("auto-connect-token");
179+
expect(decoded.idToken).toBe(a2tJwt);
174180
});
175181

176182
it("persists bearerToken and refreshToken to IndexedDB", async () => {
177183
const dbName = `persist-tokens-db-${Date.now()}`;
178184
const stamper = new Auth2Stamper(new IndexedDBAuth2StamperStorage(dbName));
179185
await stamper.init();
180-
const accessToken = makeJwt({ sub: "u", ext: { a2t: "a2t-tok" } });
186+
const accessToken = makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } });
181187
await stamper.setTokens({
182188
accessToken,
183189
idType: "Bearer",
@@ -203,7 +209,7 @@ describe("Auth2Stamper", () => {
203209
it("bearerToken returns '{idType} {accessToken}' after setTokens()", async () => {
204210
const stamper = makeStamper();
205211
await stamper.init();
206-
const accessToken = makeJwt({ sub: "test-user", ext: { a2t: "a2t-tok" } });
212+
const accessToken = makeJwt({ sub: "test-user", ext: { a2t: makeA2tJwt() } });
207213
await stamper.setTokens({ accessToken, idType: "Bearer", refreshToken: "refresh-tok" });
208214

209215
expect(stamper.bearerToken).toBe(`Bearer ${accessToken}`);
@@ -212,7 +218,7 @@ describe("Auth2Stamper", () => {
212218
it("auth2Token.sub matches the sub claim in the access token", async () => {
213219
const stamper = makeStamper();
214220
await stamper.init();
215-
const accessToken = makeJwt({ sub: "test-user", ext: { a2t: "a2t-tok" } });
221+
const accessToken = makeJwt({ sub: "test-user", ext: { a2t: makeA2tJwt() } });
216222
await stamper.setTokens({ accessToken, idType: "Bearer" });
217223

218224
expect(stamper.auth2Token?.sub).toBe("test-user");
@@ -221,7 +227,7 @@ describe("Auth2Stamper", () => {
221227
it("bearerToken returns null after clear()", async () => {
222228
const stamper = makeStamper();
223229
await stamper.init();
224-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "tok" } }), idType: "Bearer" });
230+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
225231
await stamper.clear();
226232

227233
expect(stamper.bearerToken).toBeNull();
@@ -230,7 +236,7 @@ describe("Auth2Stamper", () => {
230236
it("auth2Token returns null after clear()", async () => {
231237
const stamper = makeStamper();
232238
await stamper.init();
233-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "tok" } }), idType: "Bearer" });
239+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
234240
await stamper.clear();
235241

236242
expect(stamper.auth2Token).toBeNull();
@@ -247,7 +253,7 @@ describe("Auth2Stamper", () => {
247253
it("signs the data with ECDSA P-256 / SHA-256", async () => {
248254
const stamper = makeStamper();
249255
await stamper.init();
250-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "tok" } }), idType: "Bearer" });
256+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
251257

252258
const data = Buffer.from("test message");
253259
await stamper.stamp({ type: "OIDC", data });
@@ -270,7 +276,8 @@ describe("Auth2Stamper", () => {
270276
const stamper = makeStamper();
271277
await stamper.init();
272278

273-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "test-id-token" } }), idType: "Bearer" });
279+
const a2tJwt = makeA2tJwt();
280+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: a2tJwt } }), idType: "Bearer" });
274281

275282
const stampStr = await stamper.stamp({ type: "OIDC", data: Buffer.from("payload") });
276283
const decoded = JSON.parse(Buffer.from(stampStr, "base64url").toString("utf-8")) as {
@@ -283,7 +290,7 @@ describe("Auth2Stamper", () => {
283290
};
284291

285292
expect(decoded.kind).toBe("OIDC");
286-
expect(decoded.idToken).toBe("test-id-token");
293+
expect(decoded.idToken).toBe(a2tJwt);
287294
expect(decoded.algorithm).toBe("Secp256r1");
288295
expect(decoded.salt).toBe("");
289296
expect(typeof decoded.publicKey).toBe("string");
@@ -293,7 +300,7 @@ describe("Auth2Stamper", () => {
293300
it("uses the stored public key (base64url of raw P-256 bytes) in the OIDC stamp", async () => {
294301
const stamper = makeStamper();
295302
await stamper.init();
296-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "tok" } }), idType: "Bearer" });
303+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
297304

298305
const stampStr = await stamper.stamp({ type: "OIDC", data: Buffer.from("data") });
299306
const decoded = JSON.parse(Buffer.from(stampStr, "base64url").toString("utf-8")) as {
@@ -307,7 +314,7 @@ describe("Auth2Stamper", () => {
307314
it("works with empty data", async () => {
308315
const stamper = makeStamper();
309316
await stamper.init();
310-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "tok" } }), idType: "Bearer" });
317+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
311318
await expect(stamper.stamp({ type: "OIDC", data: Buffer.from("") })).resolves.toBeTruthy();
312319
});
313320
});
@@ -335,7 +342,7 @@ describe("Auth2Stamper", () => {
335342
it("clears the id token", async () => {
336343
const stamper = makeStamper();
337344
await stamper.init();
338-
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: "t" } }), idType: "Bearer" });
345+
await stamper.setTokens({ accessToken: makeJwt({ sub: "u", ext: { a2t: makeA2tJwt() } }), idType: "Bearer" });
339346

340347
await stamper.resetKeyPair();
341348

packages/browser-sdk/src/providers/embedded/auth2.integration.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,17 @@ function makeJwt(payload: Record<string, unknown>): string {
5959
return `${encode({ alg: "HS256" })}.${encode({ aud: [], ...payload })}.sig`;
6060
}
6161

62+
function makeA2tJwt(): string {
63+
const now = Math.floor(Date.now() / 1000);
64+
return makeJwt({ exp: now + 3600, iat: now });
65+
}
66+
6267
function mockTokenResponse(overrides: Record<string, unknown> = {}) {
6368
return {
6469
ok: true,
6570
json: async () =>
6671
Promise.resolve({
67-
access_token: makeJwt({ sub: "integration-user-1" }),
72+
access_token: makeJwt({ sub: "integration-user-1", ext: { a2t: makeA2tJwt() } }),
6873
id_token: "integration-id-token",
6974
token_type: "Bearer",
7075
expires_in: 3600,
@@ -228,7 +233,7 @@ describe("Auth2 browser flow — end-to-end", () => {
228233
await expect(stamper.stamp({ type: "OIDC", data: Buffer.from("test-payload") })).rejects.toThrow("not initialized");
229234

230235
await stamper.setTokens({
231-
accessToken: makeJwt({ sub: "test-user", ext: { a2t: "integration-id-token" } }),
236+
accessToken: makeJwt({ sub: "test-user", ext: { a2t: makeA2tJwt() } }),
232237
idType: "Bearer",
233238
});
234239
const stampStr = await stamper.stamp({ type: "OIDC", data: Buffer.from("test-payload") });

packages/embedded-provider-core/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ const sharedJestConfig = require("../../sharedJestConfig");
33
module.exports = {
44
...sharedJestConfig,
55
displayName: "@phantom/embedded-provider-core",
6+
setupFiles: ["<rootDir>/src/test/setup.ts"],
67
};

0 commit comments

Comments
 (0)