Use b64url and fix incoming data validation (#156)

This commit is contained in:
Krzysztof Wolski 2023-01-23 09:35:31 +01:00 committed by GitHub
parent 74e3f40ca5
commit b84049246b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 144 additions and 5 deletions

View file

@ -5,6 +5,10 @@ import { hasProp } from "../has-prop";
*/ */
export const hasAuthData = (data: unknown) => export const hasAuthData = (data: unknown) =>
hasProp(data, "domain") && hasProp(data, "domain") &&
data.domain &&
hasProp(data, "token") && hasProp(data, "token") &&
data.token &&
hasProp(data, "appId") && hasProp(data, "appId") &&
hasProp(data, "saleorApiUrl"); data.appId &&
hasProp(data, "saleorApiUrl") &&
data.saleorApiUrl;

View file

@ -0,0 +1,120 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { AuthData } from "./apl";
import { SaleorCloudAPL, SaleorCloudAPLConfig } from "./saleor-cloud-apl";
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const aplConfig: SaleorCloudAPLConfig = {
resourceUrl: "https://example.com",
token: "token",
};
const stubAuthData: AuthData = {
domain: "example.com",
token: "example-token",
saleorApiUrl: "https://example.com/graphql/",
appId: "42",
jwks: "{}",
};
describe("APL", () => {
afterEach(() => {
vi.resetModules();
vi.restoreAllMocks();
});
describe("SaleorCloudAPL", () => {
describe("set", () => {
it("Successful save of the auth data", async () => {
fetchMock.mockResolvedValue({
status: 200,
json: async () => ({ result: "ok" }),
ok: true,
});
const apl = new SaleorCloudAPL(aplConfig);
await apl.set(stubAuthData);
expect(fetchMock).toBeCalledWith(
"https://example.com",
{
body: JSON.stringify({
saleor_app_id: "42",
saleor_api_url: "https://example.com/graphql/",
jwks: "{}",
domain: "example.com",
token: "example-token",
}),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
method: "POST",
}
);
});
it("Raise error when register service returns non 200 response", async () => {
fetchMock.mockResolvedValue({
status: 500,
ok: false,
});
const apl = new SaleorCloudAPL(aplConfig);
await expect(apl.set(stubAuthData)).rejects.toThrow(
"Fetch returned with non 200 status code 500"
);
});
});
describe("get", () => {
describe("Read existing auth data", () => {
it("Read existing auth data", async () => {
fetchMock.mockResolvedValue({
status: 200,
ok: true,
json: async () => ({
saleor_app_id: stubAuthData.appId,
saleor_api_url: stubAuthData.saleorApiUrl,
jwks: stubAuthData.jwks,
domain: stubAuthData.domain,
token: stubAuthData.token,
}),
});
const apl = new SaleorCloudAPL(aplConfig);
expect(await apl.get(stubAuthData.saleorApiUrl)).toStrictEqual(stubAuthData);
expect(fetchMock).toBeCalledWith(
"https://example.com/aHR0cHM6Ly9leGFtcGxlLmNvbS9ncmFwaHFsLw", // base64 encoded api url
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer token",
},
method: "GET",
}
);
});
it("Return undefined when unknown domain requested", async () => {
fetchMock.mockResolvedValue({
status: 404,
ok: false,
json: async () => undefined,
});
const apl = new SaleorCloudAPL(aplConfig);
expect(await apl.get("http://unknown-domain.example.com/graphql/")).toBe(undefined);
});
});
});
});
});

View file

@ -10,6 +10,11 @@ export type SaleorCloudAPLConfig = {
}; };
const validateResponseStatus = (response: Response) => { const validateResponseStatus = (response: Response) => {
if (response.status === 404) {
debug("Auth data not found");
throw new Error("Auth data not found");
}
if (!response.ok) { if (!response.ok) {
debug("Response failed with status %s", response.status); debug("Response failed with status %s", response.status);
@ -56,8 +61,8 @@ export class SaleorCloudAPL implements APL {
} }
private getUrlForDomain(saleorApiUrl: string) { private getUrlForDomain(saleorApiUrl: string) {
// API URL has to be base64 encoded // API URL has to be base64url encoded
return `${this.resourceUrl}/${btoa(saleorApiUrl)}`; return `${this.resourceUrl}/${Buffer.from(saleorApiUrl).toString("base64url")}`;
} }
async get(saleorApiUrl: string): Promise<AuthData | undefined> { async get(saleorApiUrl: string): Promise<AuthData | undefined> {
@ -68,10 +73,20 @@ export class SaleorCloudAPL implements APL {
headers: { "Content-Type": "application/json", ...this.headers }, headers: { "Content-Type": "application/json", ...this.headers },
}).catch((error) => { }).catch((error) => {
debug("Failed to reach API call: %s", error?.message ?? "Unknown error"); debug("Failed to reach API call: %s", error?.message ?? "Unknown error");
throw new Error(`Attempt in fetch the data resulted with error: ${error}`); return undefined;
}); });
validateResponseStatus(response); if (!response) {
debug("No response from the API");
return undefined;
}
try {
validateResponseStatus(response);
} catch {
debug("Response status not valid");
return undefined;
}
const parsedResponse = (await response.json().catch((e) => { const parsedResponse = (await response.json().catch((e) => {
debug("Failed to parse response: %s", e?.message ?? "Unknown error"); debug("Failed to parse response: %s", e?.message ?? "Unknown error");