0

I have an angular application with an OAuth2 authentication that blocks my pages thanks to the 'angular-oauth2-oidc' library, I need to recover a role and permissions with this token via an api. But so I autilise AuthGuard for block route by role; but the problem is that for this AuthGuard I need the token or the role check comes first.

I therefore looked for how to manage multiple AuthGuard, and I found the solution of a creates a master that manage the call order, but my problem is due to the fact that I don’t know how to manually call OAuth2, or then I would need to override the AuthGuard, so since I can’t do either I’m a blocker.

const configAuthZero: AuthConfig = environment.idp;

export function storageFactory(): OAuthStorage {
  return localStorage
}

@NgModule({
  imports: [OAuthModule.forRoot({
    resourceServer: {
        allowedUrls: [environment.adressUrl],
        sendAccessToken: true
    }
})],
  providers: [
    InitialAuthService,
    { provide: AuthConfig, useValue: configAuthZero },
    { provide: OAuthStorage, useFactory: storageFactory },
    {
      provide: APP_INITIALIZER,
      useFactory: (initialAuthService: InitialAuthService) => () =>
        initialAuthService.initAuth(),
      deps: [InitialAuthService],
      multi: true,
    },
  ],
})
export class AuthModule { }


@Injectable({
  providedIn: "root",
})
export class InitialAuthService {
  private jwtHelper: JwtHelperService = new JwtHelperService();

  private isAuthenticatedSubject = new BehaviorSubject<boolean>(false);
  private isAuthenticated = this.isAuthenticatedSubject.asObservable();
  
  private _decodedAccessToken: any;
  private _decodedIDToken: any;

  get decodedAccessToken() {
    return this._decodedAccessToken;
  }
  get decodedIDToken() {
    return this._decodedIDToken;
  }

  get profile() {
    return this.oauthService.loadUserProfile();
  }

  constructor(
    private oauthService: OAuthService,
    private authConfig: AuthConfig,
    public router: Router,
    private authService: AuthService,
  ) { 
  }

  async initAuth(): Promise<any> {
    return new Promise<void>((resolveFn, rejectFn) => {

      this.oauthService.configure(this.authConfig);
      this.oauthService.setStorage(localStorage);
      this.oauthService.tokenValidationHandler = new JwksValidationHandler();

      this.oauthService.events
        .pipe(filter((e: any) => e.type === "token_received"))
        .subscribe(({ type }) => {
          this.handleNewToken();
        });

        this.oauthService.events
        .pipe(filter((e: any) => e.type === "token_expires"))
        .subscribe(({ type }) => {
          console.debug("token_expires");
          this.logoutSession();
        });

        this.oauthService.events
        .pipe(filter((e: any) => e.type === "token_error"))
        .subscribe(({ type }) => {
          console.debug("token_error");
          this.logoutSession();
        });

        this.oauthService.events
        .subscribe(_ => {
          this.isAuthenticatedSubject.next(this.oauthService.hasValidAccessToken());
        });
      
        
      this.oauthService.loadDiscoveryDocumentAndLogin().then(
        (isLoggedIn) => {
          if (isLoggedIn) {            
            resolveFn();
          } else {
            this.oauthService.initImplicitFlow();
            rejectFn();
          }
        },
        (error: { status: number; }) => {
          console.log({ error });
          if (error.status === 400) {
            location.reload();
          }
        }
      );
    });
  }


  private handleNewToken() {
    
    this._decodedAccessToken = this.jwtHelper.decodeToken(
      this.oauthService.getAccessToken()
    );

    this._decodedIDToken = this.jwtHelper.decodeToken(
      this.oauthService.getIdToken()
    );

    this.authService.login();
  }

  logoutSession() {
    this.oauthService.logOut();
    this.authService.logout();
  }

  isLogin() : Observable<boolean> {
   return  this.isAuthenticated;
  }
}

@Injectable()
export class AuthService {

    constructor(private userRoleService: UserRoleService) {
    }

    private user!: UserRole|null;

    isAuthorized() {
        return !!this.user;
    }

    hasRole(role: Role) {
        return  this.user!=null && this.isAuthorized() && this.user.id === role;
    }

    login() {
      this.userRoleService.getByUserRoleToken().subscribe({
        next: (res) => {
            this.user = res;
        },
        error: (e) => {
          console.error(e);
        },
        complete: () => {
        }
      });

    }

    logout() {
      this.user = null;
    }
}
@Injectable()
export class AuthGuard implements CanActivate, CanLoad {
    constructor(
        private router: Router,
        private authService: AuthService
    ) { }

    canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
        if (!this.authService.isAuthorized()) {
            this.router.navigate(['login']);
            return false;
        }

        const roles = route.data['roles'] as Role[];
        
        if (roles && !roles.some(r => this.authService.hasRole(r))) {
            this.router.navigate(['error', 'not-found']);
            return false;
        }

        return true;
    }

    canLoad(route: Route): Observable<boolean> | Promise<boolean> | boolean {
        if (!this.authService.isAuthorized()) {
            return false;
        }

        const roles = route.data && route.data["roles"] as Role[];
        if (roles && !roles.some(r => this.authService.hasRole(r))) {
            return false;
        }

        return true;
    }
}
const routes: Routes = [

 
  {
    path: 'admin',
    canLoad: [AuthGuard],
    canActivate: [AuthGuard],
    data: {
      roles: [
        "ADMIN",
      ]
    },
    loadChildren: () => import('./admin/admin-routing.module').then(m => m.AdminRoutingModule),
},
  { path: 'user', loadChildren: () => import('./auth/user/user.module').then(m => m.UserModule),},
  // Always last
  { path: '**', component: PageNotFoundComponent },

];

@NgModule({
  imports: [RouterModule.forRoot(routes),],
  exports: [RouterModule],
  declarations: [  
    
  ],  
  providers: [ 
    AuthGuard,
    AuthService 
  ], 
})
export class AppRoutingModule { }

Jack Boch
  • 127
  • 1
  • 9

1 Answers1

0

You seem to have two options:

  1. Make the APP_INITIALIZER not resolve as you do now, but only when the role service has fully resolved user roles. That way you'd be sure the roles are known by the time auth guards run;
  2. Make the guards wait for the roles to be loaded.

Some (pseudo) code to approach the second option:

@Injectable()
export class AuthService {
    // other code here

    private isDoneLoadingSubject$ = new BehaviorSubject<boolean>(false);
    public isDoneLoading$ = this.isDoneLoadingSubject$.asObservable();

    login() {
      this.userRoleService.getByUserRoleToken().subscribe({
        next: (res) => {
            this.user = res;
            this.isDoneLoadingSubject$.next(true); // <<-- added
        },
      // etc.

And the guard along these lines:

canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.authService.isDoneLoading$.pipe(
        filter(done => done),
        map(_ => {
            if (!this.authService.isAuthorized()) {
                this.router.navigate(['login']);
                return false;
            }
    
            const roles = route.data['roles'] as Role[];
            
            if (roles && !roles.some(r => this.authService.hasRole(r))) {
                this.router.navigate(['error', 'not-found']);
                return false;
            }
    
            return true;
        })
    );
}
        
Jeroen
  • 60,696
  • 40
  • 206
  • 339
  • i add the second solution in canLoad and canActivate but not work's because in first case never go on map – Jack Boch Nov 02 '22 at 12:45
  • And how use the first solution with APP_INITIALIZER I don't know how to do that – Jack Boch Nov 02 '22 at 12:47
  • You can check [the app initializer in my sample project](https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/blob/master/src/app/core/auth-app-initializer.factory.ts) which relies on [an initial login sequence](https://github.com/jeroenheijmans/sample-angular-oauth2-oidc-with-auth-guards/blob/690790b81e73aa1d6b287a5d04f26bbf70e3c4cf/src/app/core/auth.service.ts#L85-L164), you can add your loading of roles as a step before the login sequence emits it's done. – Jeroen Nov 02 '22 at 12:49