question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC - Secure Services by default

See original GitHub issue

As discussed in the 2021-11-12 Core Meeting, we want to do what we can to make services secure by default. Currently, if you don’t add something like requireAuth() manually in a services function, any GraphQL endpoints that resolve to those services will happily execute by anyone on the internet. That could be bad.

This issue will serve as a single place to discuss different ways to make services secure by default. Here “by default” means that if you did not do anything special to explicitly allow access, your service/endpoint would be secure—return an error when attempting to access with no authorization/authentication provided.

Current State

Given the following service function:

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

And the following GraphQL SDL:

export const schema = gql`
  type User {
    id: Int!
    name: String!
    email: String!
  }

  type Mutation {
    deleteUser(id: Int!): User!
  }

Anyone on the internet could perform this mutation and delete every user in the database:

import { request, gql } from 'graphql-request'

const query = gql`
  mutation DeleteUser($id: Int!) {
    deleteUser(id: $id) {
      id
    }
  }
`

for (let i=0; i<100000; i++) {
  try {
    await request('https://insecuresite.com/.netlify/functions/graphql', query, { id: i })
  } catch (e) {
    console.log(`User ${i} not found, trying next...`)
  }
}

Note that a service can be used internally as well, and the same security implications apply.

This can be prevented by simply adding a requireAuth() call inside the service to check for proper permission (either being logged in at all, or the user having a specific role):

export const deleteUser = ({ id }) => {
  requireAuth({ roles: ['admin'] })

  return db.user.delete({
    where: { id },
  })
}

However, having to add it manually is rife for human error. We want Redwood to be secure by default and help save users from themselves.

Proposal A

Use some form of requireAuth() behind the scenes and add a new function like allowAccess() to explicitly allow access.

Assuming the same service:

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

Any access to this service would raise an error. You would need to explicitly allow access:

export const deleteUser = ({ id }) => {
  allowAccess({ roles: ['admin'] })
  return db.user.delete({
    where: { id },
  })
}

Perhaps just calling allowAccess() (without arguments) allows any logged in user to access.

Some downsides to this proposal:

  • Before adding allowAccess() calls, it’s not immediately clear that any kind of authorization is happening at all, these just appear to be regular functions
  • Not backwards compatible with existing codebases

Proposal B

This proposal addresses some of the concerns in Proposal A. This solution would be backwards compatible (since it uses the same requireAuth() function we already know and love). We add a convention of exporting a function that will automatically run before all services so you only need to declare requireAuth() once:

export const beforeService = () => {
  requireAuth({ roles: ['admin'] })
}

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

If you have multiple service functions and want to not require auth for one or more of them:

export const beforeService = () => {
  requireAuth({ roles: ['admin'] }, except: ['listUsers'])
}

export const listUsers = () => {
  return db.user.findMany()
}

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

Or the opposite where you only want to lock down a few functions:

export const beforeService = () => {
  requireAuth({ roles: ['admin'] }, only: ['deleteUser'])
}

export const listUsers = () => {
  return db.user.findMany()
}

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

This proposal has the added benefit of letting you execute additional code that you always want to occur before a group of services (logging the API call, perhaps):

import { logAccess } from 'src/services/logger'

export const beforeService = ({ context, serviceName }) => {
  requireAuth({ roles: ['admin'] }, only: ['deleteUser'])
  logAccess(context.currentUser, serviceName)
}

export const deleteUser = ({ id }) => {
  return db.user.delete({
    where: { id },
  })
}

What if the service does not export a beforeService function? We have a few options:

  • Raise an error: if you decide you do not need authorization in your service then you need to make it an explicit, conscious decision by exporting a beforeService() that returns nothing
  • Not require a beforeService() call, but throw an error or at least a warning if we do not encounter a requireAuth() function call in a service
  • Require an explicit function call like skipAuth() call to really make it clear what you’re doing

Conclusion

Please add a comment if you have any additional syntax ideas!

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:5
  • Comments:44 (39 by maintainers)

github_iconTop GitHub Comments

3reactions
mojombocommented, Jan 13, 2021

I like a lot of what I see in Option 2.

  • Being backwards compatible is pretty nice.
  • It allows you to cover a whole service very easily and still be quite visible that auth is happening if you keep that export at the top of the file.
  • If each of your “resolver” functions does something totally different, you can decompose the requireAuth into each one and leave your beforeService empty.

I’d probably expect a missing beforeService not to error and just disallow access to everything.

One question here: how does it apply to non-“resolver” functions in a service that are called from other places in the service or other services?

I think a good way to explore this further will be to think about what changes we’d need to make to the tutorial.

2reactions
cannikincommented, Apr 20, 2021

Got the prompt working!

image

  • If you already passed --force it won’t bother asking
  • If you say n then it behaves the same as if the --force flag was missing (refuses to overwrite files)
  • If you say y then it basically flips force to true for the tasks that involve overwriting files
Read more comments on GitHub >

github_iconTop Results From Across the Web

The Secure HyperText Transfer Protocol RFC 2660
Secure HTTP (S-HTTP) provides independently applicable security services for transaction confidentiality, authenticity/integrity and ...
Read more >
Towards Remote Procedure Call Encryption by Default
This document describes a mechanism that, through the use of opportunistic Transport Layer Security (TLS), enables encryption of Remote Procedure Call (RPC) ...
Read more >
Security Support Provider Interface Architecture | Microsoft Learn
Because the Kerberos protocol has been the default authentication protocol since Windows 2000, all domain services support the Kerberos SSP.
Read more >
A Detailed Look at RFC 8446 (a.k.a. TLS 1.3)
One major way Cloudflare provides security is by supporting HTTPS for websites and web services such as APIs. With HTTPS (the “S” stands...
Read more >
RFC security reviews - AMS Advanced Onboarding Guide
The AWS Managed Services (AMS) change management approval process ensures that we perform a security review of changes we make in your accounts....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found