-
Notifications
You must be signed in to change notification settings - Fork 66
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
Comments
Proposal: Ballerina GraphQL Authentication and AuthorizationSummaryThe 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
MotivationAuthentication 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. DescriptionBallerina 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.
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 MethodThe 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 ConfigurationThe Check the below definition for public type GraphqlServiceConfig record {|
ListenerAuthConfig[] auth?;
// ...
|}; Check the below definition for 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";
|}; SampleCheck 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 AnnotationThe 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 HandlingAuth AnnotationThe 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. ResolverError 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
Disadvantages
Imperative MethodThe 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 ConfigurationThe Check the below definition for public type GraphqlServiceConfig record {|
ContextInit contextInit;
// ...
|}; Check the below definition for public type ContextInit isolated function (http:RequestContext requestContext, http:Request request) returns Context|error; SampleCheck 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 AnnotationThe Once the logic implementation is done the Error HandlingContext InitializationThe error handling happens based on the type of error. If it is an authentication error, the user needs to create a ResolverError 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
Disadvantages
DependenciesThis 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. |
I think this is a good proposal to start with. Just to understand it better, I have a few questions as follows,
|
Currently, there is no such way to add such additional information. The
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. 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.
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 |
With reference to the Apollo GraphQL server, it behaves like this when it comes to error handling. The error type defines only the {
"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]) |
IMO, in Ballerina GraphQL server, having the status code tightly bound with the error type is useful from end user point of view. |
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 |
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.
No. I was suggesting to bind the error type with the HTTP status code. |
Closing the issue with above 2 action items. |
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.
The text was updated successfully, but these errors were encountered: