Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ describe('UmbAuthContext', () => {

it('times the user out on a definitive invalid_grant failure', async () => {
fetchResponder = invalidGrantResponse;

// A peer tab establishes the session that is about to be rejected
const now = Math.floor(Date.now() / 1000);
channel.postMessage({ type: 'sessionUpdate', accessTokenExpiresAt: now + 60, expiresAt: now + 240 });
await aTimeout(50);
Comment thread
iOvergaard marked this conversation as resolved.

let timeOutCalls = 0;
context.timeOut = () => {
timeOutCalls++;
Expand All @@ -215,6 +221,19 @@ describe('UmbAuthContext', () => {
expect(timeOutCalls).to.equal(1);
});

it('does not time the user out when there was no session to lose', async () => {
fetchResponder = invalidGrantResponse;
let timeOutCalls = 0;
context.timeOut = () => {
timeOutCalls++;
};

await context.setInitialState();

expect(timeOutCalls).to.equal(0);
expect(context.getIsAuthorized()).to.be.false;
});

it('retries /token after a transient network failure', async () => {
fetchResponder = () => {
throw new TypeError('Failed to fetch');
Expand Down
17 changes: 12 additions & 5 deletions src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,21 +518,28 @@ export class UmbAuthContext extends UmbContextBase {

/**
* Performs the actual refresh request and applies the result.
* A definitive rejection (e.g. `invalid_grant`) marks the session as dead and times the
* user out, so the re-authentication flow starts instead of every subsequent API request
* firing its own doomed refresh attempt. Transient failures (network errors, 5xx) leave
* the session state untouched so a later attempt can retry.
* A definitive rejection (e.g. `invalid_grant`) marks the session as dead, so every
* subsequent API request does not fire its own doomed refresh attempt. When the rejected
* refresh belonged to an established session, the user is also timed out so the
* re-authentication flow starts. Transient failures (network errors, 5xx) leave the
* session state untouched so a later attempt can retry.
* @returns {Promise<boolean>} True if the refresh succeeded, otherwise false.
*/
async #performRefresh(): Promise<boolean> {
// A rejection with no session in hand means nobody is signed in — not that a session
// expired. Timing out there re-authenticates in a popup and leaves the caller to
// navigate, where a cold boot needs the ordinary login redirect.
const hadSession = !!this.#session.getValue();
const result = await this.#client.refreshToken();
if (result.response) {
this.#updateSession(result.response.expiresIn, result.response.issuedAt);
return true;
}
if (result.fatal) {
this.#sessionDead = true;
this.timeOut();
if (hadSession) {
this.timeOut();
}
}
return false;
}
Expand Down
Loading