- ABP Framework version: v8.1.3
- UI Type: Angular
- Database System: EF Core (Oracle)
- Auth Server Separated OpenID Server
I still can't get logout to work consistently in my Angular applications.
First of all, I commented the line - just for your information:
{
path: '',
pathMatch: 'full',
loadChildren: () => import('@my-home-page-package').then(m => m.MyHomeModule),
// canActivate: [AuthGuard] // This was a logout blocker
}
Now: A) If i use this code:
@Injectable()
export class MyServerErrorInterceptor implements HttpInterceptor {
constructor(private router: Router) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
this.router.navigate(['/']);
return EMPTY;
} else {
return throwError(error);
}
})
);
}
}
and click "Logout" link in the user menu, the logout seems to be working as expected from tab 1 in the browser. However if I have another page being open from the same site in tab 2 - it is not redirected to Login page: instead, I come up with an empty (no navigation menu) home page shown in this tab;
B) If i replace ['/']
with ['/login']
in the code above - both tabs react on this, however I end up with the endless loop, when both tabs try to do something and it never ends;
Also - if i have only one tab of the application opened in the browser, I still have a weird situation, when after getting connect/revocat
request (twice, btw - ??) and several 401 API requests, I see (so i see traditional openid-configuration
and so forth), so I am not actually logged out - but redirected to a home page;
C) If i emulate the situation with a token expiration - removing "local storage" data in the browser, I expect to be redirected to Login page on the next request attempt (instead of giving me error 401). So the setting (B) provides this, however setting (A) redirects me to a no-menu home page;
And in general, logout process visually does not look good: I expect if a user clicks "Logout" - he is redirected to Login page straight away. Instead, I see the navigation menu is being disappeared and still observing a home page while some server operations are taking place;
I have some Middleware in my OpenID Server module to do cleaning work on httpContext.Request.Path.Value == "/connect/logout"
scenario (B) (but works fine with scenario (A) - the exception does not happen). So I commented it, but it RANDOMLY affected the described scenario (sometimes it did, sometimes it did not):
public async Task InvokeAsync(HttpContext httpContext, IAbxRequestContext requestContext, IdentityUserManager userManager)
{
await _next(httpContext);
if (httpContext.Request.Path.Value == "/connect/logout")
{
await OnSessionEndRequestAsync(httpContext, userManager);
}
}
private async Task OnSessionEndRequestAsync(HttpContext httpContext, IdentityUserManager userManager)
{
try
{
var user = await userManager.GetUserAsync(httpContext.User); //IF IT GETS INVOKED AND CRASHES - IT SEEMS TO AFFECT THE LOGOUT
//Some cleaning routine - accessing DB, removing entries
}
catch(Exception ex)
{
_logger.LogError(ex, "Session End handling error");
}
}
Thus, I have no clue what might be a real root cause of the logout issue.
I kindly ask you to provide me with the correct settings (code) for a front-end and a back-end settings which might be relevant to the situation.
I cannot provide you with our code, sorry.
My suggestion is to start with a root cause of the exception in the call above. It is:
at System.Threading.CancellationToken.ThrowOperationCanceledException() at System.Threading.CancellationToken.ThrowIfCancellationRequested() at Volo.Abp.Identity.IdentityUserStore.FindByIdAsync(String userId, CancellationToken cancellationToken) at Castle.Proxies.IdentityUserManagerProxy.FindByIdAsync_callback(String userId) at Castle.Proxies.Invocations.UserManager
1_FindByIdAsync.InvokeMethodOnTarget() at Castle.DynamicProxy.AbstractInvocation.Proceed() at Castle.DynamicProxy.AbstractInvocation.ProceedInfo.Invoke() at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo) at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue
1.ProceedAsync() at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation) at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func
3 proceed) at Castle.DynamicProxy.AsyncInterceptorBase.ProceedAsynchronous[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo) at Volo.Abp.Castle.DynamicProxy.CastleAbpMethodInvocationAdapterWithReturnValue1.ProceedAsync() at Volo.Abp.Uow.UnitOfWorkInterceptor.InterceptAsync(IAbpMethodInvocation invocation) at Volo.Abp.Castle.DynamicProxy.CastleAsyncAbpInterceptorAdapter
1.InterceptAsync[TResult](IInvocation invocation, IInvocationProceedInfo proceedInfo, Func`3 proceed)
whereas httpContext.User
is OK (data is ok and it is authenticated).
96 Answer(s)
-
0
In fact, you just need to ensure that the refreshing page is aware that the antiforgery token has been updated before performing a
location.assign('/')
operation.To handle this, you can use the ngx-cookie-service library to update the token properly. Here is a quick example of how you can implement this:
import { DOCUMENT } from '@angular/common'; import { Component, inject } from '@angular/core'; import { CookieService } from 'ngx-cookie-service'; @Component({...}) export class AppComponent { window = inject(DOCUMENT).defaultView; cookieService = inject(CookieService); antiforgeryToken = 'XSRF-TOKEN'; constructor() { this.window.addEventListener('storage', (event) => { if (event.key !== 'access_token') { return; } const tokenRemoved = event.newValue === null; const tokenAdded = event.oldValue === null && event.newValue !== null; if (tokenRemoved || tokenAdded) { const token = this.cookieService.get(this.antiforgeryToken); //Added a set operation here this.cookieService.set(this.antiforgeryToken, token); location.assign('/'); } }); } }
-
0
It does not work, because in my case, this code is triggered only when I am being logged out, not when I am being logged-in.
In other words, visually the behavior of login process is the same as i have already described:
- Tab 1 - let's say I am logged-in;
- Tab 2 - I open the site there;
- Tab 1 - I press "Logout": the
const tokenRemoved = event.newValue === null;
line is invoked. Thelocation.assign
part is executed and I am logged out in both Tab 1 and Tab 2 and landed on the Login page of my OpenID server in both tabs; - Tab 1 - I log-in to the site;
- Tab 2 - I try to log-in from there. The breakpoint at
const tokenRemoved = event.newValue === null;
is not invoked. I get error 400 as previously mentioned;
-
0
I see the issue you're facing. In the last step, tab-2 is not notified about the token update, which leads to the error. However, to assist you further, I would need more details on how the XSRF token is managed on the backend. Could you provide more information on that?
-
0
I do not know what exactly could cause such a discrepant behavior in our solution (i.e. why "tab-2 is not notified about the token update" if it expected to do so). We do not have any specific code handling
AntiForgery
token - neither on front-end, nor on back-end side. For this reason, I have no idea which part of code I should share (I am not allowed to share the solution). I am sharing a couple of files which might be relevant:Also, I want to inform you the OpenID server log does not contain any errors or exceptions. https://drive.google.com/file/d/11GlNoSNzpZ-DPH8Wa_i2PSN-EASfRIp4/view?usp=sharing https://drive.google.com/file/d/11I0kgnxmXnlfYm7Ojh8nf3zIKBQKFTMU/view?usp=sharing
Maybe there is the way to track down the mentioned issue in some way?
-
0
hi alexander.nikonov
You can consider disabling the anti-forgery check for the logout endpoint Can you share the logs.txt file? I will check and share the code to disable it.
liming.ma@volosoft.com
Thanks.
-
0
You can consider disabling the anti-forgery check for the logout endpoint Can you share the logs.txt file? I will check and share the code to disable it.
https://drive.google.com/file/d/11InoAogKz83idmrKcEdvbp9Eciw6VE2u/view?usp=sharing
-
0
hi
There is no AntiforgeryValidationException error in your logs.
What are your current exceptions?
Thanks.
-
0
@maliming sorry - for some reason the log really did not contain the exceptions. I made a fresh test and prepared the logs of both OpenID server and AppServer. The latter one might be non-relevant, it just shows how requests try to access the back-end even after the logout (and getting non-authorized exception) - I did not manage to find the way to unsubscribe properly: https://drive.google.com/file/d/11InoAogKz83idmrKcEdvbp9Eciw6VE2u/view?usp=sharing https://drive.google.com/file/d/11JSdq3tG2jq8RizV0_MTdwiwVXz5Hc8_/view?usp=sharing
-
0
-
0
I've tried your approach. The behaviour has now changed. And when I try to log in from the second tab after I have already logged in from the first, I now see a cyclical process of re-logging in both tabs that never ends. Also, I do not think it is a good idea to turn off
Antiforgery
. So I'd prefer to wait until this multitab login problem is fixed in ABP, as your colleagues have already mentioned. -
0
Hello again, thank you for the details.However, we could not reproduce this issue on our end. By default, access token changes trigger a redirect to the home page. If you have set up a guard requiring re-login on the same page, this behavior may need customization.
If you could share more details for this part, this will help us provide a more accurate solution.
-
0
Hi @sumeyye.kurtulus. I have removed our custom code in the
app.component.ts
trying to isolate the problem. Below are the references to this version ofapp.component.ts
androute.provider.ts
. The behavior is a bit different now: there is noAntiforgery
token exception. But the passive second tab remains intact while a user is being logged-in in the first tab. And after I try to log in manually from there, there is a cyclical re-login process in BOTH tabs being started. I would like to drill down to find the root cause. But so far I have no clue - as you can see this is not a permissions issue, I have removed our custom permission guards. Should I share some other files, e.g. HTTP interceptors?https://drive.google.com/file/d/11YMuPWKR2Cpl5hILjp729QnwsEmJee_3/view?usp=sharing https://drive.google.com/file/d/11Z0YjrwSvSHsypdHfEF7j84-SzC3CiMM/view?usp=sharing
-
0
Thank you for sharing more details. However, I did not see anything that would cause this cyclical re-login problem in these files. That is why, I suspected whether you have implemented another guard on the home route.
-
0
Thank you for sharing more details. However, I did not see anything that would cause this cyclical re-login problem in these files. That is why, I suspected whether you have implemented another guard on the home route.
I cannot reproduce the cyclic reload scenario now. Probably I just forgot to remove a
Model.Filters.Add(new IgnoreAntiforgeryTokenAttribute());
last time and "cyclic reloading" on login is what happens next when the AntiForgery thing is resolved.Anyway, I would prefer to leave the AntiForgery protection as it is.
So now the question is: why is the event handler you suggested -
this.window.addEventListener('storage', (event) => { ... })
is not fired for the passive tab. As we found out, Permission Guards does not affect this. Do you have any idea what else I could check?FYI: there are no other
addEventListener
in our code. Logout is standard: calling ABP methodthis.authService.logout()
Thank you very much.
UPDATE. Trying to figure out the problem per se. From what I can see, XSRF-TOKEN is stored in Cookies (and Cookies are identical for both tabs - Angular app in tab #1 and OpenID server login page in tab #2). When I log-in in the tab #1 - the new AntiForgery token is generated and written to Cookies. Tab #2 tries to reuse it (?) and fails, because this token is already "in use". So if I for instance switch to storing AntiForgery token in Session Storage (which is different for both tabs) - it might resolve my problem?
-
0
As you have mentioned, tab-2 uses an invalid XSRF token value. It either uses the old value expecting the new one, or uses the new one and complains about anyway. Could you try storing the value with an alternative way and notify me again?
-
0
Yes - that's what I am planning to do. But I do not control Antiforgery token in any way: all the job is done automatically by ABP framework, including the name, the way it is stored, etc. The only relevant code we are now using is:
Configure<AbpAntiForgeryOptions>(options => { options.TokenCookie.Expiration = TimeSpan.FromDays(365); // options.AutoValidateIgnoredHttpMethods.Add("POST"); options.AutoValidate = false; })
So my question is how to make Antiforgery token be stored in a session storage of a browser in the first place?
-
0
So now the question is: why is the event handler you suggested - this.window.addEventListener('storage', (event) => { ... }) is not fired for the passive tab. As we found out, Permission Guards does not affect this. Do you have any idea what else I could check?
If the XSRF token is managed by the framework, it should update automatically using the approach I mentioned earlier. This should detect login/logout actions and redirect the passive tab to the home page when you log in from the active tab.
If the passive tab is not redirecting and requires a re-login, there might be another issue. I have not been able to reproduce this, so could you check if there is any custom logic preventing the expected behavior?
-
0
@sumeyye.kurtulus today I easily reproduced the problem on the ABP test project. Essenially, unlike ABP test project, we do not have a login form or link as a part of the application. So after clicking logout link, a user is redirected straight to the common Login page offered by OpenID server. I think the issue with 'storage' event subscription is now explained easily, because Login page is no longer a part of the application, it is a part of OpenID server. But we need the solution for this very same scenario. https://drive.google.com/file/d/11j-ruM0et75fN27etFdo-igstWO8lHup/view?usp=sharing
-
0
ABP test project also uses the same way for authorization if you did not specify the password flow. So, this should not cause a problem. You can also check the published solution within the latest pre-release on this PR https://github.com/abpframework/abp/pull/22066.
-
0
Both in the current ABP test solution I use for the demo (version 8.1.3) and in your latest link the same layout is used and thus there is an intermediate step, using "Login" link in the main app. It might look a very subtle nuance, but it makes a big difference, i.e. if AFTER logout but BEFORE logging in, you will go to the login form in BOTH tab - you will get the same error as me (click "Login" button in both tabs to try to login from Login form of OpenID server):
As I mentioned, we do not have this very "Login" button in our application: after a user clicks logout, he needs to be redirected straight to the Login form of OpenID server.
The corresponding exception for ABP test solution is different in this case, but still relates to AntiForgery too:
Antiforgery token validation failed. The antiforgery cookie token and request token do not match. Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The antiforgery cookie token and request token do not match. at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateTokens(HttpContext httpContext, AntiforgeryTokenSet antiforgeryTokenSet) at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext) at Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter.OnAuthorizationAsync(AuthorizationFilterContext context)
-
0
Thank you for your patience and for providing more details on your scenario — let’s wrap this up!
The antiforgery token mismatch issue you’re experiencing after logging out and trying to log back in from multiple tabs is expected behavior, tied to how browsers and the OpenID Connect flow handle session and cookie isolation.
Here’s why it happens:
- Logout invalidates the session: When you log out, the session and associated tokens (including the antiforgery token) are cleared.
- Tabs operate independently: Each tab maintains its own isolated cookie storage. After logging out, tokens in one tab may not align with tokens in another tab, causing the mismatch when trying to log back in simultaneously.
- Antiforgery tokens must match: The server expects the antiforgery token in the request to match the one in the cookie. If they differ (as they would across tabs after logout), the server rejects the request for security reasons.
Unfortunately, this behavior is inherent to browser security and not something ABP itself controls. You can also refer this documentation as my colleague mentioned before: https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-8.0#multiple-browser-tabs-and-the-synchronizer-token-pattern
I hope this clarifies the situation and provides some insight into why this behavior occurs.