Skip to content

Commit 645d0f4

Browse files
VIA-172 AJ/AS Handled fixed expiry of session and some other corner cases
1 parent 540df2b commit 645d0f4

File tree

2 files changed

+63
-66
lines changed

2 files changed

+63
-66
lines changed

auth.ts

Lines changed: 59 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import NextAuth, { type DefaultSession } from "next-auth";
77
import "next-auth/jwt";
88
import { jwtDecode } from "jwt-decode";
99
import { Logger } from "pino";
10-
import pemToCryptoKey from "@src/utils/auth/pem-to-crypto-key";
11-
import { JWT } from "@auth/core/jwt";
1210
import { generateClientAssertion } from "@src/utils/auth/generate-refresh-client-assertion";
1311

1412
export interface DecodedToken {
@@ -21,10 +19,10 @@ export interface DecodedToken {
2119
declare module "next-auth" {
2220
interface Session {
2321
user: {
24-
nhs_number: string | null,
25-
birthdate: string | null,
26-
access_token?: string,
27-
} & DefaultSession["user"]
22+
nhs_number: string,
23+
birthdate: string,
24+
access_token: string,
25+
} & DefaultSession["user"],
2826
}
2927

3028
interface Profile {
@@ -35,17 +33,20 @@ declare module "next-auth" {
3533
declare module "next-auth/jwt" {
3634
interface JWT {
3735
user: {
38-
nhs_number: string | null,
39-
birthdate: string | null,
36+
nhs_number: string,
37+
birthdate: string,
4038
},
4139
expires_at: number,
42-
refresh_token?: string,
43-
access_token?: string,
40+
refresh_token: string,
41+
access_token: string,
42+
fixedExpiry: number;
4443
}
4544
}
4645

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

48+
const MAX_SESSION_AGE_SECONDS: number = 12 * 60 * 60; // 12 hours of continuous usage
49+
const DEFAULT_ACCESS_TOKEN_EXPIRY: number = 5 * 60; // 5 minutes
4950

5051
export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
5152
const config: AppConfig = await configProvider();
@@ -61,7 +62,7 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
6162
},
6263
session: {
6364
strategy: "jwt",
64-
maxAge: 12 * 60 * 60,
65+
maxAge: MAX_SESSION_AGE_SECONDS, // 12 hours of continuous usage
6566
},
6667
trustHost: true,
6768
callbacks: {
@@ -85,52 +86,61 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
8586
return isValidToken;
8687
},
8788
async jwt({ token, account, profile}) {
88-
let updatedToken: JWT = {
89+
if (!token) {
90+
log.error("No token available in jwt callback.");
91+
return null;
92+
}
93+
94+
let updatedToken = {
8995
...token,
9096
user: {
9197
nhs_number: token.user?.nhs_number ?? "",
92-
birthdate: token.user?.birthdate ?? null,
98+
birthdate: token.user?.birthdate ?? "",
9399
},
94100
expires_at: token.expires_at ?? 0,
95101
access_token: token.access_token ?? "",
96-
refresh_token: token.refresh_token
102+
refresh_token: token.refresh_token ?? ""
97103
};
98104

99-
// Initial login - account and profile are only defined for the initial login, afterward they become undefined
100-
if (account && profile) {
101-
updatedToken = {
102-
...updatedToken,
103-
expires_at: account.expires_at ?? updatedToken.expires_at,
104-
access_token: account.access_token ?? updatedToken.access_token,
105-
refresh_token: account.refresh_token ?? updatedToken.refresh_token,
106-
user: {
107-
nhs_number: profile.nhs_number ?? updatedToken.user.nhs_number,
108-
birthdate: profile.birthdate ?? updatedToken.user.birthdate,
109-
},
110-
};
111-
}
105+
try {
106+
const nowInSeconds = Math.floor(Date.now() / 1000);
112107

113-
// Access Token missing or expired
114-
if (!updatedToken.expires_at || Date.now() >= updatedToken.expires_at * 1000) {
115-
logger.warn(`Token expired or expires_at missing. Attempting to refresh. Current refresh_token: ${updatedToken.refresh_token ? 'present' : 'missing'}`);
108+
// Maximum age reached scenario:
109+
// Invalidate session after fixedExpiry
110+
if (updatedToken.fixedExpiry && nowInSeconds >= updatedToken.fixedExpiry) {
111+
logger.info("Session has reached the max age");
112+
return null;
113+
}
116114

117-
if(!updatedToken.refresh_token) {
118-
logger.error("No refresh token available to new access token. User will be logged out.");
119-
return {
115+
// Initial login scenario:
116+
// account and profile are only defined for the initial login,
117+
// afterward they become undefined
118+
if (account && profile) {
119+
updatedToken = {
120120
...updatedToken,
121-
expires_at: 0,
122-
access_token: "",
123-
refresh_token: undefined,
121+
expires_at: account.expires_at ?? 0,
122+
access_token: account.access_token ?? "",
123+
refresh_token: account.refresh_token ?? "",
124124
user: {
125-
nhs_number: null,
126-
birthdate: null,
125+
nhs_number: profile.nhs_number ?? "",
126+
birthdate: profile.birthdate ?? "",
127127
},
128+
fixedExpiry: nowInSeconds + MAX_SESSION_AGE_SECONDS
128129
};
130+
return updatedToken;
129131
}
130132

131-
try {
132-
logger.warn("Attempting to retrieve new access token");
133-
const clientAssertion = await generateClientAssertion(await pemToCryptoKey(config.NHS_LOGIN_PRIVATE_KEY));
133+
// Refresh token scenario:
134+
// Access Token missing or expired
135+
if (!updatedToken.expires_at || nowInSeconds >= updatedToken.expires_at) {
136+
logger.info("Attempting to refresh token");
137+
138+
if (!updatedToken.refresh_token) {
139+
logger.error("Refresh token missing");
140+
return null;
141+
}
142+
143+
const clientAssertion = await generateClientAssertion(config);
134144

135145
const requestBody = {
136146
grant_type: "refresh_token",
@@ -152,46 +162,34 @@ export const { handlers, signIn, signOut, auth } = NextAuth(async () => {
152162

153163
const newTokens = tokensOrError as {
154164
access_token: string;
155-
expires_in: number;
165+
expires_in?: number;
156166
refresh_token?: string;
157167
};
158168

159169
updatedToken = {
160170
...updatedToken,
161171
access_token: newTokens.access_token,
162-
expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
172+
expires_at: nowInSeconds + (newTokens.expires_in ?? DEFAULT_ACCESS_TOKEN_EXPIRY),
163173
refresh_token: newTokens.refresh_token ?? updatedToken.refresh_token,
164-
user: {
165-
nhs_number: updatedToken.user.nhs_number,
166-
birthdate: updatedToken.user.birthdate,
167-
},
168174
};
169175

170-
logger.warn("Token successfully refreshed");
171-
} catch (error) {
172-
logger.error("Error during access_token refresh: ", error);
173-
174-
return {
175-
...updatedToken,
176-
expires_at: 0,
177-
access_token: "",
178-
refresh_token: undefined,
179-
user: {
180-
nhs_number: updatedToken.user.nhs_number ?? "",
181-
birthdate: updatedToken.user.birthdate ?? null,
182-
},
183-
};
176+
return updatedToken;
184177
}
178+
} catch (error) {
179+
log.error(error, "Error in jwt callback");
180+
return null;
185181
}
186182

187183
return updatedToken;
188184
},
185+
189186
async session({ session, token }) {
190187
if(token?.user && session.user) {
191188
session.user.nhs_number = token.user.nhs_number;
192189
session.user.birthdate = token.user.birthdate;
193190
session.user.access_token = token.access_token;
194191
}
192+
195193
return session;
196194
}
197195
},

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { AppConfig, configProvider } from "@src/utils/config";
1+
import { AppConfig } from "@src/utils/config";
2+
import pemToCryptoKey from "@src/utils/auth/pem-to-crypto-key";
23

3-
const generateClientAssertion = async (
4-
privateKey: CryptoKey,
5-
): Promise<string> => {
6-
const config: AppConfig = await configProvider();
4+
const generateClientAssertion = async (config: AppConfig): Promise<string> => {
5+
const privateKey = await pemToCryptoKey(config.NHS_LOGIN_PRIVATE_KEY);
76
const now = Math.floor(Date.now() / 1000);
87
const payload = {
98
iss: config.NHS_LOGIN_CLIENT_ID,

0 commit comments

Comments
 (0)