Open Closed

Cannot properly unsubscribe on logout (API calls are still invoked with error 401) #5711


User avatar
0
alexander.nikonov created

ABP 7.0.1 / Angular

My home page shows some information to an authenticated user via API calls. If the user logs out - these methods need not to be invoked anymore. Seems like I've tried all possible ways - and it still DOES call those methods with "Not authorized (401)" error from server after I click "Logout" button. I also have tried to call Subscription$.unsubscribe() while logging out, but it still does not work.

Another question: I can logout from any page, not just Home page. There are plenty of API call subscriptions on each of them. How am I supposed to unsubscribe from all such calls with minimal code changes??

Here is the piece of the code of my Home page:

        ngOnInit() {
      
          this.oAuthService.events
            .pipe(
              filter(event => event?.type === 'logout'),
              tap(() => { 
                this.logout$.next(null); //those are called, but API calls are still invoked
                this.logout$.complete();
              }))
            .subscribe();
      
          this.homeService.getNewsForHomePage()
            .pipe(filter(() => this.configStateService.getDeep('currentUser.isAuthenticated')), takeUntil(this.destroy), takeUntil(this.logout$))
            .subscribe((newsResponse) => {
              ...
            });
      
          this.homeService.getUrlsForHomePage()
            .pipe(filter(() => this.configStateService.getDeep('currentUser.isAuthenticated')), takeUntil(this.destroy), takeUntil(this.logout$))
            .subscribe((newsUrlParameterResponse) => {
              ...
            });
        }
      
        ngOnDestroy(): void {
          this.destroy.next(null);
          this.destroy.complete();    
        }

Moreover - when I am already at this page (where this.configStateService.getDeep('currentUser.isAuthenticated') is supposed to be false, I guess): the API calls are still invoked.


36 Answer(s)
  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello alexander.nikonov ,

    Can you please try to add this code

    import { SubscriptionService } from '@abp/ng.core';
    
    constructor(private subscriptionService: SubscriptionService)
    
    this.subscriptionService.closeAll() // add this line in logout event
    

    Please let me know if this helps you or you may provide some steps to reproduce the issue as I am unable to reproduce it.

    Thank you, Anjali

  • User Avatar
    0
    alexander.nikonov created

    To avoid compile errors, I had also to add providers: [SubscriptionService] to app.component.ts - hope it is a correct string. But anyway - closeAll() call did not help: i'm still getting the same 401 error on logout. It happens to any method which is obviously being loaded at the moment of logout. The methods are pretty typical:

    //some component
    this.fineDigitLoading = true;
    this.fineDigitsService
      .getById(fineDigit.ordinanceId, fineDigit.number, fineDigit.subdivision, fineDigit.version)
      .pipe(
        finalize(() => (this.fineDigitLoading = false)),
        takeUntil(this.unsubscriber$)
      )
      .subscribe((state: FineDigits.FineDigit) => {
         ...
      });
    
    //fineDigitsService
    getById(
        ordinanceId: number,
        number: string,
        subdivision: string,
        version: number): Observable<FineDigits.FineDigit> {
        return this.restService.request<void, FineDigits.FineDigit>({
            method: 'GET',
            url: `/api/md/fine-digits/${ordinanceId}/${number}/${version}/${subdivision === null ? '' : subdivision}`,
        },
        { apiName: this.apiName });
    }
    
    //base class for any component
    ngOnDestroy(): void {
        this.ngOnDestroyInner();
        this.unsubscriber$.next(null);
        this.unsubscriber$.complete();
    }
    

    Probably the latter approach is the reason? When the user is logging out - the current component is not being destroyed, so ngOnDestroy is not being triggered. If this is the case - please let me know the recommended approach to unsubscribe from all API calls by adjusting the code in base component class - adding 'logout' event handling which is now done in app.component.ts? Suprisingly I did not see any special handling for such scenario in ABP components...

    UPDATE: I've added this to Base component constructor, but stil keep getting the same 401 errors:

    this.oAuthService.events
      .pipe(filter(event => event?.type === 'logout'), takeUntil(this.unsubscriber$))
      .subscribe(() => {
        this.unsubscriber$.next(null);
        this.unsubscriber$.complete();
      });
      
    

    The above part IS triggered after I click "Logout" link. But after this my server API calls are still made. How it's possible if I use takeUntil(this.unsubscriber$) for all API calls?

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello alexander.nikonov,

    import { AuthService } from '@abp/ng.core';
    constructor(private authService: AuthService)
    ngOnInit()
    {
    if (this.authService.isAuthenticated) //like -> this.configStateService.getDeep('currentUser.isAuthenticated')
    {
        this.homeService.getNewsForHomePage()
        this.homeService.getNewsForHomePage()
        //other code and API which you are calling at initial...
    }
    }
    

    Could you please try with this code

    .pipe(filter(() => this.configStateService.getDeep('currentUser.isAuthenticated'))
    

    This condition will not work as it's not authorized, so will not execute the next statement. HomeComponent get calls whether user logged in or not, so without get authorized it will give error.

    please do let me know if this helps you.

    Thank you, Anjali

  • User Avatar
    0
    alexander.nikonov created

    Hi Anjali,

    unfortunately, it did not help - still getting 401 on logout in one of two methods. Suprisingly, this method has another problem - it is for some reason invoked twice, even though in debug I just got it triggered once.

    I'm attaching the Home module code - probably you will figure out what is wrong. It is actually not my code, so I am not sure I would be able to reply you all the questions. But I'm ready to assist in any possible way.

    Thank you. Home page

    Actually one more thing confuses me: after I do log out - I'm redirected to this page ("Home"): instead of this: So I need to click "Login" again to be redirected to the second dialog. But the routing in the app is the same as in test ABP app...

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello alexander.nikonov,

    After logout it will redirect to your landing page i.e. Home and if you are calling any api's inside home component which is not authorized then this 401 error occurs for that particular api's. For reproduce the same Issue I added 1 api in Home component. I am calling that api in ngOnInit ( initial method ) with login it is working fine. But if I logout then we are facing 401 status in network tab the reason is we are calling api's without authorized.

    I tried with your code to call api's which is inside ngOnInitInner which was giving me error without if (this.authService.isAuthenticated) { } block

    please check the Initiator column for which api's you are getting 401 error and try to add those api calls inside this if (this.authService.isAuthenticated) { } block

    When I tried with if (this.authService.isAuthenticated) { } block , that api will not get call and hence not get any error.

     protected ngOnInitInner() {
        if (this.authService.isAuthenticated) {
          this.homeService.getNewsForHomePage()
            .pipe(take(1))
            .subscribe((newsResponse) => {... });
          this.homeService.getUrlsForHomePage()
            .pipe(take(1))
            .subscribe((newsUrlParameterResponse) => {... });
        }
      }
    

    If the value by authService.isAuthenticated is not giving false on logout then please try this if (this.configStateService.getDeep('currentUser.isAuthenticated')) {}

    please do let me know if it helps you,

    Thank you, Anjali

  • User Avatar
    0
    alexander.nikonov created

    Hi Anjali,

    i've run debug again.

    1. when I click "Logout" on Home page - the onInit method IS get triggered with expected this.authService.isAuthenticated == false. But prior to that, I'm already getting 401 error request (sometimes - only this one request, sometimes - another one too): The "initiator" there is shown as "zone.js", later on it's getting cleared. So it's still looking confusing.

    2. our users complain about being redirected to landing page "Login" dialog, then - to Identity Server "Login" dialog (as shown on second screenshot). The request is to be redirected to the second "Login" dialog straight away... How to do that?

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello alexander.nikonov,

    Please try to add this code in app.component.ts

    import { AuthService } from '@abp/ng.core';
    import { Router } from '@angular/router';
    import { OAuthService } from 'angular-oauth2-oidc';
    import { filter, tap } from 'rxjs';
     
    constructor(private oAuthService: OAuthService , private router: Router, private authService: AuthService) {
        this.oAuthService.events
        .pipe(filter(event => event?.type === 'logout'),
          tap(() => {
            this.authService.logout().subscribe(() => {
                this.authService.init()
              this.authService.navigateToLogin();
            });
          })).subscribe();
     }
    

    please do let me know if this helps you.

    Thank you, Anjali

  • User Avatar
    0
    alexander.nikonov created

    Hi Anjali.

    Don't want to upset you, but it's even worse now: the mentioned API call is still GET called after I press 'Logout'. But now after I'm doing this - I never gets logged out: after several chaning screens I'm eventually redirected to the root page, i.e. Home page...

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello alexander.nikonov,

    Thank you for the update and apologies for the inconvenience.

    We understand your problem and looking for better solution, please allow us sometime to get back on the same.

    Thank You. Anjali

  • User Avatar
    0
    alexander.nikonov created

    Thank you! Will be waiting. Also please suggest me how to logout directly to Identity Server Login box without visiting intermediate root page (Home page). I was trying to find the way (like editing SignOut URLs in Identity Server settings via admin UI page), but it did not bring me anywhere. I believe this used to work properly in some previous ABP version. Maybe we have unintentionally have changed some relevant setting.

  • User Avatar
    1
    Anjali_Musmade created
    Support Team Support Team Member

    Hi,

    you can directly redirect to login page by attaching you root page or home component a authguard.

  • User Avatar
    0
    alexander.nikonov created

    Hi,

    you can directly redirect to login page by attaching you root page or home component a authguard.

    Great! Thank you so much - this part is now resolved.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi

    Now after logout i guess your api will also not get called, can you check please?

  • User Avatar
    0
    alexander.nikonov created

    Hi

    Now after logout i guess your api will also not get called, can you check please?

    The API is still called though - after I click Logout, but before actual logging out is complete.

  • User Avatar
    0
    alexander.nikonov created

    I had additional experiment. Please have a look:

    I've modified ngOnInit a bit:

    this.oAuthService.events
      .pipe(
        filter(event => event?.type === 'logout'),
        tap(() => {
          this.unsubscriber$.next(null);
          this.unsubscriber$.complete();
          console.log('unsubscriber null!');
        }))
        .subscribe();
    
    if (this.authService.isAuthenticated) {
      this.homeService.getNewsForHomePage()
        .pipe(tap(() => console.log('getNewsForHomePage still invoked')), takeUntil(this.unsubscriber$))
        .subscribe((newsResponse) => {
          this.newsForHomePage = newsResponse.items;
        });
      this.homeService.getUrlsForHomePage()
        .pipe(tap(() => console.log('getUrlsForHomePage still invoked')), takeUntil(this.unsubscriber$))
        .subscribe((newsUrlParameterResponse) => {
          this.urls = newsUrlParameterResponse.items;
          this.urlDigimedia = this.urls.filter(i => i.columnValue === 'DM')[0]?.stringValue;
          this.urlMasterData = this.urls.filter(i => i.columnValue === 'MD')[0]?.stringValue;
        });
    }
    

    I got 'unsubscriber null' message instantly after clicking "Logout" button. Despite this, shortly after, the method was still being invoked (getUrlsForHomePage matches '/api/ct/central-tool/parameters/values' API route). I cannot explain what's going on: The messages "getNewsForHomePage still invoked" and "getUrlsForHomePage still invoked" were never shown - obviosly because this.authService.isAuthenticated was false by the moment.

    I thought it could have something to do with using guards. But even if I remove guard for Home page, the given API call is still invoked.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hello alexander.nikonov,

    I have looked into your code provided above. I will suggest to add ngOnDestroy method to call this.unsubscriber$.next(); and this.unsubscriber$.complete(); just like in below code.

    Please check with this code if find helpful.

        this.oAuthService.events
          .pipe(
            takeUntil(this.unsubscriber$), // add this line also
          )
          .subscribe(event => {
            if (event?.type === 'logout') {
                          console.log('Logout event received');
              this.unsubscriber$.next(); 
            }
          });
          
          
      ngOnDestroy() {
        this.unsubscriber$.next();
        this.unsubscriber$.complete();
      }
    

    Please do let us know if anything else needed.

    Thank you, Anjali

  • User Avatar
    0
    alexander.nikonov created

    Hi Anjali.

    First of all, ngOnDestroy with the suggested code already present in the base class HomeComponent is inherited from. Nevertheless, I've added ngOnDestroy here for test, too.

    Also I've replace this.oAuthService.events with your suggestion.

    After pressing "Logout" button I'm getting "Logout event received" message. But API call is STILL invoked as always. There has to be explanation WHY it is called though.

    P.S. Please hold on - I want to check something...

    UPDATE: I made some experiments. Good news: In the given specific case the API call I mentioned before was also invoked by other service - this was the reason it "resurrected" all the time after logout; Bad news: In a common case, only using if (this.authService.isAuthenticated) check or similar like you suggested helps resolving the initial problem. However, it is not a good approach: we have hundreds of API calls from hundreds of components. It is not a way to go to add hundreds of "if". Instead, it is supposed that for both component destroying and user logout action unsubscriber$ needs to received proper null value and be completed:

    this.apiService
      .apiCall(...)
      .pipe(
        finalize(() => (...)),
        takeUntil(this.unsubscriber$) //this has to be enough for all cases
      )
      .subscribe((...) => {
          ...
      });
    

    The problem is that for some reason using logout event does not help to resolve this issue. I use this in Component base class (no matter, "tap" or "subscribe" actually) and this block DOES get triggered, however eventually API is still invoked for unknown for me reason:

      ngOnInit(): void {
        this.oAuthService.events
          .pipe
          (
            filter(event => event?.type === 'logout'),
            takeUntil(this.unsubscriber$),
            tap(() => {
              this.unsubscriber$.next(null);
              this.unsubscriber$.complete();
            })
          )
          .subscribe();
          
    

    I've tried to make unsubscriber$ as ReplaySubject and create its new with buffer == 1, but it does not change the things.

    I am confused even more now: it appeared that logging out works only for Home page: if I take any page - even the ABP page - after clicking "logout" button the user is eventually redirected to Home page again without landing at Identity Server Login page: and then - all the rest our services required for the page are loaded as usual... And this is workflow when I click "Logout" on Home page: Afterwards I'm redirected to Identity Server Login page as expected. All the relevant pages have canActivate: [AuthGuard] It's absolutely strange given that "Logout" functionality is the same for all pages, i.e. the same code has to be invoked. Do you have any clues? If required, I will create a new ticket for this issue...

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi

    currently app.component.ts is the bootstraped component https://angular.io/guide/bootstrapping so if you place any api call in there it will be called so you have to use conditional operator there to calling api when the state is not authenticated.

    if (this.configStateService.getDeep('currentUser.isAuthenticated')) { 
    //authenticated code here
    }
    

    you don't have to place these if conditions in any component if you have used authguard in the routing. so you have to put this if condition only on app.component.ts.

  • User Avatar
    0
    alexander.nikonov created

    currently app.component.ts is the bootstraped component https://angular.io/guide/bootstrapping so if you place any api call in there it will be called so you have to use conditional operator there to calling api when the state is not authenticated.

    Sorry, but i didn't get it. I have a lot of different components like the one below - with a bunch of API calls triggered based on different conditions - button clicks, subscriptions, route parameter values, etc.:

        @Component({ ... })
        export class ComponentHHH extends BaseComponent {
        
            //Condition method can be invoked based on various conditions in unknown moment of time
            ConditionMethodAAA() {
                ....
                this.apiServiceXXX.apiMethodYYY(...).pipe(takeUntil(this.unsubscriber$))
                    .subscribe((response) => { ... });
                ....
                this.apiServiceZZZ.apiMethodAAA(...).pipe(takeUntil(this.unsubscriber$))
                    .subscribe((response) => { ... });
                ....
            }
    
            ConditionMethodBBB() {
                ....
                this.apiServiceDDD.apiMethodSSS(...).pipe(takeUntil(this.unsubscriber$))
                    .subscribe((response) => { ... });
                ....
            }
        }
    

    If I activate another component in the menu - this.unsubscriber$ (property in "BaseComponent" class) receives null and completes. Due to this and to using "takeUntil", no matter which API calls were active, they all will be removed. I've tried to subcribe to "logout" event in "BaseComponent" and do the same I do with this.unsubscriber$ in "ngOnDestroy". However this does not work - API calls keep being invoked. Even more now: logout works only from Home page (probably these two things are unrelated, but I am not sure). I have not found explanation to this.

    So you suggest to summon "app.component.ts" to resolve the problem. But I cannot figure out, how it can help here: there is no connection between AppComponent and another components API calls.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi

    any Component except app.component.ts cannot get invoked unless they are being Activated by authguard is you ComponentHHH have authguard in app.routing.module.ts or in the registered router module?

  • User Avatar
    0
    alexander.nikonov created

    They have it in the App Routing module: The component routing module does not have it: UPDATE: to save your time a bit, I've duplicated "canActive" in my Component's routing module. But the problem did not go away.

  • User Avatar
    0
    Anjali_Musmade created
    Support Team Support Team Member

    Hi

    I am not to reproduce this on my end, is you component ngOnit being invoked when you are not logged in?

  • User Avatar
    0
    alexander.nikonov created

    I do not have ngOnInit override from component's base class. So I put the breakpoint in the base class ngOnInit method. And this method is only invoked when the user is authenticated.

  • User Avatar
    0
    masum.ulu created
    Support Team Angular Developer

    Hello alexander,

    I'm confused little bit there are a lot of different questions 🙂 Can you please explain your latest problem step by step what is your goal and what's the current problem to reach your goal ?

  • User Avatar
    0
    alexander.nikonov created

    Hi, Masum. I have encountered two issues, which may be related. However, I have created a separate ticket for the second problem. This particular ticket addresses the following concern: I am unable to find a proper method to unsubscribe from API calls within my components after logging out. Despite logging out, the API calls continue to be invoked. I apologize if this thread appears confusing. I have been updating the ticket with additional information as I uncover more details and conduct further experiments in an attempt to resolve the issue.

Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11