Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Support GraphQL Trusted Documents aka Persisted Operations for added security #9416

Merged
merged 49 commits into from
Nov 17, 2023

Conversation

dthyresson
Copy link
Contributor

@dthyresson dthyresson commented Nov 14, 2023

RedwoodJS can be setup to enforce persisted operations -- alternatively called Trusted Documents.

Use trusted documents if your GraphQL API is only for your own apps (which is the case for most GraphQL APIs) for a massively decreased attack-surface, increased performance, and decreased bandwidth usage.

At app build time, Redwood will extract the GraphQL documents (queries, etc) and make them available to the server. At run time, you can then send documentId or "hash" instead of the whole document; only accept requests with a documentId and one that it knows about.

This prevents malicious attackers from executing arbitrary GraphQL thus helping with unwanted resolver traversal or information leaking.

This PR:

  • can configure to use Trusted Documents (or not) via a graphql project toml config
[graphql]
  trustedDocuments = true
  • Updates GraphQL Code gen to use the client preset which generates the following including documents with the query/mutation and hash
    image

  • Has some smarts to make the gql-tag compatible with the trusted documents' use of the graphql function that can access the precomputed document hash

  • Updates the Apollo web client to use the persistent query link to just send the document hash

  • Configure GraphQL Handler to use the Trusted Document store

import { createGraphQLHandler } from '@redwoodjs/graphql-server'

// ...
import { store } from 'src/lib/trustedDocumentsStore'

export const handler = createGraphQLHandler({
  getCurrentUser,
  loggerConfig: { logger, options: {} },
  directives,
  sdls,
  services,
  trustedDocuments: {
    store,
    customErrors: { persistedQueryOnly: 'really trust me' },
  },
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})
  • Set a custom error message in customErrors: { persistedQueryOnly: 'really trust me' },
  • When using Trusted Docs:
{"operationName":"FindBlogPostQuery","variables":{"id":3},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"46e9823d95110ebb2ef17ef82fff5c19a468f8a6"}}}

Is sent rather than full query.

This PR also updates the GraphQL docs.

Issues

The way the gql and graphql function is "swapped" could be refactored.

In this PR, the auto-import plugin is used:

[
      'babel-plugin-auto-import',
      {
        declarations: [
          {
            // import { React } from 'react'
            default: 'React',
            path: 'react',
          },
          // A project can turn on trusted graphql documents
          // If projects do not use trusted documents (default)
          // it auto-imports the gql tag from graphql-tag
          !useTrustedDocumentsGqlTag && {
            // import gql from 'graphql-tag'
            default: 'gql',
            path: 'graphql-tag',
          },
          // if projects use trusted documents
          // then it auto-imports the gql function from the generated codegen client preset
          useTrustedDocumentsGqlTag && {
            //  import { gql } from 'src/graphql/gql'
            members: ['gql'],
            path: `web/src/graphql/gql`,
          },
        ].filter(Boolean),
      },
      'rwjs-web-auto-import',
    ],

So, if the toml says to use trustedDocuments, either gql from graphql-tag is used or we import a aliased function that was generated as part of the client present replaceGqlTagWithTrustedDocumentGraphql function.

See: packages/internal/src/generate/trustedDocuments.ts

The graphql aliased function is appended:

  if (gqlFile && gqlFile.content) {
    gqlFile.content +=
      'export function gql(source: string) { return graphql(source); }'

    const content = format(gqlFile.content, {
      trailingComma: 'es5',
      bracketSpacing: true,
      tabWidth: 2,
      semi: true,
      singleQuote: false,
      arrowParens: 'always',
      parser: 'typescript',
    })

    fs.writeFileSync(gqlFile.filename, content)
  }

This is done because the babel auto import plugin cannot use aliases.

However, one could make the case for writing a proper babel plugin instead of appending this function and importing.

This can be a refactor.

@dthyresson dthyresson marked this pull request as ready for review November 14, 2023 21:25
@dthyresson dthyresson requested a review from Tobbe November 14, 2023 21:25
@dthyresson
Copy link
Contributor Author

@Tobbe since @Josh-Walker-GM and I paired, your review is appreciated. Note the way the gql and graphql function is imported.

@dthyresson
Copy link
Contributor Author

Seeing this error

image

Somehow the RSC portion doesn't have that import?

@dthyresson
Copy link
Contributor Author

Seeing this error

image

Somehow the RSC portion doesn't have that import?

I tried to reproduce locally, here what I did:

  1. npx -y create-redwood-app@canary -y --no-git ./rsc-2
  2. cd rsc-2
  3. yarn
  4. yarn rw experimental setup-streaming-ssr -f
  5. yarn rw experimental setup-rsc
  6. yarn
  7. yarn rw build
  8. In other terminal: yarn rwfw project:copy
  9. yarn rw build
  10. yarn rw serve

RSC app seems to work ok:

image

@Tobbe
Copy link
Member

Tobbe commented Nov 17, 2023

I'll merge this one even though it fails one of the RSC tests. @dthyresson and I looked at this together, and I'm fairly sure this is down to a limitation in rwfw project:copy. The tests passes locally with rwfw project:sync.
As soon as these changes are part of a canary build CI should pass again. The plan is to verify this by retriggering CI on another open PR that previously passed. (Likely #9546)

@Tobbe Tobbe merged commit a1de078 into main Nov 17, 2023
@Tobbe Tobbe deleted the dt-generate-persisted-operations branch November 17, 2023 16:33
@Tobbe Tobbe modified the milestones: next-release, v6.5.0 Dec 1, 2023
@Urigo
Copy link

Urigo commented Dec 2, 2023

this is huge!

@Urigo
Copy link

Urigo commented Dec 2, 2023

@n1ru4l would be so happy

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:feature This PR introduces a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants