Skip to content

Commit 4f20528

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

File tree

4 files changed

+119
-41
lines changed

4 files changed

+119
-41
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}

src/classes/AuthenticationSession.ts

Lines changed: 113 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,22 @@ enum FusionAuthStatusCode {
1313
}
1414

1515
export class AuthenticationSession {
16-
public authToken = '';
16+
private static readonly sftpFusionAuthAppId = process.env.FUSION_AUTH_SFTP_APP_ID ?? '';
17+
18+
private static readonly sftpFusionAuthClientId = process.env.FUSION_AUTH_SFTP_CLIENT_ID ?? '';
19+
20+
private static readonly sftpFusionAuthClientSecret = process.env.FUSION_AUTH_SFTP_CLIENT_SECRET ?? '';
21+
22+
private authToken = '';
1723

1824
public refreshToken = '';
1925

2026
public readonly authContext;
2127

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

2430
private readonly fusionAuthClient;
2531

26-
private readonly fusionAuthAppId = process.env.FUSION_AUTH_APP_ID ?? '';
27-
2832
private twoFactorId = '';
2933

3034
private twoFactorMethods: TwoFactorMethod[] = [];
@@ -38,30 +42,68 @@ export class AuthenticationSession {
3842
this.promptForPassword();
3943
}
4044

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-
});
45+
public async getToken() {
46+
if (this.tokenWouldExpireSoon()) {
47+
await this.getAuthTokenUsingRefreshToken();
48+
}
49+
return this.authToken;
5350
}
5451

55-
public tokenExpired(): boolean {
56-
const expirationDate = new Date(this.authTokenExpiresAt);
57-
return expirationDate <= new Date();
52+
private getAuthTokenUsingRefreshToken(): Promise<void> {
53+
return new Promise<void>((resolve, reject) => {
54+
if (!AuthenticationSession.sftpFusionAuthClientId) {
55+
logger.error(
56+
'Cannot obtain a new access token without the sftp client ID.',
57+
);
58+
reject(Error('Missing sftp client ID'));
59+
return;
60+
}
61+
62+
if (!AuthenticationSession.sftpFusionAuthClientSecret) {
63+
logger.error(
64+
'Cannot obtain a new access token without the sftp client secret.',
65+
);
66+
reject(Error('Missing sftp client secret'));
67+
return;
68+
}
69+
70+
this.fusionAuthClient
71+
.exchangeRefreshTokenForAccessToken(
72+
this.refreshToken,
73+
AuthenticationSession.sftpFusionAuthClientId,
74+
AuthenticationSession.sftpFusionAuthClientSecret,
75+
'',
76+
'',
77+
)
78+
.then((clientResponse) => {
79+
this.authToken = clientResponse.response.access_token ?? '';
80+
// The exchange refresh token for access token endpoint does not return a timestamp,
81+
// it returns expires_in in seconds.
82+
// So we need to create the timestamp to be consistent with what is first
83+
// returned upon initial authentication
84+
this.authTokenExpiresAt = new Date(
85+
Date.now() + (clientResponse.response.expires_in ?? 1 * 1000),
86+
);
87+
logger.info('New access token obtained');
88+
resolve();
89+
})
90+
.catch((clientResponse: unknown) => {
91+
const message = isPartialClientResponse(clientResponse)
92+
? clientResponse.exception.error_description
93+
: '';
94+
logger.warn(`Error obtaining refresh token: ${message}`);
95+
this.authContext.reject();
96+
reject(new Error(message));
97+
});
98+
});
5899
}
59100

60-
public tokenWouldExpireSoon(minutes = 5): boolean {
61-
const expirationDate = new Date(this.authTokenExpiresAt);
101+
private tokenWouldExpireSoon(seconds = 300): boolean {
62102
const currentTime = new Date();
63-
const timeDifferenceMinutes = (expirationDate.getTime() - currentTime.getTime()) / (1000 * 60);
64-
return timeDifferenceMinutes <= minutes;
103+
const timeDifferenceSeconds = (
104+
(this.authTokenExpiresAt.getTime() - currentTime.getTime()) / (1000 * 60 * 60)
105+
);
106+
return timeDifferenceSeconds <= seconds;
65107
}
66108

67109
private promptForPassword(): void {
@@ -77,44 +119,59 @@ export class AuthenticationSession {
77119
}
78120

79121
private processPasswordResponse([password]: string[]): void {
122+
if (!AuthenticationSession.sftpFusionAuthAppId) {
123+
logger.error('SFTP application id missing. No refresh token would be returned');
124+
}
80125
this.fusionAuthClient.login({
81-
applicationId: this.fusionAuthAppId,
126+
applicationId: AuthenticationSession.sftpFusionAuthAppId,
82127
loginId: this.authContext.username,
83128
password,
84129
}).then((clientResponse) => {
85130
switch (clientResponse.statusCode) {
86-
case FusionAuthStatusCode.Success:
87-
case FusionAuthStatusCode.SuccessButUnregisteredInApp:
131+
case FusionAuthStatusCode.Success: {
88132
if (clientResponse.response.token !== undefined) {
89133
logger.verbose('Successful password authentication attempt.', {
90134
username: this.authContext.username,
91135
});
92136
this.authToken = clientResponse.response.token;
93-
this.authTokenExpiresAt = clientResponse.response.tokenExpirationInstant ?? 0;
137+
this.authTokenExpiresAt = new Date(clientResponse.response.tokenExpirationInstant ?? 0);
94138
this.refreshToken = clientResponse.response.refreshToken ?? '';
95139
this.authContext.accept();
96-
return;
140+
} else {
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+
}
118175
}
119176
}).catch((clientResponse: unknown) => {
120177
const message = isPartialClientResponse(clientResponse)
@@ -125,6 +182,29 @@ export class AuthenticationSession {
125182
});
126183
}
127184

185+
private async registerUserInApp(userId: string): Promise<void> {
186+
return this.fusionAuthClient.register(userId, {
187+
registration: {
188+
applicationId: AuthenticationSession.sftpFusionAuthAppId,
189+
},
190+
}).then((clientResponse) => {
191+
switch (clientResponse.statusCode) {
192+
case FusionAuthStatusCode.Success:
193+
logger.verbose('User registered successfully after authentication.', {
194+
userId,
195+
});
196+
break;
197+
default:
198+
logger.verbose('User registration after authentication failed.', {
199+
userId,
200+
response: clientResponse.response,
201+
});
202+
}
203+
}).catch((error) => {
204+
logger.warn('Error during user registration after authentication:', error);
205+
});
206+
}
207+
128208
private promptForTwoFactorMethod(): void {
129209
const promptOptions = this.twoFactorMethods.map(
130210
(method, index) => `[${index + 1}] ${method.method ?? ''}`,

src/classes/SftpSessionHandler.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,16 +1070,10 @@ export class SftpSessionHandler {
10701070
}
10711071

10721072
private getCurrentPermanentFileSystem(): PermanentFileSystem {
1073-
if (
1074-
this.authenticationSession.tokenExpired()
1075-
|| this.authenticationSession.tokenWouldExpireSoon()
1076-
) {
1077-
this.authenticationSession.obtainNewAuthTokenUsingRefreshToken();
1078-
}
10791073
return this.permanentFileSystemManager
10801074
.getCurrentPermanentFileSystemForUser(
10811075
this.authenticationSession.authContext.username,
1082-
this.authenticationSession.authToken,
1076+
this.authenticationSession.getToken(),
10831077
);
10841078
}
10851079
}

src/fusionAuth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export const getFusionAuthClient = (): FusionAuthClient => new FusionAuthClient(
88
export interface PartialClientResponse {
99
exception: {
1010
message: string;
11+
error?: string;
12+
error_description?: string;
1113
};
1214
}
1315

0 commit comments

Comments
 (0)