How to use ESA or ESAT to authenticate against custom provider


#1

I can’t find any resources on how to proceed and is it possible to use ESA (ember-simple-ath) or ember-simple-auth-token add-on if I have to authenticate a User agains a custom external API provider ? I mean that every User (if not authenticated) should be redirected to the custom authentication portal login page, enter user name and password and which will send back a token (JWT). This authentication API has my application registered, redirect URL (callback) and provided me with a secret key and client ID. A successful response from that API after authentication would be like that:

http://redirect_url:port/#access_token=eyJhbGciOiJSUzI1Ni.

I have already read Part 1 and Part 2, took a look at Ember JS with Rails article but still have no answer for the question. Any suggestions, advices and links would be helpful !


#2

Hey @belgoros, does your authentication API use a standard OAuth flow? It seems like it would best fit the Implicit Grant Flow, and ESA now has an implicit grant authenticator built-in. We use the Implicit Grant authenticator in our apps much in the same way you describe. The ‘dummy’ app in ESA has example of each authenticator, and the ESA docs should help you get started, and I’m pretty familiar with that code (I actually worked on the PR for the implicit grant support) so feel free to ask any questions in this thread.

First, do you have any non-protected routes? That is do you have a landing page of any kind for unauthenticated users, or do you want any user that hits your app who isn’t authenticated to be redirected to the login? We actually use the latter approach in our app and it makes the implicit grant flow slightly more complicated, but still pretty simple to set up with ESA.

At a high level you’ll want to:

  • set up an authenticator (which extends ESA implicit grant authenticator),
  • set up an authorizer (which extends ESA OAuth2 Bearer authorizer)
  • set up a session store (standard ESA store of your choosing)
  • setup a callback route (we call ours ‘callback’) which extends the ESA implicit grant callback route mixin
  • If you have a landing page, make some sort of button/action that redirects the user to your API login page, if you don’t and all your routes are protected, you’ll need to add some custom code to your callback route (I can help you with that if you need it)
  • make sure you have your authenticationRoute set up correctly so it forwards all unauthenticated visitors to either the landing page or the callback route

Anyway, hope that’s helpful. Good luck!


#3

Hi, Dan ! Thanks a lot for your response, really helpful. So my idea was to use Ember JS in front-end and Rails API as backend solution. There no non-protected routes, what means that every User trying to access the application should be first of all authenticated via a corporate API end-point, e.g. https://our.copmany.com/authorization/auth, - so a common company login page will be displayed where a User should enter his username and password. Once a User is authenticated, a corporate OAuth API redirects him to the URL registered as redirect URL (or callback) for the requesting application and assign a JWT token like that:http://redirect.url.com/#access_token=eyJhbGciO. This is how they see every User should enter an application. I feel really lost between oath2, omniauth, omniauth-omniauth2, doorkeeper (Rails side) and the previously cited Ember solutions. I’ll take a look at the dummy app in ESA repo and come back with other questions if I have some. Thank you once again.


#4

Yeah unfortunately I can’t really help with the Rails side but that sounds pretty much exactly like how our app functions, and we use a pretty spec-compliant version of the OAuth 2 implicit grant flow. So on the Ember side I’d definitely recommend using the ESA implicit grant support and I can help you with the specifics of that for sure, feel free to post questions in this thread!


#5

Here’s basically all of our authentication code, should be pretty much in line with what you want to do.

Install Ember Simple Auth

First install ember-simple-auth. Obvious first step. ember install ember-simple-auth

Create a callback route (we called ours ‘callback’)

This is the route your API will redirect the users browser to once authentication has been completed. From here the ESA implicit grant mixin will process your access token and then it can forward you on to the original desired URL, or to a default ‘routeAfterAuthentication’

ember g route <route-name>

Create authenticated route mixin

Next create a mixin in your project called ‘authenticated-route-mixin’ which extends the ESA authenticated-route-mixin with your authenticationRoute (and other) settings:

import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

import ENV from '../config/environment';

export default Ember.Mixin.create(AuthenticatedRouteMixin, {
  // NOTE: this used to be something we could set in config/environment.js but
  //       that usage was removed in ESA recently and now it must be set
  //       individually on all routes that extend AuthenticatedRouteMixin, so
  //       instead of duplicating that prop in every single one of our routes
  //       I am extending the ESA mixin with our own (of the same name) and
  //       adding the authenticationRoute property here.
  //
  //       (see https://github.com/simplabs/ember-simple-auth/pull/985)


  // these can either be static strings or computed properties (they are CPs
  // in the base ESA mixin which we're extending)

  authenticationRoute: 'callback', // use the route you generated above

  //routeAfterAuthentication: '',

  //routeIfAlreadyAuthenticated: '',
});

Extend the mixin you just created in your routes

In all your protected routes you should extend the mixin that you just created locally (which itself extends ESA’s) instead of extending ESA’s directly. This will allow you to set the authenticationRoute property once instead of having to do it on all of your routes.

e.g.:

import Ember from 'ember';
import AuthenticatedRouteMixin from "<project-name>/mixins/authenticated-route-mixin";

export default Ember.Route.extend(AuthenticatedRouteMixin, {
  ...
});

Create authorizer

Create a file called <project-root>/authorizers/application.js and put the following in it (unless you want a different authorizer):

import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';

export default OAuth2Bearer.extend();

Create an authenticator

Create an authenticator, ours is in <project-root>/authenticators/oauth2.js and extend the ESA implicit grant authenticator, and set your token endpoint:

import ENV from '../config/environment';
import OAuth2ImplicitGrant from 'ember-simple-auth/authenticators/oauth2-implicit-grant';

export default OAuth2ImplicitGrant.extend({/* any customization here, we have none */});

[optional] Define a session store

We just use the default adaptive store but if you want a custom one follow the README to define one

Add some magic to your callback route

Since you don’t want a landing page and all your routes will be protected you’ll want to add some magic to your callback route so you can use it on both ends of the authentication process. Our callback route looks like this, you may need to change a couple of the specifics of the original authentication URL to fit your backend:

import Ember from 'ember';
import CallbackRouteMixin from 'ember-simple-auth/mixins/oauth2-implicit-grant-callback-route-mixin';

import ENV from '../config/environment';

export default Ember.Route.extend(CallbackRouteMixin, {
  routeOnError: 'index',
  authenticator: 'authenticator:oauth2',

  generateAuthUrl: function(authHost, redirectHost, redirectHash) {
    let redirect_uri = redirectHost + encodeURIComponent("/#/callback");
    let client_id = ENV.APP.OAUTH_CLIENT_ID;
    let response_type = "token id_token"; // this is what we use, YMMV
    return `${authHost}/oauth/authorize?client_id=${client_id}&response_type=${response_type}&redirect_uri=${redirect_uri}&scope=openid&state=${redirectHash}`;
  },

  activate(){
    this._super();
    // if we don't have an access token, it means we're not authenticated and we should redirect the user to the API login page
    if(window.location.hash.indexOf('access_token') === -1) {
      let redirectHost = window.location.origin; // hostname of the URL we're on
      let redirectHash = encodeURIComponent(window.location.hash.slice(1).split('?')[0]); // our hash location aka our ember route
      if(redirectHash === "") {
        redirectHash = "/";
      }
      // generate an auth url which is our API server and where we want the API to redirect us back to
      let authUrl = this.generateAuthUrl(ENV.APP.OAUTH_HOST, redirectHost, redirectHash);
      // make the redirect
      window.location.replace(authUrl);
    } 
  },
});

Basically the way it works is:

  • a user will hit your app, but they are unauthenticated, so ESA realizes this and forwards the app to your authenticationRoute which you defined in your mixin above and is in this case the name of your callback route (again we just called it ‘callback’)
  • Once Ember gets here (the activate method of your callback route) it will see that there is no access token in the hash, so it will construct the authentication url and redirect to it, sending the user’s browser to your API login page with params specifying redirect_uri, etc
  • The user logs in, and once that happens the API forwards them back to the redirect_uri which you provided the API with on the way in, it includes the access_token in the hash params. Since you put your callback route in as the redirect_uri, you will be forwarded right back to your callback route, but this time you have the access_token in your hash params so ESA takes over and processes everything as a normal implicit grant flow

Anyway hope all this is helpful and again feel free to post questions here once you get that far


ESA - mock with Mirage when using implicit grant authentication
#6

Reaaaaally helpful, Dan, thank you very much ! It took me almost one week before I got such a clear explanation the way Ember will work ! It is getting more and more clear. Just super !


#7

Dan, I followed your guide lines and created a repo at GitHub. I can’t figure out:

  • how and when generateAuthUrl function is called and how we pass in the parameters ? This is where I should put my authentication provider URL ? As far as I understood, it is generateAuthUrlthat should return a complete URL to query to get the login window ?
  • do the other functions come from CallbackRouteMixin(like encodeURIComponent, for example) ?

Regards


#8

In answer to your questions:

  • generateAuthUrl is called here in the activate hook of the callback route. it’s not actually necessary to do it in a separate function like that (you could do it all in the activate hook), that was kind of an arbitrary decision on our part. As for the params… redirectHost and redirectHash are just grabbed from the browser URL and then ENV.APP.OAUTH_HOST is something you’ll have to define in your config/environment.js file (more on that below). EDIT: the activate hook is called whenever a route is “entered” or “activated”, so when ESA decides your user is unauthenticated, it forwards them to the authenticationRoute you defined, which in this case should be your callback route) and then the “activate” hook is the first thing called.
  • encodeURIComponent is a built in javascript function. The CallbackRouteMixin provides mainly some methods for extracting the auth_token from the hash params and shoving them into your ESA authenticator/store, but you don’t need to worry about all that unless you need to customize it

I noticed you called your callback route ‘dashboard’, and I’m assuming ‘dashboard’ is a route you actually want to use to fetch and render data. I would actually recommend silo-ing all of your auth related code in a separate route (and I’d actually recommend the route name ‘callback’ like we have it in our app, it’s clearer what it’s meant). All we have in our “calback” template is an outlet, and we probably don’t even need a template at all, nothing is ever rendered there. Essentially the callback route is the exit and entry point for our app, so an unauthenticated user forwards to callback route, which forwards to the API for auth, which forwards back to the callback route, which then forwards us on to the main app route (once authentication is completed by the CallbackRouteMixin, but that part should be transparent to you).

Secondly, to use the code exactly as we are you’ll want to define two vars in your config/environment.js file:

  • ENV.APP.OAUTH_HOST = "https://our.company.com";
  • ENV.APP.OAUTH_CLIENT_ID = "<client id>";

The host is just the URL for your API (and you could adapt the code to have your full path e.g. https://our.company.com/authorization/auth) and the client id is… well… your client id


#9

Thank you, Dan. I’ve updated the app by renaming dashboard route to callback. The questions I have now:

  • what URL should I access to trigger the authentication call ?
  • Should I setup callback route as root like that in app/router.js:
this.route('callback', { path: '/' });

When I start ember s and go to localhost:4200, nothing happens. If I navigate to localhost:4200/callback, I have the below errors in Chrome console:

router.js:938 Error while processing route: callback Cannot read property 'then' of undefined TypeError: Cannot read property 'then' of undefined
    at Class.authenticate (http://localhost:4200/assets/vendor.js:131953:87)
    at Class.authenticate (http://localhost:4200/assets/vendor.js:132839:35)
    at Class.activate [as _super] (http://localhost:4200/assets/vendor.js:132580:27)
    at Class.activate (http://localhost:4200/assets/ember-esa.js:473:12)
    at Class.superWrapper [as activate] (http://localhost:4200/assets/vendor.js:56334:22)
    at Class.enter (http://localhost:4200/assets/vendor.js:42746:12)
    at callHook (http://localhost:4200/assets/vendor.js:61351:38)
    at _handlerEnteredOrUpdated (http://localhost:4200/assets/vendor.js:62936:9)
    at handlerEnteredOrUpdated (http://localhost:4200/assets/vendor.js:62958:7)
    at setupContexts (http://localhost:4200/assets/vendor.js:62913:9)

What did I miss again ?


#10

So the callback route is only used by Ember for authentication, it will never display anything, it will never have links to it, it’s basically a special login route. We didn’t specify a path for our callback route meaning the path would be the default /callback.

To trigger the auth call, you’ll need to have one or more other routes, like “dashboard” for example which can have the ‘/’ path. In the dashboard route you’ll want to extend the authenticated route mixin that you created above:

// routes/dashboard.js

import Ember from 'ember';
import AuthenticatedRouteMixin from "<project-name>/mixins/authenticated-route-mixin";

export default Ember.Route.extend(AuthenticatedRouteMixin, {
  ...
});

Doing this tells ember simple auth that you want this route (dashboard) to be protected (auth is required to access it). If a user visits it without being authenticated first ESA says “woah buddy you can’t do that” and forwards them to the authenticationRoute, which in your case should be callback. Then the callback route’s activate method will be triggered, which should send you on the redirect. Then any other routes you define (based on what you said earlier) should also be protected and therefore extend the mixin as well.

Anyway, your router should look something like this:

import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
  location: config.locationType,
  rootURL: config.rootURL
});

Router.map(function() {
  // this is your authentication route that handles all of your authentication logic, both in and out
  this.route('callback');

  // this is your "home route" where you will want to actually render some stuff
  this.route('dashboard', { path: '/' });
});

#11

I updated as you suggested. When reading your explications (thank you once again for your time), everything is clear, but in practice it does not work as expected. I supposed that after applying the above modifications, my dashboard will be protected and inaccessible, but it is not the case. I can access it (at localhost:4200) without any problems. What is wrong here ? Thank you.


#12

Looks like you copied this line wrong. Your custom authenticated route mixin should be importing and extending the authenticated route mixin from ESA, not from your own project. In essence yours was importing and extending itself (honestly not even sure how that didn’t bomb out) and so it never ran any of the ESA code. You should change that line from this:

import AuthenticatedRouteMixin from '../mixins/authenticated-route-mixin';

to this:

import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

Also it looks like you added some empty methods in the authenticator you defined, I’d take those out unless you plan to customize them later. Otherwise those will overwrite the ESA authenticator that you’re extending and do nothing.


#13

Yep, - fixed it :slight_smile: I also removed unused imports for Ember and ENV and commented out methods in oauth.js.

I’ll have to change the redirect URL registered in authentication API server, because it tried to redirect me to https://my.copmany.com/oauth/authorize?client_id=my_client_id&response_type=token&redirect_uri=http://localhost:4200%2F%23%2Fcallback&scope=profile%20openid&state=/

Should I always set it as http://some.host/#/callback as it was declared in activate function in callback.js:

let redirectHost = window.location.origin;

Is it a convention to always have /#/callback? Thank you !


#14

The reason you need to redirect from the API to the callback route is that the callback route is responsible for processing the access_token, so when the API redirects it will include the access token, the callback route will process it, and then ESA decides what to do from there.

If you always wanted to redirect the user to the same route (like dashboard) after logging in, you could set that in the routeAfterAuthentication property in your AuthenticatedRouteMixin. If you wanted to redirect to whatever the user was originally trying to get to before the auth intercept you would need to do what we do and use the ‘state’ param (your server would have to support it as well).


#15

Ok almost got it :). I tried to register the client in Google API Console, and it does not accept the redirect URL http://localhost:4200/#/callback.

Invalid Redirect: http://localhost:4200/#/callback cannot contain a fragment.

Could you tell me which redirect URL should I set up to play a little bit with this Ember client ? Thank you.


#16

That’s a little tougher to work around… you could try changing your location type from “hash” to “auto” which would make it localhost:4200/callback but other than that you’d probably have to change it to just ‘/’ instead of ‘/callback’ and then put something in the application route that intercepts anything with the #access_token= and forwards it to the callback route


#17

So I’ll have to modify the following line in callback.js:

let redirect_uri = redirectHost + encodeURIComponent("/#/callback");

to

let redirect_uri = redirectHost + encodeURIComponent("/callback");

Then how should I change the following line:

let redirectHash = encodeURIComponent(window.location.hash.slice(1).split('?')[0]);

Doing so (I set up the redirectHash = "/"; and let redirect_uri = redirectHost + encodeURIComponent("/callback");, I still git 404 error from Google APIs, where the Authorised redirect URIs was set to http://localhost:4200/callback.

Thank you