Skip to content

Commit 5bf67bc

Browse files
donna-belsey-nhsanoop-surej-nhs
authored andcommitted
VIA-254 Check APIM expiry time when calling EliD and refresh if ness
Mitigates issue with being unable to set the secure cookie from a server action (see comments on VIA-254 for details); allow EliD service to get its own new token if the cached token has expired.
1 parent 9ecb5d1 commit 5bf67bc

File tree

4 files changed

+81
-23
lines changed

4 files changed

+81
-23
lines changed

src/services/eligibility-api/gateway/fetch-eligibility-content.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export const fetchEligibilityContent = async (nhsNumber: NhsNumber): Promise<Eli
4141
};
4242

4343
if (config.IS_APIM_AUTH_ENABLED) {
44-
const apimAccessToken = await getApimAccessToken();
44+
const apimAccessToken = await getApimAccessToken(config);
4545
headers = { ...headers, Authorization: `Bearer ${apimAccessToken}` };
4646
}
4747

src/utils/auth/apim/get-apim-access-token.test.ts

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { fetchAPIMAccessToken } from "@src/utils/auth/apim/fetch-apim-access-token";
22
import { getApimAccessToken, retrieveApimCredentials } from "@src/utils/auth/apim/get-apim-access-token";
3+
import { _getOrRefreshApimCredentials } from "@src/utils/auth/callbacks/get-token";
34
import { getJwtToken } from "@src/utils/auth/get-jwt-token";
45
import { AccessToken, IdToken } from "@src/utils/auth/types";
6+
import { AppConfig } from "@src/utils/config";
7+
import { appConfigBuilder } from "@test-data/config/builders";
58

69
jest.mock("@src/utils/auth/get-jwt-token", () => ({
710
getJwtToken: jest.fn(),
@@ -12,25 +15,59 @@ jest.mock("@src/utils/auth/apim/fetch-apim-access-token", () => ({
1215
}));
1316
jest.mock("sanitize-data", () => ({ sanitize: jest.fn() }));
1417

18+
jest.mock("@src/utils/auth/callbacks/get-token", () => ({
19+
_getOrRefreshApimCredentials: jest.fn(),
20+
}));
21+
22+
const mockConfig: AppConfig = appConfigBuilder().build();
23+
const nowInSeconds = 1000;
24+
25+
const apimAccessTokenFromJwt = "test-access-token" as AccessToken;
26+
const mockJwtToken = {
27+
apim: {
28+
access_token: apimAccessTokenFromJwt,
29+
expires_in: "600000",
30+
refresh_token_expires_at: "700000",
31+
},
32+
};
33+
1534
describe("getApimAccessToken", () => {
16-
it("should use access token from JWT token when APIM access token populated", async () => {
17-
(getJwtToken as jest.Mock).mockResolvedValue({
18-
apim: {
19-
access_token: "test-access-token" as AccessToken,
20-
expires_in: "600000",
21-
refresh_token_expires_at: "700000",
22-
},
35+
beforeAll(() => {
36+
jest.useFakeTimers();
37+
jest.setSystemTime(nowInSeconds * 1000);
38+
});
39+
40+
afterAll(() => {
41+
jest.useRealTimers();
42+
});
43+
44+
it("should pass JWT token to getOrRefresh method and return what it returns", async () => {
45+
const getOrRefreshedApimAccessToken = "apim-access-token";
46+
(_getOrRefreshApimCredentials as jest.Mock).mockResolvedValue({
47+
accessToken: getOrRefreshedApimAccessToken,
48+
expiresAt: nowInSeconds + 1111,
2349
});
2450

25-
const apimAccessToken = await getApimAccessToken();
51+
(getJwtToken as jest.Mock).mockResolvedValue(mockJwtToken);
52+
53+
const apimAccessToken = await getApimAccessToken(mockConfig);
2654

27-
expect(apimAccessToken).toEqual("test-access-token" as AccessToken);
55+
expect(_getOrRefreshApimCredentials).toHaveBeenCalledWith(mockConfig, mockJwtToken, nowInSeconds);
56+
expect(apimAccessToken).toEqual(getOrRefreshedApimAccessToken as AccessToken);
2857
});
2958

3059
it("should throw error if APIM access token not available in JWT token", async () => {
3160
(getJwtToken as jest.Mock).mockResolvedValue({ apim: {} });
3261

33-
await expect(getApimAccessToken()).rejects.toThrow("APIM access token is not present on JWT token");
62+
await expect(getApimAccessToken(mockConfig)).rejects.toThrow("APIM access token is not present on JWT token");
63+
});
64+
65+
it("should throw error if _getOrRefresh APIM access token returns undefined", async () => {
66+
(_getOrRefreshApimCredentials as jest.Mock).mockResolvedValue(undefined);
67+
68+
(getJwtToken as jest.Mock).mockResolvedValue(mockJwtToken);
69+
70+
await expect(getApimAccessToken(mockConfig)).rejects.toThrow("getOrRefreshApimCredentials returned undefined");
3471
});
3572
});
3673

src/utils/auth/apim/get-apim-access-token.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { ApimMissingTokenError } from "@src/utils/auth/apim/exceptions";
22
import { fetchAPIMAccessToken } from "@src/utils/auth/apim/fetch-apim-access-token";
33
import { ApimAccessCredentials, ApimTokenResponse } from "@src/utils/auth/apim/types";
4+
import { _getOrRefreshApimCredentials } from "@src/utils/auth/callbacks/get-token";
45
import { getJwtToken } from "@src/utils/auth/get-jwt-token";
56
import { AccessToken, ExpiresAt, IdToken } from "@src/utils/auth/types";
7+
import { AppConfig } from "@src/utils/config";
68
import { logger } from "@src/utils/logger";
79

810
const log = logger.child({ module: "utils-auth-apim-get-apim-access-token" });
911

10-
const getApimAccessToken = async (): Promise<AccessToken> => {
12+
const getApimAccessToken = async (config: AppConfig): Promise<AccessToken> => {
1113
/**
1214
* Gets the APIM access token from the JWT session cookie.
1315
*
@@ -22,8 +24,19 @@ const getApimAccessToken = async (): Promise<AccessToken> => {
2224
log.error({ context: { token } }, "APIM access token is not present on JWT token");
2325
throw new ApimMissingTokenError("APIM access token is not present on JWT token");
2426
}
25-
log.info(`Preparing to fetch-eligibility-content: APIM expires at ${token.apim.expires_at}`);
26-
return token.apim.access_token;
27+
28+
// Extract APIM token from token. Fetch a new one if it's about to expire
29+
const nowInSeconds = Math.floor(Date.now() / 1000);
30+
const apimAccessCredentials = await _getOrRefreshApimCredentials(config, token, nowInSeconds);
31+
32+
if (!apimAccessCredentials) {
33+
// TODO: consider error type, message and re-word if ness
34+
log.error("Error: getOrRefreshApimCredentials returned undefined");
35+
throw new Error("getOrRefreshApimCredentials returned undefined");
36+
} else {
37+
log.info(`Preparing to fetch-eligibility-content: APIM expires at ${apimAccessCredentials.expiresAt}`);
38+
return apimAccessCredentials.accessToken;
39+
}
2740
};
2841

2942
const retrieveApimCredentials = async (idToken: IdToken): Promise<ApimAccessCredentials> => {

src/utils/auth/callbacks/get-token.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ const getToken = async (
6868
};
6969

7070
async function _getOrRefreshApimCredentials(config: AppConfig, token: JWT, nowInSeconds: number) {
71-
let updatedApimCredentals: ApimAccessCredentials | undefined;
71+
// Check the APIM creds on the token; if valid return them, else if empty or expiring soon fetch new creds
72+
let apimCredentials: ApimAccessCredentials | undefined;
7273

7374
const cryproAvailable = process.env.NEXT_RUNTIME === "nodejs";
7475
if (config.IS_APIM_AUTH_ENABLED && cryproAvailable) {
@@ -79,9 +80,12 @@ async function _getOrRefreshApimCredentials(config: AppConfig, token: JWT, nowIn
7980
{ context: { existingApimCredentals: token.apim } },
8081
"getOrRefreshApimCredentials: Getting new APIM creds.",
8182
);
82-
updatedApimCredentals = await retrieveApimCredentials(token.nhs_login.id_token);
83-
log.info(`First APIM token fetched. expiry time: ${updatedApimCredentals.expiresAt}`);
84-
log.debug({ context: { updatedApimCredentals } }, "getOrRefreshApimCredentials: New APIM creds retrieved.");
83+
apimCredentials = await retrieveApimCredentials(token.nhs_login.id_token);
84+
log.info(`First APIM token fetched. expiry time: ${apimCredentials.expiresAt}`);
85+
log.debug(
86+
{ context: { updatedApimCredentals: apimCredentials } },
87+
"getOrRefreshApimCredentials: New APIM creds retrieved.",
88+
);
8589
} else {
8690
const expiryWriggleRoom = 30;
8791
const expiresSoonAt: ExpiresSoonAt = (token.apim?.expires_at - expiryWriggleRoom) as ExpiresSoonAt;
@@ -93,22 +97,26 @@ async function _getOrRefreshApimCredentials(config: AppConfig, token: JWT, nowIn
9397
);
9498

9599
log.info(`APIM token expires soon ${token.apim.expires_at} ; fetching new token`);
96-
updatedApimCredentals = await retrieveApimCredentials(token.nhs_login.id_token);
100+
apimCredentials = await retrieveApimCredentials(token.nhs_login.id_token);
97101
log.debug(
98-
{ context: { updatedApimCredentals } },
102+
{ context: { updatedApimCredentals: apimCredentials } },
99103
"getOrRefreshApimCredentials: Refreshed APIM creds retrieved.",
100104
);
101105

102-
log.info(`New APIM token fetched. expiry time: ${updatedApimCredentals.expiresAt}`);
106+
log.info(`New APIM token fetched. expiry time: ${apimCredentials.expiresAt}`);
103107
} else {
104108
log.debug(
105109
{ context: { existingApimCredentals: token.apim, timeRemaining: expiresSoonAt - nowInSeconds } },
106110
"getOrRefreshApimCredentials: APIM creds still fresh.",
107111
);
112+
apimCredentials = {
113+
accessToken: token.apim.access_token,
114+
expiresAt: token.apim.expires_at,
115+
};
108116
}
109117
}
110118
}
111-
return updatedApimCredentals;
119+
return apimCredentials;
112120
}
113121

114122
const fillMissingFieldsInTokenWithDefaultValues = (token: JWT, apimAccessCredentials?: ApimAccessCredentials): JWT => {
@@ -152,4 +160,4 @@ const updateTokenWithValuesFromAccountAndProfile = (
152160
return updatedToken;
153161
};
154162

155-
export { getToken };
163+
export { getToken, _getOrRefreshApimCredentials };

0 commit comments

Comments
 (0)