@@ -13,55 +13,92 @@ enum FusionAuthStatusCode {
13
13
}
14
14
15
15
export class AuthenticationSession {
16
- public authToken = '' ;
16
+ private authToken = '' ;
17
17
18
18
public refreshToken = '' ;
19
19
20
20
public readonly authContext ;
21
21
22
- private authTokenExpiresAt = 0 ;
22
+ private authTokenExpiresAt = new Date ( ) ;
23
23
24
24
private readonly fusionAuthClient ;
25
25
26
- private readonly fusionAuthAppId = process . env . FUSION_AUTH_APP_ID ?? '' ;
27
-
28
26
private twoFactorId = '' ;
29
27
30
28
private twoFactorMethods : TwoFactorMethod [ ] = [ ] ;
31
29
32
- public constructor ( authContext : KeyboardAuthContext ) {
30
+ private fusionAuthSftpAppId = '' ;
31
+
32
+ private fusionAuthSftpClientId = '' ;
33
+
34
+ private fusionAuthSftpClientSecret = '' ;
35
+
36
+ public constructor (
37
+ authContext : KeyboardAuthContext ,
38
+ fusionAuthSftpAppId : string ,
39
+ fusionAuthSftpClientId : string ,
40
+ fusionAuthSftpClientSecret : string ,
41
+ ) {
33
42
this . authContext = authContext ;
43
+ this . fusionAuthSftpAppId = fusionAuthSftpAppId ;
44
+ this . fusionAuthSftpClientId = fusionAuthSftpClientId ;
45
+ this . fusionAuthSftpClientSecret = fusionAuthSftpClientSecret ;
34
46
this . fusionAuthClient = getFusionAuthClient ( ) ;
35
47
}
36
48
37
49
public invokeAuthenticationFlow ( ) : void {
38
50
this . promptForPassword ( ) ;
39
51
}
40
52
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
- } ) ;
53
+ public async getToken ( ) {
54
+ if ( this . tokenWouldExpireSoon ( ) ) {
55
+ await this . getAuthTokenUsingRefreshToken ( ) ;
56
+ }
57
+ return this . authToken ;
53
58
}
54
59
55
- public tokenExpired ( ) : boolean {
56
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
57
- return expirationDate <= new Date ( ) ;
60
+ private async getAuthTokenUsingRefreshToken ( ) : Promise < void > {
61
+ try {
62
+ const clientResponse = await this . fusionAuthClient . exchangeRefreshTokenForAccessToken (
63
+ this . refreshToken ,
64
+ this . fusionAuthSftpClientId ,
65
+ this . fusionAuthSftpClientSecret ,
66
+ '' ,
67
+ '' ,
68
+ ) ;
69
+
70
+ if ( clientResponse . response . access_token ) {
71
+ this . authToken = clientResponse . response . access_token ;
72
+ // The exchange refresh token for access token endpoint does not return a timestamp,
73
+ // it returns expires_in in seconds.
74
+ // So we need to create the timestamp to be consistent with what is first
75
+ // returned upon initial authentication
76
+ this . authTokenExpiresAt = new Date (
77
+ Date . now ( ) + ( clientResponse . response . expires_in ?? 1 * 1000 ) ,
78
+ ) ;
79
+ logger . info ( 'New access token obtained :' , clientResponse . response ) ;
80
+ } else {
81
+ logger . warn ( 'No refresh token in response :' , clientResponse . response ) ;
82
+ this . authContext . reject ( ) ;
83
+ }
84
+ } catch ( error : unknown ) {
85
+ let message : string ;
86
+ if ( isPartialClientResponse ( error ) ) {
87
+ message = error . exception . message ;
88
+ } else {
89
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
90
+ }
91
+ logger . warn ( `Error obtaining refresh token: ${ message } ` ) ;
92
+ this . authContext . reject ( ) ;
93
+ }
58
94
}
59
95
60
- public tokenWouldExpireSoon ( minutes = 5 ) : boolean {
61
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
96
+ private tokenWouldExpireSoon ( seconds = 300 ) : boolean {
62
97
const currentTime = new Date ( ) ;
63
- const timeDifferenceMinutes = ( expirationDate . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 ) ;
64
- return timeDifferenceMinutes <= minutes ;
98
+ const timeDifferenceSeconds = (
99
+ ( this . authTokenExpiresAt . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 * 60 )
100
+ ) ;
101
+ return timeDifferenceSeconds <= seconds ;
65
102
}
66
103
67
104
private promptForPassword ( ) : void {
@@ -78,53 +115,101 @@ export class AuthenticationSession {
78
115
79
116
private processPasswordResponse ( [ password ] : string [ ] ) : void {
80
117
this . fusionAuthClient . login ( {
81
- applicationId : this . fusionAuthAppId ,
118
+ applicationId : this . fusionAuthSftpAppId ,
82
119
loginId : this . authContext . username ,
83
120
password,
84
121
} ) . then ( ( clientResponse ) => {
85
122
switch ( clientResponse . statusCode ) {
86
- case FusionAuthStatusCode . Success :
87
- case FusionAuthStatusCode . SuccessButUnregisteredInApp :
123
+ case FusionAuthStatusCode . Success : {
88
124
if ( clientResponse . response . token !== undefined ) {
89
125
logger . verbose ( 'Successful password authentication attempt.' , {
90
126
username : this . authContext . username ,
91
127
} ) ;
92
128
this . authToken = clientResponse . response . token ;
93
- this . authTokenExpiresAt = clientResponse . response . tokenExpirationInstant ?? 0 ;
94
- this . refreshToken = clientResponse . response . refreshToken ?? '' ;
129
+ if ( clientResponse . response . refreshToken ) {
130
+ this . refreshToken = clientResponse . response . refreshToken ;
131
+ this . authTokenExpiresAt = new Date (
132
+ clientResponse . response . tokenExpirationInstant ?? 0 ,
133
+ ) ;
134
+ } else {
135
+ logger . warn ( 'No refresh token in response :' , clientResponse . response ) ;
136
+ this . authContext . reject ( ) ;
137
+ }
95
138
this . authContext . accept ( ) ;
96
- return ;
139
+ } else {
140
+ logger . warn ( 'No auth token in response' , clientResponse . response ) ;
141
+ this . authContext . reject ( ) ;
97
142
}
98
- this . authContext . reject ( ) ;
99
143
return ;
100
- case FusionAuthStatusCode . SuccessNeedsTwoFactorAuth :
144
+ }
145
+ case FusionAuthStatusCode . SuccessButUnregisteredInApp : {
146
+ const userId : string = clientResponse . response . user ?. id ?? '' ;
147
+ this . registerUserInApp ( userId )
148
+ . then ( ( ) => { this . processPasswordResponse ( [ password ] ) ; } )
149
+ . catch ( ( error ) => {
150
+ logger . warn ( 'Error during registration and authentication:' , error ) ;
151
+ this . authContext . reject ( ) ;
152
+ } ) ;
153
+ return ;
154
+ }
155
+ case FusionAuthStatusCode . SuccessNeedsTwoFactorAuth : {
101
156
if ( clientResponse . response . twoFactorId !== undefined ) {
102
157
logger . verbose ( 'Successful password authentication attempt; MFA required.' , {
103
158
username : this . authContext . username ,
104
159
} ) ;
105
160
this . twoFactorId = clientResponse . response . twoFactorId ;
106
161
this . twoFactorMethods = clientResponse . response . methods ?? [ ] ;
107
162
this . promptForTwoFactorMethod ( ) ;
108
- return ;
163
+ } else {
164
+ this . authContext . reject ( ) ;
109
165
}
110
- this . authContext . reject ( ) ;
111
166
return ;
112
- default :
167
+ }
168
+ default : {
113
169
logger . verbose ( 'Failed password authentication attempt.' , {
114
170
username : this . authContext . username ,
115
171
response : clientResponse . response ,
116
172
} ) ;
117
173
this . authContext . reject ( ) ;
174
+ }
175
+ }
176
+ } ) . catch ( ( error ) => {
177
+ let message : string ;
178
+ if ( isPartialClientResponse ( error ) ) {
179
+ message = error . exception . message ;
180
+ } else {
181
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
118
182
}
119
- } ) . catch ( ( clientResponse : unknown ) => {
120
- const message = isPartialClientResponse ( clientResponse )
121
- ? clientResponse . exception . message
122
- : '' ;
123
183
logger . warn ( `Unexpected exception with FusionAuth password login: ${ message } ` ) ;
124
184
this . authContext . reject ( ) ;
125
185
} ) ;
126
186
}
127
187
188
+ private async registerUserInApp ( userId : string ) : Promise < void > {
189
+ try {
190
+ const clientResponse = await this . fusionAuthClient . register ( userId , {
191
+ registration : {
192
+ applicationId : this . fusionAuthSftpAppId ,
193
+ } ,
194
+ } ) ;
195
+
196
+ switch ( clientResponse . statusCode ) {
197
+ case FusionAuthStatusCode . Success :
198
+ logger . verbose ( 'User registered successfully after authentication.' , {
199
+ userId,
200
+ } ) ;
201
+ break ;
202
+ default :
203
+ logger . verbose ( 'User registration after authentication failed.' , {
204
+ userId,
205
+ response : clientResponse . response ,
206
+ } ) ;
207
+ }
208
+ } catch ( error ) {
209
+ logger . warn ( 'Error during user registration after authentication:' , error ) ;
210
+ }
211
+ }
212
+
128
213
private promptForTwoFactorMethod ( ) : void {
129
214
const promptOptions = this . twoFactorMethods . map (
130
215
( method , index ) => `[${ index + 1 } ] ${ method . method ?? '' } ` ,
@@ -205,10 +290,13 @@ export class AuthenticationSession {
205
290
} ) ;
206
291
this . authContext . reject ( ) ;
207
292
}
208
- } ) . catch ( ( clientResponse : unknown ) => {
209
- const message = isPartialClientResponse ( clientResponse )
210
- ? clientResponse . exception . message
211
- : '' ;
293
+ } ) . catch ( ( error ) => {
294
+ let message : string ;
295
+ if ( isPartialClientResponse ( error ) ) {
296
+ message = error . exception . message ;
297
+ } else {
298
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
299
+ }
212
300
logger . warn ( `Unexpected exception with FusionAuth 2FA login: ${ message } ` ) ;
213
301
this . authContext . reject ( ) ;
214
302
} ) ;
0 commit comments