@@ -13,18 +13,22 @@ enum FusionAuthStatusCode {
13
13
}
14
14
15
15
export class AuthenticationSession {
16
- public authToken = '' ;
16
+ private static readonly sftpFusionAuthAppId = process . env . FUSION_AUTH_SFTP_APP_ID ?? '' ;
17
+
18
+ private static readonly sftpFusionAuthClientId = process . env . FUSION_AUTH_SFTP_CLIENT_ID ?? '' ;
19
+
20
+ private static readonly sftpFusionAuthClientSecret = process . env . FUSION_AUTH_SFTP_CLIENT_SECRET ?? '' ;
21
+
22
+ private authToken = '' ;
17
23
18
24
public refreshToken = '' ;
19
25
20
26
public readonly authContext ;
21
27
22
- private authTokenExpiresAt = 0 ;
28
+ private authTokenExpiresAt = new Date ( ) ;
23
29
24
30
private readonly fusionAuthClient ;
25
31
26
- private readonly fusionAuthAppId = process . env . FUSION_AUTH_APP_ID ?? '' ;
27
-
28
32
private twoFactorId = '' ;
29
33
30
34
private twoFactorMethods : TwoFactorMethod [ ] = [ ] ;
@@ -38,30 +42,68 @@ export class AuthenticationSession {
38
42
this . promptForPassword ( ) ;
39
43
}
40
44
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
- } ) ;
45
+ public async getToken ( ) {
46
+ if ( this . tokenWouldExpireSoon ( ) ) {
47
+ await this . getAuthTokenUsingRefreshToken ( ) ;
48
+ }
49
+ return this . authToken ;
53
50
}
54
51
55
- public tokenExpired ( ) : boolean {
56
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
57
- return expirationDate <= new Date ( ) ;
52
+ private getAuthTokenUsingRefreshToken ( ) : Promise < void > {
53
+ return new Promise < void > ( ( resolve , reject ) => {
54
+ if ( ! AuthenticationSession . sftpFusionAuthClientId ) {
55
+ logger . error (
56
+ 'Cannot obtain a new access token without the sftp client ID.' ,
57
+ ) ;
58
+ reject ( Error ( 'Missing sftp client ID' ) ) ;
59
+ return ;
60
+ }
61
+
62
+ if ( ! AuthenticationSession . sftpFusionAuthClientSecret ) {
63
+ logger . error (
64
+ 'Cannot obtain a new access token without the sftp client secret.' ,
65
+ ) ;
66
+ reject ( Error ( 'Missing sftp client secret' ) ) ;
67
+ return ;
68
+ }
69
+
70
+ this . fusionAuthClient
71
+ . exchangeRefreshTokenForAccessToken (
72
+ this . refreshToken ,
73
+ AuthenticationSession . sftpFusionAuthClientId ,
74
+ AuthenticationSession . sftpFusionAuthClientSecret ,
75
+ '' ,
76
+ '' ,
77
+ )
78
+ . then ( ( clientResponse ) => {
79
+ this . authToken = clientResponse . response . access_token ?? '' ;
80
+ // The exchange refresh token for access token endpoint does not return a timestamp,
81
+ // it returns expires_in in seconds.
82
+ // So we need to create the timestamp to be consistent with what is first
83
+ // returned upon initial authentication
84
+ this . authTokenExpiresAt = new Date (
85
+ Date . now ( ) + ( clientResponse . response . expires_in ?? 1 * 1000 ) ,
86
+ ) ;
87
+ logger . info ( 'New access token obtained' ) ;
88
+ resolve ( ) ;
89
+ } )
90
+ . catch ( ( clientResponse : unknown ) => {
91
+ const message = isPartialClientResponse ( clientResponse )
92
+ ? clientResponse . exception . error_description
93
+ : '' ;
94
+ logger . warn ( `Error obtaining refresh token: ${ message } ` ) ;
95
+ this . authContext . reject ( ) ;
96
+ reject ( new Error ( message ) ) ;
97
+ } ) ;
98
+ } ) ;
58
99
}
59
100
60
- public tokenWouldExpireSoon ( minutes = 5 ) : boolean {
61
- const expirationDate = new Date ( this . authTokenExpiresAt ) ;
101
+ private tokenWouldExpireSoon ( seconds = 300 ) : boolean {
62
102
const currentTime = new Date ( ) ;
63
- const timeDifferenceMinutes = ( expirationDate . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 ) ;
64
- return timeDifferenceMinutes <= minutes ;
103
+ const timeDifferenceSeconds = (
104
+ ( this . authTokenExpiresAt . getTime ( ) - currentTime . getTime ( ) ) / ( 1000 * 60 * 60 )
105
+ ) ;
106
+ return timeDifferenceSeconds <= seconds ;
65
107
}
66
108
67
109
private promptForPassword ( ) : void {
@@ -77,44 +119,59 @@ export class AuthenticationSession {
77
119
}
78
120
79
121
private processPasswordResponse ( [ password ] : string [ ] ) : void {
122
+ if ( ! AuthenticationSession . sftpFusionAuthAppId ) {
123
+ logger . error ( 'SFTP application id missing. No refresh token would be returned' ) ;
124
+ }
80
125
this . fusionAuthClient . login ( {
81
- applicationId : this . fusionAuthAppId ,
126
+ applicationId : AuthenticationSession . sftpFusionAuthAppId ,
82
127
loginId : this . authContext . username ,
83
128
password,
84
129
} ) . then ( ( clientResponse ) => {
85
130
switch ( clientResponse . statusCode ) {
86
- case FusionAuthStatusCode . Success :
87
- case FusionAuthStatusCode . SuccessButUnregisteredInApp :
131
+ case FusionAuthStatusCode . Success : {
88
132
if ( clientResponse . response . token !== undefined ) {
89
133
logger . verbose ( 'Successful password authentication attempt.' , {
90
134
username : this . authContext . username ,
91
135
} ) ;
92
136
this . authToken = clientResponse . response . token ;
93
- this . authTokenExpiresAt = clientResponse . response . tokenExpirationInstant ?? 0 ;
137
+ this . authTokenExpiresAt = new Date ( clientResponse . response . tokenExpirationInstant ?? 0 ) ;
94
138
this . refreshToken = clientResponse . response . refreshToken ?? '' ;
95
139
this . authContext . accept ( ) ;
96
- return ;
140
+ } else {
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
+ }
118
175
}
119
176
} ) . catch ( ( clientResponse : unknown ) => {
120
177
const message = isPartialClientResponse ( clientResponse )
@@ -125,6 +182,29 @@ export class AuthenticationSession {
125
182
} ) ;
126
183
}
127
184
185
+ private async registerUserInApp ( userId : string ) : Promise < void > {
186
+ return this . fusionAuthClient . register ( userId , {
187
+ registration : {
188
+ applicationId : AuthenticationSession . sftpFusionAuthAppId ,
189
+ } ,
190
+ } ) . then ( ( clientResponse ) => {
191
+ switch ( clientResponse . statusCode ) {
192
+ case FusionAuthStatusCode . Success :
193
+ logger . verbose ( 'User registered successfully after authentication.' , {
194
+ userId,
195
+ } ) ;
196
+ break ;
197
+ default :
198
+ logger . verbose ( 'User registration after authentication failed.' , {
199
+ userId,
200
+ response : clientResponse . response ,
201
+ } ) ;
202
+ }
203
+ } ) . catch ( ( error ) => {
204
+ logger . warn ( 'Error during user registration after authentication:' , error ) ;
205
+ } ) ;
206
+ }
207
+
128
208
private promptForTwoFactorMethod ( ) : void {
129
209
const promptOptions = this . twoFactorMethods . map (
130
210
( method , index ) => `[${ index + 1 } ] ${ method . method ?? '' } ` ,
0 commit comments