@@ -7,8 +7,6 @@ import NextAuth, { type DefaultSession } from "next-auth";
77import "next-auth/jwt" ;
88import { jwtDecode } from "jwt-decode" ;
99import { Logger } from "pino" ;
10- import pemToCryptoKey from "@src/utils/auth/pem-to-crypto-key" ;
11- import { JWT } from "@auth/core/jwt" ;
1210import { generateClientAssertion } from "@src/utils/auth/generate-refresh-client-assertion" ;
1311
1412export interface DecodedToken {
@@ -21,10 +19,10 @@ export interface DecodedToken {
2119declare 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" {
3533declare 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
4746const 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
5051export 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 } ,
0 commit comments