DEV Community

Cover image for Social Learning Journal - Walking Skeleton
Justin Beall
Justin Beall

Posted on

Social Learning Journal - Walking Skeleton

Starting off we have a front-end and a back-end, but they are not connected. Our next step is to create a walking skeleton, which is an end-to-end test with no stubs against a system that’s deployed in production. We have several unknowns like CORS and authentication to handle in this section.

Outcomes

Create an authenticated "Hello, World!" API endpoint that will display text on the Dashboard of the React application.

API Ping Unauthorized

Let's start by solving a more simple problem. We will handle authentication in the next few steps, but for now, let's just focus on getting a request sent from the front-end to the back-end, and display the results.

In the API project, we have a /ping endpoint already. The source for this file is src/controllers/ping.

@ping.route('/', methods=['GET'])
@ping.route('/ping', methods=['GET'])
def hello_world():
    """
    Monitor endpoint
    ---
    tags:
      - ping
    responses:
      200:
        description: Hello, world!
    """
    return 'Hello, world!'
Enter fullscreen mode Exit fullscreen mode

React Hook Into API

My React skills are not as sharp as they used to be. I am using functional components and found that the lifecycle methods of componentDidMount and componentWillMount were replaced with useEffect - some explanation can be found here add-state-and-lifecycle-methods-to-function-components-with-react-hooks. In the React Firebase Tutorial I followed, already utilized this functionality.

I added axios, a promise-based HTTP client, to the project. This will help us by wrapping the API calls. Then as expected, when trying to call the endpoint I received the following HTTP error:

Access to XMLHttpRequest at 'https://dev3l-learning-journal.herokuapp.com/ping' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Enter fullscreen mode Exit fullscreen mode

Switching back to the Python API, we need to add the following code snippet to the app initialization. If we were working in a company production environment, it would not be ideal to allow * as our allow origin value. Instead, we would set it to the known front-end domains.

@app.after_request
def apply_cors(response):
    response.headers.set("Access-Control-Allow-Origin", "*")
    response.headers.set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
    response.headers.set("Access-Control-Allow-Headers",
                         "Origin, X-Requested-With, Content-Type, Accept, Authorization")

    return response
Enter fullscreen mode Exit fullscreen mode

Once deployed, we can verify that the unauthenticated endpoint returns our Hello, world! message successfully.

  useEffect(() => {
    async function fetchData() {
      const dataResponse = await axios.get('https://dev3l-learning-journal.herokuapp.com/ping');
      const message = dataResponse.data;

      setMessage(message);
    }
    fetchData();
  }, []);
Enter fullscreen mode Exit fullscreen mode

Alt Text

Authorization

Next, authentication needs to be added to the targeted endpoint. In order to do this, we will need to add Firebase to the API server. Firebase has a wide array of available features, but we are only using authentication. Given an id token, we should be able to validate the token and get a user from it. Pyrebase is a Python wrapper around Firebase calls. So, we add this to our project and work on getting our authentication hook setup. The Firebase auth object comes with a method to verify an id token:

firebase = firebase_instance()
auth = firebase.auth()
account_info = auth.get_account_info(id_token)
Enter fullscreen mode Exit fullscreen mode

The client takes a Service Account to use the API from an admin perspective. This was easy to create through the Firebase Console Settings page. The tricky part is that a credential JSON file is used to validate the user on initialization. Since we don't have a true secrets management system, I used a bit of a hacky method to swap out the private_key variable in the JSON file with an environment variable.

import json
import os

import pyrebase

FIREBASE_API_KEY = os.environ.get("FIREBASE_API_KEY", "api_key")
FIREBASE_AUTH_DOMAIN = os.environ.get("FIREBASE_AUTH_DOMAIN", "auth_domain")
FIREBASE_DATABASE_URL = os.environ.get("FIREBASE_DATABASE_URL", "database_url")
FIREBASE_STORAGE_BUCKET = os.environ.get("FIREBASE_STORAGE_BUCKET", "storage_bucket")
FIREBASE_ADMIN_PRIVATE_KEY = os.environ.get("FIREBASE_ADMIN_PRIVATE_KEY", "admin_private_key")

SERVICE_ACCOUNT_PATH = "./data/firebase-adminsdk.json"
SERVICE_ACCOUNT_PATH_TEMP = "./data/_firebase-adminsdk.json"

config = {
    "apiKey": FIREBASE_API_KEY,
    "authDomain": FIREBASE_AUTH_DOMAIN,
    "databaseURL": FIREBASE_DATABASE_URL,
    "storageBucket": FIREBASE_STORAGE_BUCKET,
    "serviceAccount": SERVICE_ACCOUNT_PATH_TEMP
}

_firebase = None


def firebase_instance():
    global _firebase

    if _firebase:
        return _firebase

    # handle newlines
    firebase_admin_private_key = FIREBASE_ADMIN_PRIVATE_KEY.replace("\\n", "\n")

    with open(SERVICE_ACCOUNT_PATH) as service_account_file:
        service_account_json = json.load(service_account_file)

    service_account_json['private_key'] = firebase_admin_private_key

    with open(SERVICE_ACCOUNT_PATH_TEMP, 'w') as temp_service_account_file:
        json.dump(service_account_json, temp_service_account_file)

    _firebase = pyrebase.initialize_app(config)
    return _firebase

Enter fullscreen mode Exit fullscreen mode

With the addition of Firebase we need to add some environment variables to the Heroku application:

Alt Text

These are similar to variables entered into Netlify, with the addition of a FIREBASE_ADMIN_PRIVATE_KEY variable. This value is copied from the adminsdk.json downloaded from Firebase when the Service Account was created. The file in the project firebase-adminsdk.json is checked into GIT but the created filed _firebase-adminsdk.json is listed in .gitignore, so it does not get updated and accidentally pushed to the public.

Using flask_auth, we create an authentication decorator that can be added to endpoints:

from flask_httpauth import HTTPTokenAuth
from requests import HTTPError

from src.dao.firebase import firebase_instance

auth = HTTPTokenAuth(scheme='Bearer')


@auth.verify_token
def verify_token(id_token):
    # firebase singleton
    auth = firebase_instance().auth()

    try:
        auth.get_account_info(id_token)
    except HTTPError:
        return False

    return True
Enter fullscreen mode Exit fullscreen mode

At this point in time, we do not care about the user, only that a valid id_token was passed to the endpoint. Next, we create a new endpoint ping_authenticated that utilizes the @auth.login_required decorator.

@ping.route('/ping_authenticated', methods=['GET'])
@auth.login_required
def hello_world_authenticated():
    """
    Authenticated monitor endpoint
    ---
    tags:
      - ping_authenticated
    responses:
      200:
        description: Hello, authenticated world!
    """
    return 'Hello, authenticated world!'
Enter fullscreen mode Exit fullscreen mode

Authenticated Requests

Now that we have the API set up, let's switch gears back to the front-end. First, we need to create a service that returns an axios object that has the appropriate header added.

// axios_service.js
import axios from 'axios';

export async function axiosRequest(currentUser) {
  const idToken = await currentUser.getIdToken(true);
  axios.defaults.headers.common['Authorization'] = 'Bearer ' + idToken;

  return axios;
}
Enter fullscreen mode Exit fullscreen mode

Finally, we tie it all together in the Dashboard.

  useEffect(() => {
    async function fetchData() {
      const dataResponse = await axios.get(SERVER_API_URL + '/ping');
      const message = dataResponse.data;
      setMessage(message);

      const request = await axiosRequest(currentUser);
      const authenticatedDataResponse = await request.get(SERVER_API_URL + '/ping_authenticated');
      const authenticatedMessage = authenticatedDataResponse.data;
      setAuthenticatedMessage(authenticatedMessage);
    }
    fetchData();
  }, []);
Enter fullscreen mode Exit fullscreen mode

Alt Text

Conclusion

Although we have not added a lot of valuable functionality to our end users, we have crushed a ton of technical risk and paved the path forward to a more complicated solution. By leveraging Firebase's authentication, we have created an OAuth solution within the system - with only a few dozen lines of code on the back and front ends. In addition, using the Walking Skeleton technique, the proposed solution has been validated to work in the production environment. Moving forward in future posts, we will start implementing valuable features that will add muscle and meat to the end product.

Discussion (0)