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