Hi Everyone,

Welcome to another post of the building a Chrome extension MVP series.

Last time, I figured out how to call an external API with jQuery Ajax. It was a piece of cake and I was done in less than two hours. What came next was a lot more challenging, because it took me three days working nights and weekends to make it work.

So ladies and gentlemen, today, I want to share with you how I managed to call a localhost Rest API from a Chrome Extension content script.

Step 1 โ€“ Develop A REST API

For developing a REST API backend, I used Flask, the best python web microframework in the world.

I wonโ€™t go into detail here, because this is not a Flask tutorial, but I did following steps to have REST API ready:

  • Create a user model for storing user credentials
  • Add methods for generating and authentication user tokens
  • Add routes, that will call methods above and send a response

Now, I can simply call a python function, that will generate an authentication token with the bcrypt library, give it an expiration period and store it in the User model.

The function will be called after the user submits his login credentials to access the private REST API URL. Flask then verifies if exists and provides him with a new token.

The authentication token is then used for every consequent call until it expires itself. Then the authentication process will start again from the beginning.

A simple command below tells me the Flask REST API is working as expected

curl -u eyJhbGciOiJIUzUxMiIsImlhdCI6MTU3ODQxNzkxNSwiZXhwIjoxNTc4NDE4NTE1fQ.eyJpZCI6MX0.h8BVgKxjnsyebTExyy4kIYPpoQblz9xavPsYzZ9uaUzyhlkEZxithpccxc9qWbof8iG5ll_NPA7lOgdBNI5tQw:unused -i -X GET http://domain.local/api/resource

Step 2 โ€“ Change Ajax GET URL With Localhost API Path

First, letโ€™s edit jQuery Ajax call in insert_html.js with my REST API URL and add authentication parameters into the POST header.

$.ajax({
    type: 'POST',
    url: "http://domain.local/api/resource",
    dataType: "application/json; charset=utf-8",
    username: "username",
    password: "pass",
    processData: false,
    contentType: "application/json",
    success:function(data){
        alert("success:"+JSON.stringify(data));
    },
    error: function (xhr, ajaxOptions, thrownError) { //Add these parameters to display the required response
        alert(xhr.status);
        alert(xhr.responseText);
    },
});

Reloading plugin and refreshing google.com causes a double alert popup, which means something is wrong and Ajax call failed.

XHR status returned zero.

And the responseText is undefined.

There is no error in the console log, just a bunch of warnings. One of them looks suspicious.

Mixed Content: The page at 'https://www.google.com/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http//domain.local/api/resource'. This request has been blocked; the content must be served over HTTPS.

Damn it. I know security is important, but this such a hurdle when you want to play in a safe localhost sandbox. Now I have to make my localhost domains secure and sign them with an SSL certificate. My gut tells me it will take me another day of trial and error.

Roadblock โ€“ Setup HTTPS On MacOS Localhost with Nginx

Create a git ignored folder, e.g. project/cert/ and generate a self-signed certificate with the command below.

openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout domain.key -out domain.crt

Then fill whatever input, that is required in the process. I usually skip most of them except Common name.

* Country Name = skip
* State or Province Name = skip
* Locality Name = skip
* Organization Name = skip
* Organizational Unit Name = skip
* Common Name = domain.local
* Email Address = skip

Now add the new self-signed certificate to macOS keychain, so it is included in the trusted domains list.

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /pathtoproject/domain/uth/cert/domain.crt

The last step is to set up SSL in the Nginx configuration.

ssl on;
ssl_certificate /pathtoproject/domain/app/auth/domain.crt;
ssl_certificate_key /pathtoproject/domain/auth/domain.key;

Change the port from 80 to 443 and any URL with HTTP to HTTPS.

listen 443;

Restart Nginx and see what happens.

sudo brew services restart nginx

Unfortunately, the chrome URL bar shows, that the URL path is not trusted. I must have followed the wrong tutorial and the certificate was not created correctly.

After a few hours of googling I stuck at a promising open issue onย Github explaining, that the self-signing process has changed on MacOS. So letโ€™s try this approach one last time before giving up.

openssl req \
    -newkey rsa:2048 \
    -x509 \
    -nodes \
    -keyout financia.key \
    -new \
    -out domain.crt \
    -subj /CN=domain.local \
    -reqexts SAN \
    -extensions SAN \
    -config <(cat /System/Library/OpenSSL/openssl.cnf \
        <(printf '[SAN]\nsubjectAltName=DNS:domain.local')) \
    -sha256 \
    -days 3650

After deleting the old certificate and adding a new one to keychain, Chrome finally accepts my REST API URL as trusted and secured!

Now check the developer console if Ajax call ended with success.

A new error appears and I think itโ€™s the time to take a break and try my luck the next day.

Status Code: 405 METHOD NOT ALLOWED

Roadblock โ€“ Fix 405, 400 and 401 REST API Responses

The cause for the โ€œ405 method not allowedโ€ error is missing the POST method in my Flask route. I am sending a POST method header, but my API accepts only GET requests.

Fixing this bug reveals another error.

Status Code: 400 BAD REQUEST

The only content I am sending in POST request is the login credentials. So letโ€™s comment it out to eliminate any degrees of freedom.

// username: "username",
// password: "pass",

No luck. I still get a bad request response. Maybe I should comment out authentication decorators in my backend as well.

# @auth_login

Now at least I get an error, that I can debug from Gunicorn logs.

Status Code: 500 INTERNAL SERVER ERROR

I feel burned out and out of ideas so I am just trying random things like changing the POST method to the GET method. Surprisingly, the change leads to a new error.

Status Code: 401 UNAUTHORIZED

Now I just need to send login credentials correctly. After googling for 20 minutes I find the correct syntax for sending credentials in the GET request header.

headers: {
    "Authorization": "Basic " + btoa(USERNAME + ":" + PASSWORD)
},

But I keep getting an unauthorized response. Accidentally, I notice, that I had a wrong query in my backend, that prevents the request to be authenticated.

Such a dumb mistake tells me I should take a break and keep trying tomorrow. At least I can go to sleep knowing the GET request finally returns 200 status code, albeit with an empty response in the body.

Roadblock โ€“ Fix No Response Ajax Call With CORS

No response issue is caused by Chrome security check when it blocks any cross-domain responses, that donโ€™t have the same origin flag.

I will simply use a Flask library for CORS handling. So letโ€™s install the plugin and initialize it in the Flask app.

pipenv install -U flask-cors

Cool, now my response is not empty anymore as seen in the screenshot below. My REST API returned the authentication token as expected.

Step 3 โ€“ Using Local Chrome Storage Set And Get Methods

With the auth token downloaded from REST API, I need to store somewhere for future use. One way to make any variable persistent in chrome is to use Chrome provided set function.

chrome.storage.local.set({ "auth_token": JSON.stringify(data.token) })

Once the variable was saved to local storage, it can be retrieved back by the get function.

var auth_token = chrome.storage.local.get(["auth_token"]);

It would not be a proper journey if I didnโ€™t get a programming error right away ๐Ÿ™‚

Error in invocation of storage.get(optional [string|array|object] keys, function callback): No matching signature.

The documentation mentions the necessity of having a function callback, so letโ€™s fix that.

chrome.storage.local.set({ "auth_token": JSON.stringify(data.token) } , function() {})

var auth_token = chrome.storage.local.get(["auth_token"] , function() {});

Now I get variable undefined.ย  Maybe because I put the code outside the Ajax call? So I move it inside Ajax success brackets like this.

success:function(data){
    chrome.storage.local.set({ "auth_token": JSON.stringify(data.token) } , function() {})
    var auth_token = chrome.storage.local.get(["auth_token"] , function() {});
}

Console log still returns auth token undefined. Probably the variable I am printing needs to be inside the get function, which makes sense after all.

chrome.storage.local.get(["auth_token"] , function(payload) {
    var auth_token = payload;
    console.log(auth_token);
});

Cool. I think I progressed far enough and I deserve a well-earned sleep. The last three days were frustrating having to deal with issues not related to my MVP product at all. Luckily, I overcame them by old good brute force.

My MVP is far from done though. If I have to give an estimate, I finished 15% of the product. The rest still lies ahead of me.

The journey has been rewarding so far. I learned so much about Chrome and Javascript and I hope I will make more extensions in the future. See you next time.

Share it!