Angular Guard - oidc-client returns null user when url is provided manually

783 views Asked by At

My Angular 11 app uses oidc-client with Implicit flow. I have protected one of my routes (which is supposed to be available only for authenticated users) with guard. It works just fine when I navigate to restricted area (let's say http://localhost:4200/restricted-area/) using links in my application. However, the problem is that when I try to navigate to that very page "manually" (e.g. typing url in my browser), application treats me as if I am not authenticated. The same issue comes up when I simply refresh the page after navigating "properly". Interestingly, in both cases after redirect to main page I am logged in again.

As a workaround, I was thinking about implementing additional manual extraction of token from session storage in the beginning of [canActivate] method, but I hope there is better way to solve this.

AuthGuard implementation:

import { Injectable } from '@angular/core';
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { AuthService } from './shared/services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {

  constructor(private router: Router, private authService: AuthService) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    if (this.authService.isAuthenticated()) { return true; }
    alert('Authentication required.');
    this.router.navigate(['/menu'], { queryParams: { redirect: state.url }, replaceUrl: true });
    return false;
  }

}

AuthService implementation looks as follows:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { UserManager, UserManagerSettings, User } from 'oidc-client';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class AuthService  {

  private _authNavStatusSource = new BehaviorSubject<boolean>(false);
  authNavStatus$ = this._authNavStatusSource.asObservable();

  private manager = new UserManager(getClientSettings());
  private user: User | null;

  constructor(private http: HttpClient) {
    this.manager.getUser().then(user => {
      this.user = user;
      this._authNavStatusSource.next(this.isAuthenticated());
    });
  }

  login() {
    return this.manager.signinRedirect();
  }

  async completeAuthentication() {
      this.user = await this.manager.signinRedirectCallback();
      this._authNavStatusSource.next(this.isAuthenticated());
  }

  register(userRegistration: any) {
    // return this.http.post(this.configService.authApiURI + '/account', userRegistration).pipe(catchError(this.handleError)); // temporary disabled
  }

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

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

  get name(): string {
    return this.user != null ? this.user.profile.name : '';
  }

  async signout() {
    await this.manager.signoutRedirect();
  }
}

export function getClientSettings(): UserManagerSettings {
  return {
      authority: 'http://xxxxxxxxxx',
      client_id: 'xxxxxxxxxxx',
      redirect_uri: 'http://xxxxxxxxxxx/auth-callback',
      post_logout_redirect_uri: 'http://xxxxxxxxxxx',
      response_type:"id_token token",
      scope:"xxxxxxxxxxxxxxxxxxxxxxxxxx",
      filterProtocolClaims: true,
      loadUserInfo: true,
      automaticSilentRenew: true,
      silent_redirect_uri: 'xxxxxxxxxxxxxxxxxxxxxxxxx'
  };
}

Routing (app):

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthCallbackComponent } from './auth-callback/auth-callback.component';
import { AuthGuard } from './auth.guard';
import { MenuComponent } from './menu/menu.component';

const routes: Routes = [
  { path: 'menu', component: MenuComponent },
  { path: 'auth-callback', component: AuthCallbackComponent },
  { path: 'suppliers', redirectTo: 'suppliers', pathMatch: 'full', canActivate: [AuthGuard] },
  { path: '**', redirectTo: '/menu', pathMatch: 'full' }
];

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

Routing (parent module):

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from '../auth.guard';
import { SupplierContactFormComponent } from './supplier-contact-form/supplier-contact-form.component';
import { SupplierFormComponent } from './supplier-form/supplier-form.component';
import { SupplierListComponent } from './supplier-list/supplier-list.component';
import { SupplierViewComponent } from './supplier-view/supplier-view.component';
import { SupplierResolver } from './supplier.resolver';
import { SupplierComponent } from './supplier/supplier.component';

const routes: Routes = [
  {
    path: 'suppliers', canActivate: [AuthGuard], children: [
      { path: '', component: SupplierListComponent },
      { path: ':supplierId', component: SupplierComponent, resolve: { supplier: SupplierResolver }, children: [
        { path: '', pathMatch: 'full', component: SupplierViewComponent },
        { path: 'edit', component: SupplierFormComponent},
        { path: 'create-contact', component: SupplierContactFormComponent },
        { path: 'edit-contact/:contactId', component: SupplierContactFormComponent },
      ] },
    ]
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class SupplierRoutingModule { }
0

There are 0 answers