Skip to content

Commit 1ae60e5

Browse files
committed
Register unregistered users & update obtain new token
Signed-off-by: Fon E. Noel NFEBE <[email protected]>
1 parent b9ab43c commit 1ae60e5

7 files changed

+570
-416
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: 131 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,92 @@ enum FusionAuthStatusCode {
1313
}
1414

1515
export class AuthenticationSession {
16-
public authToken = '';
16+
private authToken = '';
1717

1818
public refreshToken = '';
1919

2020
public readonly authContext;
2121

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

2424
private readonly fusionAuthClient;
2525

26-
private readonly fusionAuthAppId = process.env.FUSION_AUTH_APP_ID ?? '';
27-
2826
private twoFactorId = '';
2927

3028
private twoFactorMethods: TwoFactorMethod[] = [];
3129

32-
public constructor(authContext: KeyboardAuthContext) {
30+
private fusionAuthSftpAppId = '';
31+
32+
private fusionAuthSftpClientId = '';
33+
34+
private fusionAuthSftpClientSecret = '';
35+
36+
public constructor(
37+
authContext: KeyboardAuthContext,
38+
fusionAuthSftpAppId: string,
39+
fusionAuthSftpClientId: string,
40+
fusionAuthSftpClientSecret: string,
41+
) {
3342
this.authContext = authContext;
43+
this.fusionAuthSftpAppId = fusionAuthSftpAppId;
44+
this.fusionAuthSftpClientId = fusionAuthSftpClientId;
45+
this.fusionAuthSftpClientSecret = fusionAuthSftpClientSecret;
3446
this.fusionAuthClient = getFusionAuthClient();
3547
}
3648

3749
public invokeAuthenticationFlow(): void {
3850
this.promptForPassword();
3951
}
4052

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-
});
53+
public async getToken() {
54+
if (this.tokenWouldExpireSoon()) {
55+
await this.getAuthTokenUsingRefreshToken();
56+
}
57+
return this.authToken;
5358
}
5459

55-
public tokenExpired(): boolean {
56-
const expirationDate = new Date(this.authTokenExpiresAt);
57-
return expirationDate <= new Date();
60+
private async getAuthTokenUsingRefreshToken(): Promise<void> {
61+
try {
62+
const clientResponse = await this.fusionAuthClient.exchangeRefreshTokenForAccessToken(
63+
this.refreshToken,
64+
this.fusionAuthSftpClientId,
65+
this.fusionAuthSftpClientSecret,
66+
'',
67+
'',
68+
);
69+
70+
if (clientResponse.response.access_token) {
71+
this.authToken = clientResponse.response.access_token;
72+
// The exchange refresh token for access token endpoint does not return a timestamp,
73+
// it returns expires_in in seconds.
74+
// So we need to create the timestamp to be consistent with what is first
75+
// returned upon initial authentication
76+
this.authTokenExpiresAt = new Date(
77+
Date.now() + (clientResponse.response.expires_in ?? 1 * 1000),
78+
);
79+
logger.info('New access token obtained :', clientResponse.response);
80+
} else {
81+
logger.warn('No refresh token in response :', clientResponse.response);
82+
this.authContext.reject();
83+
}
84+
} catch (error: unknown) {
85+
let message: string;
86+
if (isPartialClientResponse(error)) {
87+
message = error.exception.message;
88+
} else {
89+
message = error instanceof Error ? error.message : JSON.stringify(error);
90+
}
91+
logger.warn(`Error obtaining refresh token: ${message}`);
92+
this.authContext.reject();
93+
}
5894
}
5995

60-
public tokenWouldExpireSoon(minutes = 5): boolean {
61-
const expirationDate = new Date(this.authTokenExpiresAt);
96+
private tokenWouldExpireSoon(seconds = 300): boolean {
6297
const currentTime = new Date();
63-
const timeDifferenceMinutes = (expirationDate.getTime() - currentTime.getTime()) / (1000 * 60);
64-
return timeDifferenceMinutes <= minutes;
98+
const timeDifferenceSeconds = (
99+
(this.authTokenExpiresAt.getTime() - currentTime.getTime()) / (1000 * 60 * 60)
100+
);
101+
return timeDifferenceSeconds <= seconds;
65102
}
66103

67104
private promptForPassword(): void {
@@ -78,53 +115,101 @@ export class AuthenticationSession {
78115

79116
private processPasswordResponse([password]: string[]): void {
80117
this.fusionAuthClient.login({
81-
applicationId: this.fusionAuthAppId,
118+
applicationId: this.fusionAuthSftpAppId,
82119
loginId: this.authContext.username,
83120
password,
84121
}).then((clientResponse) => {
85122
switch (clientResponse.statusCode) {
86-
case FusionAuthStatusCode.Success:
87-
case FusionAuthStatusCode.SuccessButUnregisteredInApp:
123+
case FusionAuthStatusCode.Success: {
88124
if (clientResponse.response.token !== undefined) {
89125
logger.verbose('Successful password authentication attempt.', {
90126
username: this.authContext.username,
91127
});
92128
this.authToken = clientResponse.response.token;
93-
this.authTokenExpiresAt = clientResponse.response.tokenExpirationInstant ?? 0;
94-
this.refreshToken = clientResponse.response.refreshToken ?? '';
129+
if (clientResponse.response.refreshToken) {
130+
this.refreshToken = clientResponse.response.refreshToken;
131+
this.authTokenExpiresAt = new Date(
132+
clientResponse.response.tokenExpirationInstant ?? 0,
133+
);
134+
} else {
135+
logger.warn('No refresh token in response :', clientResponse.response);
136+
this.authContext.reject();
137+
}
95138
this.authContext.accept();
96-
return;
139+
} else {
140+
logger.warn('No auth token in response', clientResponse.response);
141+
this.authContext.reject();
97142
}
98-
this.authContext.reject();
99143
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: {
101156
if (clientResponse.response.twoFactorId !== undefined) {
102157
logger.verbose('Successful password authentication attempt; MFA required.', {
103158
username: this.authContext.username,
104159
});
105160
this.twoFactorId = clientResponse.response.twoFactorId;
106161
this.twoFactorMethods = clientResponse.response.methods ?? [];
107162
this.promptForTwoFactorMethod();
108-
return;
163+
} else {
164+
this.authContext.reject();
109165
}
110-
this.authContext.reject();
111166
return;
112-
default:
167+
}
168+
default: {
113169
logger.verbose('Failed password authentication attempt.', {
114170
username: this.authContext.username,
115171
response: clientResponse.response,
116172
});
117173
this.authContext.reject();
174+
}
175+
}
176+
}).catch((error) => {
177+
let message: string;
178+
if (isPartialClientResponse(error)) {
179+
message = error.exception.message;
180+
} else {
181+
message = error instanceof Error ? error.message : JSON.stringify(error);
118182
}
119-
}).catch((clientResponse: unknown) => {
120-
const message = isPartialClientResponse(clientResponse)
121-
? clientResponse.exception.message
122-
: '';
123183
logger.warn(`Unexpected exception with FusionAuth password login: ${message}`);
124184
this.authContext.reject();
125185
});
126186
}
127187

188+
private async registerUserInApp(userId: string): Promise<void> {
189+
try {
190+
const clientResponse = await this.fusionAuthClient.register(userId, {
191+
registration: {
192+
applicationId: this.fusionAuthSftpAppId,
193+
},
194+
});
195+
196+
switch (clientResponse.statusCode) {
197+
case FusionAuthStatusCode.Success:
198+
logger.verbose('User registered successfully after authentication.', {
199+
userId,
200+
});
201+
break;
202+
default:
203+
logger.verbose('User registration after authentication failed.', {
204+
userId,
205+
response: clientResponse.response,
206+
});
207+
}
208+
} catch (error) {
209+
logger.warn('Error during user registration after authentication:', error);
210+
}
211+
}
212+
128213
private promptForTwoFactorMethod(): void {
129214
const promptOptions = this.twoFactorMethods.map(
130215
(method, index) => `[${index + 1}] ${method.method ?? ''}`,
@@ -205,10 +290,13 @@ export class AuthenticationSession {
205290
});
206291
this.authContext.reject();
207292
}
208-
}).catch((clientResponse: unknown) => {
209-
const message = isPartialClientResponse(clientResponse)
210-
? clientResponse.exception.message
211-
: '';
293+
}).catch((error) => {
294+
let message: string;
295+
if (isPartialClientResponse(error)) {
296+
message = error.exception.message;
297+
} else {
298+
message = error instanceof Error ? error.message : JSON.stringify(error);
299+
}
212300
logger.warn(`Unexpected exception with FusionAuth 2FA login: ${message}`);
213301
this.authContext.reject();
214302
});

0 commit comments

Comments
 (0)