diff --git a/.env.example b/.env.example index 12dd2928..d387a783 100644 --- a/.env.example +++ b/.env.example @@ -40,4 +40,6 @@ PERMANENT_API_BASE_PATH=${LOCAL_TEMPORARY_AUTH_TOKEN} # See https://fusionauth.io/docs/v1/tech/apis/api-keys FUSION_AUTH_HOST=${FUSION_AUTH_HOST} FUSION_AUTH_KEY=${FUSION_AUTH_KEY} -FUSION_AUTH_APP_ID=${FUSION_AUTH_APP_ID} +FUSION_AUTH_SFTP_APP_ID=${FUSION_AUTH_SFTP_APP_ID} +FUSION_AUTH_SFTP_CLIENT_ID=${FUSION_AUTH_SFTP_CLIENT_ID} +FUSION_AUTH_SFTP_CLIENT_SECRET=${FUSION_AUTH_SFTP_CLIENT_SECRET} diff --git a/src/classes/AuthenticationSession.ts b/src/classes/AuthenticationSession.ts index 666dae14..29bc2e49 100644 --- a/src/classes/AuthenticationSession.ts +++ b/src/classes/AuthenticationSession.ts @@ -13,6 +13,12 @@ enum FusionAuthStatusCode { } export class AuthenticationSession { + private static readonly sftpFusionAuthAppId = process.env.FUSION_AUTH_SFTP_APP_ID ?? ''; + + private static readonly sftpFusionAuthClientId = process.env.FUSION_AUTH_SFTP_CLIENT_ID ?? ''; + + private static readonly sftpFusionAuthClientSecret = process.env.FUSION_AUTH_SFTP_CLIENT_SECRET ?? ''; + public authToken = ''; public refreshToken = ''; @@ -23,8 +29,6 @@ export class AuthenticationSession { private readonly fusionAuthClient; - private readonly fusionAuthAppId = process.env.FUSION_AUTH_APP_ID ?? ''; - private twoFactorId = ''; private twoFactorMethods: TwoFactorMethod[] = []; @@ -39,13 +43,40 @@ export class AuthenticationSession { } public obtainNewAuthTokenUsingRefreshToken(): void { - this.fusionAuthClient.exchangeRefreshTokenForAccessToken(this.refreshToken, '', '', '', '') + if (!AuthenticationSession.sftpFusionAuthClientId) { + logger.error( + 'Cannot obtain new access token without sftp client id.', + ); + return; + } + + if (!AuthenticationSession.sftpFusionAuthClientSecret) { + logger.error( + 'Cannot obtain new access token without sftp client secret.', + ); + return; + } + + this.fusionAuthClient.exchangeRefreshTokenForAccessToken( + this.refreshToken, + AuthenticationSession.sftpFusionAuthClientId, + AuthenticationSession.sftpFusionAuthClientSecret, + '', + '', + ) .then((clientResponse) => { this.authToken = clientResponse.response.access_token ?? ''; + // The exchange refresh token for access token endpoint dose not return a timestamp, + // it returns expires_in in seconds. + // So we need to create the timestamp to be consistent with what is first + // returned upon initial authentication + this.authTokenExpiresAt = Date.now() + (clientResponse + .response.expires_in ?? 1 * 1000); + logger.info('New access token obtained'); }) .catch((clientResponse: unknown) => { const message = isPartialClientResponse(clientResponse) - ? clientResponse.exception.message + ? clientResponse.exception.error_description : ''; logger.warn(`Error obtaining refresh token : ${message}`); this.authContext.reject(); @@ -77,14 +108,16 @@ export class AuthenticationSession { } private processPasswordResponse([password]: string[]): void { + if (!AuthenticationSession.sftpFusionAuthAppId) { + logger.error('SFTP application id missing. No refresh token would be returned'); + } this.fusionAuthClient.login({ - applicationId: this.fusionAuthAppId, + applicationId: AuthenticationSession.sftpFusionAuthAppId, loginId: this.authContext.username, password, }).then((clientResponse) => { switch (clientResponse.statusCode) { - case FusionAuthStatusCode.Success: - case FusionAuthStatusCode.SuccessButUnregisteredInApp: + case FusionAuthStatusCode.Success: { if (clientResponse.response.token !== undefined) { logger.verbose('Successful password authentication attempt.', { username: this.authContext.username, @@ -93,11 +126,22 @@ export class AuthenticationSession { this.authTokenExpiresAt = clientResponse.response.tokenExpirationInstant ?? 0; this.refreshToken = clientResponse.response.refreshToken ?? ''; this.authContext.accept(); - return; + } else { + this.authContext.reject(); } - this.authContext.reject(); return; - case FusionAuthStatusCode.SuccessNeedsTwoFactorAuth: + } + case FusionAuthStatusCode.SuccessButUnregisteredInApp: { + const userId: string = clientResponse.response.user?.id ?? ''; + this.registerUserInApp(userId) + .then(() => { this.processPasswordResponse([password]); }) + .catch((error) => { + logger.warn('Error during registration and authentication:', error); + this.authContext.reject(); + }); + return; + } + case FusionAuthStatusCode.SuccessNeedsTwoFactorAuth: { if (clientResponse.response.twoFactorId !== undefined) { logger.verbose('Successful password authentication attempt; MFA required.', { username: this.authContext.username, @@ -105,16 +149,18 @@ export class AuthenticationSession { this.twoFactorId = clientResponse.response.twoFactorId; this.twoFactorMethods = clientResponse.response.methods ?? []; this.promptForTwoFactorMethod(); - return; + } else { + this.authContext.reject(); } - this.authContext.reject(); return; - default: + } + default: { logger.verbose('Failed password authentication attempt.', { username: this.authContext.username, response: clientResponse.response, }); this.authContext.reject(); + } } }).catch((clientResponse: unknown) => { const message = isPartialClientResponse(clientResponse) @@ -125,6 +171,29 @@ export class AuthenticationSession { }); } + private async registerUserInApp(userId: string): Promise { + return this.fusionAuthClient.register(userId, { + registration: { + applicationId: AuthenticationSession.sftpFusionAuthAppId, + }, + }).then((clientResponse) => { + switch (clientResponse.statusCode) { + case FusionAuthStatusCode.Success: + logger.verbose('User registered successfully after authentication.', { + userId, + }); + break; + default: + logger.verbose('User registration after authentication failed.', { + userId, + response: clientResponse.response, + }); + } + }).catch((error) => { + logger.warn('Error during user registration after authentication:', error); + }); + } + private promptForTwoFactorMethod(): void { const promptOptions = this.twoFactorMethods.map( (method, index) => `[${index + 1}] ${method.method ?? ''}`, diff --git a/src/fusionAuth.ts b/src/fusionAuth.ts index ca710ee3..ec29dfa5 100644 --- a/src/fusionAuth.ts +++ b/src/fusionAuth.ts @@ -8,6 +8,8 @@ export const getFusionAuthClient = (): FusionAuthClient => new FusionAuthClient( export interface PartialClientResponse { exception: { message: string; + error?: string; + error_description?: string; }; }