angularoauth-2.0angular2-routingaccess-tokenangular2-router3

How to handle hash fragments from oauth redirect urls in Angular2 rc3 routing


I'm trying to find a way to handle setting up an Angular2 Typescript route (using the 3.0.0-alpha.8 router) that will handle routes that begin with hash fragments.

The app I'm working on handles all login externally (something I have no control over) through a rails backend with oauth2. Redirecting users to the external login page works fine but when the redirect url, always some form of http://localhost:4200#access_token=TOKEN (where TOKEN is a series of numbers and letters) is sent back but I can't figure out how to set up a route that can handle the # sign so I can catch it and redirect to the appropriate component.

In a previous Angular1 app the ui-router was able to use in a route of:

.state('accessToken', {
  url: '/access_token=:token',
  controller: 'LoginController',
  params: { token: null }
})

and this had no problem accepting the redirect url that was sent back and would then pass everything over to the LoginController to handle the rest of the authentication/token business on the front end.

This app however is Angular2 and Typescript and the router query params seem way less flexible and I'm having trouble implementing a similar solution. I've been going based on this section in the docs but all of the examples are building of something else, ex /heroes before getting to the complicated part of the query params, ex /heroes/:id. I searched through stackoverflow as well and wasn't able to find anything that worked with Angular2 and Typescript and the current router.

This is my current (non working) solution:

import { provideRouter, RouterConfig } from '@angular/router';

import { HomeComponent } from './components/home/home.component';
import { TestComponent } from './components/test/test.component';


export const appRoutes: RouterConfig = [
  {
    path: '',
    component: HomeComponent,
    terminal: true
  },
  {
    path: 'access_token',
    component: TestComponent
  }
];

export const APP_ROUTER_PROVIDERS = [
  provideRouter(appRoutes)
];

If I take the redirect url that is sent back and modify it (purely for testing purposes) to something like http://localhost:4200/access_token=TOKEN it works fine. Unfortunately I don't actually have control over the format of the redirect url in real life, and I am unable to come up with a solution that can handle the fact that it begins with a hash fragment rather than a / and then my query params. All of the examples of routing with complicated symbols or characters that I can find begin with a /.

I tried modifying my solution above to be :access_token, which did not work, as well as listing it as a child route under the base route like so:

{
  path: '',
  component: HomeComponent,
  terminal: true,
  children: [
    { path: 'access_token',  component: TestComponent },
  ]
}

which resulted in the following console error: platform-browser.umd.js:2312 EXCEPTION: Error: Uncaught (in promise): Error: Cannot match any routes: ''

I feel like there absolutely has to be a clean solution to this, especially since so many APIs handle their authentication through a redirect url like this but no matter how much I dig through the docs I can't seem to find it. Any advice on how to implement this would be much appreciated.


Solution

  • I was eventually able to find a solution that uses the preferred PathLocationStrategy but also pulls the token out of the oauth redirect uri before the part of the url after the hash fragment is dropped (from the final answer here which is pulled from the QueryParams and Fragment section in the following blog post).

    Essentially I updated the redirect url when registering my application with doorkeeper/oauth2 to be http://localhost:4200/login/ (which leads the redirect url containing the token to look like http://localhost:4200/login/#access_token=TOKEN) and added the following route:

    {
      path: 'login',
      component: LoginComponent
    }
    

    This catches the redirect url but drops everything after the hash fragment, removing the token I needed. To prevent it from dropping everything after the hash fragment I added the following code to the constructor of my LoginComponent:

    constructor(private activatedRoute: ActivatedRoute, 
                private router: Router, 
                private tokenService: TokenService) {
    
    // Pulls token from url before the hash fragment is removed
    
    const routeFragment: Observable<string> = activatedRoute.fragment;
    routeFragment.subscribe(fragment => {
      let token: string = fragment.match(/^(.*?)&/)[1].replace('access_token=', '');
      this.tokenService.setToken(token);
    });
    

    }

    How exactly you choose to handle the token is up to you (I have a TokenService with methods to set, retrieve, and clear it from localStorage) but this is how you access the portion of the url after the hash fragment. Feel free to update/post here if anyone has a better solution.

    UPDATE: Small update to the above login component code to deal with 'fragment is possibly null' typescript errors in Angular v4.2.0 & strictNullChecks set to true in the tsconfig.json in case anyone needs it. Functionality is the same:

    let routeFragment = this.activatedRoute.fragment.map(fragment => fragment);
    
    routeFragment.subscribe(fragment => {
      let f = fragment.match(/^(.*?)&/);
      if(f) {
       let token: string = f[1].replace('access_token=', '');
       this.tokenService.setToken(token);
    }
    

    Note: Since RxJS 6, the map operator has been made pipeable which means that you have to pass it in the pipe method of Observable as seen below:

    import { map } from 'rxjs/operators';
    
    // ...
    
    this.activatedRoute.fragment
      .pipe(map(fragment => fragment))
      .subscribe(fragment => {
        let f = fragment.match(/^(.*?)&/);
        if(f) {
          let token: string = f[1].replace('access_token=', '');
          this.tokenService.setToken(token);
        }