4
votes

After updating the Auth0 login sample to use HashLocationStrategy in app.module.ts:

import { LocationStrategy, HashLocationStrategy } from '@angular/common';
// (...)
@NgModule({
  providers: [
    {provide: LocationStrategy, useClass: HashLocationStrategy},
    appRoutingProviders,
    AUTH_PROVIDERS
  ],  
//(...)

The Auth0 Lock authenticated event is not raised anymore:

import { Injectable } from '@angular/core';
import { tokenNotExpired } from 'angular2-jwt';

// Avoid name not found warnings
declare var Auth0Lock: any;

@Injectable()
export class Auth0Service {

  // Configure Auth0
  lock = new Auth0Lock('I21EAjbbpf...', '....au.auth0.com', {});

  constructor() {
    // Add callback for lock `authenticated` event
    this.lock.on("authenticated", (authResult) => {
      // Use the token in authResult to getProfile() and save it to localStorage
      this.lock.getProfile(authResult.idToken, function(error, profile) {
        if (error) {
          // Handle error
          return;
        }

        localStorage.setItem('id_token', authResult.idToken);
        localStorage.setItem('profile', JSON.stringify(profile));
      });
    });    
  }
// (...)
1
what error are you getting?Bean0341

1 Answers

12
votes

The reason you're experiencing this issue is because the Angular 2 router will automatically cleanup the URL upon route navigation causing Auth0 Lock to never see the data required to authenticate the user. Judging from GitHub, this behavior was not always like this, but it's the current one. See RC2 Router strips extra arguments from the path after matching a route and navigation should not preserve query params and fragment for some background.

Upon performing the login Auth0 will request your browser to navigate into an URL similar to this:

http://example.com/#access_token=RENH3twuqx&id_token=eyJ0.epcOidRwc.Qdx3ac&token_type=Bearer

This URL contains all the necessary information for Lock to recognize that the user is authenticated, however, the previously mentioned Angular router behavior means that before Lock having a chance to process this information, the authentication data contained in the URL fragment is stripped, leaving the URL as (http://example.com/#/). This happens because you most likely have configured a catch-all route that matches any URL.

Assuming you have the following routes configured:

const appRoutes: Routes = [
  { path: '', component: HomeComponent },
  { path: '**', redirectTo: '' }
];

DISCLAIMER: The first solution that will be shown below was provided as a workaround that proved functional for Angular 2.0.0, Angular router 3.0.0 used with Lock 10.2. Since then, it seems the router and/or Lock suffered changes that made the initial workaround fail. I'm providing a second workaround that seems to be functional with Angular 2.4.1, Angular router 3.4.1 and Lock 10.7.


Workaround #1 - (angular/[email protected], angular/[email protected], [email protected])

One possible way to try to circumvent this default behavior is to perform the following steps:

  1. Add an activation guard to the route that is handling the authentication callback request in such way as to not allow the route to activate if the current URL seems like to be the result of login (for example, contains the access_token keyword in its fragment.
  2. Upon the authenticated event being triggered force the navigation to your desired route so that the application recognizes the login.

You could create the following class:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Location } from '@angular/common';

@Injectable()
export class AuthenticationCallbackActivateGuard implements CanActivate {

  constructor(private location: Location) { }

  canActivate() {
    // You may want to make a more robust check here
    return this.location.path(true).indexOf("access_token") === -1;
  }
}

Register it as a guard for your home route:

const appRoutes: Routes = [
  { path: '', component: HomeComponent, canActivate: [AuthenticationCallbackActivateGuard] },
  { path: '**', redirectTo: '' }
];

export const appRoutingProviders: any[] = [
  AuthenticationCallbackActivateGuard
];

And finally, navigate to your route after authentication:

this.lock.on('authenticated', (authResult) => {
  localStorage.setItem('id_token', authResult.idToken);
  this.router.navigate([''], {});
});

Workaround #2 - (angular/[email protected], angular/[email protected], [email protected])

Similar to what was done before, but the imperative navigation is done on the guard itself with the authentication callback data provided as fragment so that Lock is able to see this information when processing events. Since navigation moved to the guard, you no longer need to do the navigation on the lock authenticated event.

Create the following class:

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { Location } from '@angular/common';
import { Router } from '@angular/router';

@Injectable()
export class AuthenticationCallbackActivateGuard implements CanActivate {

  constructor(private router: Router, private location: Location) { }

  canActivate() {
    var path = this.location.path(true);

    // You may want to make a more robust check here
    var isAuthenticationCallback = path.indexOf("access_token") !== -1;

    if (isAuthenticationCallback) {
      this.router.navigate([''], { fragment: path });

      return false;
    }

    return true;
  }
}

Register it as a guard for your home route:

const appRoutes: Routes = [
  { path: '', component: HomeComponent, canActivate: [AuthenticationCallbackActivateGuard] },
  { path: '**', redirectTo: '' }
];

export const appRoutingProviders: any[] = [
  AuthenticationCallbackActivateGuard
];

And finally, handle the authentication event:

this.lock.on('authenticated', (authResult) => {
  localStorage.setItem('id_token', authResult.idToken);
});