EVE SSO authentication flow in JS and Python

This past week, I wrote about the EVE SSO flow in Python using Flask. Here's how to do it with a separate frontend using Vue.js.

Several months ago, I wrote that I dove into the world of the dedicated JS frontend frameworks and decided on Vue.js. Since then, I've been putting more time into learning it. I find that it's easier to do things on "both sides" using Flask and its bundled template rendering engine, Jinja, but as I get more experience with Vue.js, the more I think about using it in projects instead of HTML from Flask.

As a challenge, I took the EVE SSO flow app in Flask and made another one to practice using Vue.js. The result can be found on Github at Celeo/Vue-Flask-EVE-SSO (I know, what a name). I'll skip over most of the backend, as it's a cut-down version of the other app's backend. Instead, let's look more at the front and the communication between the two.

Libraries

Vue.js, much like Flask, comes as a "minified" approach to its role: instead of including everything right "out of the box", it comes with what you need to get up and running and provides all the extra stuff for larger apps on the side. This results in a framework that is easy to get setup and working while still retaining the ability to make more complicated apps, like those that include routing.

For the frontend, to Vue.js I added vue-router for page routing, vue-resource for communication with the backend, URI.js for query string parsing, and jsrsasign for JWT verification, in addition to the standard items in the items contained in the webpack-simple Vue project template.

For the backend, to Flask I added Flask-CORS to enable cross-origin resource sharing, Preston to handle the EVE SSO and CREST authentication, and PyWJT to create and sign JWTs.

Flow

There are 2 different .vue files in the project, meaning there are two different pages that the user can interact with. The first page that the user will likely land on, coming into the app, is the App page. This page is restricted to logged-in users only, as can be seen in main.js:

1
2
3
4
5
6
7
8
9
10
router.beforeEach(function(transition) {
if (!transition.to.path.startsWith('/login')) {
let name = localStorage.getItem('character_name')
if (!name || name == undefined) {
transition.redirect('/login')
return
}
}
transition.next()
})

This makes use of vue-router's router.beforeEach hook. Whenever the user navigates to a page in the app, their destination is checked. If they aren't going to the login page and they're not logged in (a key in localStorage), then they're redirected to the login page. If they are logged in, then their destination is not modified and the page they requested will load. The App page isn't much to look at, as the work is all in the Login page.

Taking a look at the code, the template is broken into two sections: #start and #back, as identified by the two high-level divs' ids. The start div is shown when the variable back is false, which it defaults to. The other div, #back, is shown when back is true, set by the created hook's parsing of the URL.

When the user has landed on this page (usually from being redirected to it after attempting to load the root URL), their URL does not have a query string, so the bulk of the login in the created hook is skipped, jumping to the three line logic in the else:

1
2
3
this.$http.get('http://localhost:5000/').then((response) => {
this.url = response.data.url
})

This is the first call to the backend. A simple, unadorned GET, it requests the URL to redirect the client to in order to start the EVE SSO process. That URL, like in the Flask-only app, is generated by Preston. The bulk of the login here is for that process, so we'll scroll back up to that.

JWTs

JSON web tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

- jwt.io/

This app uses JWTs to send the user's authenticated character name from the backend to the frontend. Why? Well, in the apps that I've written with the EVE SSO, the character's name is the key to permissions, so all components of the website need to be able to trust it. The backend gets its from CREST, and then hands it off to the frontend.

Also, I wanted to learn how to use JWTs.

The Python code to generate the JWT is very simple:

1
return jwt.encode({'character_name': character_name}, app.config['SECRET_KEY'], algorithm='HS256')

Note that this token is signed, not encrypted - someone could catch the token in transit and look at the contents. In this use case, the contents don't matter, the fact that they're from the backend is the important piece.

The code to verify the token and get the contents are two separate calls to the jsrsasign library:

1
2
3
let isValid = rs.jws.JWS.verifyJWT(token, config['secret_key'], {alg: ['HS256']})
if (isValid) {
let payloadObj = rs.jws.JWS.readSafeJSONString(rs.b64utoutf8(token.split(".")[1]))

After this, it's just getting the name from the token's payload, and storing it in localStorage and the flow is complete. In a user-facing app, I'd store the character name in a store and drop the entire JWT into the localStorage so the app could load the name from the verifiable storage into the store on init. This way, changes to the logged-in state could be seen across components.

Pitfalls

First off, it took me a good couple of minutes to understand that the tokens aren't encrypted (though they can be), but instead signed. This turned out to be fine for what I wanted to do, just be aware of the difference.

Next, getting jsrsasign to build in webpack was a hassle. I'm not a webpack expert; I just use the setup provided by the Vue project template. I found a solution on a Github issue for the Pug engine: add node: {fs: "empty"} to the webpack configuration.


Update

After working with tokens more, I realized that I didn't really need to perform full verification on the client of the token's signature. It doesn't matter if someone somehow feeds the client an invalid token or changes the stored data - as soon as the client makes a request to the server with that invalid token, the server will know and send a response back to the client denying them access. The client doesn't need to perform verification, it just needs to be able to read the token's contents. To this end, I now use jwt-decode in the client to read the contents of the token.