IdentityModel / oidc-client-js

OpenID Connect (OIDC) and OAuth2 protocol support for browser-based JavaScript applications
Apache License 2.0
2.43k stars 842 forks source link

Oidc-client identityserver infinite loop angular 10 app on signin-redirect #1324

Closed Que2701 closed 3 years ago

Que2701 commented 3 years ago

I have an angular 10 app running on localhost:4200. In my app-routing.module.ts I have configured root path to load the AdminComponent, so if you hit localhost:4200 it will try to load AdminComponent but before it does; I have a authguardservice that checks to see if the user is logged in and if not; it redirects the user to identity server (where he gets a login screen to get the token).

I do not have control on the identity server provider, it is hosted by another company

The connection is to IdentityServer4 with code flow and PKCE.

I researched quiet a bit on this issue, and a lot of people faced the same issue. however I could not find a solution that works for me. probably because I am new to angular and oidc? https://github.com/damienbod/angular-auth-oidc-client/issues/180 https://github.com/damienbod/angular-auth-oidc-client/issues/829 https://stackoverflow.com/questions/57574867/oidc-client-infinite-loop-when-calling-signinredirect

Here is my setup

app-routing.module.ts

const routes: Routes = [
  { path: 'signout-callback', component: SignoutRedirectCallbackComponent   },
  { path: '', component: AdminComponent, canActivate: [AuthGuardService] },
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuardService],
    runGuardsAndResolvers: 'pathParamsChange',
    resolve: { client: ClientResolver },
    children: [
      {
        path: 'dashboard',
        component: DashboardComponent,
        resolve: {
          client: ClientResolver
        }
      }
    ]
  },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {scrollPositionRestoration: 'top'})],
  exports: [RouterModule]
})
export class AppRoutingModule { }

auth-guard.service.ts

@Injectable()
export class AuthGuardService implements CanActivate{
  constructor(private authService:AuthService) { }

  canActivate():boolean{
    if(this.authService.isLoggedIn()) {
      return true;
    }

    this.authService.startAuthentication();
      return false;
  }
}

auth.service.ts

@Injectable({
    providedIn: 'root'
})
export class AuthService{
    private userManager: any;
    private user: User = null;

    constructor(){
        this.userManager = new UserManager(this.getClientSettings());
        this.userManager.getUser().then(user =>{
            this.user = user;
        }); 
    }

    isLoggedIn(): boolean{
        return this.user != null && !this.user.expired;
    }

    getClaims(): any{
        return this.user.profile;
    }

    getAuthorizationHeaderValue(): string{
        return `${this.user.token_type}${this.user.access_token}`;
    }

    startAuthentication(): Promise<void>{
        return this.userManager.signinRedirect();
    }

    completeAuthentication(): Promise<void>{
        return this.userManager.signinRedirectCallback().then(user =>{
            this.user = user;
        });
    }

    public logout(): Promise<void>{
        return this.userManager.signoutRedirect();
    }

    completeLogout(){
        this.user = null;
        return this.userManager.signoutRedirectCallback();
    }

    public getUser() : Promise<User>{
        return this.userManager.getUser();
    }

    getUserToken(){
        this.userManager.getUser().then(user => {
            if(!!user && !user.expired){
                console.log(`User : ${user.profile}`);
                return user.access_token;
            }else{
                return null;
            }
        });
    }

    getAccessToken(): string {
        this.userManager.getUser().then(user =>{
            if(!!user && !user.expired){
                return user.access_token;
            }
        });
        return this.user.access_token;
    }

    LoginRedirectHandler(): Promise<any> {
        console.log("LoginRedirectHandler");
        return this.userManager.getUser().then((user) => {
          // avoid page refresh errors
          if (user === null || user === undefined) {
            return this.userManager.signinRedirectCallback(null);
          }
        });
    }

    getClientSettings() : UserManagerSettings{
        return {
            authority: `${environment.authority}`,
            client_id: environment.clientId,
            client_secret: environment.clientSecret,
            redirect_uri: `${environment.clientRoot}assets/signin-callback.html`,
            automaticSilentRenew: true,
            silent_redirect_uri: `${environment.clientRoot}assets/silent-callback.html`,
            post_logout_redirect_uri: `${environment.clientRoot}signout-callback`,
            response_type: 'code',
            response_mode: 'query',
            scope: environment.clientScope,
            filterProtocolClaims: true,
            loadUserInfo: false
        }
    }
}

When I run the app on localhost:4200 I immediately get redirected to identity server, which is good. Punch in email and password and then identity server redirects to signin-callback.html page login-redirect

The user object has a id_token value as you can see here in the snipped below. and this is also good signin-recirect

And in the network tab of the developer tools, I can see the token is there in the response section , refer to snipped below. so all good so far token

Also in my auth.service.ts constructor the identity server user gets assigned to the property user auth-service

HERE's THE ISSUE: After identity server authenticates and sends us back a user, the signin-callback.html page then routes to "/admin" and here, the authguardservice runs again and tries to check if user is logged in, by calling the isLoggedIn() method of the authservice . (as you can see below) auth-guard-service

The only thing that isLoggedIn()method does, is check if the user object is null or not. and my surprise it is null :( even though we've recently set it's value in the constructor of the authservice

    isLoggedIn(): boolean{
        return this.user != null && !this.user.expired;
    }

the-issue And then this results in our famous endless loop (KMN). You can suggest a link also, because this issue was experienced by a lot of people and I will let you know if the link works for me

brockallen commented 3 years ago

Is there a record in sessionStorage for the user's session? If so, then as far as oidc-client is concerned the user is logged in.

Que2701 commented 3 years ago

@brockallen yes there is ( unfortunately for me ), the issue is persisting the user object from identity server into the angular app layer.

brockallen commented 3 years ago

Then that will be lost when the user hits refresh or you have page transitions

Que2701 commented 3 years ago

@brockallen okay I hear you, so what I did; I just went to sessionStorage and retrieved the user from there and no more endless loop

    isLoggedIn(): boolean{
        this.user = JSON.parse(sessionStorage.getItem("oidc.user")) as User;
        return this.user != null && !this.user.expired;
    }