Skip to content

Commit 43fe475

Browse files
VIA-172 AS Extract auth signIn callback functionality into a new file
1 parent 17dff95 commit 43fe475

File tree

4 files changed

+114
-21
lines changed

4 files changed

+114
-21
lines changed

auth.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import NHSLoginAuthProvider from "@src/app/api/auth/[...nextauth]/provider";
22
import { SESSION_LOGOUT_ROUTE } from "@src/app/session-logout/constants";
33
import { SSO_FAILURE_ROUTE } from "@src/app/sso-failure/constants";
4-
import type { DecodedToken } from "@src/utils/auth/types";
54
import { AppConfig, configProvider } from "@src/utils/config";
65
import { logger } from "@src/utils/logger";
76
import NextAuth from "next-auth";
87
import "next-auth/jwt";
9-
import { jwtDecode } from "jwt-decode";
108
import { Logger } from "pino";
119
import { generateClientAssertion } from "@src/utils/auth/generate-refresh-client-assertion";
10+
import { isValidSignIn } from "@src/utils/auth/callbacks/isValidSignIn";
1211

1312
const log: Logger = logger.child({ module: "auth" });
1413

@@ -34,23 +33,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
3433
trustHost: true,
3534
callbacks: {
3635
async signIn({ account }) {
37-
if (!account || typeof account.id_token !== "string") {
38-
log.info("Access denied from signIn callback. Account or id_token missing.");
39-
return false;
40-
}
41-
42-
const decodedToken = jwtDecode<DecodedToken>(account.id_token);
43-
const { iss, aud, identity_proofing_level } = decodedToken;
44-
45-
const isValidToken =
46-
iss === config.NHS_LOGIN_URL &&
47-
aud === config.NHS_LOGIN_CLIENT_ID &&
48-
identity_proofing_level === "P9";
49-
50-
if (!isValidToken) {
51-
log.info(`Access denied from signIn callback. iss: ${iss}, aud: ${aud}, identity_proofing_level: ${identity_proofing_level}`);
52-
}
53-
return isValidToken;
36+
return isValidSignIn(config, log, account);
5437
},
5538

5639
async jwt({ token, account, profile}) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { isValidSignIn } from "@src/utils/auth/callbacks/isValidSignIn";
2+
import { AppConfig } from "@src/utils/config";
3+
import { Account } from "next-auth";
4+
import { jwtDecode } from "jwt-decode";
5+
import { Logger } from "pino";
6+
7+
jest.mock("jwt-decode");
8+
9+
describe("isValidSignIn", () => {
10+
const mockConfig = {
11+
NHS_LOGIN_URL: "https://mock.nhs.login",
12+
NHS_LOGIN_CLIENT_ID: "mock-client-id",
13+
} as AppConfig;
14+
15+
const mockLog = {
16+
info: jest.fn(),
17+
} as unknown as Logger;
18+
19+
beforeEach(() => {
20+
jest.clearAllMocks();
21+
});
22+
23+
it("should return false and logs if account is null", () => {
24+
expect(isValidSignIn(mockConfig, mockLog, null)).toBe(false);
25+
expect(mockLog.info).toHaveBeenCalledWith(
26+
"Access denied from signIn callback. Account or id_token missing.",
27+
);
28+
});
29+
30+
it("should return false and logs if account is undefined", () => {
31+
expect(isValidSignIn(mockConfig, mockLog, undefined)).toBe(false);
32+
expect(mockLog.info).toHaveBeenCalledWith(
33+
"Access denied from signIn callback. Account or id_token missing.",
34+
);
35+
});
36+
37+
it("should return false and logs if id_token is not a string", () => {
38+
expect(
39+
isValidSignIn(mockConfig, mockLog, {
40+
id_token: 123,
41+
} as unknown as Account),
42+
).toBe(false);
43+
expect(mockLog.info).toHaveBeenCalledWith(
44+
"Access denied from signIn callback. Account or id_token missing.",
45+
);
46+
});
47+
48+
it("should return true if token is valid", () => {
49+
const mockAccount = { id_token: "valid-token" } as Account;
50+
51+
(jwtDecode as jest.Mock).mockReturnValue({
52+
iss: mockConfig.NHS_LOGIN_URL,
53+
aud: mockConfig.NHS_LOGIN_CLIENT_ID,
54+
identity_proofing_level: "P9",
55+
});
56+
57+
const result = isValidSignIn(mockConfig, mockLog, mockAccount);
58+
expect(result).toBe(true);
59+
expect(mockLog.info).not.toHaveBeenCalled();
60+
});
61+
62+
it("should return false and logs if token is invalid", () => {
63+
const mockAccount = { id_token: "invalid-token" } as Account;
64+
65+
(jwtDecode as jest.Mock).mockReturnValue({
66+
iss: "incorrect-issuer",
67+
aud: "incorrect-audience",
68+
identity_proofing_level: "P0",
69+
});
70+
71+
const result = isValidSignIn(mockConfig, mockLog, mockAccount);
72+
expect(result).toBe(false);
73+
expect(mockLog.info).toHaveBeenCalledWith(
74+
"Access denied from signIn callback. iss: incorrect-issuer, aud: incorrect-audience, identity_proofing_level: P0",
75+
);
76+
});
77+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Account } from "next-auth";
2+
import { AppConfig } from "@src/utils/config";
3+
import { jwtDecode } from "jwt-decode";
4+
import type { DecodedToken } from "@src/utils/auth/types";
5+
import { Logger } from "pino";
6+
7+
const isValidSignIn = (
8+
config: AppConfig,
9+
log: Logger,
10+
account: Account | null | undefined,
11+
) => {
12+
if (!account || typeof account.id_token !== "string") {
13+
log.info(
14+
"Access denied from signIn callback. Account or id_token missing.",
15+
);
16+
return false;
17+
}
18+
19+
const decodedToken = jwtDecode<DecodedToken>(account.id_token);
20+
const { iss, aud, identity_proofing_level } = decodedToken;
21+
22+
const isValidToken =
23+
iss === config.NHS_LOGIN_URL &&
24+
aud === config.NHS_LOGIN_CLIENT_ID &&
25+
identity_proofing_level === "P9";
26+
27+
if (!isValidToken) {
28+
log.info(
29+
`Access denied from signIn callback. iss: ${iss}, aud: ${aud}, identity_proofing_level: ${identity_proofing_level}`,
30+
);
31+
}
32+
return isValidToken;
33+
};
34+
35+
export { isValidSignIn };

src/utils/auth/generate-refresh-client-assertion.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,12 @@ describe("generateClientAssertion", () => {
2828
});
2929

3030
beforeEach(() => {
31-
// Reset mocks before each test
3231
(pemToCryptoKey as jest.Mock).mockReset();
3332
randomUUIDSpy.mockClear();
3433
subtleSignSpy.mockClear();
3534
});
3635

3736
afterAll(() => {
38-
// Restore original implementations after all tests in this describe block
3937
randomUUIDSpy.mockRestore();
4038
subtleSignSpy.mockRestore();
4139
});

0 commit comments

Comments
 (0)