From 2103bf520b95fb7bf6e115b7af44c88547d8717f Mon Sep 17 00:00:00 2001 From: Liam Lloyd-Tucker Date: Mon, 22 Jun 2026 16:16:15 -0700 Subject: [PATCH] Revert "Allow user authentication via SFTP token" --- .../api/src/middleware/authentication.test.ts | 102 ------------------ packages/api/src/middleware/authentication.ts | 100 +++++++++-------- 2 files changed, 56 insertions(+), 146 deletions(-) diff --git a/packages/api/src/middleware/authentication.test.ts b/packages/api/src/middleware/authentication.test.ts index 0e319016..5a8c9ae3 100644 --- a/packages/api/src/middleware/authentication.test.ts +++ b/packages/api/src/middleware/authentication.test.ts @@ -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", () => { @@ -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" }, @@ -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()); @@ -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" }, @@ -888,7 +787,6 @@ describe("verifyUserOrAdminOrDelegatedCallAuthentication", () => { jest .spyOn(fusionAuthClient, "introspectAccessToken") .mockImplementationOnce(async () => expiredTokenIntrospectionResponse) - .mockImplementationOnce(async () => expiredTokenIntrospectionResponse) .mockImplementationOnce(async () => successfulIntrospectionResponse); await verifyUserOrAdminOrDelegatedCallAuthentication( diff --git a/packages/api/src/middleware/authentication.ts b/packages/api/src/middleware/authentication.ts index 5cd54949..41162726 100644 --- a/packages/api/src/middleware/authentication.ts +++ b/packages/api/src/middleware/authentication.ts @@ -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 => { + 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, @@ -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, @@ -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(); @@ -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);