angular-auth-oidc-client icon indicating copy to clipboard operation
angular-auth-oidc-client copied to clipboard

[Question]: Cannot log migrating from 13 to 14, with StsConfigLoader and AuthInterceptor

Open AlvaroP95 opened this issue 1 year ago • 2 comments

What Version of the library are you using? Library 14.0.0 (also tried with 14.1.5), Angular 14.1.1, rxjs 7.5.0, typescript 4.6.3

Everything was working perfecly fine with this magnific library on Angular 14, with angular-auth-oidc-client v11, v12 and v13. When I updated the library to v14, loggin stopped working. I only had to make the getAccessToken() as observable migration change.

At first I was getting a big, but not infinite loop, in AuthInterceptor's getAccessToken() , which can be avoided with

if (req.url.includes(environment.apiUrl))

But neither way allows me to loggin after, it just redirects to unauthorized after I signIn with this.oidcSecurityService.authorize().

My code, which worked in v11, v12 and v13:

core.module.ts

export const httpLoaderFactory = (httpClient: HttpClient) => {
  const envInjector = inject(EnvironmentInjector);
  const config$ = httpClient
    .get<any>(`${environment.authServerUrl}/tenant/config`)
    .pipe(
      map((customConfig: any) => {
        environment.logo = customConfig.logo;
        environment.backgroundImage = customConfig.backgroundImage;
        const lang = envInjector.runInContext(() => getLanguageToSet());
        const origin = lang === LanguageCodes.English
          ? customConfig.origin
          : `${customConfig.origin}/${lang}`;

        const config: OpenIdConfiguration = {
          authority: customConfig.stsServer,
          redirectUrl: origin,
          responseType: "code",
          clientId: customConfig.clientId,
          scope: "openid profile email inspections",
          postLogoutRedirectUri: origin,
          silentRenew: true,
          silentRenewUrl: `${origin}/silent-renew.html`,
          postLoginRoute: getPostLoginRoute(),
          forbiddenRoute: "/forbidden",
          unauthorizedRoute: "/unauthorized",
          maxIdTokenIatOffsetAllowedInSeconds: 20,
          logLevel: 1,
          historyCleanupOff: true,
          disableIatOffsetValidation: true,
        };

        return config;
      })
    )
  
  return new StsConfigHttpLoader(config$);
};


@NgModule({
  declarations: [
    NavComponent,
    LoginComponent,
  ],
  imports: [
    AuthModule.forRoot({
      loader: {
        provide: StsConfigLoader,
        useFactory: httpLoaderFactory,
        deps: [HttpClient],
      },
    }),
    HttpClientModule,
    RouterModule,
  ],
  providers: [
    AuthService,
    AuthGuard,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true,
    },
  ],
  exports: [AuthModule, NavComponent],
})
export class CoreModule {}

auth-interceptor.ts

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  private oidcSecurityService: OidcSecurityService;

  constructor(private injector: Injector, private router: Router) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    let requestToForward = req;
    if (this.oidcSecurityService === undefined) {
      this.oidcSecurityService = this.injector.get(OidcSecurityService);
    }

    if (this.oidcSecurityService !== undefined) {
      if (req.url.includes(environment.apiUrl)) { // Added in v14 to avoid the big getAccessToken loop
        try { // I had to add this try catch in v13, because it stopped the app otherwise
          this.oidcSecurityService.getAccessToken().subscribe({ // Before v14, it was not an observable, it was a constant
            next: token => {
              if (token !== "" && !req.headers.has(InterceptorSkipHeader)) {
                const tokenValue = `Bearer ${token}`;
                requestToForward = req.clone({
                  setHeaders: { Authorization: tokenValue },
                });
              }
            },
            error: error => console.error(error)
          });
        } catch { /* */ }
      }
    }

    return next.handle(requestToForward).pipe(
      catchError((err: HttpErrorResponse) => {
        if (err.status === 401) {
          this.router.navigateByUrl("/login");
        }

        return throwError(() => new Error(err.message));
      })
    );
  }
}

app.component.ts

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.sass"],
})
export class AppComponent implements OnInit {

  isAuthenticated: boolean;
  userData = userData;

  constructor(
    public oidcSecurityService: OidcSecurityService,
    private authService: AuthService
  ) { }

  ngOnInit(): void {
    this.oidcSecurityService
      .checkAuth()
      .subscribe(auth => {
        this.isAuthenticated = auth.isAuthenticated;

        if (auth.isAuthenticated) {
          this.authService.userData.subscribe(data => {
            setUserData(data.userData);
            this.userData = data.userData;
          });
        }
      });
  }

app.component.html

<app-nav  *ngIf='!isAuthenticated || userData'>
  <router-outlet></router-outlet>
</app-nav>

auth-guard.ts

@Injectable({ providedIn: "root" })
export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
  canActivateChild(
    _childRoute: ActivatedRouteSnapshot,
    _state: RouterStateSnapshot
  ): Observable<boolean> {
    return this.checkUser();
  }
  constructor(
    private router: Router,
    private oidcSecurityService: OidcSecurityService
  ) {}

  canActivate(
    _route: ActivatedRouteSnapshot,
    _state: RouterStateSnapshot
  ): Observable<boolean> {
    return this.checkUser();
  }

  canLoad(_state: Route): Observable<boolean> {
    return this.checkUser();
  }

  private checkUser(): Observable<boolean> {
      return this.oidcSecurityService.isAuthenticated$.pipe(
        map(isAuthorized => {
          if (!isAuthorized.isAuthenticated) {
            const url = window.location.pathname + window.location.search;

            if (!url?.includes("?code"))
              localStorage.setItem("page-before-unauthorized", url);

            // If it starts with "?code", it means it is logging in, so avoid navigation
            if (!localStorage.getItem("page-before-unauthorized")?.includes("?code")
              || !url?.includes("?code")
            )
              this.router.navigate(["unauthorized"]);

            return false;
          }

          return true;
        })
      );
  }
}

AlvaroP95 avatar Apr 06 '23 16:04 AlvaroP95

I have the same problem аfter updating to 14 or 15 version. Auth interceptor starts inifinite loop, when query is sending. But if I just emulate observable for StsConfigHttpLoader it works perfect. And it not depends how I get token in interceptor. Did you find decision?

function httpLoaderFactory(http: HttpClient): StsConfigHttpLoader {
    // const query = http.get<{ identityServerUrl: string }>(  //infinite intercept
    //     '/api/authorization/getidentityserverconfiguration',
    // );
    const query = of({ // it works good
        identityServerUrl: 'http://localhost:9505',
    });
    const config$ = query.pipe(
        map((config: { identityServerUrl: string }) =>
            getOidcConfiguration(appUrl, config.identityServerUrl),
        ),
    );

    return new StsConfigHttpLoader(config$);
}

Now I fix problem with fetch() instead httpClient.

XadGar avatar Apr 13 '23 11:04 XadGar

I fixed it replacing isAuthenticated$ with awaiting this

  async isAuthenticated() {        
    const state$ = this.oidcSecurityService.isAuthenticated();
    return lastValueFrom(state$);
  }

AlvaroP95 avatar Apr 25 '23 15:04 AlvaroP95