question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Auth callback continually redirecting

See original GitHub issue

Issue type

I’m submitting a … (check one with “x”)

  • bug report
  • feature request

Issue description

Current behavior: I’ve followed the documentation for setting up OAuth authentication (which I’ve done in the ngx-admin starter app) - instead of Google however I’m setting up Azure AD B2C authentication.

The login request works fine, it goes to Microsoft’s auth servers and then returns with an id_token in the callback url. However, when the callback happens and the router directs to the nb-oauth2-callback.component.ts, the ‘Authenticating…’ message shows up briefly, and then instead of redirecting to the homescreen as I’ve defined it to redirect to (as per the docs), it instead goes through the login process again, repeating the process in a never-ending loop.

The url of the callback from the Microsoft auth server looks like this: https://localhost:5001/auth/callback#id_token=eyJ0eXAiOiJKV1Q-REST-OF-TOKEN

I’ve tried to do some debugging, and have noticed a couple of things when going through the code -

There’s an exception in the token-parceler.js file: Cannot read property 'name' of null at NbAuthTokenParceler.push...

And a ‘no match’ exception in the router.js file with the url parameter in 23. this.Recognizer showing as ‘auth/callback#id_tokeneyJ0eX-REST-OF-TOKEN’.

Not sure if this is relevant to the redirect looping issue however seems like the token isn’t being recognised/intercepted from the url header and that the router is treating it as a url fragment.

Really appreciate any help in figuring this out!

Expected behavior:

Steps to reproduce: In the ngx-admin starter app, I’ve created a nb-oauth2-login.component.ts like so:

import { Component, OnDestroy } from '@angular/core';
import { NbAuthOAuth2Token, NbAuthResult, NbAuthService } from '@nebular/auth';
import { takeWhile } from 'rxjs/operators';

@Component({
  selector: 'ngx-oauth2-login',
  template: `
  <nb-layout>
    <nb-layout-column>
      <nb-card>
        <nb-card-body>
          <p>Current User Authenticated: {{ !!token }}</p>
          <p>Current User Token: {{ token|json }}</p>
          <button class="btn btn-success" *ngIf="!token" (click)="login()">Sign In</button>
          <button class="btn btn-warning" *ngIf="token" (click)="logout()">Sign Out</button>
        </nb-card-body>
      </nb-card>
    </nb-layout-column>
  </nb-layout>
  `,
})
export class NbOAuth2LoginComponent implements OnDestroy {

  alive = true;
  token: NbAuthOAuth2Token;

  constructor(private authService: NbAuthService) {
    this.authService.onTokenChange()
      .pipe(takeWhile(() => this.alive))
      .subscribe((token: NbAuthOAuth2Token) => {
        this.token = null;
        if (token && token.isValid()) {
          this.token = token;
        }
      });
  }

  login() {
    this.authService.authenticate('AzureADB2C')
      .pipe(takeWhile(() => this.alive))
      .subscribe((authResult: NbAuthResult) => {
      });
  }

  logout() {
    this.authService.logout('AzureADB2C')
      .pipe(takeWhile(() => this.alive))
      .subscribe((authResult: NbAuthResult) => {
      });
  }

  ngOnDestroy(): void {
    this.alive = false;
  }
}

And a nb-oauth2-callback.component.ts like so:

import { Component, OnDestroy } from '@angular/core';
import { NbAuthResult, NbAuthService } from '@nebular/auth';
import { Router } from '@angular/router';
import { takeWhile } from 'rxjs/operators';

@Component({
  selector: 'ngx-oauth2-callback',
  template: `
      Authenticating...
  `,
})
export class NbOAuth2CallbackComponent implements OnDestroy {

  alive = true;

  constructor(private authService: NbAuthService, private router: Router) {
    this.authService.authenticate('AzureADB2C')
      .pipe(takeWhile(() => this.alive))
      .subscribe((authResult: NbAuthResult) => {
        if (authResult.isSuccess() && authResult.getRedirect()) {
          this.router.navigateByUrl(authResult.getRedirect());
        }
      });
  }

  ngOnDestroy(): void {
    this.alive = false;
  }
}

Set up my core.module.ts file with an authentication strategy like so:

export const NB_CORE_PROVIDERS = [
  ...DataModule.forRoot().providers,
  ...NbAuthModule.forRoot({
    strategies: [
      NbOAuth2AuthStrategy.setup({
        name: 'AzureADB2C',
        clientId: '************************************',
        clientSecret: '',
        authorize: {
          // tslint:disable-next-line:max-line-length
          endpoint: 'https://login.microsoftonline.com/tfp/pixeldr.onmicrosoft.com/b2c_1_pxdrsignin/oauth2/v2.0/authorize',
          responseType: NbOAuth2ResponseType.TOKEN, // have changed in the nebular/auth package (oauth2-strategy.options.js) from 'token' to 'id_token',
          scope: 'https://PixelDr.onmicrosoft.com/api/profile openid profile',
          redirectUri: 'https://localhost:5001/auth/callback',
        },
        token: {
          endpoint: 'id_token',
          class: NbAuthJWTToken, // changed from NbAuthOAuth2Token as ADB2C returns JWT
        },
        redirect: {
          success: '/pages/overview',
        },
      }),
    ],
    forms: {
      login: {
        socialLinks: socialLinks,
      },
      register: {
        socialLinks: socialLinks,
      },
    },
  }).providers,

And finally modified the app-routing.module.ts like so:

const routes: Routes = [
  { path: 'pages', loadChildren: 'app/pages/pages.module#PagesModule' },
  {
    path: 'auth',
    component: NbAuthComponent,
    children: [
      {
        path: '',
        component: NbOAuth2LoginComponent,
      },
      {
        path: 'login',
        component: NbLoginComponent,
      },
      {
        path: 'register',
        component: NbRegisterComponent,
      },
      {
        path: 'logout',
        component: NbLogoutComponent,
      },
      {
        path: 'request-password',
        component: NbRequestPasswordComponent,
      },
      {
        path: 'reset-password',
        component: NbResetPasswordComponent,
      },
      {
        path: 'callback',
        component: NbOAuth2CallbackComponent,
      },
    ],
  },
  { path: '', redirectTo: 'pages', pathMatch: 'full' },
  { path: '**', redirectTo: 'pages' },
];

const config: ExtraOptions = {
  useHash: false,
};

This is all using the latest Nebular release.

Thanks again in advance for any help!

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Comments:13

github_iconTop GitHub Comments

1reaction
jjgriff93commented, Aug 9, 2018

Hi @nnixaa - thank you, after comparing the above I noticed for some reason that I was missing map(fragment => this.parseHashAsQueryParams(fragment)), which looks like it was the key ingredient - now working 😃 I’ll do some tests over the next few days and integrate identity properly to see if all of the values are parsed properly, but it’s not continually redirecting anymore which is great news.

Can’t thank you enough for your help! 😃

0reactions
nnixaacommented, Aug 9, 2018

Hey @jjgriff93, I guess your router setting useHash is set to false, isn’t it? I most likely missed that part modifying that example. This one works fine on my side (note I have to remove some of imported files as I don’t have those):

import { ModuleWithProviders, NgModule, Optional, SkipSelf, Injectable, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
// tslint:disable-next-line:max-line-length
import { NbOAuth2AuthStrategy, NbAuthModule, NbOAuth2ResponseType, NbAuthOAuth2Token, NbAuthResult, NbOAuth2AuthStrategyOptions, NbAuthStrategyClass } from '@nebular/auth';
import { NbSecurityModule, NbRoleProvider } from '@nebular/security';
import { of as observableOf } from 'rxjs';

import { throwIfAlreadyLoaded } from './module-import-guard';
import { DataModule } from './data/data.module';
import { map } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';
import { ActivatedRoute } from '@angular/router';
import { NB_WINDOW } from '@nebular/theme';
import { AnalyticsService } from './utils/analytics.service';
import { I18nService } from './data/i18n.service';

const socialLinks = [
  {
    url: 'https://www.facebook.com/************/',
    target: '_blank',
    icon: 'socicon-facebook',
  },
];

export class NbSimpleRoleProvider extends NbRoleProvider {
  getRole() {
    // here you could provide any role based on any auth flow
    return observableOf('guest');
  }
}

// Override 'token' to 'id_token' for Azure AD B2C
(NbOAuth2ResponseType as any)['TOKEN'] = 'id_token';

// Create new token for Azure auth so it returns id_token instead of access_token
export class NbAuthAzureToken extends NbAuthOAuth2Token {

  // let's rename it to exclude name clashes
  static NAME = 'nb:auth:azure:token';

  getValue(): string {
    return this.token.id_token;
  }
}

@Injectable()
export class NbAzureADB2CAuthStrategy extends NbOAuth2AuthStrategy {

  // we need this methos for strategy setup
  static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] {
    return [NbAzureADB2CAuthStrategy, options];
  }

  protected redirectResults: any = {
    [NbOAuth2ResponseType.CODE]: () => observableOf(null),

    [NbOAuth2ResponseType.TOKEN]: () => {
      return observableOf(this.activatedRoute.snapshot.fragment).pipe(
        map(fragment => this.parseHashAsQueryParams(fragment)),
        map((params: any) => !!(params && (params.id_token || params.error))),
      );
    },
  };

  constructor(http: HttpClient,
              private activatedRoute: ActivatedRoute,
              @Inject(NB_WINDOW) window: any) {
    super(http, activatedRoute, window);
  }
}

export const NB_CORE_PROVIDERS = [
  ...DataModule.forRoot().providers,
  ...NbAuthModule.forRoot({
    strategies: [
      NbAzureADB2CAuthStrategy.setup({
        name: 'AzureADB2C',
        clientId: '******************************',
        clientSecret: '',
        authorize: {
          // tslint:disable-next-line:max-line-length
          endpoint: 'https://login.microsoftonline.com/tfp/******.onmicrosoft.com/b2c_1_pxdrsignin/oauth2/v2.0/authorize',
          responseType: NbOAuth2ResponseType.TOKEN, // have overloaded this to return id_token instead of token for Azure auth
          scope: 'https://******.onmicrosoft.com/api/profile openid profile',
          redirectUri: 'https://localhost:5001/auth/callback',
        },
        token: {
          class: NbAuthAzureToken, // changed from NbAuthOAuth2Token
        },
        redirect: {
          success: '/dashboard/overview',
        },
      }),
    ],
    forms: {
      login: {
        socialLinks: socialLinks,
      },
      register: {
        socialLinks: socialLinks,
      },
    },
  }).providers,

  NbSecurityModule.forRoot({
    accessControl: {
      guest: {
        view: '*',
      },
      user: {
        parent: 'guest',
        create: '*',
        edit: '*',
        remove: '*',
      },
    },
  }).providers,
  {
    provide: NbRoleProvider, useClass: NbSimpleRoleProvider,
  },
  AnalyticsService,
  I18nService,
];

@NgModule({
  imports: [
    CommonModule,
  ],
  exports: [
    NbAuthModule,
  ],
  declarations: [],
})
export class CoreModule {
  constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
    throwIfAlreadyLoaded(parentModule, 'CoreModule');
  }

  static forRoot(): ModuleWithProviders {
    return <ModuleWithProviders>{
      ngModule: CoreModule,
      providers: [
        ...NB_CORE_PROVIDERS,
        NbAzureADB2CAuthStrategy,
      ],
    };
  }
}

To sum up, the main issue is “where to look for the token”:

      return observableOf(this.activatedRoute.snapshot.fragment).pipe(

As some of the backend services return it as url hash (fragment), some as query params, so we need to be very careful with this and somehow take this into account inside of the strategy itself.

Read more comments on GitHub >

github_iconTop Results From Across the Web

App infinitely redirecting after login - Auth0 Community
Common reasons are 1) Auth0 developer keys are being used instead of your own credentials for a social connection or 2) the browser...
Read more >
c# - Authorize callback endpoint keeps redirecting to the user ...
A native mobile application consuming the API, with JWT authentication, running on http://localhost:8100. Rightnow im trying to authenticate the ...
Read more >
The Authorization Response - OAuth 2.0 Simplified
With the Implicit grant ( response_type=token ) the authorization server generates an access token immediately and redirects to the callback URL with the ......
Read more >
Callbacks | NextAuth.js
The redirect callback is called anytime the user is redirected to a callback URL (e.g. on signin or signout). By default only URLs...
Read more >
Sign users in to your SPA using the redirect model | Okta ...
Redirect URI: http://localhost:3000/login/callback; Post Logout Redirect URI(s) ... In this section you create a sample SPA and add redirect authentication ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found