[Support Guide]: How to apply Access Control for Netlify Functions?

Last checked by Netlify Support Team: August 2023

Introduction:

That is a great question, we do get it from time to time. But the answer is not that straightforward.

First, there are 2 ways someone might think this works:

Can I save my Function invocation count by restricting the function to be called only from my frontend?

The answer to that is no. Unfortunately, it just won’t work. No matter what you do, the moment you deploy a Netlify Function, its endpoint is accessible to everyone and it can be subjected to abuse. Sorry to say but there’s absolutely no way to protect your function invocation count yet. The reasons for this will be discussed further in this post. However, the good part is, we haven’t seen a lot of users whose functions were abused in a way that cost them any money, so this isn’t a frequent problem in practice. Which brings us to the second question which we can help with:

Can I make sure only my site is able to receive a response from my function?

Technically, yes, it’s possible. But it’s a lot more complicated than that to implement. The primary reason for that is, browsers make it really easy to see network requests that are being made by a web app. Furthermore, there are options to easily copy the requests as cURL and thus, that makes it more difficult for developers to make the endpoints secure. Here’s why:

Since the endpoints can be accessed by anyone, you need to determine a way to distinguish your application from the rest of the requests. How can you do that? Here are a few approaches:

Approach 1: Using default request Headers

You could try to use request headers that a browser generally automatically sends along with the requests like the referer HTTP request header for example. In the serverless function, you could implement something like:

if (event.headers.referer.includes('mydomain.com')) {
 // process the function
} else {
  return {
    statusCode: 401,
    body: JSON.stringify('Unauthorized')
  }
}

The upside is, it’s really easy to implement this. The downside is, it’s a very primitive layer of security. It won’t take a lot of guesses for someone to try and spoof the referer header of their request to get successful in getting a response.

You could also add multiple checks for example, you could check for referer, origin, host and multiple such headers to determine whether to return a response or not.

This would definitely be stronger than a single check, but again, someone can simply try all the headers in the developer tools and then would eventually be successful.

Approach 2: Using custom request headers or body

You could also send a custom header or a payload with each request and check for that inside your function. For example, in your frontend, you could do:

fetch('url', {
  headers: {
    security: 12345
  }
})

In your serverless function:

if (event.headers?.security === 12345) {
  // process the function
} else {
  return {
    statusCode: 401,
    body: JSON.stringify('Unauthorized')
  }
}

So, this one could be a little more secure than others, in the sense, it won’t be ‘guessable’. A person might try to use the common headers explained previously and would be unsuccessful, but as already pointed many times, they could see a pattern of you trying to use this header multiple times and thus, they could guess it eventually.

You could try to spice things up by randomly generating this header value with a specific logic for example, using Date.now() to get the timestamp and multiplying it with some random number or something like that. Considering you’ve set a logic in your serverless function to decode this ‘secret key’, it still won’t work because your serverless function would run on a different timezone than the client device which would make the time-based verifications difficult.

Furthermore, all of these approaches are even easier to breach for users who know their way around the command line. All it would take is something like copy as cURL and paste it and get a response as it would contain the exact ‘secret’ data that you used to make a request.

Approach 3: Using authorisation

This is a typical approach used by many websites and this is the reason why we need authentication on the web. As you can see, unauthenticated calls like above could be bypassed and could allow anyone to see the response. But, when you protect your serverless functions by something like authorization, it becomes a lot more secure. For example, if you now require the API call to to be called with Authorization: Bearer <token> if would make the function accessible only by that user. Yes, the same user could still copy-paste the cURL command, but there’s not much use doing that, as they could anyways see the response in browser. But now we can be sure that the function can only respond to ‘authenticated’ users and not others. When on Netlify, you could use Netlify Identity along with functions.

For example, if your user is logged in into the Netlify Identity, your fetch call could look something like:

let auth = new GoTrue({
  APIUrl: 'https://<your domain name>/.netlify/identity',
})
fetch('url', {
  headers : {
    Authorization: `Bearer ${auth.currentUser().login.token.access_token}` 
  }
})

And in your serverless function:

exports.handler = async (event, context) => {
  if (context?.clientContext?.user) {
    // process the function
  } else {
    return {
      statusCode: 401,
      body: JSON.stringify('Unauthorized')
    }
  }
}

This would make sure only logged in users are able to get a response from the function. There won’t be an ethical way for non-logged in users to get a response. However, this won’t restrict the access to the frontend itself.

Note: You could do the same thing with cookies instead of headers.

But does that mean there’s no way to allow anonymous access to functions and still keep the response secure? Well, you can increase the security even more than explained above, but it still has its limitations. This brings us to:

Approach 4: Using Netlify Identity

This is way more complicated to implement, but the only ‘most secure’ way to do this. Here’s how this will work:

  1. Create a new (random) user in Netlify Identify.
  2. Log the user in.
  3. Get the authorization token and use it to call the serverless function.
  4. Verify if the auth token is valid in the function.
  5. Process the function and delete the user.

To do this, you’d need to:

  1. Keep your Netlify Identity registration preferences to open:

  1. Set Autoconfirm to true:

Then, in your frontend:

import GoTrue from 'gotrue-js'
let auth = new GoTrue({
  APIUrl: `${location.origin}/.netlify/identity`
})
async function setupAuth() {
  let email = ''
  let domain = ''
  let password = ''
  let domainCharacters = 'abcdefghijklmnopqrstuvwxyz'
  let emailCharacters = domainCharacters + '1234567890'
  let passwordCharacters = emailCharacters + '!@#$%^&*()-+<>ABCDEFGHIJKLMNOP'
  for (let stringLength = 0; stringLength < 5; stringLength++) {
    email += emailCharacters.charAt(Math.floor(Math.random() * emailCharacters.length))
    domain += domainCharacters.charAt(Math.floor(Math.random() * domainCharacters.length))
    password += passwordCharacters.charAt(Math.floor(Math.random() * passwordCharacters.length))
  }
  emailCharacters = null
  domainCharacters = null
  passwordCharacters = null
  email += '@' + domain + '.com'
  await auth.signup(email, password)
  let login = await auth.login(email, password)
  email = null
  domain = null
  password = null
  return login.token.access_token
}
setupAuth().then(response => {
  fetch('url' {
    headers: {
      Authorization: `Bearer ${response}`
    }
  })
})

Basically, first you’re setting up a function to randomly generate an email address and password. The above example can be simplified to generate random user without all those character configurations. However, do not hardcode the value, as chances are your app might be used by more than one person at a time, and if you hardcode a value, you won’t be able to provision both the users.

Then, in your serverless function:

const fetch = require('node-fetch')
exports.handler = async (event, context) => {
  const {identity, user} = context?.clientContext
  if (user) {
    return fetch(`${identity.url}/admin/users/${user.sub}`, {
      method: 'DELETE',
      headers: {
        Authorization: `Bearer ${identity.token}`
      }
    }).then(() => {
      // process the function
    })
  } else {
    return {
      statusCode: 401,
      body: JSON.stringify('Unauthorized')
    }
  }
}

Note: The above requires node-fetch version 2.x.

So in here, we validate the user and delete it immediately. This is so that the same person won’t be able to use tools like cURL to copy the headers from the browser and re-use the auth token as the user will have been deleted. Now there are downsides to this method too:

  1. As you might have guessed, with the above config, you can’t keep Identity signup disabled or invite-only. It must be open.
  2. On the Identity Level 0 Tier (Starter and Pro), you get only 1000 users in a particular billing cycle. So, considering you won’t be using any Identity members for any other usage, this will work only for the first 1000 API calls.
  3. It’s not 100% secure too. Since the Identity registration preferences are set to open, it’s possible for anyone to create a user on your site on your behalf. And once they create a user, they could get an auth token and call the endpoint.

Also, this is what I said above, the function invocation count will be counted regardless of whether you secure it or not, because even to check the security rules, the function has to run and return some content (even an error).

Approach 5: Using Netlify Edge Functions

You can use Netlify Edge Functions to protect your Functions as well. Considering they’re cheaper than Functions, that could be a viable choice. You can add the Edge Functions for /.netlify/functions/* path (or for a specific function), and within that Edge Function, check if the request should pass to your function or not. This would ramp up your Edge Function invocation count, while keeping your Functions invocation count in check.

Conclusion:

So here we’ve covered some ways using which you can add some layers of security to your Functions endpoint. None of them assure you a 100% security, but it’ll just make if difficult for a malicious user to breach some levels of security to extract data out of your endpoint. All in all, you should consider your comfort, the budget and time constraints of your project among other things to decide what way would be best for you.

Also, if you’ve any more ways to protect your endpoints, please share them with the forums.

5 Likes

It is the Referer header (single r) which is in fact a misspelling.

The Referrer-Policy header (with two r’s) can of course remove the Referer header.

Thank you. I have updated the spellings.