Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question]: Endless Redirect Loop After Authentication in Standalone Angular App #2045

Open
arharutyu opened this issue Nov 24, 2024 · 7 comments
Labels

Comments

@arharutyu
Copy link

What Version of the library are you using?
18.0.2

Description:

I'm using the angular-auth-oidc-client library in my standalone Angular app (version 18.2.8), with no Angular modules. When I call checkAuth() at the app entry point (app.component.ts), the following issue occurs:

  1. After successful authentication, I am redirected back to the /callback endpoint, which is unprotected.
  2. When I attempt to redirect in callback component to the correct protected route, I end up in an endless redirect loop between /callback and IDP.
  3. If the callback component/route is empty, the redirect works as expected, going from /callback to the root route (home).

Expected Behavior:
After successful authentication, the user should be redirected to the originally requested protected route, not stuck in a redirect loop.

Current Behavior:
After authentication, the app is stuck in a redirect loop between /callback and IDP.

Steps to Reproduce:

  1. Use angular-auth-oidc-client v18.0.2 in a standalone Angular app (no Angular modules).
  2. /callback is unprotected, all other routes are protected, with '/' redirecting to '/home'
  3. Call checkAuth() in the app.component.ts.
  4. After successful authentication, the app redirects to the /callback route.
  5. If I attempt to redirect to a protected route within callback, it results in an endless redirect loop.
  6. If the callback component/route is empty, the redirect properly takes me to the home route (root).

Environment:
Angular version: 18.2.8
angular-auth-oidc-client version: 18.0.2
Browser: Chrome or Edge

What I've Tried:
Ensured the callback route is correctly configured in the routing module.
Tried adding logic in the callback component to navigate to the required route, but it still results in the redirect loop.

Questions:
Why is this endless redirect loop happening?
How can I ensure that the callback works as expected and navigate to the protected route after authentication?

Code Snippets:
Relevant code snippets provided, further can be provided if needed!

app.routes.ts

export const routes: Routes = [
    {
        path: '',
        children:
            [
                {
                    path: 'callback',
                    component: CallbackComponent,
                },
		// ... other unprotected routes
                {
                    path: '',
                    component: LayoutComponent,
                    canActivateChild: [authGuard],
                    children: [
                        {
                            path: '',
                            redirectTo: 'home',
                            pathMatch: 'full'
                        },
                        {
                            path: 'home',
                            component: HomeComponent,
                        },
			// ... other protected routes
                    ]
                }
            ]
    }
];

app.component.ts

  private readonly AuthService = inject(AuthService);

  ngOnInit() {
    this.AuthService.initializeAuth();
  }

auth.service.ts

  constructor() {
    this.oidcSecurityService.isAuthenticated$.subscribe(
      (result) => {
        this._isAuthenticated$.next(result.isAuthenticated);
      }
    );
  }

  initializeAuth() {
    this.oidcSecurityService.checkAuth().subscribe(({ isAuthenticated, userData}) => {
      console.log(isAuthenticated);
      console.log(userData)
    });
  }
@spunzmann
Copy link

@arharutyu after moving from version 18.0.1 to 18.0.2 I had the same behavior. After some debuging I learned that a new config property was introduced in 18.02 which caused the issue for me:

The new property OpenIdConfiguration.checkRedirectUrlWhenCheckingIfIsCallback (defaults to true) enables a newly introduced feature in UrlService.isCallbackFromSts that will do a more sophisticated check to determine whether the provided URL represents the redirect URL and therefore should be treated as a callback.

Setting this property to false solved my problem as it essentially reverts back to the same logic that was applied in version 18.0.1.

Symptomy of the problem seems to be the same, but not sure if the root cause is the same, but I thought it is worth sharing.

@leopoliveira
Copy link

@arharutyu after moving from version 18.0.1 to 18.0.2 I had the same behavior. After some debuging I learned that a new config property was introduced in 18.02 which caused the issue for me:

The new property OpenIdConfiguration.checkRedirectUrlWhenCheckingIfIsCallback (defaults to true) enables a newly introduced feature in UrlService.isCallbackFromSts that will do a more sophisticated check to determine whether the provided URL represents the redirect URL and therefore should be treated as a callback.

Setting this property to false solved my problem as it essentially reverts back to the same logic that was applied in version 18.0.1.

Symptomy of the problem seems to be the same, but not sure if the root cause is the same, but I thought it is worth sharing.

This solved my problem too.
I had this and lost two entire days looking for a solution without success.

@spunzmann how did you discover that ?

@rammba
Copy link

rammba commented Jan 6, 2025

Hello @arharutyu, is this maybe similar to my issue #2040?

@jaigtz88
Copy link

jaigtz88 commented Jan 7, 2025

@spunzmann , how do you set OpenIdConfiguration.checkRedirectUrlWhenCheckingIfIsCallback to false?

@spunzmann
Copy link

@leopoliveira I simply debugged my application with the browsers dev tools and eventually figured it out. I got lucky I guess ;-)

@jaigtz88 the property checkRedirectUrlWhenCheckingIfIsCallback is defined in the OpenIdConfiguration interface. So you set it wherever you define the OpenIdConfiguration in your application. This property has been introduced with 18.0.2

@onkobu
Copy link

onkobu commented Feb 5, 2025

I have the same issue with 18.0.2. Coming from 17.x with a simple ng update …@18 yielded a broken login. I can confirm that the first steps work properly. The service requests the code from the OIDC issuer, also queries config via HTTP and does not proceed any further. With the property set to false it takes the issued code and fetches the token, too.

It gets even weirder with a filled localstorage. Since that token is very likely expired it gets loaded instead and it looks as if the user is not logged in.

This behavior is very hard to track and it took me 2 days to find this issue. I am curious, too, how this could slip through testing. Users of this library will not test this but use a mock for the service instead.

@arharutyu
Copy link
Author

arharutyu commented Feb 12, 2025

As this issue cropped up when I was on a timeline and needed to implement redirect to the requested URL the user is accessing prior to login I went ahead with a workaround. By the time @spunzmann commented I had switched to dynamically loading config and for some reason their suggestion wasn't working for me either.

To avoid the endless redirect loop

  1. auth guard stores requested url in local storage before redirecting to login
  2. redirectUrl stays same to '/callback' (and app will go there after successful login then redirect to home)
  3. home component then redirects to requested URL from local storage

This is the relevant code I used in case it's useful for others:

auth-config-loader.factory.ts

export const httpLoaderFactory = (httpClient: HttpClient) => {
  const config$ = httpClient.get<any>('/assets/config.json').pipe(
    map((customConfig: any) => {
      return {
        authority: customConfig.authority,
        redirectUrl: window.location.origin + '/callback',
        postLogoutRedirectUri: window.location.origin + '/logout',
        clientId: 'xxx',
        scope: 'xxx',
        responseType: 'code',
        silentRenew: true,
        silentRenewUrl: window.location.origin + '/silent-renew.html',
        renewTimeBeforeTokenExpiresInSeconds: 10,
        checkRedirectUrlWhenCheckingIfIsCallback: false,
      };
    })
  );

  return new StsConfigHttpLoader(config$);
};

main.ts

import { provideAuth, StsConfigLoader } from 'angular-auth-oidc-client';

appConfig.providers = [
  ...(appConfig.providers || []),
  {
    provide: APP_INITIALIZER,
    useFactory: loadConfig,
    deps: [ConfigService],
    multi: true,
  },
  provideAuth({
    loader: {
      provide: StsConfigLoader,
      useFactory: httpLoaderFactory,
      deps: [HttpClient],
    }
  })
];

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

Updated AuthService (relevant functions only):
auth.service.ts

  constructor(private router: Router) {
// initialize auth now called in app component to allow dynamic loading of config providing authority URL and configs
  }

  initializeAuth() {
    if (!this._isInitialized$.getValue()) {
      combineLatest([
        this.oidcSecurityService.checkAuth(),
        this.oidcSecurityService.getAccessToken(),
      ])
        .pipe(
          // filter out null values to avoid first API call after app start failing
          filter(([authResult, accessToken]) => authResult?.isAuthenticated && !!accessToken)
        )
        .subscribe({
          next: ([authResult, accessToken]) => {
            this._isAuthenticated$.next(authResult.isAuthenticated);
            this._accessToken$.next(accessToken);

            this._isInitialized$.next(true);
          },
          error: (error) => {
            console.error('Failed to initialize authentication:', error);
          },
        });
    }
  }

  postLoginRedirect() {
    const postLoginRedirectUrl = localStorage.getItem('postLoginRedirectUrl');
    if (postLoginRedirectUrl) {
      this.router.navigateByUrl(postLoginRedirectUrl);
      localStorage.removeItem('postLoginRedirectUrl');
    }
  }

auth.guard.ts

export const authGuard = () => {
  const authService = inject(AuthService);
  const router = inject(Router);

  return authService.isAuthenticated$.pipe(
    take(1),
    map(isAuthenticated => {
      if (isAuthenticated) {
        return true; // User is authenticated, allow route access
      } else {
        const postLoginRedirectUrl = router.getCurrentNavigation()?.extractedUrl.toString() || '/';
        localStorage.setItem('postLoginRedirectUrl', postLoginRedirectUrl); // capture url being navigated to for post login url
        authService.redirectToLogin(); // Redirect to login
        return false;
      }
    })
  );
};

app.component.ts

  private readonly AuthService = inject(AuthService);

  ngOnInit() {
    this.AuthService.initializeAuth();
  }

home.component.ts

  private authService = inject(AuthService)
  private idleKeepAliveService = inject(IdleKeepAliveService)

  constructor() {
    this.authService.postLoginRedirect();
    // initialize idle service after successful login
    this.idleKeepAliveService.initialize();
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants