3
3
getFusionAuthClient ,
4
4
isPartialClientResponse ,
5
5
} from '../fusionAuth' ;
6
+ import { AuthTokenRefreshError } from '../errors/AuthTokenRefreshError' ;
6
7
import type { KeyboardAuthContext } from 'ssh2' ;
7
8
import type { TwoFactorMethod } from '@fusionauth/typescript-client' ;
8
9
@@ -13,55 +14,105 @@ enum FusionAuthStatusCode {
13
14
}
14
15
15
16
export class AuthenticationSession {
16
- public authToken = '' ;
17
+ private authToken = '' ;
17
18
18
19
public refreshToken = '' ;
19
20
20
21
public readonly authContext ;
21
22
22
- private authTokenExpiresAt = 0 ;
23
+ private authTokenExpiresAt = new Date ( ) ;
23
24
24
25
private readonly fusionAuthClient ;
25
26
26
- private readonly fusionAuthAppId = process . env . FUSION_AUTH_APP_ID ?? '' ;
27
-
28
27
private twoFactorId = '' ;
29
28
30
29
private twoFactorMethods : TwoFactorMethod [ ] = [ ] ;
31
30
32
- public constructor ( authContext : KeyboardAuthContext ) {
31
+ private fusionAuthAppId = '' ;
32
+
33
+ private fusionAuthClientId = '' ;
34
+
35
+ private fusionAuthClientSecret = '' ;
36
+
37
+ public constructor (
38
+ authContext : KeyboardAuthContext ,
39
+ fusionAuthAppId : string ,
40
+ fusionAuthClientId : string ,
41
+ fusionAuthClientSecret : string ,
42
+ ) {
33
43
this . authContext = authContext ;
44
+ this . fusionAuthAppId = fusionAuthAppId ;
45
+ this . fusionAuthClientId = fusionAuthClientId ;
46
+ this . fusionAuthClientSecret = fusionAuthClientSecret ;
34
47
this . fusionAuthClient = getFusionAuthClient ( ) ;
35
48
}
36
49
37
50
public invokeAuthenticationFlow ( ) : void {
38
51
this . promptForPassword ( ) ;
39
52
}
40
53
41
- public obtainNewAuthTokenUsingRefreshToken ( ) : void {
42
- this . fusionAuthClient . exchangeRefreshTokenForAccessToken ( this . refreshToken , '' , '' , '' , '' )
43
- . then ( ( clientResponse ) => {
44
- this . authToken = clientResponse . response . access_token ?? '' ;
45
- } )
46
- . catch ( ( clientResponse : unknown ) => {
47
- const message = isPartialClientResponse ( clientResponse )
48
- ? clientResponse . exception . message
49
- : '' ;
50
- logger . warn ( `Error obtaining refresh token : ${ message } ` ) ;
51
- this . authContext . reject ( ) ;
52
- } ) ;
54
+ public async getAuthToken ( ) {
55
+ if ( this . tokenWouldExpireSoon ( ) ) {
56
+ await this . getAuthTokenUsingRefreshToken ( ) ;
57
+ }
58
+ return this . authToken ;
53
59
}
54
60
55
- public tokenExpired ( ) : boolean {
56
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
57
- return expirationDate <= new Date ( ) ;
61
+ private async getAuthTokenUsingRefreshToken ( ) : Promise < void > {
62
+ let clientResponse ;
63
+ try {
64
+ /**
65
+ * Fusion auth sdk wrongly mandates last two params (scope, user_code)
66
+ * hence the need to pass two empty strings here.
67
+ * See: https://github.com/FusionAuth/fusionauth-typescript-client/issues/42
68
+ */
69
+ clientResponse = await this . fusionAuthClient . exchangeRefreshTokenForAccessToken (
70
+ this . refreshToken ,
71
+ this . fusionAuthClientId ,
72
+ this . fusionAuthClientSecret ,
73
+ '' ,
74
+ '' ,
75
+ ) ;
76
+ } catch ( error : unknown ) {
77
+ let message : string ;
78
+ if ( isPartialClientResponse ( error ) ) {
79
+ message = error . exception . message ;
80
+ } else {
81
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
82
+ }
83
+ logger . verbose ( `Error obtaining refresh token: ${ message } ` ) ;
84
+ throw new AuthTokenRefreshError ( `Error obtaining refresh token: ${ message } ` ) ;
85
+ }
86
+
87
+ if ( ! clientResponse . response . access_token ) {
88
+ logger . warn ( 'No access token in response:' , clientResponse . response ) ;
89
+ throw new AuthTokenRefreshError ( 'Response does not contain access_token' ) ;
90
+ }
91
+
92
+ if ( ! clientResponse . response . expires_in ) {
93
+ logger . warn ( 'Response lacks token TTL (expires_in):' , clientResponse . response ) ;
94
+ throw new AuthTokenRefreshError ( 'Response lacks token TTL (expires_in)' ) ;
95
+ }
96
+
97
+ /**
98
+ * The exchange refresh token for access token endpoint does not return a timestamp,
99
+ * it returns expires_in in seconds.
100
+ * So we need to create the timestamp to be consistent with what is first
101
+ * returned upon initial authentication
102
+ */
103
+ this . authToken = clientResponse . response . access_token ;
104
+ this . authTokenExpiresAt = new Date (
105
+ Date . now ( ) + ( clientResponse . response . expires_in * 1000 ) ,
106
+ ) ;
107
+ logger . debug ( 'New access token obtained:' , clientResponse . response ) ;
58
108
}
59
109
60
- public tokenWouldExpireSoon ( minutes = 5 ) : boolean {
61
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
110
+ private tokenWouldExpireSoon ( expirationThresholdInSeconds = 300 ) : boolean {
62
111
const currentTime = new Date ( ) ;
63
- const timeDifferenceMinutes = ( expirationDate . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 ) ;
64
- return timeDifferenceMinutes <= minutes ;
112
+ const remainingTokenLife = (
113
+ ( this . authTokenExpiresAt . getTime ( ) - currentTime . getTime ( ) ) / 1000
114
+ ) ;
115
+ return remainingTokenLife <= expirationThresholdInSeconds ;
65
116
}
66
117
67
118
private promptForPassword ( ) : void {
@@ -83,48 +134,96 @@ export class AuthenticationSession {
83
134
password,
84
135
} ) . then ( ( clientResponse ) => {
85
136
switch ( clientResponse . statusCode ) {
86
- case FusionAuthStatusCode . Success :
87
- case FusionAuthStatusCode . SuccessButUnregisteredInApp :
137
+ case FusionAuthStatusCode . Success : {
88
138
if ( clientResponse . response . token !== undefined ) {
89
139
logger . verbose ( 'Successful password authentication attempt.' , {
90
140
username : this . authContext . username ,
91
141
} ) ;
92
142
this . authToken = clientResponse . response . token ;
93
- this . authTokenExpiresAt = clientResponse . response . tokenExpirationInstant ?? 0 ;
94
- this . refreshToken = clientResponse . response . refreshToken ?? '' ;
143
+ if ( clientResponse . response . refreshToken ) {
144
+ this . refreshToken = clientResponse . response . refreshToken ;
145
+ this . authTokenExpiresAt = new Date (
146
+ clientResponse . response . tokenExpirationInstant ?? 0 ,
147
+ ) ;
148
+ } else {
149
+ logger . warn ( 'No refresh token in response :' , clientResponse . response ) ;
150
+ this . authContext . reject ( ) ;
151
+ }
95
152
this . authContext . accept ( ) ;
96
- return ;
153
+ } else {
154
+ logger . warn ( 'No auth token in response' , clientResponse . response ) ;
155
+ this . authContext . reject ( ) ;
97
156
}
98
- this . authContext . reject ( ) ;
99
157
return ;
100
- case FusionAuthStatusCode . SuccessNeedsTwoFactorAuth :
158
+ }
159
+ case FusionAuthStatusCode . SuccessButUnregisteredInApp : {
160
+ const userId : string = clientResponse . response . user ?. id ?? '' ;
161
+ this . registerUserInApp ( userId )
162
+ . then ( ( ) => { this . processPasswordResponse ( [ password ] ) ; } )
163
+ . catch ( ( error ) => {
164
+ logger . warn ( 'Error during registration and authentication:' , error ) ;
165
+ this . authContext . reject ( ) ;
166
+ } ) ;
167
+ return ;
168
+ }
169
+ case FusionAuthStatusCode . SuccessNeedsTwoFactorAuth : {
101
170
if ( clientResponse . response . twoFactorId !== undefined ) {
102
171
logger . verbose ( 'Successful password authentication attempt; MFA required.' , {
103
172
username : this . authContext . username ,
104
173
} ) ;
105
174
this . twoFactorId = clientResponse . response . twoFactorId ;
106
175
this . twoFactorMethods = clientResponse . response . methods ?? [ ] ;
107
176
this . promptForTwoFactorMethod ( ) ;
108
- return ;
177
+ } else {
178
+ this . authContext . reject ( ) ;
109
179
}
110
- this . authContext . reject ( ) ;
111
180
return ;
112
- default :
181
+ }
182
+ default : {
113
183
logger . verbose ( 'Failed password authentication attempt.' , {
114
184
username : this . authContext . username ,
115
185
response : clientResponse . response ,
116
186
} ) ;
117
187
this . authContext . reject ( ) ;
188
+ }
189
+ }
190
+ } ) . catch ( ( error ) => {
191
+ let message : string ;
192
+ if ( isPartialClientResponse ( error ) ) {
193
+ message = error . exception . message ;
194
+ } else {
195
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
118
196
}
119
- } ) . catch ( ( clientResponse : unknown ) => {
120
- const message = isPartialClientResponse ( clientResponse )
121
- ? clientResponse . exception . message
122
- : '' ;
123
197
logger . warn ( `Unexpected exception with FusionAuth password login: ${ message } ` ) ;
124
198
this . authContext . reject ( ) ;
125
199
} ) ;
126
200
}
127
201
202
+ private async registerUserInApp ( userId : string ) : Promise < void > {
203
+ try {
204
+ const clientResponse = await this . fusionAuthClient . register ( userId , {
205
+ registration : {
206
+ applicationId : this . fusionAuthAppId ,
207
+ } ,
208
+ } ) ;
209
+
210
+ switch ( clientResponse . statusCode ) {
211
+ case FusionAuthStatusCode . Success :
212
+ logger . verbose ( 'User registered successfully after authentication.' , {
213
+ userId,
214
+ } ) ;
215
+ break ;
216
+ default :
217
+ logger . verbose ( 'User registration after authentication failed.' , {
218
+ userId,
219
+ response : clientResponse . response ,
220
+ } ) ;
221
+ }
222
+ } catch ( error ) {
223
+ logger . warn ( 'Error during user registration after authentication:' , error ) ;
224
+ }
225
+ }
226
+
128
227
private promptForTwoFactorMethod ( ) : void {
129
228
const promptOptions = this . twoFactorMethods . map (
130
229
( method , index ) => `[${ index + 1 } ] ${ method . method ?? '' } ` ,
@@ -205,10 +304,13 @@ export class AuthenticationSession {
205
304
} ) ;
206
305
this . authContext . reject ( ) ;
207
306
}
208
- } ) . catch ( ( clientResponse : unknown ) => {
209
- const message = isPartialClientResponse ( clientResponse )
210
- ? clientResponse . exception . message
211
- : '' ;
307
+ } ) . catch ( ( error ) => {
308
+ let message : string ;
309
+ if ( isPartialClientResponse ( error ) ) {
310
+ message = error . exception . message ;
311
+ } else {
312
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
313
+ }
212
314
logger . warn ( `Unexpected exception with FusionAuth 2FA login: ${ message } ` ) ;
213
315
this . authContext . reject ( ) ;
214
316
} ) ;
0 commit comments