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,25 +14,107 @@ enum FusionAuthStatusCode {
13
14
}
14
15
15
16
export class AuthenticationSession {
16
- public authToken = '' ;
17
+ private authToken = '' ;
18
+
19
+ public refreshToken = '' ;
17
20
18
21
public readonly authContext ;
19
22
23
+ private authTokenExpiresAt = new Date ( ) ;
24
+
20
25
private readonly fusionAuthClient ;
21
26
22
27
private twoFactorId = '' ;
23
28
24
29
private twoFactorMethods : TwoFactorMethod [ ] = [ ] ;
25
30
26
- 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
+ ) {
27
43
this . authContext = authContext ;
44
+ this . fusionAuthAppId = fusionAuthAppId ;
45
+ this . fusionAuthClientId = fusionAuthClientId ;
46
+ this . fusionAuthClientSecret = fusionAuthClientSecret ;
28
47
this . fusionAuthClient = getFusionAuthClient ( ) ;
29
48
}
30
49
31
50
public invokeAuthenticationFlow ( ) : void {
32
51
this . promptForPassword ( ) ;
33
52
}
34
53
54
+ public async getAuthToken ( ) {
55
+ if ( this . tokenWouldExpireSoon ( ) ) {
56
+ await this . getAuthTokenUsingRefreshToken ( ) ;
57
+ }
58
+ return this . authToken ;
59
+ }
60
+
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 ) ;
108
+ }
109
+
110
+ private tokenWouldExpireSoon ( expirationThresholdInSeconds = 300 ) : boolean {
111
+ const currentTime = new Date ( ) ;
112
+ const remainingTokenLife = (
113
+ ( this . authTokenExpiresAt . getTime ( ) - currentTime . getTime ( ) ) / 1000
114
+ ) ;
115
+ return remainingTokenLife <= expirationThresholdInSeconds ;
116
+ }
117
+
35
118
private promptForPassword ( ) : void {
36
119
this . authContext . prompt (
37
120
{
@@ -46,50 +129,101 @@ export class AuthenticationSession {
46
129
47
130
private processPasswordResponse ( [ password ] : string [ ] ) : void {
48
131
this . fusionAuthClient . login ( {
132
+ applicationId : this . fusionAuthAppId ,
49
133
loginId : this . authContext . username ,
50
134
password,
51
135
} ) . then ( ( clientResponse ) => {
52
136
switch ( clientResponse . statusCode ) {
53
- case FusionAuthStatusCode . Success :
54
- case FusionAuthStatusCode . SuccessButUnregisteredInApp :
137
+ case FusionAuthStatusCode . Success : {
55
138
if ( clientResponse . response . token !== undefined ) {
56
139
logger . verbose ( 'Successful password authentication attempt.' , {
57
140
username : this . authContext . username ,
58
141
} ) ;
59
142
this . authToken = clientResponse . response . token ;
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
+ }
60
152
this . authContext . accept ( ) ;
61
- return ;
153
+ } else {
154
+ logger . warn ( 'No auth token in response' , clientResponse . response ) ;
155
+ this . authContext . reject ( ) ;
62
156
}
63
- this . authContext . reject ( ) ;
64
157
return ;
65
- 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 : {
66
170
if ( clientResponse . response . twoFactorId !== undefined ) {
67
171
logger . verbose ( 'Successful password authentication attempt; MFA required.' , {
68
172
username : this . authContext . username ,
69
173
} ) ;
70
174
this . twoFactorId = clientResponse . response . twoFactorId ;
71
175
this . twoFactorMethods = clientResponse . response . methods ?? [ ] ;
72
176
this . promptForTwoFactorMethod ( ) ;
73
- return ;
177
+ } else {
178
+ this . authContext . reject ( ) ;
74
179
}
75
- this . authContext . reject ( ) ;
76
180
return ;
77
- default :
181
+ }
182
+ default : {
78
183
logger . verbose ( 'Failed password authentication attempt.' , {
79
184
username : this . authContext . username ,
80
185
response : clientResponse . response ,
81
186
} ) ;
82
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 ) ;
83
196
}
84
- } ) . catch ( ( clientResponse : unknown ) => {
85
- const message = isPartialClientResponse ( clientResponse )
86
- ? clientResponse . exception . message
87
- : '' ;
88
197
logger . warn ( `Unexpected exception with FusionAuth password login: ${ message } ` ) ;
89
198
this . authContext . reject ( ) ;
90
199
} ) ;
91
200
}
92
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
+
93
227
private promptForTwoFactorMethod ( ) : void {
94
228
const promptOptions = this . twoFactorMethods . map (
95
229
( method , index ) => `[${ index + 1 } ] ${ method . method ?? '' } ` ,
@@ -170,10 +304,13 @@ export class AuthenticationSession {
170
304
} ) ;
171
305
this . authContext . reject ( ) ;
172
306
}
173
- } ) . catch ( ( clientResponse : unknown ) => {
174
- const message = isPartialClientResponse ( clientResponse )
175
- ? clientResponse . exception . message
176
- : '' ;
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
+ }
177
314
logger . warn ( `Unexpected exception with FusionAuth 2FA login: ${ message } ` ) ;
178
315
this . authContext . reject ( ) ;
179
316
} ) ;
0 commit comments