Angular-auth-oidc-client: TypeError when implementing a `CanLoad` guard and `postLoginRoute` having no effect

Created on 11 Mar 2021  路  22Comments  路  Source: damienbod/angular-auth-oidc-client

Describe the bug

  • TypeError when implementing a CanLoad guard.
  • authorize({customParams: {postLoginRoute}}) seems to do nothing

To Reproduce

In order to reproduce the behavior, I made a minimal example where the SSO is a docker-containerized Keycloak:

  1. git clone https://gitlab.com/hadrien-toma/issue && cd issue
  2. Install modules: cd antique-white/workspace && yarn install ; cd ..
  3. Run Keycloak (it will create a realm my-realm, a client with ID my-client-id and a user with admin/admin as username/password): scripts/antique-white/run/keycloak/index.bash
  4. Run the Angular application: scripts/antique-white/run/my-realm/my-client-id/index.bash

Expected behavior

ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'usePushedAuthorisationRequests' of null
TypeError: Cannot read property 'usePushedAuthorisationRequests' of null
    at LoginService.login (angular-auth-oidc-client.js:3876)
    at OidcSecurityService.authorize (angular-auth-oidc-client.js:4083)
    at MapSubscriber.project (app.module.ts:75)
    at MapSubscriber._next (map.js:29)
    at MapSubscriber.next (Subscriber.js:49)
    at CombineLatestSubscriber.notifyNext (combineLatest.js:73)
    at InnerSubscriber._next (InnerSubscriber.js:11)
    at InnerSubscriber.next (Subscriber.js:49)
    at BehaviorSubject._subscribe (BehaviorSubject.js:14)
    at BehaviorSubject._trySubscribe (Observable.js:42)
    at resolvePromise (zone-evergreen.js:798)
    at resolvePromise (zone-evergreen.js:750)
    at zone-evergreen.js:860
    at ZoneDelegate.invokeTask (zone-evergreen.js:399)
    at Object.onInvokeTask (core.js:28500)
    at ZoneDelegate.invokeTask (zone-evergreen.js:398)
    at Zone.runTask (zone-evergreen.js:167)
    at drainMicroTaskQueue (zone-evergreen.js:569)
    at ZoneTask.invokeTask [as invoke] (zone-evergreen.js:484)
    at invokeTask (zone-evergreen.js:1621)

Desktop:

  • Ubuntu 18.04
  • Chrome Version 88.0.4324.182 (Official Build) (64-bit)
  • "angular-auth-oidc-client": "^11.6.2",
investigate

All 22 comments

Hey thanks,

that minimal example is pretty big 馃槂
what are you trying to achieve? I dont understand what you are trying to do to be honest :) Can you clarify?

Thanks

Hello @FabianGosebrink and thanks for answering! Sorry if it seems pretty big:

There is the application, which lazy loads one guarded route named routes. The CanLoad guard on this route throws the previous error if I remove the try/catch section of the app.module.ts. I don't really know what to say appart from that, can you detail what is not clear in order for me to complete it?

The concerned code is mainly the one in the app.module.ts:

import { APP_INITIALIZER, Injectable, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { CanLoad, Route, RouterModule, UrlSegment } from '@angular/router';
import { MyRealmMyClientIdNotFoundComponent } from './components/my-realm-my-client-id-not-found/my-realm-my-client-id-not-found.component';
import { MyRealmMyClientIdRedirectComponent } from './components/my-realm-my-client-id-redirect/my-realm-my-client-id-redirect.component';
import { HttpClient } from '@angular/common/http';
import { AuthModule, AuthWellKnownEndpoints, OidcConfigService, OidcSecurityService, OpenIdConfiguration } from 'angular-auth-oidc-client';
import { combineLatest, forkJoin, of } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';

export interface OidcConfig {
  passedConfig: OpenIdConfiguration;
  passedAuthWellKnownEndpoints?: AuthWellKnownEndpoints;
}

export function configureAuth(httpClient: HttpClient, oidcConfigService: OidcConfigService) {
  const setupAction$ = httpClient
      .get(`assets/index.json`)
      .pipe(
          switchMap((assetsConfig: any) => forkJoin([of(assetsConfig), httpClient.get<OidcConfig>(assetsConfig.stsServer)])),
          switchMap(([assetsConfig, oidcConfig]) => {
              const config: OpenIdConfiguration = {
                  // 1锔忊儯 The oidcConfig fetched is used as a base...
                  ...oidcConfig,
                  // 2锔忊儯 over this base, a customization is applied at build level...
                  forbiddenRoute: 'forbidden',
                  postLogoutRedirectUri: `${window.location.origin}/post-logout-redirect`,
                  redirectUrl: `${window.location.origin}/redirect`,
                  silentRenew: true,
                  silentRenewUrl: `${window.location.origin}/silent-renew.html`,
                  unauthorizedRoute: 'unauthorized',
                  // 3锔忊儯 Finally, the runtime customization layer is applied so users can always customize it on their side
                  ...assetsConfig
              };
              return oidcConfigService.withConfig(config);
          })
      );

  return () => setupAction$.toPromise();
}

@Injectable()
export class IsAuthenticatedCanLoad implements CanLoad {
    constructor(private oidcSecurityService: OidcSecurityService) {}

    canLoad(route: Route, segments: UrlSegment[]) {
        return combineLatest([
            this.oidcSecurityService.isAuthenticated$
        ]).pipe(
            map(([isAuthenticated]) => {
                console.log({ isAuthenticated });
                if (!isAuthenticated) {
                    try {
                        this.oidcSecurityService.authorize();
                    } catch (error) {
                        console.log('apps/my-realm/my-client-id/src/app/app.module.ts', { error });
                        setTimeout(() => {
                            this.oidcSecurityService.authorize({
                              customParams: {
                                postLoginRoute: '/routes/route-B'
                              }
                            });
                        }, 1000);
                    }
                    return false;
                } else {
                    return true;
                }
            })
        );
    }
}

@NgModule({
  declarations: [
    AppComponent,
    MyRealmMyClientIdRedirectComponent,
    MyRealmMyClientIdNotFoundComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    RouterModule.forRoot(
        [
            {
                path: '',
                pathMatch: 'full',
                redirectTo: 'routes'
            },
            {
                path: 'routes',
                canLoad: [IsAuthenticatedCanLoad],
                loadChildren: () => import('@workspace/apps/my-realm/my-client-id/route').then((module) => module.AppsMyRealmMyClientIdRouteModule)
            },
            {
                path: 'redirect',
                pathMatch: 'full',
                component: MyRealmMyClientIdRedirectComponent
            },
            {
                path: '**',
                component: MyRealmMyClientIdNotFoundComponent
            }
        ],
        {
            initialNavigation: 'enabled',
            enableTracing: false
        }
    ),
    AuthModule.forRoot()
  ],
  providers: [
    OidcConfigService,
    {
        provide: APP_INITIALIZER,
        useFactory: configureAuth,
        deps: [ HttpClient, OidcConfigService],
        multi: true
    },
    IsAuthenticatedCanLoad
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Do you need help to understand a specific part of it? :persevere: Did you reach to reproduce the issue or at least to run the application? Do you need help to do so? :persevere:

Hey, thanks for this. This is helpful indeed. Can you confirm that this is an SSR / Angular Universal App? Why dont you use the postLoginRoute from the config? Or is there a reason you put this as a custom param? I will try a sample with a canload guard when I have time and will let you know. Thanks!

Hey, thank you, glad it clarified a little bit :slightly_smiling_face:.

  • Even if there is an App-Shell, this is the only part which is rendered server side and it seems to have no impact in the error I get because even in dev mode the error is still the same (classical ng/nx serve like).
  • The postLoginRoute depend on the route the user typed in the URL in order to redirect directly to this "maybe deep" route instead of simply redirecting always to /redirect, losing the context the user typed in the URL. So this postLoginRoute cannot be known before, that is why I need to use it in the authorize() method. I understood this by reading this issue, am I wrong? If so, how can I finally provide to the user the route originally asked?

Moreover, I think the TypeError I got is not linked to the type of the guard (CanActivate or CanLoad may have no importance), as far as I understand, it is most likely to be a bug on the lifecycle of the lib (the error being: TypeError: Cannot read property 'usePushedAuthorisationRequests' of null at LoginService.login...)... Maybe because of the 2 HTTP requests flow (first one to get the assets/index.json and the other to get the JSON behind the .well-known endpoint)? What do you think about this assumption?

As you can notice, my stsServer and some other OpenID configs depend on the assets/index.json file which is different according to each deployment of the application (philosophy: one production build, then custom configurations via assets).

So as far as I can see until now you protected your first route already. You need an accessible route to handle the login mechanism or you can use the AutoLoginGuard from the lib. Have you tried having a publicly accessible route before?

The routes route is the only one I want to protect, an eventual sibling should be public because not having any guard, I think I don't get your point :persevere:

I don't see any issue to have another first level route named for instance public-routes, not having any guard, is there a problem I don't see?

So I added the above-mentioned "public-routes" route to the MCVE, as expected it is working (you can access public-routes without being authenticated) because the guard does not run, I don't see any link to the issue on this part, can you confirm I am not wrong?

Thanks for labeling @damienbod, but I don't understand why it is not considered a bug, can you explain it to me :persevere: ?

Reminder: without the awful try/catch/setTimeout workaround I put, it is erroring...

Hey,

thanks for your answers. Sorry if I still do not get what the problem is.

I don't see any issue to have another first level route named for instance public-routes, not having any guard

If you do not see an issue, that good, isnt it?

So I added the above-mentioned "public-routes" route to the MCVE, as expected it is working because

Yes because the redirect comes back and can be processed in the publically available route. You said it works. Is it okay now?

The only problem left now is, that you want to have the postLoginRoute dynamically, right?

Thanks

Okay so you consider having the try/catch/setTimeout is the way to go?
Yes it is working: because of this workaround...

The only problem left now is, that you want to have the postLoginRoute dynamically, right?

No because I don't want to use a try/catch...

Hey, sorry for being not clear enough.

You just confirmed that a public route is solving your issue

So I added the above-mentioned "public-routes" route to the MCVE, as expected it is working because ...

is that okay? The public route works for you? Are you achieving what you want with this?

To be more precise: We have two problems here:

1) Guard is not working when it is for an entrypoint for a lazy loaded lib -> You solved this with a public route?
2) Having the postLoginRoute dynamically with a parameter.

are that the two issues?

Thanks

No problem, I thank you a lot for discussing here and for the work you do in this lib.

is that okay? The public route works for you? Are you achieving what you want with this?

I can access it publicly, but it was not what I needed, I only added it to demonstrate to you there is no issue when trying to have a public route but it is not my concern about the try/catch...

Guard is not working when it is for an entrypoint for a lazy loaded lib -> You solved this with a public route?

I really don't see the link between the guarded route and the public one... Anyway : no it is not solved because as soon as I
remove the try/catch, the error comes when I try to access routes/route-a:

ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'usePushedAuthorisationRequests' of null
TypeError: Cannot read property 'usePushedAuthorisationRequests' of null
    at LoginService.login (angular-auth-oidc-client.js:3876)
    at OidcSecurityService.authorize (angular-auth-oidc-client.js:4083)
    at MapSubscriber.project (app.module.ts:75)
    at MapSubscriber._next (map.js:29)
    at MapSubscriber.next (Subscriber.js:49)
    at CombineLatestSubscriber.notifyNext (combineLatest.js:73)
    at InnerSubscriber._next (InnerSubscriber.js:11)
    at InnerSubscriber.next (Subscriber.js:49)
    at BehaviorSubject._subscribe (BehaviorSubject.js:14)
    at BehaviorSubject._trySubscribe (Observable.js:42)
    at resolvePromise (zone-evergreen.js:798)
    at resolvePromise (zone-evergreen.js:750)
    at zone-evergreen.js:860
    at ZoneDelegate.invokeTask (zone-evergreen.js:399)
    at Object.onInvokeTask (core.js:28500)
    at ZoneDelegate.invokeTask (zone-evergreen.js:398)
    at Zone.runTask (zone-evergreen.js:167)
    at drainMicroTaskQueue (zone-evergreen.js:569)
    at ZoneTask.invokeTask [as invoke] (zone-evergreen.js:484)
    at invokeTask (zone-evergreen.js:1621)

Having the postLoginRoute dynamically with a parameter.

What do you call a parameter here? I don't want or need any queryParam, I would like to handle this use case :

  • in a private navigation, a user type in URL /routes/route-a
  • because the user is not authenticated and the routes route is guarded, the user is redirected to the SSO
  • the SSO redirect the user to /redirect and I would like to now redirect the user to /routes/route-a

I thought postLoginRoute what for this purpose, am I right? If yes, it is not working...

Thanks @damienbod for the change of label :heart: !

Hey, I think I do understand your problem now. Your case is that when you have a route which is public and a route which is secured the redirect from the sts lands NOT at the same what the user typed in already when he wanted to access the protected route manually . In addition to that you are using the Guard with CanLoad but the AutoLoginGuard does not implement this interface yet.

So what you can do is: Call the checkAuth() method in your app component as usual and handle the redirect there. So save the route, check if it is there when coming back from the sts and then delete and redirect if a route is present. Don't do it in the guard itself. THe guard only checks if authenticated and if not, does a login/redirect to the sts.

Thanks @FabianGosebrink, your explanation makes sense to me. I will try what you proposed in the second part and will come back with the results here.

Nevertheless, do you think I can help to implement this feature at the guard level or it is not at all a good practice? Other question about helping : do you think adding keycloak in the repository in order to showcase scenarios could be interesting? If yes I can help here too. There are many examples of all the features of this already very useful lib but I find it lacks a "demo" place or something like that to show/try/e2e-test them, what do you think?

On it ;-) We'll keep you posted here. Thanks.

@FabianGosebrink following your fix, can you summarize what have I to implement on my side to make the MCVE working? I suppose this changed the last paragraph you described, isn't it? :persevere:

@FabianGosebrink @hadrien-toma Thanks for this. The fix will be released in version 11.6.4

@FabianGosebrink following your fix, can you summarize what have I to implement on my side to make the MCVE working? I suppose this changed the last paragraph you described, isn't it? 馃槪

We tried to describe it in the docs. You will have to: Use the AutoLoginGuard from the lib and protect the routes you want to protect. AUto Login will be executed on this routes and you will be automatically redirected TO this route once your have been redirected to your app again after logging in. You have to call checkAuth() in your app component.

So you have to do:

1) Use the Guard on your Routes, canActivate and canLoad, both should work
2) Use the checkAuth() call in your app component.

See here

@FabianGosebrink Thanks for the instructions, I can confirm I am able to route the user correctly after authentication is successful by using checkAuth() as you proposed in your second point but only with the use of the CanLoad guard with the try/catch/setTimeout workaround : as soon as I use the AutoLoginGuard I get the error mentioned in this issue.

May you have any key to solve this?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Expelz picture Expelz  路  4Comments

haidelber picture haidelber  路  3Comments

mikeandersun picture mikeandersun  路  4Comments

jhossy picture jhossy  路  4Comments

yelhouti picture yelhouti  路  4Comments