Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 5 additions & 3 deletions @stellar/typescript-wallet-sdk/src/walletSdk/Auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,15 @@ export class Sep10 {
}
}

const validateToken = (token: string) => {
/** @internal Exported for testing only. Not part of the public API. */
export const validateToken = (token: string) => {
const parsedToken = decode(token);
if (!parsedToken) {
throw new InvalidTokenError();
}
if (parsedToken.expiresAt < Math.floor(Date.now() / 1000)) {
throw new ExpiredTokenError(parsedToken.expiresAt);
const exp = parsedToken.payload?.exp;
if (exp && exp < Math.floor(Date.now() / 1000)) {
throw new ExpiredTokenError(exp);
}
};

Expand Down
95 changes: 95 additions & 0 deletions @stellar/typescript-wallet-sdk/test/auth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { sign, decode } from "jws";

import { validateToken } from "../src/walletSdk/Auth";
import {
InvalidTokenError,
ExpiredTokenError,
} from "../src/walletSdk/Exceptions";

const createToken = (payload: Record<string, unknown>): string => {
return sign({
header: { alg: "HS256", typ: "JWT" },
payload,
secret: "test-secret",
});
};

describe("jws.decode return structure", () => {
// In SEP-10, authentication happens via Stellar transaction signing, not JWT
// signature verification. The JWT is a bearer token issued by the anchor after
// the wallet proves ownership of its Stellar account. The SDK only decodes the
// payload to read claims (exp, iss, sub) — verifying the JWT signature
// client-side is not part of the SEP-10 trust model.
it("should expose SEP-10 claims via payload, not as top-level properties", () => {
const token = createToken({
iss: "https://anchor.example.com",
sub: "GABC1234",
iat: 1700000000,
exp: 1700003600,
client_domain: "wallet.example.com",
});
const decoded = decode(token);

expect(decoded).toHaveProperty("header");
expect(decoded).toHaveProperty("payload");

expect(decoded.payload.exp).toBe(1700003600);
expect(decoded.payload.iss).toBe("https://anchor.example.com");
expect(decoded.payload.sub).toBe("GABC1234");
expect(decoded.payload.iat).toBe(1700000000);
expect(decoded.payload.client_domain).toBe("wallet.example.com");
});
});

describe("validateToken", () => {
it("should accept a valid, non-expired token", () => {
const futureExp = Math.floor(Date.now() / 1000) + 3600;
const token = createToken({
iss: "https://anchor.example.com",
sub: "GABC1234",
iat: Math.floor(Date.now() / 1000),
exp: futureExp,
});

expect(() => validateToken(token)).not.toThrow();
});

it("should throw ExpiredTokenError for an expired token", () => {
const pastExp = Math.floor(Date.now() / 1000) - 3600;
const token = createToken({
iss: "https://anchor.example.com",
sub: "GABC1234",
iat: Math.floor(Date.now() / 1000) - 7200,
exp: pastExp,
});

expect(() => validateToken(token)).toThrow(ExpiredTokenError);
});

it("should throw ExpiredTokenError for a token with exp=1", () => {
const token = createToken({
iss: "https://anchor.example.com",
sub: "GABC1234",
exp: 1,
});

expect(() => validateToken(token)).toThrow(ExpiredTokenError);
});

it("should throw InvalidTokenError for a malformed token", () => {
expect(() => validateToken("not-a-valid-jwt")).toThrow(InvalidTokenError);
});

it("should throw InvalidTokenError for an empty string", () => {
expect(() => validateToken("")).toThrow(InvalidTokenError);
});

it("should accept a token without an exp claim", () => {
const token = createToken({
iss: "https://anchor.example.com",
sub: "GABC1234",
});

expect(() => validateToken(token)).not.toThrow();
});
});
3 changes: 2 additions & 1 deletion @stellar/typescript-wallet-sdk/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"baseUrl": "src/",
"outDir": "lib",
"declaration": true,
"declarationDir": "lib"
"declarationDir": "lib",
"stripInternal": true
},
"include": ["src"]
}
Loading