Connect your SPA with your Back-end

January 06, 2019

cookies

Alright, so you feel pretty strong in your frontend development skills, learned a framework like React, and now want build some back-end applications. You create some APIs, learn about databases, authentication, all is well.

Then you may begin to wonder — how do I connect my front-end SPA with my back-end application? Or more specifically, how does one implement authentication when you can't pass data directly into something like a template?

This is the question I will answer in this post, though the answer really comes down to one thing — cookies.

Basic Idea

Let's say someone submits valid login credentials from the browser (i.e., signs in). We will use a library — which will be discussed more thoroughly in a moment— that will create a "session" with this user, enabling the user to access things that require being logged in. So what is a "session?"

Well, it's basically the server and client passing back and forth a special kind of http cookie known as a session-cookie. The backend application checks for this cookie when the client attempts to access protected routes. Furthermore, it is the job of the client to include this cookie when making HTTP requests to restricted content from the app, such as access to a user's inbox. No cookie, no protected content. You can also set expirations on your cookies — expired cookies also won't get access privileges.

Again, it's a back and forth.

  1. The client logs in and gets a session-cookie from the server
  2. The client makes a request to restricted content on the server, including the cookie it just received
  3. The server gets the cookie via the HTTP request; if the cookie is valid, the app responds with the restricted content and a new cookie for the client to include on the next request, otherwise redirect with a 403 error.
  4. This process repeats for subsequent requests until the user logs out, in which case the app does not respond with a cookie. Since the app didn't respond with a cookie (or at least not one that will provide authentication), the client has nothing to offer the backend for protected content until it logs in again.

This is why HTTPS is so important. If communication between the browser and server is not encrypted, someone could potentially intercept your cookie and use it to access your account information, which is bad.

Since we are talking about security, cookies have a feature that helps protect users from XSS-attackshttpOnly.

An excerpt from OWASP's article on HttpOnly:

According to Michael Howard, Senior Security Program Manager in the Secure Windows Initiative group at Microsoft, the majority of XSS attacks target theft of session cookies. A server could help mitigate this issue by setting the HttpOnly flag on a cookie it creates, indicating the cookie should not be accessible on the client.

Essentially, without this feature, a hacker could potentially run a script in a user's browser and access a session-cookie via the document.cookie API.

Okay, now that that's out of the way, let's get to some code!

Preqrequisites


  • Flask knowledge (or some backend and/or Python experience)
  • Knowledge of a JS framework (I'll be using React)

Flask


I like Flask a lot, primarily because I like Python a lot. It also has a pretty solid community and good documentation for the major libraries. Regardless, the general ideas discussed here can be applied by developers of any stack.

Now session management is something you can attempt to do on your own, but unless you know what you're doing, it's probably best to use a library. I use the flask-login library, which is pretty much the go to for session management in the Flask community. From the flask-login docs:

Flask-Login provides user session management for Flask. It handles the common tasks of logging in, logging out, and remembering your users’ sessions over extended periods of time.

Let's look at some of the tools that flask-login provides. We will break down these imports one by one.

from flask_login import login_required, login_user,\
                        current_user, logout_user

Before we get into these methods, however, it's important to note none of this will work unless you have configured a secret-key for your app:

By default, Flask-Login uses sessions for authentication. This means you must set the secret key on your application, otherwise Flask will give you an error message telling you to do so.

If you are not exactly sure what all this means, I suggest you take a look at the Flask docs on Sessions.

login__user

Let's see what this looks like in action:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        user = User.query.filter_by(username=request.form['username']).first()
        if user is None or not user.check_password(request.form['password']):
            return jsonify({"status":401, "msg": "Invalid username and/or password" }), 401  
        login_user(user)        
        return jsonify({
            "user": current_user.username,            
             "msg": "Logged in!" })

So what is login_user doing anyway? Well, it is creating an active session with the client who made the request.

When you log in a user using login_user, you are essentially saying, "Based on the credentials submitted, I trust this person and will give this person a signed-cookie to use on future requests." It is still the job of the client to include this cookie on subsequent requests, but we will cover that in a bit.

Configuring login_user takes a bit of work though and will not be covered here, but here are the docs.

logout_user

Logging out the user is really easy:

@app.route('/logout')
def logout():
    logout_user()
    return jsonify({"msg": "Logged out!"})

It does the opposite of login_user in that it closes the session and doesn't give the user a cookie. Since we didn't return a cookie, the client now has nothing to give the server for auth routes.

login_required

This is middleware that will check for that all-important session. If the user presents a valid session, they make it through the middleware, otherwise the user is redirected with 403 .

@app.route('/post/<post_id>/delete', methods=['POST'])
@login_required
def delete_post(post_id):    
    if post_id is None:
        return jsonify({"msg":"You did not send me anything"})
    post = Post.query.filter_by(id=post_id).first()
    if post_id is None:
        return jsonify({"msg":"Post not found. Are you trying to break me?"}), 204
    db.session.delete(post)
    db.session.commit()
    return jsonify({"msg": "Post removed!"})

current_user

When a user logs in, it's because that person is trying to access data associated with that user. Consequently, the back-end needs to know who the user is for db queries, etc. This is what current_user is — the user returned from the <User> table when we log a user in.

Consider the following code:

post = Post(body=request.form['text'], author=current_user)

Really all that is being done here is that current_user is referencing the user contained in the session created with login_user. Python is really nice, isn't it?

Front-end


The front end work comes down to one thing really, and that is including credentials when using the Fetch API.

getPosts() {
    fetch('http://api.something.com/post' , {      
      credentials: 'include',
    })
    .then(res => res.json())
    .then(response => {            
      if (response.status !== 200) {
        throw(response)
      }      
      this.setState({posts: response.posts})            
      return 
    })
    .catch(e => {      
      navigate(`/err/`)
      return 
    })
  }

Remember how I said the only thing the client needs to do is include the cookie sent by the server — that is what we're doing we set credentials: 'include'. Otherwise, the client won't include the cookie since the Fetch API does not include credential by default, which is a good thing.

Conclusion


Really this whole post can be boiled down to a few basic principles, but sometimes it's nice to have things explained. Anyway, let's review said basic principles:

  • If a client submits valid username and password that client will get a cookie until the user decides to log out or the cookie expires. This cookie will be used to authenticate future requests.
  • Once a user submits a valid login, all the client must do it is include the cookie provided by the server on subsequent requests.

From there, things like rerouting can be handled programmatically by the SPA. Regardless of what a potential hacker does to manipulate the front-end and access privileged pages, the perpetrator will not be able to access data without providing legitimate credentials, and that is really awesome.