Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
102 changes: 0 additions & 102 deletions packages/api/src/middleware/authentication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,76 +261,6 @@ describe("verifyUserAuthentication", () => {
},
);
});

test("should add email and subject to the request body if the SFTP token is valid", async () => {
const request = createRequest({
headers: { Authorization: "Bearer test" },
});
jest
.spyOn(fusionAuthClient, "introspectAccessToken")
.mockImplementationOnce(async () => failedIntrospectionResponse)
.mockImplementationOnce(async () => successfulIntrospectionResponse);
await verifyUserAuthentication(request, createResponse(), jest.fn());

const {
body: { emailFromAuthToken, userSubjectFromAuthToken },
} = request as {
body: { emailFromAuthToken: string; userSubjectFromAuthToken: string };
};
expect(emailFromAuthToken).toBe(testEmail);
expect(userSubjectFromAuthToken).toBe(testSubject);
});

test("should throw unauthorized if both backend and SFTP tokens are invalid", async () => {
const request = createRequest({
headers: { Authorization: "Bearer test" },
});
jest
.spyOn(fusionAuthClient, "introspectAccessToken")
.mockImplementation(async () => failedIntrospectionResponse);
await verifyUserAuthentication(
request,
createResponse(),
(err: unknown) => {
expect(typeof err).toBe("object");
expect(err).not.toBeNull();
if (typeof err === "object" && err !== null) {
expect("statusCode" in err).toBe(true);
if ("statusCode" in err) {
expect(err.statusCode).toBe(401);
}
}
},
);
});

test("should throw 429 if first introspect call returns 429", async () => {
const request = createRequest({
headers: { Authorization: "Bearer test" },
});
const testError = Object.assign(new Error("Rate Limit Exceeded"), {
statusCode: 429,
});
jest
.spyOn(fusionAuthClient, "introspectAccessToken")
.mockImplementationOnce(async () => {
throw testError;
});
await verifyUserAuthentication(
request,
createResponse(),
(err: unknown) => {
expect(typeof err).toBe("object");
expect(err).not.toBeNull();
if (typeof err === "object" && err !== null) {
expect("statusCode" in err).toBe(true);
if ("statusCode" in err) {
expect(err.statusCode).toBe(429);
}
}
},
);
});
});

describe("verifyAdminAuthentication", () => {
Expand Down Expand Up @@ -387,10 +317,6 @@ describe("verifyAdminAuthentication", () => {
});

describe("verifyUserOrAdminAuthentication", () => {
beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});
test("should add subject and email to the request body if user token is valid", async () => {
const request = createRequest({
headers: { Authorization: "Bearer test" },
Expand Down Expand Up @@ -422,7 +348,6 @@ describe("verifyUserOrAdminAuthentication", () => {
jest
.spyOn(fusionAuthClient, "introspectAccessToken")
.mockImplementationOnce(async () => expiredTokenIntrospectionResponse)
.mockImplementationOnce(async () => expiredTokenIntrospectionResponse)
.mockImplementationOnce(async () => successfulIntrospectionResponse);

await verifyUserOrAdminAuthentication(request, createResponse(), jest.fn());
Expand Down Expand Up @@ -465,32 +390,6 @@ describe("verifyUserOrAdminAuthentication", () => {
);
});

test("should add subject and email to the request body if SFTP token is valid", async () => {
const request = createRequest({
headers: { Authorization: "Bearer test" },
});
jest
.spyOn(fusionAuthClient, "introspectAccessToken")
.mockImplementationOnce(async () => expiredTokenIntrospectionResponse)
.mockImplementationOnce(async () => successfulIntrospectionResponse)
.mockImplementationOnce(async () => successfulIntrospectionResponse);

await verifyUserOrAdminAuthentication(request, createResponse(), jest.fn());
const {
body: { userSubjectFromAuthToken, userEmailFromAuthToken },
} = request as {
body: {
userSubjectFromAuthToken: string;
userEmailFromAuthToken: string;
};
};
expect(userSubjectFromAuthToken).toBe(testSubject);
expect(userEmailFromAuthToken).toBe(testEmail);
expect(
fieldsFromUserOrAdminAuthentication.validate(request.body).error,
).toBeFalsy();
});

test("should throw unauthorized if both tokens are expired", async () => {
const request = createRequest({
headers: { Authorization: "Bearer test" },
Expand Down Expand Up @@ -888,7 +787,6 @@ describe("verifyUserOrAdminOrDelegatedCallAuthentication", () => {
jest
.spyOn(fusionAuthClient, "introspectAccessToken")
.mockImplementationOnce(async () => expiredTokenIntrospectionResponse)
.mockImplementationOnce(async () => expiredTokenIntrospectionResponse)
.mockImplementationOnce(async () => successfulIntrospectionResponse);

await verifyUserOrAdminOrDelegatedCallAuthentication(
Expand Down
100 changes: 56 additions & 44 deletions packages/api/src/middleware/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ import { isObjectWithStatusCode } from "./handleError";
import { HTTP_STATUS } from "@pdc/http-status-codes";

const emailKey = "email";
const subjectKey = "sub";

const getValueFromAuthToken = async (
authenticationToken: string,
key: "email" | "sub",
applicationId: string,
): Promise<string> => {
const introspectionResponse = await fusionAuthClient.introspectAccessToken(
applicationId,
authenticationToken,
);
if (!introspectionResponse.wasSuccessful()) {
throw new createError.Unauthorized(
`Token validation failed: ${
introspectionResponse.exception.message ?? ""
}`,
);
}
if (
!introspectionResponse.response.active ||
typeof introspectionResponse.response[key] !== "string" ||
introspectionResponse.response[key] === ""
) {
throw new createError.Unauthorized("Invalid token");
}

return introspectionResponse.response[key];
};

const getOptionalValueFromAuthToken = async (
authenticationToken: string,
Expand Down Expand Up @@ -101,38 +129,6 @@ const getAuthTokenFromRequest = (
return secondWordInAuthorizationHeader ?? "";
};

const getSubjectAndEmailFromUserAuthToken = async (
authenticationToken: string,
): Promise<{ subject: string; email: string }> => {
try {
return await getValuesFromAuthToken(
authenticationToken,
process.env["FUSIONAUTH_BACKEND_APPLICATION_ID"] ?? "",
);
} catch (err) {
if (
isObjectWithStatusCode(err) &&
err.statusCode === HTTP_STATUS.CLIENT_ERROR.UNAUTHORIZED.valueOf()
) {
return await getValuesFromAuthToken(
authenticationToken,
process.env["FUSIONAUTH_SFTP_APPLICATION_ID"] ?? "",
);
}
throw err;
}
};

const getSubjectAndEmailFromAdminAuthToken = async (
authenticationToken: string,
): Promise<{ subject: string; email: string }> => {
const { subject, email } = await getValuesFromAuthToken(
authenticationToken,
process.env["FUSIONAUTH_ADMIN_APPLICATION_ID"] ?? "",
);
return { subject, email };
};

const verifyUserAuthentication = async (
req: Request<
unknown,
Expand All @@ -150,8 +146,10 @@ const verifyUserAuthentication = async (
if (authenticationToken === "") {
throw new createError.Unauthorized("Invalid Authorization header format");
}
const { subject, email } =
await getSubjectAndEmailFromUserAuthToken(authenticationToken);
const { email, subject } = await getValuesFromAuthToken(
authenticationToken,
process.env["FUSIONAUTH_BACKEND_APPLICATION_ID"] ?? "",
);
req.body.emailFromAuthToken = email;
req.body.userSubjectFromAuthToken = subject;
next();
Expand Down Expand Up @@ -206,23 +204,37 @@ const verifyUserOrAdminAuthentication = async (
throw new createError.Unauthorized("Invalid Authorization header format");
}
try {
const { subject, email } =
await getSubjectAndEmailFromUserAuthToken(authenticationToken);
const subject = await getValueFromAuthToken(
authenticationToken,
subjectKey,
process.env["FUSIONAUTH_BACKEND_APPLICATION_ID"] ?? "",
);
const email = await getValueFromAuthToken(
authenticationToken,
emailKey,
process.env["FUSIONAUTH_BACKEND_APPLICATION_ID"] ?? "",
);
req.body.userSubjectFromAuthToken = subject;
req.body.userEmailFromAuthToken = email;
next();
} catch (err) {
if (
isObjectWithStatusCode(err) &&
err.statusCode === HTTP_STATUS.CLIENT_ERROR.UNAUTHORIZED.valueOf()
) {
const { subject, email } =
await getSubjectAndEmailFromAdminAuthToken(authenticationToken);
} catch (_) {
try {
const subject = await getValueFromAuthToken(
authenticationToken,
subjectKey,
process.env["FUSIONAUTH_ADMIN_APPLICATION_ID"] ?? "",
);
const email = await getValueFromAuthToken(
authenticationToken,
emailKey,
process.env["FUSIONAUTH_ADMIN_APPLICATION_ID"] ?? "",
);
req.body.adminSubjectFromAuthToken = subject;
req.body.adminEmailFromAuthToken = email;
next();
} catch (innerErr) {
next(innerErr);
}
next(err);
}
} catch (err) {
next(err);
Expand Down