Two common mistakes developers make when using a private API key in their application are:
- Including the API key in the source code
- Including the API key in client-side HTTP requests
Revealing private API keys and authorization tokens to users through client-side HTTP requests and in your source code exposes you and your application to a high risk of abuse. If a malicious user uses your API key for their application, it can cost you money and potentially prevent you from using the API by exceeding your usage limits.
To avoid this, it’s recommended that you perform any requests that require authentication from the back-end of your application (a server) rather than the browser. However, if you need to make requests on the fly on the client-side, a quick way to accomplish this more securely is to use a proxy server to wrap requests with the necessary headers/query parameters containing your keys.
What is a Proxy Server?
A proxy server acts as a middle-man between your application and the API. There are 3 steps:
- You send a request to the server.
- The server sends that request to the API.
- The server responds to your request with the response from the API.
The proxy server I’m going to demonstrate here is only slightly more secure than using your keys in the browser as, without further precautions (such as domain restrictions), other users could simply make requests through your proxy. I’ll explain further precautions you can take at the end of this post.
Basic Proxy Server Example
Here’s an example proxy server I wrote using Deno. I used Deno for because Deno Deploy is offering free hosting while it's in public beta (as of May 2022). If you visit Deno Deploy and start a playground project, you can try this out for yourself using this code!
If you're using Node, the same principles apply, but you'll have to rewrite the code to use Express or another Node-based web application framework.
This server makes requests to the Twitter API. To authenticate my requests, I must include an “Authorization” header with a bearer token as its value. I'll send requests to the proxy server and it will attach the Authorization header and send the request to the API.
To make my requests through the proxy, I’ll replace the API domain with my server's domain.
Here's an example:
- My server is hosted at
https://my-project.deno.dev
- I want to send a request to the Twitter API at
https://api.twitter.com/2/tweets?ids=1261326399320715264
To send the request via my proxy server from my application, I would use this combined URL: https://my-project.deno.dev/2/tweets?ids=1261326399320715264
import { serve } from "https://deno.land/std@0.137.0/http/server.ts";
const bearer = "Bearer MYBEARERTOKEN";
async function handler(req) {
const url = new URL(req.url);
const pathname = url.pathname;
// Replace server domain with Twitter API domain for the proxy request
const fetchURL = `https://api.twitter.com${pathname}${url.search}`;
// Only make proxy request if there is a URL path
if (pathname.length > 1) {
const res = await fetch(fetchURL, {
headers: { Authorization: bearer }
});
// If there is a response body, send it as text to the application.
if (res.body) {
return new Response(await res.text(), {
headers: { "Access-Control-Allow-Origin": "*" }
});
}
}
// If there is no URL path, display "Hello World"
return new Response("Hello World");
}
serve(handler);
Further Precautions
As I mentioned earlier, this simple proxy server will prevent users from seeing your private API keys in your source code and HTTP requests, but a user might still make requests using your proxy server.
A precaution you can take to prevent this is to restrict requests from other domains. Simply change the Response header "Access-Control-Allow-Origin" from "*" to the hostname you want to accept requests from. Example: { "Access-Control-Allow-Origin": "https://www.google.com" }
And that's it! If you have any thoughts on this method of protecting API credentials or other precautions you recommend, please mention them in the comments below!
Top comments (2)
How "Access-Control-Allow-Origin" will protect you from not some browser client-side app will do the requests, but just another app (server) will act as a proxy doing the request where "Access-Control-Allow-Origin" has no effect?
Isn't it more correct to say that if the server has an existing authentication mechanism (e.g. OAuth2.0) to simply use its sliding expiration session token (access token) where this token has a short life and renewing it will cost lots of efforts and additionally, the proxy server can put a quota per app user in case a app user account is used for automatic reauthentication?
I would love to give this a try, but I can't get my Angular app to deploy to Deno. It should be a fairly simple process, however I'm receiving errors that modules are not found. Do you have any idea about that?