feat: validate the token on first load

If the client thinks it has a valid token (i.e. if the token was set on another umbraco instance or it has expired on the server or been revoked), it will still try and use it. The first authenticated request will then return a 401 prompting the client to show the "time out" screen. This is not entirely correct, as the user might simply expect to see the login screen directly.

This PR aims to introduce a simple server request to validate the token if one is present. We do this by trying to exchange the stored refresh_token to an access_token only on the first load. This has two benefits:

1. We let the server tell us directly if it thinks the stored token is useful.
2. We get a freshly minted access_token that is now valid for the configured timeout period and wont accidentally expire during the next 2 seconds thereby prompting the "time out" screen anyway.
This commit is contained in:
Jacob Overgaard
2024-05-13 15:20:17 +02:00
parent 29c91a9335
commit 57e1cf86a9
3 changed files with 49 additions and 23 deletions

View File

@@ -7,6 +7,7 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
export class UmbAppAuthController extends UmbControllerBase {
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#isFirstCheck = true;
constructor(host: UmbControllerHost) {
super(host);
@@ -37,7 +38,18 @@ export class UmbAppAuthController extends UmbControllerBase {
const isAuthorized = this.#authContext.getIsAuthorized();
if (isAuthorized) {
return true;
// If this is the first time we are checking the authorization state (i.e. on first load), we need to make sure
// that the token is still valid. If it is not, we need to start the authorization flow.
// If the token is still valid, we can return true.
if (this.#isFirstCheck) {
this.#isFirstCheck = false;
const isValid = await this.#authContext.validateToken();
if (isValid) {
return true;
}
} else {
return true;
}
}
// Make a request to the auth server to start the auth flow

View File

@@ -307,29 +307,17 @@ export class UmbAuthFlow {
return Promise.resolve(this.#tokenResponse.accessToken);
}
// if the refresh token is not set (maybe the provider doesn't support them)
if (!this.#tokenResponse?.refreshToken) {
this.#timeoutSignal.next();
return Promise.reject('Missing refreshToken.');
}
const success = await this.makeRefreshTokenRequest();
const request = new TokenRequest({
client_id: this.#clientId,
redirect_uri: this.#redirectUri,
grant_type: GRANT_TYPE_REFRESH_TOKEN,
code: undefined,
refresh_token: this.#tokenResponse.refreshToken,
extras: undefined,
});
await this.#performTokenRequest(request);
if (!this.#tokenResponse) {
if (!success) {
this.clearTokenStorage();
this.#timeoutSignal.next();
return Promise.reject('Missing tokenResponse.');
}
return Promise.resolve(this.#tokenResponse.accessToken);
return this.#tokenResponse
? Promise.resolve(this.#tokenResponse.accessToken)
: Promise.reject('Missing tokenResponse.');
}
/**
@@ -364,18 +352,36 @@ export class UmbAuthFlow {
await this.#performTokenRequest(request);
}
async makeRefreshTokenRequest(): Promise<boolean> {
if (!this.#tokenResponse?.refreshToken) {
return false;
}
const request = new TokenRequest({
client_id: this.#clientId,
redirect_uri: this.#redirectUri,
grant_type: GRANT_TYPE_REFRESH_TOKEN,
code: undefined,
refresh_token: this.#tokenResponse.refreshToken,
extras: undefined,
});
return this.#performTokenRequest(request);
}
/**
* This method will make a token request to the server using the refresh token.
* If the request fails, it will sign the user out (clear the token state).
*/
async #performTokenRequest(request: TokenRequest): Promise<void> {
async #performTokenRequest(request: TokenRequest): Promise<boolean> {
try {
this.#tokenResponse = await this.#tokenHandler.performTokenRequest(this.#configuration, request);
this.#saveTokenState();
return true;
} catch (error) {
// If the token request fails, it means the code or refresh token is invalid
this.clearTokenStorage();
console.error('Token request error', error);
this.clearTokenStorage();
return false;
}
}
}

View File

@@ -174,6 +174,15 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
return this.#authFlow.performWithFreshTokens();
}
/**
* Validates the token against the server and returns true if the token is valid.
* @memberof UmbAuthContext
* @returns True if the token is valid, otherwise false
*/
async validateToken(): Promise<boolean> {
return this.#authFlow.makeRefreshTokenRequest();
}
/**
* Clears the token storage.
* @memberof UmbAuthContext
@@ -188,7 +197,6 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
* @memberof UmbAuthContext
*/
timeOut() {
this.clearTokenStorage();
this.#isAuthorized.setValue(false);
this.#isTimeout.next();
}