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

Complete the story of Ballerina GraphQL security #1771

Closed
ldclakmal opened this issue Aug 13, 2021 · 9 comments
Closed

Complete the story of Ballerina GraphQL security #1771

ldclakmal opened this issue Aug 13, 2021 · 9 comments
Assignees
Labels
Area/Security Issues related to stdlib security module/graphql Issues related to Ballerina GraphQL module Points/3 Team/PCM Protocol connector packages related issues Type/Task

Comments

@ldclakmal
Copy link
Member

ldclakmal commented Aug 13, 2021

Description:
This is to complete the story of Ballerina GraphQL security, since the GraphQL spec does not enforce about this. It is an open ended question in the industry. There are pros and cons in each approach taken. Considering all the above, we need to solidify the complete story of Ballerina GraphQL security.

@ldclakmal ldclakmal added Type/Task Area/Security Issues related to stdlib security module/graphql Issues related to Ballerina GraphQL module Team/PCM Protocol connector packages related issues labels Aug 13, 2021
@ldclakmal ldclakmal self-assigned this Sep 14, 2021
@ldclakmal
Copy link
Member Author

ldclakmal commented Sep 28, 2021

Proposal: Ballerina GraphQL Authentication and Authorization

Summary

The GraphQL API probably needs to control which users can see and interact with the various data it provides. This enhances the Ballerina GraphQL APIs with authentication and authorization support.

Goals

  • Introduce GraphQL authentication
  • Introduce GraphQL authorization
  • Introduce proper error handling for authentication and authorization

Motivation

Authentication and authorization for the APIs are the primitive way of handling the security of the APIs. Authentication is determining whether a given user is logged in and subsequently determining which user someone is. Authorization is then determining what a given user has permission to do or see. This concept applies to GraphQL APIs as well.

The opinion of the GraphQL spec is to "delegate authorization logic to the business logic layer". It doesn’t have any strong opinion on authentication, besides the fact that creating "fully hydrated user-objects instead of passing auth-tokens down to our business logic".

In summary, Ballerina should have a way to authenticate and authorize the GraphQL APIs.

Description

Ballerina services can be secured with the following application layer security mechanisms. As of now, the Ballerina HTTP, WebSocket, gRPC modules have the support for these.

  1. Basic Auth
    1.1. File user store
    1.2. LDAP user store
  2. JWT Auth
  3. OAuth2

The [Design] Ballerina Authn/Authz - Swan Lake Release has proposed the common approaches for all the standard library protocols. The same can be applied to GraphQL as well.

Declarative Method

The declarative method of authentication and authorization of the GraphQL service is driven by the GraphQL service annotation (service configuration). This is 100% the same as HTTP, WebSocket, gRPC service authentication, and authorization.

Service Configuration

The @graphql:ServiceConfig annotation can accept the configurations related to the security protocol and optionally the authorization configurations which are referred to as ‘scopes’ in Ballerina.

Check the below definition for @graphql:ServiceConfig.

public type GraphqlServiceConfig record {|
   ListenerAuthConfig[] auth?;
   // ...
|};

Check the below definition for graphql:ListenerAuthConfig.

public type ListenerAuthConfig FileUserStoreConfigWithScopes |
                               LdapUserStoreConfigWithScopes |
                               JwtValidatorConfigWithScopes |
                               OAuth2IntrospectionConfigWithScopes;
 
public type FileUserStoreConfigWithScopes record {|
  FileUserStoreConfig fileUserStoreConfig;
  string|string[] scopes?;
|};
 
public type LdapUserStoreConfigWithScopes record {|
  LdapUserStoreConfig ldapUserStoreConfig;
  string|string[] scopes?;
|};
 
public type JwtValidatorConfigWithScopes record {|
  JwtValidatorConfig jwtValidatorConfig;
  string|string[] scopes?;
|};
 
public type OAuth2IntrospectionConfigWithScopes record {|
  OAuth2IntrospectionConfig oauth2IntrospectionConfig;
  string|string[] scopes?;
|};
 
public type FileUserStoreConfig record {|
   *auth:FileUserStoreConfig;
|};
 
public type LdapUserStoreConfig record {|
   *auth:LdapUserStoreConfig;
|};
 
public type JwtValidatorConfig record {|
   *jwt:ValidatorConfig;
   string scopeKey = "scope";
|};
 
public type OAuth2IntrospectionConfig record {|
   *oauth2:IntrospectionConfig;
   string scopeKey = "scope";
|};
Sample

Check the below code sample for a GraphQL service secured with JWT.

import ballerina/graphql;
 
@graphql:ServiceConfig {
   auth: [
       {
           jwtValidatorConfig: {
               issuer: "wso2",
               audience: "ballerina",
               signatureConfig: {
                   certFile: "/path/to/public.crt"
               }
           },
           scopes: ["admin"]
       }
   ]
}
service /graphql on new graphql:Listener(9090) {
   resource function get greeting() returns string {
       return "Hello, World!";
   }
}

Engage Auth Annotation

The auth annotation is evaluated when there is an incoming request. The 'Authorization' header is evaluated based on the configured auth protocol and validated based on the configured attributes.

Example: 'Authorization: Bearer eyJhbGciOi[...omitted for brevity...]adQssw5c' header is evaluated against the JWT Auth protocol and the JWT is validated based on the configurations of the above sample.

If the validation is successful, it is passed to the resolver level, if the validation is unsuccessful an error message is sent back as the HTTP response, formatted according to the GraphQL spec.

Error Handling

Auth Annotation

The error handling happens based on the type of error. If it is an authentication error, the response is an HTTP 401 response and if it is an authorization error, the response is an HTTP 403 error. For both cases, the payload is in a JSON error format as per the GraphQL spec.

Resolver

Error handling happens commonly for any type of error. The error is formatted as per the GraphQL spec and returned as an HTTP 500 response.

Advantages

  • All the GraphQL APIs can be authenticated and authorized at a single point.
  • Users do not need to be worried more about how authentication and authorization works.

Disadvantages

  • Users have no control over the customizations of business logic level authorizations.
  • Users have no flexibility for fine-grained control of the fields.
  • This is an all-or-nothing approach from the user's point of view.

Imperative Method

The imperative method of authentication and authorization of the GraphQL service is driven by the GraphQL context. This approach is a little bit different from HTTP, WebSocket, gRPC service authentication, and authorization.

Service Configuration

The @graphql:ServiceConfig annotation can accept the function pointer which has the user implementation related to the security protocol and optionally the authorization configurations which are referred to as ‘scopes’ in Ballerina.

Check the below definition for @graphql:ServiceConfig.

public type GraphqlServiceConfig record {|
   ContextInit contextInit;
   // ...
|};

Check the below definition for graphql:ContextInit.

public type ContextInit isolated function (http:RequestContext requestContext, http:Request request) returns Context|error;
Sample

Check the below code sample for a GraphQL service secured with JWT.

import ballerina/graphql;
import ballerina/http;
import ballerina/jwt;
 
isolated function initContext(http:RequestContext requestContext,  
                              http:Request request) 
                              returns graphql:Context|error {
   jwt:Payload|http:Unauthorized authnResult = handler.authenticate(request);
   if authnResult is http:Unauthorized {
       return error graphql:AuthnError(<string> authnResult?.body);
   } else {
       graphql:Context context = new;
       check context.add("user", authnResult?.sub);
       check context.add("scope", authnResult?.scp);
       return context;
   }
}
 
@graphql:ServiceConfig {
   contextInit: initContext
}
service /graphql on new graphql:Listener(9090) {
   resource function get greeting(graphql:Context context) returns string|error {
       if context.get("scope") is string && context.get("scope") == "admin" {
           return "Hello, " + context.get("user") + "!";
       } else {
           return error graphql:AuthzError("You are not authorized.");
       }
   }
}

final http:ListenerJwtAuthHandler handler = new({
   issuer: "wso2",
   audience: "ballerina",
   signatureConfig: {
       certFile: "/path/to/public.crt"
   }
});

Engage ContextInit Annotation

The context annotation is evaluated when there is an incoming request. The incoming http:Request, http:RequestContext is passed as the parameters of the function and the user can do intended authentication and authorization logic as needed.

Once the logic implementation is done the graphql:Context is returned for the success case. For the error case, there can be an error returned.

Error Handling

Context Initialization

The error handling happens based on the type of error. If it is an authentication error, the user needs to create a graphql:AuthnError and return. Then, the error is converted as an HTTP 401 response. If it is an authorization error, the user needs to create a graphql:AuthzError and return. Then, the error is converted as an HTTP 403 response. For both cases, the payload is in a JSON error format as per the GraphQL spec.

Resolver

Error handling happens commonly for any type of error. The error is formatted as per the GraphQL spec and returned as an HTTP 500 response.

Advantages

  • Users have full control for the customizations of business logic level authorizations.
  • Users have the flexibility for fine-grained control of the fields.

Disadvantages

  • Authentication and authorization logics may be duplicated for all the GraphQL APIs.
  • Users do need to be worried more about how authentication and authorization works.

Dependencies

This proposal depends on the proposal of Ballerina Authn/Authz Design. This has proposed the design of auth protocols like Basic Auth, JWT Auth, and OAuth2. Those proposed APIs are used by the APIs introduced with this proposal.

Also, this proposal depends on the proposal of GraphQL Context. This has proposed the design of GraphQL context which is used to pass the meta-information to the GraphQL resolver (resource/remote) functions. It is used by this proposal to pass the auth-related information.

@shafreenAnfar
Copy link
Contributor

shafreenAnfar commented Oct 1, 2021

I think this is a good proposal to start with.

Just to understand it better, I have a few questions as follows,

  1. Do graphql:AuthnError and graphql:AuthzError have a way to include additional information such as code, timestamp, etc which goes in to the extension section of the error response ?

  2. As per the design if users return the relevant error in the function, it will generate the required error response, which is good. Is that the same as other popular libraries ?

  3. In the case of imperative approach, security code gets in the way of the business logic. Is there a way to improve that a bit further ? I believe libraries such as shield does address that up to some extent by separating the security logic from the rest.

@ldclakmal
Copy link
Member Author

ldclakmal commented Oct 1, 2021

I think this is a good proposal to start with.

Just to understand it better, I have a few questions as follows,

  1. Do graphql:AuthnError and graphql:AuthzError have a way to include additional information such as code, timestamp, etc which goes in to the extension section of the error response ?

Currently, there is no such way to add such additional information. The graphql:AuthnError and graphql:AuthzError is a sub-type of graphql:Error. IMO, this is common improvement that we need to address for all error responses in Ballerina GraphQL module. When we do that, it will be applicable for these 2 error types also.

  1. As per the design if users return the relevant error in the function, it will generate the required error response, which is good. Is that the same as other popular libraries ?

Actually, this is a good point to discuss. When the error is thrown at the resolver level, it is always 200 status response for whatever the error type. That is how the popular libraries and Ballerina behave.
If the error is thrown at a middleware before the resolver, the response status code is decided by the user. I could find both approaches like returning 400 status response and 401 status response (403 status response is very rare). This is beyond the control of the library and it's up to the user IMO.

So, with this proposed design for Ballerina, the declarative way returns either 401 or 403 and imperative way it is controllable by the user (401, 403 or 500). But in both approaches, there is no control of the status code when it comes to resolver level.

  1. In the case of imperative approach, security code gets in the way of the business logic. Is there a way to improve that a bit further ? I believe libraries such as shield does address that up to some extent by separating the security logic from the rest.

Yes. GraphQL shield library creates permissions as another layer of abstraction. It is an implementation on top of one of the GraphQL middleware project, which is similar to our concept of Interceptors. So first, we need to have similar kind of interceptors proposal for GraphQL, if we going with that approach IMO.

A separate issue [1] is created to track the design of GraphQL interceptor.

[1] #2001

@ldclakmal
Copy link
Member Author

ldclakmal commented Oct 4, 2021

With reference to the Apollo GraphQL server, it behaves like this when it comes to error handling.

The error type defines only the extensions.code field of the Apollo error JSON structure. There are set of predefined errors with codes and user can create custom error types as well.

{
  "errors": [
    {
      "message": "Context creation failed: You must be logged in.",
      "extensions": {
        "code": "UNAUTHENTICATED",

resolver level errors - 500/400 status code response with above error json format

middleware level errors - 400 status code response with above error json format (but the status code can be configured after v2.6.0 [1])

[1] apollographql/apollo-server#1709 (comment)

@ldclakmal
Copy link
Member Author

ldclakmal commented Oct 4, 2021

IMO, in Ballerina GraphQL server, having the status code tightly bound with the error type is useful from end user point of view.
As an example graphql:Error is bound to 500 status code (similar to Apollo server) and graphql:AuthnError is bound to 401 status code. Based on user's logic he/she can return whatever the error type is needed. Based on that the status code of the response is defined.

@shafreenAnfar @ThisaruGuruge @DimuthuMadushan WDYT?

@shafreenAnfar
Copy link
Contributor

If I understood correctly, as per the above comment each error type is bound to a HTTP status code. I think we can take an opinionated stand here as GraphQL has nothing to do with HTTP status codes. Maybe we can use 400 series for these kind of errors.

But are you also suggesting we should bind something like extension.code to HTTP status code ?

@ldclakmal
Copy link
Member Author

If I understood correctly, as per the above comment each error type is bound to a HTTP status code. I think we can take an opinionated stand here as GraphQL has nothing to do with HTTP status codes. Maybe we can use 400 series for these kind of errors.

Yes. This is what Apollo server also does at the moment for the default case. But after v2.6.0 there is a possibility to configure that if the user wants.

But are you also suggesting we should bind something like extension.code to HTTP status code ?

No. I was suggesting to bind the error type with the HTTP status code. extension.code is also tightly bound to the error type, but it is not there at the moment.

@ldclakmal
Copy link
Member Author

Closing the issue with above 2 action items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area/Security Issues related to stdlib security module/graphql Issues related to Ballerina GraphQL module Points/3 Team/PCM Protocol connector packages related issues Type/Task
Projects
None yet
Development

No branches or pull requests

2 participants