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

login_required error in web browser console when calling `signinRedirectCallback` #1393

Open daxnet opened 3 years ago

daxnet commented 3 years ago

I'm using ASP.NET Core IdentityServer4 as the IdP and oidc-client library in my Angular project to integrate the id service. However, after user login, following error occurred twice in the web browser console:

VM10 vendor.js:12638 ERROR Error: Uncaught (in promise): ErrorResponse: login_required
ErrorResponse: login_required
    at new e (VM10 vendor.js:49237)
    at t [as _processSigninParams] (VM10 vendor.js:49237)
    at t [as validateSigninResponse] (VM10 vendor.js:49237)
    at VM10 vendor.js:49237
    at ZoneDelegate.invoke (VM7 polyfills.js:10689)
    at Object.onInvoke (VM10 vendor.js:34843)
    at ZoneDelegate.invoke (VM7 polyfills.js:10688)
    at Zone.run (VM7 polyfills.js:10451)
    at VM7 polyfills.js:11593
    at ZoneDelegate.invokeTask (VM7 polyfills.js:10723)
    at resolvePromise (VM7 polyfills.js:11530)
    at VM7 polyfills.js:11437
    at asyncGeneratorStep (VM10 vendor.js:61578)
    at _throw (VM10 vendor.js:61601)
    at ZoneDelegate.invoke (VM7 polyfills.js:10689)
    at Object.onInvoke (VM10 vendor.js:34843)
    at ZoneDelegate.invoke (VM7 polyfills.js:10688)
    at Zone.run (VM7 polyfills.js:10451)
    at VM7 polyfills.js:11593
    at ZoneDelegate.invokeTask (VM7 polyfills.js:10723)

My ASP.NET Core IdentityServer4 has the following client configuration:

public static IEnumerable<Client> GetClients() =>
    new[]
    {
        new Client
        {
            RequireConsent = false,
            ClientId = "web",
            ClientName = "Abacuza Administrator",
            AllowedGrantTypes = GrantTypes.Implicit,
            AllowedScopes =
            {
                IdentityServerConstants.StandardScopes.OpenId,
                IdentityServerConstants.StandardScopes.Profile,
                IdentityServerConstants.StandardScopes.Email,
                "roles",
                "api"
            },
            RedirectUris = { "http://localhost:4200/auth-callback" },
            PostLogoutRedirectUris = {"http://localhost:4200/"},
            AllowedCorsOrigins = {"http://localhost:4200"},
            AllowAccessTokensViaBrowser = true,
            AlwaysSendClientClaims = true,
            AlwaysIncludeUserClaimsInIdToken = true,
            AccessTokenLifetime = 3600
        }
    };

In my Angular front-end app, I created the following AuthService:

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

  private loginChangedSubject = new Subject<boolean>();
  private userManager : UserManager;
  private user: User | null = null;

  public loginChanged = this.loginChangedSubject.asObservable();

  constructor() {
    this.userManager  = new UserManager(this.getUserManagerSettings());
  }

  public async login() {
    await this.userManager.signinRedirect();
  }

  public async logout() {
    await this.userManager.signoutRedirect();
  }

  public get currentUser(): User | null {
    return this.user;
  }

  public async completeAuthentication(): Promise<void> {
    const user = await this.userManager.signinRedirectCallback();
    if (this.user !== user) {
      this.user = user;
      this.loginChangedSubject.next(this.checkUser(user));
    }
  }

  public isAuthenticated = (): Promise<boolean> => {
    return this.userManager.getUser()
      .then(user => {
        if (this.user !== user) {
          this.loginChangedSubject.next(this.checkUser(user));
          this.user = user;
        }
        return this.checkUser(user);
      });
  }

  public userHasRole(role: string): boolean {
    const roles: string[] = this.user == null ? [] : this.user.profile.role;
    return roles.findIndex(item => item === role) >= 0;
  }

  public get isAdmin(): boolean {
    return this.userHasRole('admin');
  }

  private checkUser = (user: User | null): boolean => !!user && !user.expired;

  private getUserManagerSettings(): UserManagerSettings {
    return {
      authority: 'http://localhost:9050/',
      client_id: 'web',
      redirect_uri: 'http://localhost:4200/auth-callback',
      post_logout_redirect_uri: 'http://localhost:4200/',
      response_type: 'id_token token',
      scope: 'openid profile email roles api',
      filterProtocolClaims: true,
      loadUserInfo: true,
      automaticSilentRenew: true,
      revokeAccessTokenOnSignout: true
    };
  }
}

Since I configured the redirect_uri to be under the route auth-callback, I then created the following AuthCallback component:


@Component({
  selector: 'app-auth-callback',
  templateUrl: './auth-callback.component.html',
  styleUrls: ['./auth-callback.component.scss']
})
export class AuthCallbackComponent implements OnInit {

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

 async ngOnInit() {
    await this.authService.completeAuthentication();
    this.router.navigate(['/'], { replaceUrl: true });
  }
}

And certainly I put it into the route config such that auth-callback could be redirected to the AuthCallbackComponent:

{ path: 'auth-callback', component: AuthCallbackComponent },

Finally, I used the AuthService in one of my app component:

@Component({
  selector: 'app-main-side-bar',
  templateUrl: './main-side-bar.component.html',
  styleUrls: ['./main-side-bar.component.scss']
})
export class MainSideBarComponent implements OnInit {

  public user: User | null = null;
  public isAdmin: boolean = false;

  constructor (private authService: AuthService) {
    this.authService.loginChanged
      .subscribe(authenticated => {
        if (authenticated) {
          this.user = this.authService.currentUser;
          this.isAdmin = this.authService.isAdmin;
        } else {
          this.user = null;
        }
      });
  }

  ngOnInit(): void {
    this.authService.isAuthenticated()
      .then(_ => {
        this.user = this.authService.currentUser;
        this.isAdmin = this.authService.isAdmin;
      });
  }
}

In the above code, when authService.completeAuthentication() is called in the AuthCallbackComponent, it will then call the signinRedirectCallback method, this is where the error was reported. I just tried several approaches to refine the code flow, but looks like none worked. Did I miss anything? Could someone please help on this?

Thanks in advance!