GraphQL backend — authorization & authentication

mahendranv

Mahendran

Posted on May 31, 2021

GraphQL backend — authorization & authentication

GraphQL backend — authorization & authentication

Background

In any system where users interacting with each other, authorization & authentication are the key elements that controls what each user can do. To read more on authentication and authorization try RBAC. To demonstrate the usecases, I present few roles and resources below. This is an e-commerce website.

Role Remarks
Visitor There is no session yet, user can enroll in the system to become Buyer/Seller
Buyer Buyer can place order and update his address-profile
Seller Seller can add product and update order status once Buyer place an order

For simplicity resources are just few GraphQL mutations and users are hardcoded in the system. You can get the sample project source at github.

Let's start with graphql schema and move towards authentication.

  1. Background
  2. 🧑‍💻 Code it
    1. Ground preparation
    2. Components to know
      1. Filters
      2. RequestContextHolder
      3. RequestManager
      4. Aspect [AOP]
    3. RequestManager
    4. Request filter
    5. Custom annotations
    6. Aspect
  3. 🚀 Run it
    1. Why do we need method level control?
  4. Future scope
  5. 📖 Resources

🧑‍💻 Code it

Ground preparation

First define a schema with few queries that meant for each role. They just return a string when each mutation is invoked. Catch is each user need specific auth token to access the resource.

    type Mutation {
        registerUser: String
        placeOrder: String
        addProduct: String
    }
Enter fullscreen mode Exit fullscreen mode

And the respective DGSComponent that expose the resource looks like this:


@DgsComponent
class DummyResource {

    @DgsMutation
    fun registerUser(): String = "dummy-member-id"

    @DgsMutation
    fun placeOrder(): String = "dummy-order-id"

    @DgsMutation
    fun addProduct(): String = "dummy-product-id"

}

Enter fullscreen mode Exit fullscreen mode

In a typical system, each of them will need input, since we focus on roles and how do we protect each mutation from other roles, all other parts are omitted for brevity. We expect the system to throw error when authentication fails. Below is the DummyUserService class where system identifies user role given an auth-token.


@Service
class DummyUserService {

    val sessions = mapOf(
        "token-buyer" to Roles.BUYER,
        "token-seller" to Roles.SELLER
    )

    fun identifyRole(token: String?): Roles {
        return sessions[token] ?: Roles.VISITOR
    }
}

enum class Roles {
    VISITOR,
    BUYER,
    SELLER
}

Enter fullscreen mode Exit fullscreen mode

Above wraps up the overall setup for roles and resources. Let's wire up authentication.


Components to know

We'll use following components to set up authentication. They are born of springboot & aspectj and not specific to graphql. So, even RestController can benefit using the below ones. Oneliner on each piece before start implementing them. How do they interact is illustrated in below diagram.

Components in orange will be created by us. Green one is provided by springboot. And the blue wrapper is generated by aspectj for us.

Filters

Filters are the requst interceptors. They can process any incoming servlet request before it reaches the resolver (in our case the mutation). We'll use OncePerRequestFilter to augment our incoming request with Role related information.

RequestContextHolder

RequestContextHolder can hold session related information (user id, token, role) for a servlet request. However, we own the logic to build a session context. At any point of time, this can be accessed within the system to identify session.

RequestManager

This is created by us to wrap RequestContextHolder and provide nice interface to other components in play. Business logic to build a session context is done here.

Aspect [AOP]

Aspect oriented programming is a programming paradigm where any component can be augmented to provide common functionalities, so that each controller (mutation) doesn't have to hold any repeating business logic (authentication). This is achieved by creating proxy classes around the target class. Head over to wiki to know more about this.

That covers each component and it's role in authentication. Let's code to have further clarity. I'll build bottom up to so that we'll know the dependency between each.


RequestManager

Requst manager reads header from incoming request and store it to RequestContextHolder. It uses UserService created in Ground preparation section. It also hosts few helper methods to ease up session data retrieval in latter parts. Code should be self-explanatory with inline comments.


@Component
class DummyRequestManager {

    @Autowired
    private lateinit var userService: DummyUserService

    // Save the session info per request. Retrieve it throughout the request
    fun saveSession(request: HttpServletRequest) {
        // Retrieve auth token from request - if any
        val token: String? = request.getHeader(HEADER_TOKEN)

        // Identify role
        val role = userService.identifyRole(token)

        // attribute the request with session. This will be available throughout the session
        request.setAttribute(
            KEY_SESSION, DummySession(
                role = role
            )
        )
    }

    // A non-null guaranteed session. Everyone has a role here.
    fun getSession(): DummySession {
        val session = RequestContextHolder
            .getRequestAttributes()!!.getAttribute(KEY_SESSION, RequestAttributes.SCOPE_REQUEST)
        return session as DummySession
    }

    /**
     * Convenience method to retrieve role
     */
    fun getUserRole(): Roles = getSession().role

    companion object {
        private const val KEY_SESSION = "userSession"
        private const val HEADER_TOKEN = "x-auth-token"
    }
}

data class DummySession( val role: Roles)

Enter fullscreen mode Exit fullscreen mode

Request filter

Request filters are the interceptors for any request. Logging and session creation are the common usecase for this component. Let's create a DummyRequestFilter and invoke request manager to create session.


/**
 * Interceptor that executed once per http request
 */
@Component
class DummyRequestFilter : OncePerRequestFilter() {

    @Autowired
    private lateinit var requestManager: DummyRequestManager

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {

        // Feed the request to request manager for session preparation
        requestManager.saveSession(request)

        // Resume with request
        filterChain.doFilter(request, response)
    }
}

Enter fullscreen mode Exit fullscreen mode

Custom annotations

With above two pieces in place, we identified user role per request. Next is to retrict access for each mutation. Let's create few annotations for each role.


@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class BuyerOnly

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class SellerOnly

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class VisitorOnly

Enter fullscreen mode Exit fullscreen mode

Marking the territory for each user.


File: "DummyResource.kt"

@DgsComponent
class DummyResource {

    @VisitorOnly
    @DgsMutation
    fun registerUser(): String = "dummy-member-id"

    @BuyerOnly
    @DgsMutation
    fun placeOrder(): String = "dummy-order-id"

    @SellerOnly
    @DgsMutation
    fun addProduct(): String = "dummy-product-id"

}
Enter fullscreen mode Exit fullscreen mode

Aspect

We have session and resource marked for each role. All that's left is to read the annotation and role and authenticate requests. For this purpose, we have Aspects — aspects can run before-after-even around a pointcut. Here, pointcut refers to our mutation method — the safety net logic that we execute before mutation is called advice. Together they're called aspect. So, our final piece “the aspect” is here.


@Component
@Aspect
class DummyAspect {

    @Autowired
    private lateinit var requestManager: DummyRequestManager

    @Before("@annotation(SellerOnly)")
    fun restrictSellerOnly(joinPoint: JoinPoint) {
        val role = requestManager.getUserRole()
        if (role != Roles.SELLER) {
            throw GraphQLException("This operation is specific to Seller accounts")
        }
    }

    @Before("@annotation(BuyerOnly)")
    fun restrictBuyerOnly(joinPoint: JoinPoint) {
        val role = requestManager.getUserRole()
        if (role != Roles.BUYER) {
            throw GraphQLException("This operation is specific to Buyer accounts")
        }
    }

    @Before("@annotation(VisitorOnly)")
    fun restrictVisitorOnly(joinPoint: JoinPoint) {
        val role = requestManager.getUserRole()
        if (role != Roles.BUYER) {
            throw GraphQLException("Please logout from existing session")
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

That's it.


🚀 Run it

Try running the mutation in curl or graphiQL playground [http://localhost:8080/graphiql].

// Seller adds product succussfully

curl 'http://localhost:8080/graphql' \
  -H 'x-auth-token: token-seller' \
  -H 'Content-Type: application/json' \
  --data-raw '{"query":"mutation { addProduct }","variables":null}' \
  --compressed

{"data":{"addProduct":"dummy-product-id"}}

Enter fullscreen mode Exit fullscreen mode
// Buyer cannot add product

  curl 'http://localhost:8080/graphql' \
  -H 'x-auth-token: token-buyer' \
  -H 'Content-Type: application/json' \
  --data-raw '{"query":"mutation {\n  \n  addProduct\n  \n}","variables":null}' \
  --compressed
{
  "errors": [
    {
      "message": "This operation is specific to Seller accounts",
      "locations": [],
      "extensions": {
        "errorType": "UNKNOWN"
      }
    }
  ],
  "data": {
    "addProduct": null
  }
}

Enter fullscreen mode Exit fullscreen mode

Why do we need method level control?

Following query is possible with graphql.


mutation { 
    addProduct 
    placeOrder 
}

### header
"x-auth-token":"token-seller"

Enter fullscreen mode Exit fullscreen mode

In a single graphQL http-request you can stack up multiple operations. In such cases, only one operation fails while the other execute successfully. Pretty cool right 😎?.

{
  "errors": [
    {
      "message": "This operation is specific to Buyer accounts",
      "locations": [],
      "extensions": {
        "errorType": "UNKNOWN"
      }
    }
  ],
  "data": {
    "addProduct": "dummy-product-id",
    "placeOrder": null
  }
}
Enter fullscreen mode Exit fullscreen mode

Future scope

With this scaffold we can build resources with granular control. With simple annotations added to each method, it is possible to restrict nested objects without modifying much to the business logic in resolvers.

For this example, we took Role in focus. Let's say I need to create a resource specific to a user, like an address. I don't have to (shouldn't) pass in user-id in the input. It is cross verified against the token and available in session. A full context like user's client/locale etc. will be availble to tailor perfect response.

In a reallife system, the userservice would be table query or federated service provider. For performance, the session information should be cached and remote calls must be avoided to get bit of performance boost.

📖 Resources

  1. Github repo
  2. Aspect oriented programming
  3. Role Based Access Control
💖 💪 🙅 🚩
mahendranv
Mahendran

Posted on May 31, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related