@@ -13,55 +13,87 @@ 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
+ this . authToken = clientResponse . response . access_token ?? '' ;
71
+ // The exchange refresh token for access token endpoint does not return a timestamp,
72
+ // it returns expires_in in seconds.
73
+ // So we need to create the timestamp to be consistent with what is first
74
+ // returned upon initial authentication
75
+ this . authTokenExpiresAt = new Date (
76
+ Date . now ( ) + ( clientResponse . response . expires_in ?? 1 * 1000 ) ,
77
+ ) ;
78
+ logger . info ( 'New access token obtained' ) ;
79
+ } catch ( error : unknown ) {
80
+ let message : string ;
81
+ if ( isPartialClientResponse ( error ) ) {
82
+ message = error . exception . message ;
83
+ } else {
84
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
85
+ }
86
+ logger . warn ( `Error obtaining refresh token: ${ message } ` ) ;
87
+ this . authContext . reject ( ) ;
88
+ }
58
89
}
59
90
60
- public tokenWouldExpireSoon ( minutes = 5 ) : boolean {
61
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
91
+ private tokenWouldExpireSoon ( seconds = 300 ) : boolean {
62
92
const currentTime = new Date ( ) ;
63
- const timeDifferenceMinutes = ( expirationDate . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 ) ;
64
- return timeDifferenceMinutes <= minutes ;
93
+ const timeDifferenceSeconds = (
94
+ ( this . authTokenExpiresAt . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 * 60 )
95
+ ) ;
96
+ return timeDifferenceSeconds <= seconds ;
65
97
}
66
98
67
99
private promptForPassword ( ) : void {
@@ -78,53 +110,93 @@ export class AuthenticationSession {
78
110
79
111
private processPasswordResponse ( [ password ] : string [ ] ) : void {
80
112
this . fusionAuthClient . login ( {
81
- applicationId : this . fusionAuthAppId ,
113
+ applicationId : this . fusionAuthSftpAppId ,
82
114
loginId : this . authContext . username ,
83
115
password,
84
116
} ) . then ( ( clientResponse ) => {
85
117
switch ( clientResponse . statusCode ) {
86
- case FusionAuthStatusCode . Success :
87
- case FusionAuthStatusCode . SuccessButUnregisteredInApp :
118
+ case FusionAuthStatusCode . Success : {
88
119
if ( clientResponse . response . token !== undefined ) {
89
120
logger . verbose ( 'Successful password authentication attempt.' , {
90
121
username : this . authContext . username ,
91
122
} ) ;
92
123
this . authToken = clientResponse . response . token ;
93
- this . authTokenExpiresAt = clientResponse . response . tokenExpirationInstant ?? 0 ;
124
+ this . authTokenExpiresAt = new Date ( clientResponse . response . tokenExpirationInstant ?? 0 ) ;
94
125
this . refreshToken = clientResponse . response . refreshToken ?? '' ;
95
126
this . authContext . accept ( ) ;
96
- return ;
127
+ } else {
128
+ this . authContext . reject ( ) ;
97
129
}
98
- this . authContext . reject ( ) ;
99
130
return ;
100
- case FusionAuthStatusCode . SuccessNeedsTwoFactorAuth :
131
+ }
132
+ case FusionAuthStatusCode . SuccessButUnregisteredInApp : {
133
+ const userId : string = clientResponse . response . user ?. id ?? '' ;
134
+ this . registerUserInApp ( userId )
135
+ . then ( ( ) => { this . processPasswordResponse ( [ password ] ) ; } )
136
+ . catch ( ( error ) => {
137
+ logger . warn ( 'Error during registration and authentication:' , error ) ;
138
+ this . authContext . reject ( ) ;
139
+ } ) ;
140
+ return ;
141
+ }
142
+ case FusionAuthStatusCode . SuccessNeedsTwoFactorAuth : {
101
143
if ( clientResponse . response . twoFactorId !== undefined ) {
102
144
logger . verbose ( 'Successful password authentication attempt; MFA required.' , {
103
145
username : this . authContext . username ,
104
146
} ) ;
105
147
this . twoFactorId = clientResponse . response . twoFactorId ;
106
148
this . twoFactorMethods = clientResponse . response . methods ?? [ ] ;
107
149
this . promptForTwoFactorMethod ( ) ;
108
- return ;
150
+ } else {
151
+ this . authContext . reject ( ) ;
109
152
}
110
- this . authContext . reject ( ) ;
111
153
return ;
112
- default :
154
+ }
155
+ default : {
113
156
logger . verbose ( 'Failed password authentication attempt.' , {
114
157
username : this . authContext . username ,
115
158
response : clientResponse . response ,
116
159
} ) ;
117
160
this . authContext . reject ( ) ;
161
+ }
162
+ }
163
+ } ) . catch ( ( error ) => {
164
+ let message : string ;
165
+ if ( isPartialClientResponse ( error ) ) {
166
+ message = error . exception . message ;
167
+ } else {
168
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
118
169
}
119
- } ) . catch ( ( clientResponse : unknown ) => {
120
- const message = isPartialClientResponse ( clientResponse )
121
- ? clientResponse . exception . message
122
- : '' ;
123
170
logger . warn ( `Unexpected exception with FusionAuth password login: ${ message } ` ) ;
124
171
this . authContext . reject ( ) ;
125
172
} ) ;
126
173
}
127
174
175
+ private async registerUserInApp ( userId : string ) : Promise < void > {
176
+ try {
177
+ const clientResponse = await this . fusionAuthClient . register ( userId , {
178
+ registration : {
179
+ applicationId : this . fusionAuthSftpAppId ,
180
+ } ,
181
+ } ) ;
182
+
183
+ switch ( clientResponse . statusCode ) {
184
+ case FusionAuthStatusCode . Success :
185
+ logger . verbose ( 'User registered successfully after authentication.' , {
186
+ userId,
187
+ } ) ;
188
+ break ;
189
+ default :
190
+ logger . verbose ( 'User registration after authentication failed.' , {
191
+ userId,
192
+ response : clientResponse . response ,
193
+ } ) ;
194
+ }
195
+ } catch ( error ) {
196
+ logger . warn ( 'Error during user registration after authentication:' , error ) ;
197
+ }
198
+ }
199
+
128
200
private promptForTwoFactorMethod ( ) : void {
129
201
const promptOptions = this . twoFactorMethods . map (
130
202
( method , index ) => `[${ index + 1 } ] ${ method . method ?? '' } ` ,
@@ -205,10 +277,13 @@ export class AuthenticationSession {
205
277
} ) ;
206
278
this . authContext . reject ( ) ;
207
279
}
208
- } ) . catch ( ( clientResponse : unknown ) => {
209
- const message = isPartialClientResponse ( clientResponse )
210
- ? clientResponse . exception . message
211
- : '' ;
280
+ } ) . catch ( ( error ) => {
281
+ let message : string ;
282
+ if ( isPartialClientResponse ( error ) ) {
283
+ message = error . exception . message ;
284
+ } else {
285
+ message = error instanceof Error ? error . message : JSON . stringify ( error ) ;
286
+ }
212
287
logger . warn ( `Unexpected exception with FusionAuth 2FA login: ${ message } ` ) ;
213
288
this . authContext . reject ( ) ;
214
289
} ) ;
0 commit comments