Skip to content

Commit c11bdd4

Browse files
committed
Register unregistered users & update obtain new token
Signed-off-by: Fon E. Noel NFEBE <fenn25.fn@gmail.com>
1 parent b9ab43c commit c11bdd4

9 files changed

+610
-421
lines changed

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,6 @@ PERMANENT_API_BASE_PATH=${LOCAL_TEMPORARY_AUTH_TOKEN}
4040
# See https://fusionauth.io/docs/v1/tech/apis/api-keys
4141
FUSION_AUTH_HOST=${FUSION_AUTH_HOST}
4242
FUSION_AUTH_KEY=${FUSION_AUTH_KEY}
43-
FUSION_AUTH_APP_ID=${FUSION_AUTH_APP_ID}
43+
FUSION_AUTH_SFTP_APP_ID=${FUSION_AUTH_SFTP_APP_ID}
44+
FUSION_AUTH_SFTP_CLIENT_ID=${FUSION_AUTH_SFTP_CLIENT_ID}
45+
FUSION_AUTH_SFTP_CLIENT_SECRET=${FUSION_AUTH_SFTP_CLIENT_SECRET}

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"dotenv": "^16.3.1",
6262
"logform": "^2.3.2",
6363
"node-fetch": "^2.7.0",
64+
"require-env-variable": "^4.0.1",
6465
"ssh2": "^1.14.0",
6566
"tmp": "^0.2.1",
6667
"uuid": "^9.0.0",

src/classes/AuthenticationSession.ts

Lines changed: 144 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
getFusionAuthClient,
44
isPartialClientResponse,
55
} from '../fusionAuth';
6+
import { AuthTokenRefreshError } from '../errors/AuthTokenRefreshError';
67
import type { KeyboardAuthContext } from 'ssh2';
78
import type { TwoFactorMethod } from '@fusionauth/typescript-client';
89

@@ -13,55 +14,105 @@ enum FusionAuthStatusCode {
1314
}
1415

1516
export class AuthenticationSession {
16-
public authToken = '';
17+
private authToken = '';
1718

1819
public refreshToken = '';
1920

2021
public readonly authContext;
2122

22-
private authTokenExpiresAt = 0;
23+
private authTokenExpiresAt = new Date();
2324

2425
private readonly fusionAuthClient;
2526

26-
private readonly fusionAuthAppId = process.env.FUSION_AUTH_APP_ID ?? '';
27-
2827
private twoFactorId = '';
2928

3029
private twoFactorMethods: TwoFactorMethod[] = [];
3130

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+
) {
3343
this.authContext = authContext;
44+
this.fusionAuthAppId = fusionAuthAppId;
45+
this.fusionAuthClientId = fusionAuthClientId;
46+
this.fusionAuthClientSecret = fusionAuthClientSecret;
3447
this.fusionAuthClient = getFusionAuthClient();
3548
}
3649

3750
public invokeAuthenticationFlow(): void {
3851
this.promptForPassword();
3952
}
4053

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;
5359
}
5460

55-
public tokenExpired(): boolean {
56-
const expirationDate = new Date(this.authTokenExpiresAt);
57-
return expirationDate <= new Date();
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);
58108
}
59109

60-
public tokenWouldExpireSoon(minutes = 5): boolean {
61-
const expirationDate = new Date(this.authTokenExpiresAt);
110+
private tokenWouldExpireSoon(expirationThresholdInSeconds = 300): boolean {
62111
const currentTime = new Date();
63-
const timeDifferenceMinutes = (expirationDate.getTime() - currentTime.getTime()) / (1000 * 60);
64-
return timeDifferenceMinutes <= minutes;
112+
const remainingTokenLife = (
113+
(this.authTokenExpiresAt.getTime() - currentTime.getTime()) / 1000
114+
);
115+
return remainingTokenLife <= expirationThresholdInSeconds;
65116
}
66117

67118
private promptForPassword(): void {
@@ -83,48 +134,96 @@ export class AuthenticationSession {
83134
password,
84135
}).then((clientResponse) => {
85136
switch (clientResponse.statusCode) {
86-
case FusionAuthStatusCode.Success:
87-
case FusionAuthStatusCode.SuccessButUnregisteredInApp:
137+
case FusionAuthStatusCode.Success: {
88138
if (clientResponse.response.token !== undefined) {
89139
logger.verbose('Successful password authentication attempt.', {
90140
username: this.authContext.username,
91141
});
92142
this.authToken = clientResponse.response.token;
93-
this.authTokenExpiresAt = clientResponse.response.tokenExpirationInstant ?? 0;
94-
this.refreshToken = clientResponse.response.refreshToken ?? '';
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+
}
95152
this.authContext.accept();
96-
return;
153+
} else {
154+
logger.warn('No auth token in response', clientResponse.response);
155+
this.authContext.reject();
97156
}
98-
this.authContext.reject();
99157
return;
100-
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: {
101170
if (clientResponse.response.twoFactorId !== undefined) {
102171
logger.verbose('Successful password authentication attempt; MFA required.', {
103172
username: this.authContext.username,
104173
});
105174
this.twoFactorId = clientResponse.response.twoFactorId;
106175
this.twoFactorMethods = clientResponse.response.methods ?? [];
107176
this.promptForTwoFactorMethod();
108-
return;
177+
} else {
178+
this.authContext.reject();
109179
}
110-
this.authContext.reject();
111180
return;
112-
default:
181+
}
182+
default: {
113183
logger.verbose('Failed password authentication attempt.', {
114184
username: this.authContext.username,
115185
response: clientResponse.response,
116186
});
117187
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);
118196
}
119-
}).catch((clientResponse: unknown) => {
120-
const message = isPartialClientResponse(clientResponse)
121-
? clientResponse.exception.message
122-
: '';
123197
logger.warn(`Unexpected exception with FusionAuth password login: ${message}`);
124198
this.authContext.reject();
125199
});
126200
}
127201

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+
128227
private promptForTwoFactorMethod(): void {
129228
const promptOptions = this.twoFactorMethods.map(
130229
(method, index) => `[${index + 1}] ${method.method ?? ''}`,
@@ -205,10 +304,13 @@ export class AuthenticationSession {
205304
});
206305
this.authContext.reject();
207306
}
208-
}).catch((clientResponse: unknown) => {
209-
const message = isPartialClientResponse(clientResponse)
210-
? clientResponse.exception.message
211-
: '';
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+
}
212314
logger.warn(`Unexpected exception with FusionAuth 2FA login: ${message}`);
213315
this.authContext.reject();
214316
});

0 commit comments

Comments
 (0)