Authorization & Application Logic

Hi all!

I’m working on building an app using Redwood, and trying to contribute whatever I run into if I miss something.

A big thing I’m dealing with atm is Authorization. I don’t think this is specific to Redwood, but more to Authorization in GraphQL servers in general, and seeing as how Redwood is sort of a collection of best practices, I thought it might be helpful to share my thoughts!

(Maybe you’ve already been discussing this topic, but I didn’t see a forum topic yet, so I figured I’d make one!)

My thoughts

Schema Directives

I’ve found a lot of examples for dealing with Authorization using schema directives. Initially it makes a lot of sense and is super easy to do!

E.g.


type User {
  id: ID!
  name: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
}

type Query {
  posts: [Post!]!
  users: [User!]!
}

type Mutation {
  updatePost(id: ID!) @youCanOnlyDoThisIfItsYourPost
  deletePost(id: ID!) @youCanOnlyDoThisIfItsYourPost
}

This provides a very clear overview for what you can and cannot do! However there are a few limitations to this approach.

Business Logic Layer

Let’s say you want a post to only be shown if it’s status is Published, but not if it’s Draft, unless you are the author of that Post! You could probably do this using a schema directive. However if you do this with all your rules, you could to end up with a very nasty schema!

What seems obvious to do now is doing authorization in your resolver. E.g.


// services/post.js

const posts = (_input, { context }) => {
  return db.posts.findMany({
    where: {
      OR: [{ status: "Published" }, { author: { id: context.user.id } }]
    }
  });
};

This returns all the posts that are Published OR that you own, perfect!

The problem however is that there’s multiple ways to get posts. You can also get them through the users query! This means you would need to duplicate your authorization logic.

In this case it would be great to move your authorization logic into it’s own layer. To generalize this more, it would be great to separate your business logic layer from your resolver! This is what’s described in https://graphql.org/learn/thinking-in-graphs/

Resulting into something like this:


// models/Post.js

export const Post = {
  all(author) {
    return db.posts.findMany({
      where: {
        author,
        OR: [{ status: "Published" }, { author: { id: context.user.id } }]
      }
    });
  }
}

// services/post.js

export const posts = (_input, { context }) => {
  return context.models.Post.all()
};

// services/user.js

export const users = (_input, { context }) => {
  return context.models.User.all()
};

export const User = {
  posts: (_args, { root, context }) => context.models.Post.all(root)
}

(Contrived example, but I think you get the gist!)

This puts your business logic in a dedicated layer.

What is suggested in the graphl.org article is:

GraphQL -> Business Logic/Auth layer -> Persistence Layer

And the current setup in Redwood is:

GraphQL -> Persistence Layer (prisma)

My proposal is to insert a layer in between these 2 by default when generating services or scaffolds. I think next to solving some problems with Authorization, this could also help users find a place for their business logic.

Perhaps even a combination with basic directives for admin groups and ownership.

What do you think?

Hope this helps, cheers!

3 Likes

We actually think of services as our business logic layer. I have an idea for the ability to add middleware/ plugin layer to services:

https://github.com/redwoodjs/redwood/blob/master/packages/api/src/makeServices.ts#L1-L9

Authentication/ Authorization is the next big thing on our radar and we would love your help!

Another user @chris-hailstorm has made some suggestions into how to approach such a system. Perhaps we should all collaborate on getting this done?

Thanks for the reply!

I see your point about services being the application layer. At first I tried to do:

import { posts } from 'src/services/posts'

export const User = {
  posts: (_args, { root, context }) => posts(root)
}

(With a slightly different posts function than mentioned before)

But I had some cognitive trouble with this since now the posts function is both a Model and a Resolver.

But now that I think of it I can’t come up with any reason why that would be bad.

I’d love to help where I can!

1 Like