1

I am setting up a blue/green deployment and am trying to change the redirectUri below based on the current url the user is viewing (redirectUri: this.router.url + '/callback',). I am receiving Uncaught TypeError: Cannot read property 'router' of undefined with the below configuration.

import { APP_BASE_HREF } from '@angular/common';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { CoreModule } from './@core/core.module';
import { AuthGuard } from './auth-guard.service';

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { ThemeModule } from './@theme/theme.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NbOAuth2AuthStrategy,
        NbAuthModule,
        NbOAuth2ResponseType,
        NbOAuth2GrantType,
        NbAuthOAuth2Token,
       } from '@nebular/auth';
import { OAuth2LoginComponent } from './auth/oauth2-login.component';
import { OAuth2CallbackComponent } from './auth/oauth2-callback.component';
import { environment } from '../environments/environment';
import { Router } from '@angular/router';

@NgModule({
  declarations: [AppComponent, OAuth2LoginComponent, OAuth2CallbackComponent ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    NgbModule.forRoot(),
    ThemeModule.forRoot(),
    CoreModule.forRoot(),
    NbAuthModule.forRoot({
      forms: {},
      strategies: [
        NbOAuth2AuthStrategy.setup({
          baseEndpoint: environment.authUrl,
          name: 'cognito',
          clientId: environment.clientId,
          authorize: {
            endpoint: '/oauth2/authorize',
            responseType: NbOAuth2ResponseType.CODE,
            scope: 'aws.cognito.signin.user.admin',
            redirectUri: this.router.url + '/callback',
          },
          redirect: {
            success: '/pages/dashboard',
          },
          token: {
            endpoint: '/oauth2/token',
            grantType: NbOAuth2GrantType.AUTHORIZATION_CODE,
            class: NbAuthOAuth2Token,
            redirectUri: this.router.url + '/callback',
          },
          refresh: {
            endpoint: 'refresh-token',
            grantType: NbOAuth2GrantType.REFRESH_TOKEN,
          },
        }),
       ],
    }),
    AppRoutingModule,
  ],
  bootstrap: [AppComponent],
  providers: [
    AuthGuard,
    { provide: APP_BASE_HREF, useValue: '/' },
  ],
})
export class AppModule {
  constructor(private router: Router) {}
}

I've also tried using redirectUri: window.location.origin + '/callback' which works locally, but is null when built for production.

Andrew Campbell
  • 17,665
  • 2
  • 16
  • 25
  • Can us use there some factor maybe? – Stefan Sep 01 '19 at 18:33
  • 1
    I'm sorry I don't understand the question. – Andrew Campbell Sep 01 '19 at 18:39
  • Maybe you need `window.location.href`? – Sergey Sep 03 '19 at 19:12
  • 3
    If you work with different URIs maybe it's worth setting them inside your `environment` files? You said that `window.location.origin` fails on the server is there any errors or did you find the problem why it fails on the server? – Sergey Sep 03 '19 at 19:13
  • I think it has to do with server side rendering. I saw others start to use InjectionToken, but couldn’t figure that out with my use case. I can’t set them in the environment files since I want it to be a prod deployment that gets promoted to be the green environment. So, the code doesn’t change or get deployed again between those 2. The blue environment becomes the active (green) prod environment. – Andrew Campbell Sep 03 '19 at 19:24
  • There were no errors. It was just null in the generated path. – Andrew Campbell Sep 03 '19 at 19:26
  • 1
    Have you tried using just `/callback` instead of any origin? – Tarun Lalwani Sep 04 '19 at 02:36
  • Yeah, I tried that. The `baseEndpoint` is configured for AWS Cognito so it is under a separate domain. The code currently passes this callback URL which cognito validates is a configured callback and then redirects upon successful login to the callback. I want the value (e.g. `site-blue.com/callback` when coming from `site-blue.com` and `site.com/callback` when coming from `site.com`) to be passed along based on where the user is coming from. – Andrew Campbell Sep 04 '19 at 03:05
  • See if you find anything in https://github.com/angular/angular-cli/issues/10957 ? I would also try `document.location.origin` and see if it changes anything, I doubt but still worth a try. Also which browser are you testing in? – Tarun Lalwani Sep 04 '19 at 07:42

4 Answers4

2

Note that class level decorators are applied to the constructor, before any instance of the class is created. So the router property isn't available for the decorator. In the example this.router.url + '/callback' refers to the global this instead, it's strange that there is no compilation errors.

Regarding window.location, in the aot compilation mode, which is default for the prod builds, expressions in the decorator are executed by Angular compiler at compile time, so window.location isn't available there. Take a look at this GitHub issue: AOT replaces window.location object to null

As a workaround you can dynamically initialize the NbOAuth2AuthStrategy, like:

@NgModule({
  imports: [
    ...
    NbAuthModule.forRoot({
      strategies: [
        NbOAuth2AuthStrategy.setup({
          name: 'cognito'
        })
      ],
      ...
    })
  ],
  ...
})
export class AppModule {
  constructor(
    authService: NbAuthService, // force construction of the auth service
    oauthStrategy: NbOAuth2AuthStrategy
  ) {
    // window.location should be available here
    this.oauthStrategy.setOpitions({
      name: 'cognito',
      ...
    });
  }
}

As I found out, it's important to add the NbAuthService to the constructor arguments as well as NbOAuth2AuthStrategy. It looks like the service initializes the strategy during construction, so it should be constructed before the strategy initialization.

Also note that the setOptions() method completely overrides the options from the module decorator, so the whole strategy initialization should be moved from the decorator to the constructor.

I've also found this GitHub issue, which helped me to find the correct solution.

Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
  • Thanks for the response. This is picking up a strategy and setting my options, but when redirecting on login, it is still going to the original default URL. I'm wondering if it is a scoping issue? – Andrew Campbell Sep 04 '19 at 21:57
  • 1
    @AndrewCampbell Indeed, I've created a sample, and the previous solution doesn't work. I've finally found the working solution, see the updated answer. The `NbAuthService` should be injected to the module constructor as well. – Valeriy Katkov Sep 05 '19 at 09:27
  • Fantastic, thanks! It works for me as well. Another note (for others that stumble across this question) is that a default implementation cannot be defined or it will load that one before this code executes to overwrite it. – Andrew Campbell Sep 06 '19 at 01:49
0

If you want to do something like that you can use an injection token and create a factory function that returns the value you want. that will be run in the browser and you will see the value you want.

const REDIRECT_URI = new InjectionToken('REDIRECT_URI');

export function redirectUriFactory {
  return `${location.protocol}/${location.host}/callback`
}
    @NgModule(...)
    class MyModule {
      forRoot() {
        return {
          ngModule: MyModule,
          providers: [
            { provide: REDIRECT_URI, useFactory: redirectUriFactory }
          ]
        }
      }
    }

I did not test this, but InjectionToken with Factory is the way to go when it's coming to AOT

More info https://github.com/angular/angular-cli/issues/10957

KLTR
  • 1,263
  • 2
  • 14
  • 37
0

In the code sample you provided, it seems that you are trying to access the router object this.router in @NgModule annotation before instantiation of class 'AppModule'.

The instance variables are not available in the Annotations. Angular uses [Dependency Injection][1] to provide object instance through constructor.

Looking to your case, though I am not much aware of the NbOAuth2AuthStrategy module, but you can look for options to configure the strategy of auth from within constructor after defining the module in import section of @NgModule annotation. Consider this code snippet, it may be helpful to you. Look for placeholders in code below marked by << >>


    NbAuthModule.forRoot({
        forms: {},
      strategies: [
        NbOAuth2AuthStrategy.setup({
          baseEndpoint: environment.authUrl,
          name: 'cognito',
          clientId: environment.clientId,
          authorize: {
            endpoint: '/oauth2/authorize',
            responseType: NbOAuth2ResponseType.CODE,
            scope: 'aws.cognito.signin.user.admin',
            redirectUri: '<<SET SOME DEFAULT URL>>',
          },
          redirect: {
            success: '/pages/dashboard',
          },
          token: {
            endpoint: '/oauth2/token',
            grantType: NbOAuth2GrantType.AUTHORIZATION_CODE,
            class: NbAuthOAuth2Token,
            redirectUri: '<<SET SOME DEFAULT URL>>',
          },
          refresh: {
            endpoint: 'refresh-token',
            grantType: NbOAuth2GrantType.REFRESH_TOKEN,
          },
        }),
       ...


    export class AppModule {
      constructor(private router: Router) {
        <<Reconfigure your NbOAuth2AuthStrategy>>
        NbOAuth2AuthStrategy.setup....
      }
    }


Hope the solution works for you.



  [1]: https://angular.io/guide/dependency-injection
Nahid Ali
  • 636
  • 3
  • 11
0

Your solution won't work because, as Valeriy Katkov mentioned, the class level decorators are applied to the constructor, before any instance of the class is created. So, you won't be able to inject a router into a decorator.

In order to be able to inject the router, you need to move your implementation inside the class. It's possible by means of setOptions method of NbAuthStrategy instance, however, there are some issues, see for example here or here that need to be overcome. In order to make it work you should move the strategy configuration to a component which extends the NbAuthComponent (this is important) for example:

export class AppComponent extends NbAuthComponent {

  constructor(auth: NbAuthService,
              location: Location,
              private router: Router,
              authStrategy: NbPasswordAuthStrategy) {
    super(auth, location);

    authStrategy.setOptions({
      name: 'username',
      login: {
        alwaysFail: false,
        endpoint: 'test',
        method: 'post',
        requireValidToken: false,
        redirect: {
          success: this.router.url + '/success-callback',
          failure: this.location.path() + '/callback'
        },
        defaultErrors: ['Damn'],
        defaultMessages: ['Great'],
      },
    });
  }
}

Also I propose you to use this.location.path() instead of this.router.url because this.router.url will provide you not the url you are in, but the url of the compoment level. this.location.path() will give you the full path of the page you are in.

Here is a StackBlitz example with a working solution.

Please, let me know if smth remained unclear for you or requires some additional detailization.

Sergey Mell
  • 7,780
  • 1
  • 26
  • 50