14

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.

Leniel Maccaferri
  • 94,281
  • 40
  • 348
  • 451
NColey
  • 515
  • 1
  • 5
  • 18

1 Answers1

16

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);
    }
Edric
  • 18,215
  • 11
  • 68
  • 81
NColey
  • 515
  • 1
  • 5
  • 18
  • Do you need to pull the `accessToken` from the window.location.hash? I'm using a somewhat different example based on the `ActivatedRoute` but it also has an observable fragment and the result of the fragment is the data after the #. – J. Fritz Barnes Feb 04 '17 at 01:48
  • @J.FritzBarnes Hi, so the original answer I posted above was valid as of July 2016 but is not what I am currently using now that Angular 2.0 final is out (I am on version 2.4.0). I just updated my answer to the way I am currently retrieving the token which I suspect is probably similar to the way you are pulling it and is the way I would recommend over pulling it from window.location.hash (when I first encountered this problem solutions were limited and I didn't go back and update once I switched to pulling it from the route fragment). Thanks for pointing that out! – NColey Feb 05 '17 at 02:37
  • @NColey did you test your solution in IE? when user agent get redirected back to angular site to url like /login?token=blah in IE I get 404 error which is super weird, works in chrome and firefox. Also if I just hit that same url in the url adress in the browser it works. Any ideas how to fix? – Evgeny Fedorenko Jun 27 '17 at 15:04
  • Hi @EvgenyFedorenko, the app this code is in has been live for about a year now and we haven't had any login issues on IE during that time that I'm aware of. Perhaps the 404 error is coming from an unrelated issue? I would also check whatever your oauth setup is to make sure the redirect url has `/login` on the end maybe it's redirecting to somewhere else. – NColey Jun 28 '17 at 16:37
  • @NColey If it helps, I've updated your answer to include an updated code snippet for RxJS 6. Anyways, thanks for your answer! Greatly appreciated. – Edric Oct 04 '18 at 08:44
  • This approach works for me if I use `RouterModule.forRoot(routes, `{useHash: false})`, but if I set that property to `true` then the redirect call reloads my application from scratch, losing the both the url (/login in your case) and the parameters. Is there any way to do it with `useHash=true`? – Urs Beeli May 05 '20 at 20:19
  • I would avoid using `observable` `this.activatedRoute.fragment` here and rather use readily available fragment `this.activatedRoute.snapshot.fragment` . Apply string manipulations and extractions after that. – Paramvir Singh Karwal Feb 25 '21 at 18:28