diff --git a/docs/oidc-client-ts.api.md b/docs/oidc-client-ts.api.md index 65372e72a..a9e23b85b 100644 --- a/docs/oidc-client-ts.api.md +++ b/docs/oidc-client-ts.api.md @@ -629,7 +629,8 @@ export type SigninRedirectArgs = RedirectParams & ExtraSigninRequestArgs; // @public (undocumented) export class SigninRequest { - constructor({ url, authority, client_id, redirect_uri, response_type, scope, state_data, response_mode, request_type, client_secret, nonce, resource, skipUserInfo, extraQueryParams, extraTokenParams, disablePKCE, ...optionalParams }: SigninRequestArgs); + // (undocumented) + static create({ url, authority, client_id, redirect_uri, response_type, scope, state_data, response_mode, request_type, client_secret, nonce, resource, skipUserInfo, extraQueryParams, extraTokenParams, disablePKCE, ...optionalParams }: SigninRequestArgs): Promise; // (undocumented) readonly state: SigninState; // (undocumented) @@ -734,7 +735,16 @@ export type SigninSilentArgs = IFrameWindowParams & ExtraSigninRequestArgs; // @public (undocumented) export class SigninState extends State { - constructor(args: { + // (undocumented) + readonly authority: string; + // (undocumented) + readonly client_id: string; + // (undocumented) + readonly client_secret: string | undefined; + readonly code_challenge: string | undefined; + readonly code_verifier: string | undefined; + // (undocumented) + static create(args: { id?: string; data?: unknown; created?: number; @@ -748,19 +758,11 @@ export class SigninState extends State { extraTokenParams?: Record; response_mode?: "query" | "fragment"; skipUserInfo?: boolean; - }); - // (undocumented) - readonly authority: string; - // (undocumented) - readonly client_id: string; - // (undocumented) - readonly client_secret: string | undefined; - readonly code_challenge: string | undefined; - readonly code_verifier: string | undefined; + }): Promise; // (undocumented) readonly extraTokenParams: Record | undefined; // (undocumented) - static fromStorageString(storageString: string): SigninState; + static fromStorageString(storageString: string): Promise; // (undocumented) readonly redirect_uri: string; // (undocumented) @@ -839,7 +841,7 @@ export class State { readonly created: number; readonly data?: unknown; // (undocumented) - static fromStorageString(storageString: string): State; + static fromStorageString(storageString: string): Promise; // (undocumented) readonly id: string; // (undocumented) diff --git a/package-lock.json b/package-lock.json index 833714649..3bfe70ceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,13 @@ "version": "2.3.0", "license": "Apache-2.0", "dependencies": { - "crypto-js": "^4.1.1", "jwt-decode": "^3.1.2" }, "devDependencies": { "@microsoft/api-extractor": "^7.35.0", "@testing-library/jest-dom": "^6.0.0", - "@types/crypto-js": "^4.0.2", "@types/jest": "^29.2.3", + "@types/node": "^20.8.2", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", "esbuild": "^0.17.0", @@ -1827,12 +1826,6 @@ "@types/node": "*" } }, - "node_modules/@types/crypto-js": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.2.tgz", - "integrity": "sha512-t33RNmTu5ufG/sorROIafiCVJMx3jz95bXUMoPAZcUD14fxMXnuTzqzXZoxpR0tNx2xpw11Dlmem9vGCsrSOfA==", - "dev": true - }, "node_modules/@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -1935,9 +1928,9 @@ "peer": true }, "node_modules/@types/node": { - "version": "14.18.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", - "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", + "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", "dev": true }, "node_modules/@types/qs": { @@ -3104,10 +3097,6 @@ "node": ">= 8" } }, - "node_modules/crypto-js": { - "version": "4.1.1", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" - }, "node_modules/css.escape": { "version": "1.5.1", "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", @@ -8549,12 +8538,6 @@ "@types/node": "*" } }, - "@types/crypto-js": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.2.tgz", - "integrity": "sha512-t33RNmTu5ufG/sorROIafiCVJMx3jz95bXUMoPAZcUD14fxMXnuTzqzXZoxpR0tNx2xpw11Dlmem9vGCsrSOfA==", - "dev": true - }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -8657,9 +8640,9 @@ "peer": true }, "@types/node": { - "version": "14.18.36", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.36.tgz", - "integrity": "sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ==", + "version": "20.8.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", + "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", "dev": true }, "@types/qs": { @@ -9463,10 +9446,6 @@ "which": "^2.0.1" } }, - "crypto-js": { - "version": "4.1.1", - "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" - }, "css.escape": { "version": "1.5.1", "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=", diff --git a/package.json b/package.json index 48d1e49db..3073909f8 100644 --- a/package.json +++ b/package.json @@ -39,14 +39,13 @@ "prepare": "husky install" }, "dependencies": { - "crypto-js": "^4.1.1", "jwt-decode": "^3.1.2" }, "devDependencies": { "@microsoft/api-extractor": "^7.35.0", "@testing-library/jest-dom": "^6.0.0", - "@types/crypto-js": "^4.0.2", "@types/jest": "^29.2.3", + "@types/node": "^20.8.2", "@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/parser": "^6.4.1", "esbuild": "^0.17.0", diff --git a/src/OidcClient.test.ts b/src/OidcClient.test.ts index 68b35497a..68b66116e 100644 --- a/src/OidcClient.test.ts +++ b/src/OidcClient.test.ts @@ -259,14 +259,14 @@ describe("OidcClient", () => { it("should deserialize stored state and return state and response", async () => { // arrange - const item = new SigninState({ + const item = (await SigninState.create({ id: "1", authority: "authority", client_id: "client", redirect_uri: "http://app/cb", scope: "scope", request_type: "type", - }).toStorageString(); + })).toStorageString(); jest.spyOn(subject.settings.stateStore, "get").mockImplementation(() => Promise.resolve(item)); // act @@ -314,7 +314,7 @@ describe("OidcClient", () => { it("should deserialize stored state and call validator", async () => { // arrange - const item = new SigninState({ + const item = await SigninState.create({ id: "1", authority: "authority", client_id: "client", diff --git a/src/OidcClient.ts b/src/OidcClient.ts index b4f341664..ae0a0a2d3 100644 --- a/src/OidcClient.ts +++ b/src/OidcClient.ts @@ -73,7 +73,7 @@ export class OidcClient { protected readonly _tokenClient: TokenClient; public constructor(settings: OidcClientSettings); - public constructor(settings: OidcClientSettingsStore, metadataService: MetadataService); + public constructor(settings: OidcClientSettingsStore, metadataService: MetadataService); public constructor(settings: OidcClientSettings | OidcClientSettingsStore, metadataService?: MetadataService) { this.settings = settings instanceof OidcClientSettingsStore ? settings : new OidcClientSettingsStore(settings); @@ -114,7 +114,7 @@ export class OidcClient { const url = await this.metadataService.getAuthorizationEndpoint(); logger.debug("Received authorization endpoint", url); - const signinRequest = new SigninRequest({ + const signinRequest = await SigninRequest.create({ url, authority: this.settings.authority, client_id: this.settings.client_id, @@ -154,7 +154,7 @@ export class OidcClient { throw null; // https://github.com/microsoft/TypeScript/issues/46972 } - const state = SigninState.fromStorageString(storedStateString); + const state = await SigninState.fromStorageString(storedStateString); return { state, response }; } @@ -284,7 +284,7 @@ export class OidcClient { throw null; // https://github.com/microsoft/TypeScript/issues/46972 } - const state = State.fromStorageString(storedStateString); + const state = await State.fromStorageString(storedStateString); return { state, response }; } diff --git a/src/SigninRequest.test.ts b/src/SigninRequest.test.ts index 44e1d8a7c..60782dffa 100644 --- a/src/SigninRequest.test.ts +++ b/src/SigninRequest.test.ts @@ -8,7 +8,7 @@ describe("SigninRequest", () => { let subject: SigninRequest; let settings: SigninRequestArgs; - beforeEach(() => { + beforeEach(async () => { settings = { url: "http://sts/signin", client_id: "client", @@ -18,18 +18,19 @@ describe("SigninRequest", () => { authority : "op", state_data: { data: "test" }, }; - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); }); - describe("constructor", () => { - it.each(["url", "client_id", "redirect_uri", "response_type", "scope", "authority"])("should require a %s param", (param) => { + describe("create", () => { + it.each(["url", "client_id", "redirect_uri", "response_type", "scope", "authority"])("should require a %s param", async (param) => { // arrange Object.assign(settings, { [param]: undefined }); // act - expect(() => new SigninRequest(settings)) - // assert - .toThrow(param); + const act = async () => await SigninRequest.create(settings); + + // assert + await expect(act).rejects.toThrow(param); }); }); @@ -65,139 +66,139 @@ describe("SigninRequest", () => { expect(subject.url).toContain("state=" + subject.state.id); }); - it("should include prompt", () => { + it("should include prompt", async () => { // arrange settings.prompt = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("prompt=foo"); }); - it("should include display", () => { + it("should include display", async () => { // arrange settings.display = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("display=foo"); }); - it("should include max_age", () => { + it("should include max_age", async () => { // arrange settings.max_age = 42; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("max_age=42"); }); - it("should include ui_locales", () => { + it("should include ui_locales", async () => { // arrange settings.ui_locales = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("ui_locales=foo"); }); - it("should include id_token_hint", () => { + it("should include id_token_hint", async () => { // arrange settings.id_token_hint = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("id_token_hint=foo"); }); - it("should include login_hint", () => { + it("should include login_hint", async () => { // arrange settings.login_hint = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("login_hint=foo"); }); - it("should include acr_values", () => { + it("should include acr_values", async () => { // arrange settings.acr_values = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("acr_values=foo"); }); - it("should include a resource", () => { + it("should include a resource", async () => { // arrange settings.resource = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("resource=foo"); }); - it("should include multiple resources", () => { + it("should include multiple resources", async () => { // arrange settings.resource = ["foo", "bar"]; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("resource=foo&resource=bar"); }); - it("should include response_mode", () => { + it("should include response_mode", async () => { // arrange settings.response_mode = "fragment"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("response_mode=fragment"); }); - it("should include request", () => { + it("should include request", async () => { // arrange settings.request = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("request=foo"); }); - it("should include request_uri", () => { + it("should include request_uri", async () => { // arrange settings.request_uri = "foo"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("request_uri=foo"); }); - it("should include extra query params", () => { + it("should include extra query params", async () => { // arrange settings.extraQueryParams = { "hd": "domain.com", @@ -205,20 +206,20 @@ describe("SigninRequest", () => { }; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("hd=domain.com&foo=bar"); }); - it("should store extra token params in state", () => { + it("should store extra token params in state", async () => { // arrange settings.extraTokenParams = { "resourceServer": "abc", }; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.state.extraTokenParams).toEqual({ @@ -226,24 +227,24 @@ describe("SigninRequest", () => { }); }); - it("should include code flow params", () => { + it("should include code flow params", async () => { // arrange settings.response_type = "code"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("code_challenge="); expect(subject.url).toContain("code_challenge_method=S256"); }); - it("should include nonce", () => { + it("should include nonce", async () => { // arrange settings.nonce = "random_nonce"; // act - subject = new SigninRequest(settings); + subject = await SigninRequest.create(settings); // assert expect(subject.url).toContain("nonce="); diff --git a/src/SigninRequest.ts b/src/SigninRequest.ts index c5066cb87..fed5447fe 100644 --- a/src/SigninRequest.ts +++ b/src/SigninRequest.ts @@ -48,12 +48,17 @@ export interface SigninRequestArgs { * @public */ export class SigninRequest { - private readonly _logger = new Logger("SigninRequest"); + private static readonly _logger = new Logger("SigninRequest"); public readonly url: string; public readonly state: SigninState; - public constructor({ + private constructor(args: { url: string; state: SigninState }) { + this.url = args.url; + this.state = args.state; + } + + public static async create({ // mandatory url, authority, client_id, redirect_uri, response_type, scope, // optional @@ -64,7 +69,8 @@ export class SigninRequest { extraTokenParams, disablePKCE, ...optionalParams - }: SigninRequestArgs) { + }: SigninRequestArgs): Promise { + if (!url) { this._logger.error("ctor: No url passed"); throw new Error("url"); @@ -90,7 +96,7 @@ export class SigninRequest { throw new Error("authority"); } - this.state = new SigninState({ + const state = await SigninState.create({ data: state_data, request_type, code_verifier: !disablePKCE, @@ -109,9 +115,9 @@ export class SigninRequest { parsedUrl.searchParams.append("nonce", nonce); } - parsedUrl.searchParams.append("state", this.state.id); - if (this.state.code_challenge) { - parsedUrl.searchParams.append("code_challenge", this.state.code_challenge); + parsedUrl.searchParams.append("state", state.id); + if (state.code_challenge) { + parsedUrl.searchParams.append("code_challenge", state.code_challenge); parsedUrl.searchParams.append("code_challenge_method", "S256"); } @@ -128,6 +134,6 @@ export class SigninRequest { } } - this.url = parsedUrl.href; + return new SigninRequest({ url: parsedUrl.href, state }); } } diff --git a/src/SigninState.test.ts b/src/SigninState.test.ts index 0f026bf68..a07879682 100644 --- a/src/SigninState.test.ts +++ b/src/SigninState.test.ts @@ -4,11 +4,11 @@ import { SigninState } from "./SigninState"; describe("SigninState", () => { - describe("constructor", () => { + describe("create", () => { - it("should call base ctor", () => { + it("should call base ctor", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ id: "5", created: 6, data: 7, @@ -26,9 +26,9 @@ describe("SigninState", () => { expect(subject.data).toEqual(7); }); - it("should accept redirect_uri", () => { + it("should accept redirect_uri", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", client_id: "client", scope: "scope", @@ -40,9 +40,9 @@ describe("SigninState", () => { expect(subject.redirect_uri).toEqual("http://cb"); }); - it("should accept code_verifier", () => { + it("should accept code_verifier", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", client_id: "client", redirect_uri: "http://cb", @@ -55,9 +55,9 @@ describe("SigninState", () => { expect(subject.code_verifier).toEqual("5"); }); - it("should generate code_verifier", () => { + it("should generate code_verifier", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", client_id: "client", redirect_uri: "http://cb", @@ -70,11 +70,11 @@ describe("SigninState", () => { expect(subject.code_verifier).toBeDefined(); }); - it("should generate code_challenge", () => { + it("should generate code_challenge", async () => { // arrange // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", client_id: "client", redirect_uri: "http://cb", @@ -87,9 +87,9 @@ describe("SigninState", () => { expect(subject.code_challenge).toBeDefined(); }); - it("should accept client_id", () => { + it("should accept client_id", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", redirect_uri: "http://cb", scope: "scope", @@ -101,9 +101,9 @@ describe("SigninState", () => { expect(subject.client_id).toEqual("client"); }); - it("should accept authority", () => { + it("should accept authority", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ client_id: "client", redirect_uri: "http://cb", scope: "scope", @@ -115,9 +115,9 @@ describe("SigninState", () => { expect(subject.authority).toEqual("test"); }); - it("should accept request_type", () => { + it("should accept request_type", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", client_id: "client", redirect_uri: "http://cb", @@ -129,9 +129,9 @@ describe("SigninState", () => { expect(subject.request_type).toEqual("xoxo"); }); - it("should accept extraTokenParams", () => { + it("should accept extraTokenParams", async () => { // act - const subject = new SigninState({ + const subject = await SigninState.create({ authority: "authority", client_id: "client", redirect_uri: "http://cb", @@ -147,9 +147,9 @@ describe("SigninState", () => { }); }); - it("can serialize and then deserialize", () => { + it("can serialize and then deserialize", async () => { // arrange - const subject1 = new SigninState({ + const subject1 = await SigninState.create({ data: { foo: "test" }, created: 1000, code_verifier: true, @@ -162,7 +162,7 @@ describe("SigninState", () => { // act const storage = subject1.toStorageString(); - const subject2 = SigninState.fromStorageString(storage); + const subject2 = await SigninState.fromStorageString(storage); // assert expect(subject2).toEqual(subject1); diff --git a/src/SigninState.ts b/src/SigninState.ts index 203157ed9..410e20880 100644 --- a/src/SigninState.ts +++ b/src/SigninState.ts @@ -32,13 +32,14 @@ export class SigninState extends State { public readonly skipUserInfo: boolean | undefined; - public constructor(args: { + private constructor(args: { id?: string; data?: unknown; created?: number; request_type?: string; - code_verifier?: string | boolean; + code_verifier?: string; + code_challenge?: string; authority: string; client_id: string; redirect_uri: string; @@ -50,16 +51,8 @@ export class SigninState extends State { }) { super(args); - if (args.code_verifier === true) { - this.code_verifier = CryptoUtils.generateCodeVerifier(); - } - else if (args.code_verifier) { - this.code_verifier = args.code_verifier; - } - - if (this.code_verifier) { - this.code_challenge = CryptoUtils.generateCodeChallenge(this.code_verifier); - } + this.code_verifier = args.code_verifier; + this.code_challenge = args.code_challenge; this.authority = args.authority; this.client_id = args.client_id; @@ -72,6 +65,37 @@ export class SigninState extends State { this.skipUserInfo = args.skipUserInfo; } + public static async create(args: { + id?: string; + data?: unknown; + created?: number; + request_type?: string; + + code_verifier?: string | boolean; + authority: string; + client_id: string; + redirect_uri: string; + scope: string; + client_secret?: string; + extraTokenParams?: Record; + response_mode?: "query" | "fragment"; + skipUserInfo?: boolean; + }): Promise { + const code_verifier = args.code_verifier === true + ? CryptoUtils.generateCodeVerifier() + : args.code_verifier ? args.code_verifier : undefined; + + const code_challenge = code_verifier + ? await CryptoUtils.generateCodeChallenge(code_verifier) + : undefined; + + return new SigninState({ + ...args, + code_verifier, + code_challenge, + }); + } + public toStorageString(): string { new Logger("SigninState").create("toStorageString"); return JSON.stringify({ @@ -92,9 +116,9 @@ export class SigninState extends State { }); } - public static fromStorageString(storageString: string): SigninState { + public static fromStorageString(storageString: string): Promise { Logger.createStatic("SigninState", "fromStorageString"); const data = JSON.parse(storageString); - return new SigninState(data); + return SigninState.create(data); } } diff --git a/src/State.test.ts b/src/State.test.ts index 87113e869..c8848a280 100644 --- a/src/State.test.ts +++ b/src/State.test.ts @@ -91,7 +91,7 @@ describe("State", () => { }); }); - it("can serialize and then deserialize", () => { + it("can serialize and then deserialize", async () => { // arrange const subject1 = new State({ data: { foo: "test" }, created: 1000, request_type:"type", @@ -99,7 +99,7 @@ describe("State", () => { // act const storage = subject1.toStorageString(); - const subject2 = State.fromStorageString(storage); + const subject2 = await State.fromStorageString(storage); // assert expect(subject2).toEqual(subject1); diff --git a/src/State.ts b/src/State.ts index 668a6b1f2..0c020ad6d 100644 --- a/src/State.ts +++ b/src/State.ts @@ -43,7 +43,7 @@ export class State { }); } - public static fromStorageString(storageString: string): State { + public static async fromStorageString(storageString: string): Promise { Logger.createStatic("State", "fromStorageString"); return new State(JSON.parse(storageString)); } @@ -62,7 +62,7 @@ export class State { if (item) { try { - const state = State.fromStorageString(item); + const state = await State.fromStorageString(item); logger.debug("got item from key:", key, state.created); if (state.created <= cutoff) { diff --git a/src/utils/CryptoUtils.ts b/src/utils/CryptoUtils.ts index b1dc0b2d6..66e77782f 100644 --- a/src/utils/CryptoUtils.ts +++ b/src/utils/CryptoUtils.ts @@ -1,18 +1,20 @@ -import CryptoJS from "crypto-js/core.js"; -import sha256 from "crypto-js/sha256.js"; -import Base64 from "crypto-js/enc-base64.js"; -import Utf8 from "crypto-js/enc-utf8.js"; - import { Logger } from "./Logger"; const UUID_V4_TEMPLATE = "10000000-1000-4000-8000-100000000000"; +const toBase64 = (val: ArrayBuffer): string => + btoa([...new Uint8Array(val)] + .map((chr) => String.fromCharCode(chr)) + .join("")); + /** * @internal */ export class CryptoUtils { private static _randomWord(): number { - return CryptoJS.lib.WordArray.random(1).words[0]; + const arr = new Uint32Array(1); + crypto.getRandomValues(arr); + return arr[0]; } /** @@ -35,10 +37,12 @@ export class CryptoUtils { /** * PKCE: Generate a code challenge */ - public static generateCodeChallenge(code_verifier: string): string { + public static async generateCodeChallenge(code_verifier: string): Promise { try { - const hashed = sha256(code_verifier); - return Base64.stringify(hashed).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + const encoder = new TextEncoder(); + const data = encoder.encode(code_verifier); + const hashed = await crypto.subtle.digest("SHA-256", data); + return toBase64(hashed).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } catch (err) { Logger.error("CryptoUtils.generateCodeChallenge", err); @@ -50,7 +54,8 @@ export class CryptoUtils { * Generates a base64-encoded string for a basic auth header */ public static generateBasicAuth(client_id: string, client_secret: string): string { - const basicAuth = Utf8.parse([client_id, client_secret].join(":")); - return Base64.stringify(basicAuth); + const encoder = new TextEncoder(); + const data = encoder.encode([client_id, client_secret].join(":")); + return toBase64(data); } } diff --git a/test/setup.ts b/test/setup.ts index 34b5b46de..a38681ba8 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -1,6 +1,12 @@ import { Log } from "../src"; +import { TextEncoder } from "util"; +import { webcrypto } from "node:crypto"; beforeAll(() => { + globalThis.TextEncoder = TextEncoder; + Object.assign(globalThis.crypto, { + subtle: webcrypto.subtle, + }); globalThis.fetch = jest.fn(); const unload = () =>