Skip to content

Commit 1c6f999

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 1c6f999

9 files changed

+609
-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: 143 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,104 @@ 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+
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+
}
58107
}
59108

60-
public tokenWouldExpireSoon(minutes = 5): boolean {
61-
const expirationDate = new Date(this.authTokenExpiresAt);
109+
private tokenWouldExpireSoon(expirationThresholdInSeconds = 300): boolean {
62110
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;
65115
}
66116

67117
private promptForPassword(): void {
@@ -83,48 +133,96 @@ export class AuthenticationSession {
83133
password,
84134
}).then((clientResponse) => {
85135
switch (clientResponse.statusCode) {
86-
case FusionAuthStatusCode.Success:
87-
case FusionAuthStatusCode.SuccessButUnregisteredInApp:
136+
case FusionAuthStatusCode.Success: {
88137
if (clientResponse.response.token !== undefined) {
89138
logger.verbose('Successful password authentication attempt.', {
90139
username: this.authContext.username,
91140
});
92141
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+
}
95151
this.authContext.accept();
96-
return;
152+
} else {
153+
logger.warn('No auth token in response', clientResponse.response);
154+
this.authContext.reject();
97155
}
98-
this.authContext.reject();
99156
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: {
101169
if (clientResponse.response.twoFactorId !== undefined) {
102170
logger.verbose('Successful password authentication attempt; MFA required.', {
103171
username: this.authContext.username,
104172
});
105173
this.twoFactorId = clientResponse.response.twoFactorId;
106174
this.twoFactorMethods = clientResponse.response.methods ?? [];
107175
this.promptForTwoFactorMethod();
108-
return;
176+
} else {
177+
this.authContext.reject();
109178
}
110-
this.authContext.reject();
111179
return;
112-
default:
180+
}
181+
default: {
113182
logger.verbose('Failed password authentication attempt.', {
114183
username: this.authContext.username,
115184
response: clientResponse.response,
116185
});
117186
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);
118195
}
119-
}).catch((clientResponse: unknown) => {
120-
const message = isPartialClientResponse(clientResponse)
121-
? clientResponse.exception.message
122-
: '';
123196
logger.warn(`Unexpected exception with FusionAuth password login: ${message}`);
124197
this.authContext.reject();
125198
});
126199
}
127200

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+
128226
private promptForTwoFactorMethod(): void {
129227
const promptOptions = this.twoFactorMethods.map(
130228
(method, index) => `[${index + 1}] ${method.method ?? ''}`,
@@ -205,10 +303,13 @@ export class AuthenticationSession {
205303
});
206304
this.authContext.reject();
207305
}
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+
}
212313
logger.warn(`Unexpected exception with FusionAuth 2FA login: ${message}`);
213314
this.authContext.reject();
214315
});

0 commit comments

Comments
 (0)