-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move token management to dedicated class
We had organically coupled token management with the initial authentication flow, but they don't actually belong together. This separates token management (e.g. utilization of refresh tokens) from the SSH authentication system. It also refactors the sftp session handler to use the token manager rather than the authentication session. Finally, the tokens are now retrieved just-in-time by the permanent file system (rather than being passed during the creation of the permanent file system). This is a critical fix because (1) it prevents certain paths that would lead to stale tokens but also (2) it means that creating a permanent file system becomes a synchronous operation. This also resolves a bug where the failure to generate a token could result in a hanging sftp connection. While doing these refactors we took out a redundant environment variable. Issue #288 Permanent file system errors can result in hung connections Issue #289 Duplicated FusionAuth env vars
- Loading branch information
Showing
9 changed files
with
493 additions
and
534 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { logger } from '../logger'; | ||
import { AuthTokenRefreshError } from '../errors/AuthTokenRefreshError'; | ||
import { | ||
getFusionAuthClient, | ||
isPartialClientResponse, | ||
} from '../fusionAuth'; | ||
|
||
export class AuthTokenManager { | ||
public readonly username: string; | ||
|
||
private readonly fusionAuthClient; | ||
|
||
private refreshToken = ''; | ||
|
||
private authToken = ''; | ||
|
||
private authTokenExpiresAt = new Date(); | ||
|
||
private fusionAuthClientId = ''; | ||
|
||
private fusionAuthClientSecret = ''; | ||
|
||
public constructor( | ||
username: string, | ||
refreshToken: string, | ||
fusionAuthClientId: string, | ||
fusionAuthClientSecret: string, | ||
) { | ||
this.username = username; | ||
this.refreshToken = refreshToken; | ||
this.fusionAuthClientId = fusionAuthClientId; | ||
this.fusionAuthClientSecret = fusionAuthClientSecret; | ||
this.fusionAuthClient = getFusionAuthClient(); | ||
} | ||
|
||
public async getAuthToken() { | ||
if (this.tokenWouldExpireSoon()) { | ||
await this.resetAuthTokenUsingRefreshToken(); | ||
} | ||
return this.authToken; | ||
} | ||
|
||
private async resetAuthTokenUsingRefreshToken(): Promise<void> { | ||
let clientResponse; | ||
try { | ||
/** | ||
* Fusion auth sdk wrongly mandates last two params (scope, user_code) | ||
* hence the need to pass two empty strings here. | ||
* See: https://github.com/FusionAuth/fusionauth-typescript-client/issues/42 | ||
*/ | ||
clientResponse = await this.fusionAuthClient.exchangeRefreshTokenForAccessToken( | ||
this.refreshToken, | ||
this.fusionAuthClientId, | ||
this.fusionAuthClientSecret, | ||
'', | ||
'', | ||
); | ||
} catch (error: unknown) { | ||
let message: string; | ||
if (isPartialClientResponse(error)) { | ||
message = error.exception.error_description ?? error.exception.message; | ||
} else { | ||
message = error instanceof Error ? error.message : JSON.stringify(error); | ||
} | ||
logger.verbose(`Error obtaining refresh token: ${message}`); | ||
throw new AuthTokenRefreshError(`Error obtaining refresh token: ${message}`); | ||
} | ||
|
||
if (!clientResponse.response.access_token) { | ||
logger.warn('No access token in response:', clientResponse.response); | ||
throw new AuthTokenRefreshError('Response does not contain access_token'); | ||
} | ||
|
||
if (!clientResponse.response.expires_in) { | ||
logger.warn('Response lacks token TTL (expires_in):', clientResponse.response); | ||
throw new AuthTokenRefreshError('Response lacks token TTL (expires_in)'); | ||
} | ||
|
||
/** | ||
* The exchange refresh token for access token endpoint does 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.authToken = clientResponse.response.access_token; | ||
this.authTokenExpiresAt = new Date( | ||
Date.now() + (clientResponse.response.expires_in * 1000), | ||
); | ||
logger.debug('New access token obtained:', clientResponse.response); | ||
} | ||
|
||
private tokenWouldExpireSoon(expirationThresholdInSeconds = 300): boolean { | ||
const currentTime = new Date(); | ||
const remainingTokenLife = ( | ||
(this.authTokenExpiresAt.getTime() - currentTime.getTime()) / 1000 | ||
); | ||
return remainingTokenLife <= expirationThresholdInSeconds; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.