Skip to content

Commit c853692

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

7 files changed

+556
-415
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: 117 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,87 @@ 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+
this.authToken = clientResponse.response.access_token ?? '';
71+
// The exchange refresh token for access token endpoint does not return a timestamp,
72+
// it returns expires_in in seconds.
73+
// So we need to create the timestamp to be consistent with what is first
74+
// returned upon initial authentication
75+
this.authTokenExpiresAt = new Date(
76+
Date.now() + (clientResponse.response.expires_in ?? 1 * 1000),
77+
);
78+
logger.info('New access token obtained');
79+
} catch (error: unknown) {
80+
let message: string;
81+
if (isPartialClientResponse(error)) {
82+
message = error.exception.message;
83+
} else {
84+
message = error instanceof Error ? error.message : JSON.stringify(error);
85+
}
86+
logger.warn(`Error obtaining refresh token: ${message}`);
87+
this.authContext.reject();
88+
}
5889
}
5990

60-
public tokenWouldExpireSoon(minutes = 5): boolean {
61-
const expirationDate = new Date(this.authTokenExpiresAt);
91+
private tokenWouldExpireSoon(seconds = 300): boolean {
6292
const currentTime = new Date();
63-
const timeDifferenceMinutes = (expirationDate.getTime() - currentTime.getTime()) / (1000 * 60);
64-
return timeDifferenceMinutes <= minutes;
93+
const timeDifferenceSeconds = (
94+
(this.authTokenExpiresAt.getTime() - currentTime.getTime()) / (1000 * 60 * 60)
95+
);
96+
return timeDifferenceSeconds <= seconds;
6597
}
6698

6799
private promptForPassword(): void {
@@ -78,53 +110,93 @@ export class AuthenticationSession {
78110

79111
private processPasswordResponse([password]: string[]): void {
80112
this.fusionAuthClient.login({
81-
applicationId: this.fusionAuthAppId,
113+
applicationId: this.fusionAuthSftpAppId,
82114
loginId: this.authContext.username,
83115
password,
84116
}).then((clientResponse) => {
85117
switch (clientResponse.statusCode) {
86-
case FusionAuthStatusCode.Success:
87-
case FusionAuthStatusCode.SuccessButUnregisteredInApp:
118+
case FusionAuthStatusCode.Success: {
88119
if (clientResponse.response.token !== undefined) {
89120
logger.verbose('Successful password authentication attempt.', {
90121
username: this.authContext.username,
91122
});
92123
this.authToken = clientResponse.response.token;
93-
this.authTokenExpiresAt = clientResponse.response.tokenExpirationInstant ?? 0;
124+
this.authTokenExpiresAt = new Date(clientResponse.response.tokenExpirationInstant ?? 0);
94125
this.refreshToken = clientResponse.response.refreshToken ?? '';
95126
this.authContext.accept();
96-
return;
127+
} else {
128+
this.authContext.reject();
97129
}
98-
this.authContext.reject();
99130
return;
100-
case FusionAuthStatusCode.SuccessNeedsTwoFactorAuth:
131+
}
132+
case FusionAuthStatusCode.SuccessButUnregisteredInApp: {
133+
const userId: string = clientResponse.response.user?.id ?? '';
134+
this.registerUserInApp(userId)
135+
.then(() => { this.processPasswordResponse([password]); })
136+
.catch((error) => {
137+
logger.warn('Error during registration and authentication:', error);
138+
this.authContext.reject();
139+
});
140+
return;
141+
}
142+
case FusionAuthStatusCode.SuccessNeedsTwoFactorAuth: {
101143
if (clientResponse.response.twoFactorId !== undefined) {
102144
logger.verbose('Successful password authentication attempt; MFA required.', {
103145
username: this.authContext.username,
104146
});
105147
this.twoFactorId = clientResponse.response.twoFactorId;
106148
this.twoFactorMethods = clientResponse.response.methods ?? [];
107149
this.promptForTwoFactorMethod();
108-
return;
150+
} else {
151+
this.authContext.reject();
109152
}
110-
this.authContext.reject();
111153
return;
112-
default:
154+
}
155+
default: {
113156
logger.verbose('Failed password authentication attempt.', {
114157
username: this.authContext.username,
115158
response: clientResponse.response,
116159
});
117160
this.authContext.reject();
161+
}
162+
}
163+
}).catch((error) => {
164+
let message: string;
165+
if (isPartialClientResponse(error)) {
166+
message = error.exception.message;
167+
} else {
168+
message = error instanceof Error ? error.message : JSON.stringify(error);
118169
}
119-
}).catch((clientResponse: unknown) => {
120-
const message = isPartialClientResponse(clientResponse)
121-
? clientResponse.exception.message
122-
: '';
123170
logger.warn(`Unexpected exception with FusionAuth password login: ${message}`);
124171
this.authContext.reject();
125172
});
126173
}
127174

175+
private async registerUserInApp(userId: string): Promise<void> {
176+
try {
177+
const clientResponse = await this.fusionAuthClient.register(userId, {
178+
registration: {
179+
applicationId: this.fusionAuthSftpAppId,
180+
},
181+
});
182+
183+
switch (clientResponse.statusCode) {
184+
case FusionAuthStatusCode.Success:
185+
logger.verbose('User registered successfully after authentication.', {
186+
userId,
187+
});
188+
break;
189+
default:
190+
logger.verbose('User registration after authentication failed.', {
191+
userId,
192+
response: clientResponse.response,
193+
});
194+
}
195+
} catch (error) {
196+
logger.warn('Error during user registration after authentication:', error);
197+
}
198+
}
199+
128200
private promptForTwoFactorMethod(): void {
129201
const promptOptions = this.twoFactorMethods.map(
130202
(method, index) => `[${index + 1}] ${method.method ?? ''}`,
@@ -205,10 +277,13 @@ export class AuthenticationSession {
205277
});
206278
this.authContext.reject();
207279
}
208-
}).catch((clientResponse: unknown) => {
209-
const message = isPartialClientResponse(clientResponse)
210-
? clientResponse.exception.message
211-
: '';
280+
}).catch((error) => {
281+
let message: string;
282+
if (isPartialClientResponse(error)) {
283+
message = error.exception.message;
284+
} else {
285+
message = error instanceof Error ? error.message : JSON.stringify(error);
286+
}
212287
logger.warn(`Unexpected exception with FusionAuth 2FA login: ${message}`);
213288
this.authContext.reject();
214289
});

0 commit comments

Comments
 (0)