This article is going to go over some of the basics of connecting your Ember Octane based app to an OAuth2 Flask server (which we created in the last article ). This is a fairly straight forward and quick process with the help of a few plugins that make the whole flow much simpler.
The main piece of the puzzle is Ember Simple Auth. It’s a lightweight session management tool which will take care of automatically managing and renewing your tokens in the background and eliminates a massive amount of boilerplate code on new projects.
To get started either create a new ember app or edit an existing one.
First step is to install ember simple auth with ember-cli
ember install ember-simple-auth
This will add a session service to your project that you can query for the current state of the user. This will let create a simple login component for the user to enter their username and password
<div class="login-form container"> <!-- hide everything if we're already authenticated, you could put a welcome message instead--> {{#unless this.session.isAuthenticated}} {{#if this.errorMessage}} <div class="error">Error: {{this.errorMessage}}</div> {{/if}} <form class="col s12"> <div class="row"> <div class="input-field col s4 offset-s4"> <Input id="email" type="email" class="validate" @value={{this.username}}/> <label for="email">E-mail</label> </div> </div> <div class="row"> <div class="input-field col s4 offset-s4"> <Input id="password" type="text" class="validate" @value={{this.password}}/> <label for="password">Password</label> </div> </div> </form> <button type="button" class="login-btn btn" {{on "click" this.login}} role="button"> <span>Login</span></button> </div> {{/unless}} </div> {{yield}}
import Component from '@glimmer/component'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class LoginComponent extends Component { @service session; @service router; @tracked errorMessage = ''; @tracked username; @tracked password; @action async login() { try { await this.session.authenticate('authenticator:custom-oauth2', 'password', this.username, this.password); } catch(error) { this.errorMessage = error.error || error; } //if we are authenticated navigate back to the homepage if (this.session.isAuthenticated) { this.router.transitionTo('index'); } } @action async invalidateSession() { this.session.invalidate(); } }
The next piece that needs added for ember-simple-auth is a custom authenticator, this is the piece that tells ember-simple-auth how to connect and authenticate with your authentication server. These usually don’t contain more than just your server details. Using our example from the last article, that would look something like this :
ember g authenticator custom-oauth2
import ENV from "my-app/config/environment"; import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant'; import { inject as service } from '@ember/service'; const PASSWORD_GRANT = "password"; export default class CustomAuthenticator extends OAuth2PasswordGrant { @service session; serverTokenEndpoint = ENV.TOKEN_ENDPOINT; clientId = ENV.CLIENT_ID; serverTokenRevocationEndpoint = ENV.REVOKE_TOKEN_ENDPOINT; async authenticate(provider, username, password, scope = [], headers = {}) { if(provider=== PASSWORD_GRANT) { return super.authenticate(username, password, scope, headers); } }
Now we could make this simpler and just use the default authenticate method, but there’s a few advantages to defining a custom authenticate function that we’ll go over in detail in the next article.
If you are having issues with the server accepting your username and password, the best place to debug is to set a breakpoint in your server code here :
@oauth_bp.route('/oauth/authorize', methods=['GET', 'POST']) def authorize(): user = current_user() if request.method == 'GET': try: grant = authorization.validate_consent_request(end_user=user) except OAuth2Error as error: return error.error return render_template('authorize.html', user=user, grant=grant) if not user and 'username' in request.form: email = request.form.get('username') user = User.query.filter_by(email=email).first() return authorization.create_authorization_response(grant_user=user)
You should be able to step through the authlib code and find out where it is failing, in my experiance it’s usually in one of two places, either the client configuration is missing the needed grant type, or the user’s password is not being validated correctly. Double check the config settings stored for your client id in your database, and make sure they match your settings in the authenticator! Also check what’s being posted to the server from the browsers network tab, and compare it to your client settings in the backend database.
The ENV variables are just the settings for talking to your authenitcation server, and in my case look something like the following. I personally like to use ember-cli-dotenv to store my variables which makes it easy to have different setting for each test environment
CLIENT_ID=CUSTOM_CLIENT_ID_CREATED_IN_FLASK TOKEN_ENDPOINT=https://localhost:5000/oauth/token REVOKE_TOKEN_ENDPOINT=https://localhost:5000/oauth/revoke
So the final step is how do you us this to secure your pages? There’s a ember-simple-auth mixin that does all the heavy lifting, that you can use like the following in your route classes.
import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin'; export default class ListUsersRoute extends Route.extend(AuthenticatedRouteMixin, {}) { @service store; model() { return this.store.findAll('user') } }
This will only allow authenticated users to access this route, all others will be redirected to the default page assigned in ember-simple-auth which is the /login route.
Using ember-simple-auth to access your Rest server
This is also trivial with ember-simple-auth
generate you application adapter if you don’t already have one
ember g adapter application
and add the following:
import JSONAPIAdapter from '@ember-data/adapter/json-api'; import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; import { computed } from '@ember/object'; export default class ApplicationAdapter extends JSONAPIAdapter.extend(DataAdapterMixin) { host = ENV.API_HOST + ':' + ENV.API_PORT; namespace = 'api'; @computed('session.data.authenticated.access_token') get headers() { let headers = {}; if (this.session.isAuthenticated) { // OAuth 2 headers['Authorization'] = `Bearer ${this.session.data.authenticated.access_token}`; } return headers; } }
and that it, now every ember data request will automatically add your session token to the request headers, flask will parse out that token and validate your access before processing the request.
Hopefully this was helpfull, Ember-simple-auth is usually one of the first add-ons I install in a new Ember project as it really makes these interactions easy without a whole lot of code to write. In the next article we’ll take this code and extend it to allow you to login with Facebook or Google.