This repository provides a minimal Ember
Octane application for authentication
and authorization with GitHub. It uses
ember-simple-auth
and
torii
to provide the authentication and authorization.
This guide is an update and reworking of the guide provided by
ember-simple-auth
,
but it uses Octane syntax and eschews jQuery. It also uses a production-ready
gatekeeper server instead of an http-mock
server to provide the token
exchange.
The basic flow of authenticating with GitHub deserves an outline that is personalized for Ember:
- When the user clicks the login button, the Ember application contacts
https://github.com/login/oauth/authorize
with your app’sclient_id
. GitHub presents a new window asking the user to authorize access to their GitHub account from your app. The user grants this. - GitHub redirects back to your app with an
authorizationCode
. This is proof of authorization, but your app still doesn’t know who the user is (so it cannot connect to aUser
model, for example). - Your app sends the
authorizationCode
to a gatekeeper server you maintain, which in turnPOST
s the code tohttps://github.com/login/oauth/access_token
. In development, this can be mocked. In production, it must be a separate server, about which more below, as we will use a separate server throughout. - GitHub responds to the
POST
with anaccess_token
, which your gatekeeper server forwards to your app. - Your app uses the
access_token
in anAuthorization: Bearer <access_token>
header request to, say,https://api.github.com/user
, which returns the GitHub user’s user info, at which point you can match the username with yourUser
model or whatever.
As with the original ember-simple-auth
guide, the approach here mirrors any
OAuth2 explicit grant flow used also by Facebook, Google, etc.
You can find a sequence diagram for the flow here.
This section of the guide is for newcomers to OAuth, GitHub, and Ember. For experts, the short version is:
- Register your future app with GitHub and get its keys.
- Create a new Ember app
- Install
ember-simple-auth
,torii
,ember-cli-dotenv
,ember-data-github
, andember-fetch
. - Wire up the app with
application
,index
, andlogin
routes. - Configure Torii using the environment variables set in
.env
.
After logging in to GitHub, go to your developer settings and, in the “OAuth Apps” section, click on “New OAuth App.” Fill in the form much like in this image. The first three fields can be filled out per your wishes, but take care with the Authorization callback URL.
Note that the callback URL is http://localhost:4200/torii/redirect.html
. Torii
has deprecated calling back to a non-static html
page as doing so is a
security vulnerability. Previously, it was normal to redirect back to the
Ember app itself.
You will likely want to register different application keys for development
and production, as the authorization callback URL will be different in both
cases. For development, http://localhost:4200/torii/redirect.html
is
sufficient. For production, it will be
http://<your-great-app-domain>/torii/redirect.html
.
Once you register your application, you will have a chance to add an application logo. Most importantly, you will be given a client ID and a client secret. The client secret must be kept secret and should never be included in the Ember application's source! You will use the client ID in your web application and the client secret in your back end token exchange service.
First, install Node.js. If you have a Mac and are using Homebrew, this is as simple as running
brew install node
in the terminal.
Next, install ember-cli
and use it to generate a new
Ember Octane app. Once the app is generated, you can enter into the newly
created directory and spin up the server.
npm install -g ember-cli
ember new my-ember-auth-octane-app -b @ember/octane-app-blueprint
cd my-ember-auth-octane-app
ember serve
After a bit, the terminal will show a “Slowest Nodes” screen, and then you can
open http://localhost:4200
on your browser to see the Welcome page. Quit the
server by typing ctrl-C in the terminal, which will return you to the prompt.
One of our needed addons requires the preexistence of two files before it will
install, so create a new file, config/dotenv.js
:
// config/dotenv.js
module.exports = function() {
return {
clientAllowedKeys: [
"GITHUB_DEV_REDIRECT_URI",
"GITHUB_DEV_CLIENT_ID",
"DEV_TOKEN_EXCHANGE_URL"
],
failOnMissingKey: false
};
};
Then create .env
, which should look something like this:
# .env
GITHUB_DEV_CLIENT_ID=<your GitHub client id created above>
GITHUB_DEV_REDIRECT_URI=http://localhost:4200/torii/redirect.html
DEV_TOKEN_EXCHANGE_URL=http://localhost:9999/authenticate
Now install the addons and ensure that Git forgets about .env
.
ember install ember-simple-auth
ember install torii
ember install ember-cli-dotenv
ember install ember-fetch
ember install ember-data-github
echo ".env" >> .gitignore`
Here, ember-simple-auth
provides the underlying authentication layer, which
includes session
, an Ember
service that
provides authentication persistence even when the server is restarted. Next,
torii
provides the abstraction needed to write the GitHub-specific
authorization “provider.” ember-cli-dotenv
allows us to use environment
variables defined in a .env
file in our configuration files. Next,
ember-fetch
uses JavaScript’s Fetch
API to make
network requests, superseding jQuery’s $.ajax()
method. Finally, the
ember-data-github
addon provides adapters, serializers, and models for
working with GitHub.
Our app will have an index route at /
and a login route at /login
. Also,
once we get to using ember-simple-auth
, we’ll benefit from an application
route, too, so let’s create those first, along with some controllers we’ll
later need to control actions:
ember generate route application
ember generate route index
ember generate route login
ember generate controller application
ember generate controller login
Ember-cli will ask about overwriting app/templates/application.hbs
. That’s
fine to do so.
Now let’s create our skeletal templates by editing the three files in
app/templates
:
If you are familiar with Ember but unfamiliar with prefixing properties with
this.
in Handlebars templates, this is a new Ember convention reflected in
Octane, where any property defined on the controller (or in a component) is
referred to with this.
in the template. this.config
and this.session
are
properties that will be defined on the route controllers in later steps.
When you spin up the server again with ember serve
, you should see, at
http://localhost:4200
, something like this:
Incidentally, while the server was spinning up, it probably complained that Torii has not yet been configured, so let’s do that.
Torii is configured in the file config/environment.js
. Somewhere around the
seventh line, perhaps underneath rootURL: "/"
, add:
torii: {
sessionServiceName: "session",
providers: {
"github-oauth2": {
scope: "repo user",
apiKey: process.env.GITHUB_DEV_CLIENT_ID,
redirectUri: process.env.GITHUB_DEV_REDIRECT_URI,
tokenExchangeUri: process.env.DEV_TOKEN_EXCHANGE_URL
}
}
},
When you spin up the server again, it will not complain about Torii’s being unconfigured.
OAuth is officially an authorization protocol, but is commonly used also for
authentication when the initial authorization code is obtained over https
.
GitHub uses the OAuth authorization code grant
type, which requires two
steps. As noted at the top, the first step uses your client ID to get a
temporary authorization code. The temporary authorization code acts as a
single use bridge to authorization.
Start by making use of the ember-simple-auth
mixins that expose certain
methods to routes. Add them like this:
// app/routes/application.js
import Route from "@ember/routing/route";
import ApplicationRouteMixin from "ember-simple-auth/mixins/application-route-mixin";
export default class ApplicationRoute extends Route.extend(
ApplicationRouteMixin
) {}
// app/routes/index.js
import Route from "@ember/routing/route";
import AuthenticatedRouteMixin from "ember-simple-auth/mixins/authenticated-route-mixin";
export default class IndexRoute extends Route.extend(AuthenticatedRouteMixin) {
}
// app/routes/login.js
import Route from "@ember/routing/route";
import UnauthenticatedRouteMixin from "ember-simple-auth/mixins/unauthenticated-route-mixin";
export default class LoginRoute extends Route.extend(
UnauthenticatedRouteMixin
) {}
Now if you start up your app, you’ll notice that the app immediately redirects
to the login
route, or http://localhost:4200/login
. The
AuthenticatedRouteMixin
makes routes available only to authenticated users.
The UnauthenticatedRouteMixin
does the reverse. Hence, once the user
authenticates, they will no longer be able to visit the login
route.
Next, you can add properties and actions to the app to prepare it for authentication.
First, instantiate the session
service and add a config
property on the
application
controller and add the session
service to the login
controller. Then we’ll also add login()
and logout()
actions:
// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import config from "../config/environment";
export default class ApplicationController extends Controller {
@service session;
config = config.torii.providers["github-oauth2"];
@action
logout() {
this.session.invalidate();
}
}
// app/controllers/login.js
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class LoginController extends Controller {
@service session;
@action
login() {
this.session.authenticate("authenticator:torii", "github");
}
}
These examples use some new Ember features, including native class syntax and
the @service
and @action
decorators. This blog
post
describes these new syntactic features in greater detail. You can also read
more about decorators in Ember in the Decorators
RFC.
Also, note on the second line of app/controllers/application.js
that you
should import config
from the config
directory defined in terms of your
application.
If you restart your webserver now with ember serve
, you should see your
GitHub client id, created at the beginning of this guide, appear on the second
line of your app, after “Secret API Key:.”
Of course, clicking on the “Log in to GitHub” button will not do anything,
because we have not yet defined the torii
authenticator or the github
provider and because the template hasn’t connected the button to the login()
action. Those are the next two steps.
First, generate an authenticator for Torii.
ember generate authenticator torii
This creates a file, app/authenticators/torii.js
, the contents of which we
can replace with:
// app/authenticators/torii.js
import { inject as service } from "@ember/service";
import ToriiAuthenticator from "ember-simple-auth/authenticators/torii";
export default ToriiAuthenticator.extend({
torii: service()
});
Next, we create the GitHub provider by creating a new folder,
app/torii-providers/
, and creating a file inside it,
app/torii-providers/github.js
:
// app/torii-providers/github.js
import GitHubOAuth2Provider from "torii/providers/github-oauth2";
export default GitHubOAuth2Provider.extend({
});
Change the login
template to send the login()
action when the button is pressed.
The “Log Out” button was already associated with the logout()
action, but
because it has been impossible to authenticate, it has also been impossible to
see that button.
If you start up the server again and now click on “Log In to GitHub,” a small
popup window should appear asking for authorization, showing a GitHub icon and
your GitHub user avatar. Upon clicking the green “Authorize” button, the popup
should disappear and dump you back to your app, but now on the index
route.
Underneath “Index Route,” you should also now see a line displaying the GitHub
authorization code.
We’ve now established the mechanism to obtain an authorization code from
GitHub. This doesn't authorize us fully to use the GitHub APIs, although
ember-simple-auth
considers us authenticated at this point, hence
this.session.isAuthenticated
evaluates to true
.
The “Logout” button in the application
template now appears, and if you
click on it, you will be dumped back to the login
route. Logging in,
however, no longer requires authorization via the GitHub popup, because you
have already authorized your app. The popup still appears, but it flashes for
a second and then disappears.
This completes the second step of the outline at the top of this guide. Next is performing the token exchange to enable getting user data from GitHub.
Here the app has to move out of Emberdom for a bit, because GitHub will not permit token exchange to happen on a client-side application. Luckily, spinning up a separate gatekeeper server is somewhat straightforward, and there even is a Node application, Gatekeeper, designed to make the process even easier.
In short, the server must receive the authorization code your Ember app receives
from GitHub and forward it back to GitHub in a POST
request, wait for a
response, and send it back to your Ember app.
Gatekeeper expects the authorization code to come at the end of a URL in the
format of http://<gatekeeper-server>/authenticate/<authorization-code>
,
which is why we set the value of DEV_TOKEN_EXCHANGE_URL
to
http://localhost:9999/authenticate
in the .env
file. This is the URL we
will shortly be using.
Outside of your Ember app’s directory, clone and prepare the Gatekeeper server:
git clone https://github.com/prose/gatekeeper.git
cd gatekeeper
npm install
npm install dotenv
echo ".env" >> .gitignore
Now, as with the .env
file in the Ember app, create a similar one in the
gatekeeper
directory:
OAUTH_CLIENT_ID=<your GitHub client id created above>
OAUTH_CLIENT_SECRET=<your GitHub client secret created above>
Note that for the token exchange, you need both the client id and the client
secret. These are the environment variable names Gatekeeper is expecting, so
do not change them. Finally, make the Gatekeeper server aware of the
environment variables by adding, at the very top of index.js
:
require("dotenv").config();
Start the Gatekeeper server with:
node index.js
The server should report to the console something like this:
Configuration
oauth_client_id: 5cb***
oauth_client_secret: 429***
oauth_host: github.com
oauth_port: 443
oauth_path: /login/oauth/access_token
oauth_method: POST
Gatekeeper, at your service: http://localhost:9999
The lines for the oauth_client_id
and oauth_client_secret
should look
similar to your client id and client secret. If they read GIT***
, then the
environment variables did not catch. Make sure you added the dotenv
configuration line at the top of index.js
.
Once more, though our app looks authenticated, it’s not authorized to use the GitHub APIs. Even though we authorized the app to have access to our GitHub account in order to get the authorization code. It’s confusing.
torii
providers make use of the open()
hook, so that’s the hook we will
add to our provider:
// app/torii-providers/github.js
import GitHubOAuth2Provider from "torii/providers/github-oauth2";
import ajax from "ember-fetch/ajax";
import config from "../config/environment";
export default GitHubOAuth2Provider.extend({
open() {
return this._super().then(async ({ authorizationCode, provider }) => {
const gatekeeperURL =
config.torii.providers["github-oauth2"].tokenExchangeUri;
const response = await ajax(`${gatekeeperURL}/${authorizationCode}`)
return { provider, access_token: response.token };
});
}
});
This adds an access_token
property to the session
service as well as a
provider
property.
Spin up the server once more and log in. Next, use the Ember
Inspector to inspect the
session
service by clicking on “Container,” then “service”, then “session,”
then “session” under “Own properties.” Next, hover over the “content: {
authenticated: [Object] }” line and click on the “>$E.” Return to the console
and inspect what was logged to it.
The authenticated
object should have three properties: access_token
,
authenticator
, and provider
. If you copy the token and use it in place of
<OAUTH-TOKEN>
in this curl
command:
curl -H "Authorization: Bearer <OAUTH-TOKEN>" https://api.github.com/user
You should receive your own user data flashed to the terminal. This sets the stage for the final step, incorporating the access token in fetching the user data from within Ember.
In previous versions of ember-simple-auth
, this next step was cleanly
accomplished with an authorizer
, but authorizers have been
deprecated.
Instead, we follow the instructions given by ember-simple-auth
: “simply get
the session data from the session service and inject it where needed.” The
access token is inside the session data, and the only place where we need to
inject it is in one adapter.
First, update the index
route and template to read in the logged in user.
ember-data-github
provides ad hoc models such as a “github-user
” model you
can access in the model
hook:
// app/routes/index.js
import Route from "@ember/routing/route";
import AuthenticatedRouteMixin from "ember-simple-auth/mixins/authenticated-route-mixin";
export default class IndexRoute extends Route.extend(AuthenticatedRouteMixin) {
model() {
return this.store.findRecord("github-user", "#");
}
}
Here, the #
is an id representing the currently logged in user for
ember-data-github
.
If you launch the server now, you will get a blank page, and the console will
tell you that the call to https://api.github.com/user
was unauthorized. We
can, however, extend the adapter for the github-user
model and inject
authorization.
ember generate adapter github-user
Now replace app/adapters/github-user.js
with:
// app/adapters/github-user.js
import { computed } from "@ember/object";
import GitHubUserAdapter from "ember-data-github/adapters/github-user";
import DataAdapterMixin from "ember-simple-auth/mixins/data-adapter-mixin";
export default GitHubUserAdapter.extend(DataAdapterMixin, {
headers: computed("session.data.authenticated.access_token", function() {
const headers = {};
if (this.session.isAuthenticated) {
headers.Authorization = `Bearer ${
this.session.data.authenticated.access_token
}`;
}
return headers;
})
});
This adapter injects an authorization header into the GitHub request now. The
DataAdapterMixin
is a mixin provided by ember-simple-auth
that injects the
session
service. Otherwise, when you start up your server one last time, log
in via the login
route, and get dumped back to the index
route, you should
now see your avatar, your GitHub login name, and your full name.
Congrats!
This all done, you have a crude Ember Octane app that authenticates with
GitHub, returning the logged in user. This model can then be used to populate
your “real” User
model, perhaps.
Additionally, in production you cannot rely on using your local Gatekeeper
server, but luckily Gatekeeper includes documentation on deploying to
Heroku and similar
services. Then, update the appropriate line in your Ember app’s .env
to
point to the Heroku url, not http://localhost:9999/authenticate
.
Finally, I’ve created a repo of the “finished” version of this guide, and it is available at @muziejus/ember-simple-auth-github-octane.
Here are some other links provided by @srvance, @aspala, @marcoow, and @jordan-storz, the authors of the original version of this guide:
- simple-auth-torii-github-demo is a repo created to follow the steps of this guide. All elements of the guide except the token exchange service are contained here.
- github-stars has multiple implementations of the
same app to display your starred repos in various web app frameworks. The
emberjs
directory contains the implementation for Ember. - GitHub Social Authentication with Ember Simple Auth and Torii addresses some very specific issues but has a helpful overall recipe to get things working.
- Real-world Authentication with Ember Simple Auth
adds
ember-simple-auth
to the application developed in its preceding post. - The GitHub OAuth Web Application Flow tells you what GitHub expects of your app.