I recently worked on a project for the Astro 1.0 Hackathon, that has some authentication and a paid section. Naturally, I'd like the content in the paid section to only be available to... paying customers. For example, there are loads of images, but also markdown files, and other static assets that I'd like to protect.
Since Astro currently doesn't really have a middleware system, it's been kind of tricky to figure out how to protect static assets, or how to kind of... intercept a request.
Netlify Edge Functions to the rescue
But! Then along came Netlify Edge Functions. Netlify Edge Functions can be used in a similar way to middleware — we can essentially put an Edge Function in front of the Netlify Function that serves our Astro application, and do something with the request; like for example check authentication status!
The difference between Edge Functions and Regular Functions
Edge Functions are run on the edge, which I've taken to mean they're available in more regions than regular functions, or; potentially a region that's closer to your user. They also have a faster start up time, because they use Deno instead of Node.js. This also means that you have to keep in mind that you can't use any dependencies that depend on Node.js built-ins when writing Edge Functions, which may or may not be really inconvenient.
Additionally, Netlify Edge Functions have a really nice concept of forwarding or next()
ing a request, which will essentially defer the handler to the next handler in line;
export default async function(req, context) {
console.log('Handling request!');
// if we transform the request, we have to call `context.next()`
return await context.next();
// if we want to just 'forward' the request to the next handler, we can just return
return;
}
That's right, you can have multiple Edge Functions handling a response. From the Netlify Documentation, a request for /admin
would invoke the auth
function first, and then the injector
function:
[[edge_functions]]
path = "/admin"
function = "auth"
[[edge_functions]]
path = "/admin"
function = "injector"
[[edge_functions]]
path = "/blog/*"
function = "auth"
The order of execution is:
- Netlify Edge Functions (potentially multiple)
- Evaluating redirect rules from your
_redirect
if present - Static content
- Netlify Function
Protecting static assets
Now that we know how to use Edge Functions to act as a middleware of sorts, we can start getting our hands dirty. First of all, I've created my project with:
npm init astro@latest -- --template minimal
This will scaffold a barebones Astro starter project. Next up, install the @astrojs/netlify
adapter:
npm i -S @astrojs/netlify
And add the adapter to your configuration:
import { defineConfig } from 'astro/config';
+ import netlify from '@astrojs/netlify';
// https://astro.build/config
export default defineConfig({
+ adapter: netlify()
});
As an aside, you can also run Astro directly on Netlify Edge Functions, but in this case my project uses some dependencies that are not compatible with Deno
We'll also create a protected/
folder inside the public
folder that has been scaffolded by Astro, and add an image to it, for example: my-app/public/protected/my-image.png
.
Great! We can now create our Edge Function. In the root of your project, create a edge-functions/
folder, that contains a protected.js
:
my-project/edge-functions/protected.js
:
export default async (req) => {
const url = new URL(req.url);
const protectedRoutes = new URLPattern({pathname: '/protected/:img'});
const match = protectedRoutes.exec(url);
const auth = url.searchParams.has('auth');
if(match && !auth) return new Response(null, {status: 403});
return;
};
In this case, because we're not transforming the request, we're simply
return
ing instead of callingcontext.next()
In this file we use an URLPattern to see whether the request matches a protected asset or not. If it does, we want to see if a user is authenticated. For demoing purposes, we'll just check if the auth
query parameter is present (for example: https://my-app.netlify.app/protected/foo.png?auth=true
). If the auth
query parameter is present, we just return, which will defer the request to the next step in Netlify's execution line (netlify edge functions -> redirect rules -> static assets -> netlify function), which in this case means the static image will be served to the browser. If the auth
query parameter is absent, the user will get a 403 Forbidden
response, and the static asset will not be served.
In a real application, you'll probably want to check the authentication status of the user by verifying the JWT, or whatever other authentication mechanism you use, for example:
import { verify } from "https://deno.land/x/djwt@v2.4/mod.ts";
const encoder = new TextEncoder();
const keyBuf = encoder.encode(Deno.env.get('JWT_SECRET'));
const key = await crypto.subtle.importKey( "raw", keyBuf, {name: "HMAC", hash: "SHA-256"}, true, ["sign", "verify"]);
export default async (req, context) => {
const url = new URL(req.url);
const protectedRoutes = new URLPattern({pathname: '/protected/:img'});
const match = protectedRoutes.exec(url);
if(match) {
try {
const jwt = context?.cookies?.get('jwt') ?? '';
await verify(jwt, key);
return;
} catch(e) {
return new Response(null, {status: 403});
}
}
return;
};
Next up, we have to create a netlify.toml
file in the root of our project, and declare our Edge Function:
[[edge_functions]]
path = "/protected/*"
function = "protected"
This means that for every request that matches /protected/*
, it will execute our protected.js
Edge Function.
Deploying
Excellent, we should now be all set, and have some protection of our static assets in place.
To wrap things up, all we have to do is add the following deploy
script to our package.json
:
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
+ "deploy": "astro build && cp -R ./edge-functions ./netlify && netlify deploy --prod --build --dir=netlify",
"preview": "astro preview"
},
The reason we have our edge-functions/
folder in the root of our project is that running astro build
will wipe the contents of the netlify/
folder, so if we'd have the source code of our edge function in the netlify/
folder, it would get overwritten whenever we run astro build
. To work around this, we just keep the edge-functions/
folder in our root, and at build-time, we copy them to the netlify/
folder with a simple command: cp -R ./edge-functions ./netlify
.
Do note that if you're deploying with the Netlify CLI, you'll now have to use the --build
flag as well to correctly deploy your Edge Function, e.g.: netlify deploy --prod --build --dir=netlify
.
And when we finally run npm run deploy
, we can see our application works exactly as expected:
-
https://astro-protected-assets.netlify.app/protected/foo.png
-> 403 Forbidden, image is not shown -
https://astro-protected-assets.netlify.app/protected/foo.png?auth=true
-> 200 OK, image is shown
You can find a link to the demo here, and a link to the source code here.
Top comments (1)
Great post!
I've been trying to figure out a good way to protect assets in a pet astro project.
I've just been doing a protected API route for each resource, this seems much simpler though! Thanks for sharing.