API Security
One of the the most common questions I get asked is what happens if a user out on the internet sends queries that we don't want run. For example how do we stop him from fetching all users or the emails of users. Our answer to this is that it is not an issue as this cannot happen, let me explain.
GraphJin runs in one of two modes development
or production
, this is controlled via the config value production: false
when it's false it's running in development mode and when true, production. In development mode all the named queries (including mutations) are saved to the allow list ./config/allow.list
. While in production mode when GraphJin starts only the queries from this allow list file are registered with the database as prepared statements.
Prepared statements are designed by databases to be fast and secure. They protect against all kinds of sql injection attacks and since they are pre-processed and pre-planned they are much faster to run then raw sql queries. Also there's no GraphQL to SQL compiling happening in production mode which makes your queries lighting fast as they are directly sent to the database with almost no overhead.
In short in production only queries listed in the allow list file ./config/allow.list
can be used, all other queries will be blocked.
How to think about the allow list?
The allow list file is essentially a list of all your exposed API calls and the data that passes within them. It's very easy to build tooling to do things like parsing this file within your tests to ensure fields like credit_card_no
are not accidently leaked. It's a great way to build compliance tooling and ensure your user data is always safe.
This is an example of a named query, getUserWithProducts
is the name you've given to this query it can be anything you like but should be unique across all you're queries. Only named queries are saved in the allow list in development mode.
#
AuthenticationYou can only have one type of auth enabled either Rails or JWT.
#
Ruby on RailsAlmost all Rails apps use Devise or Warden for authentication. Once the user is authenticated a session is created with the users ID. The session can either be stored in the users browser as a cookie, memcache or redis. If memcache or redis is used then a cookie is set in the users browser with just the session id.
GraphJin can handle all these variations including the old and new session formats. Just enable the right auth
config based on how your rails app is configured.
#
Cookie session store#
Memcache session store#
Redis session store#
JWT TokensFor JWT tokens we currently support tokens from a provider like Auth0 or if you have a custom solution then we look for the user_id
in the subject
claim of of the id token
. If you pick Auth0 then we derive two variables from the token user_id
and user_id_provider
for to use in your filters.
We can get the JWT token either from the authorization
header where we expect it to be a bearer
token or if cookie
is specified then we look there.
For validation a secret
or a public key (ecdsa or rsa) is required. When using public keys they have to be in a PEM format file.
#
Firebase AuthFirebase auth also uses JWT the keys are auto-fetched from Google and used according to their documentation mechanism. The audience
config value needs to be set to your project id and everything else is taken care for you.
#
HTTP HeadersHeader auth is usually the best option to authenticate requests to the action endpoints. For example you might want to use an action to refresh a materalized view every hour and only want a cron service like the Google AppEngine Cron service to make that request in this case a config similar to the one above will do.
The exists: true
parameter ensures that only the existance of the header is checked not its value. The value
parameter lets you confirm that the value matches the one assgined to the parameter. This helps in the case you are using a shared secret to protect the endpoint.
#
Named AuthIn addition to the default auth configuration you can create additional named auth configurations to be used
with features like actions
. For example while your main GraphQL endpoint uses JWT for authentication you may want to use a header value to ensure your actions can only be called by clients having access to a shared secret
or security header.
#
ActionsActions is a very useful feature that is currently work in progress. For now the best use case for actions is to
refresh database tables like materialized views or call a database procedure to refresh a cache table, etc. An action creates an http endpoint that anyone can call to have the SQL query executed. The below example will create an endpoint /api/v1/actions/refresh_leaderboard_users
any request send to that endpoint will cause the sql query to be executed. the auth_name
points to a named auth that should be used to secure this endpoint. In future we have big plans to allow your own custom code to run using actions.
#
Using CURL to test a query#
Access ControlIt's common for APIs to control what information they return or insert based on the role of the user. In GraphJin we have two primary roles user
and anon
the first for users where a user_id
is available the latter for users where it's not.
Define authenticated request?
An authenticated request is one where GraphJin can extract an user_id
based on the configured authentication method (jwt, rails cookies, etc).
The user
role can be divided up into further roles based on attributes in the database. For example when fetching a list of users, a normal user can only fetch his own entry while an admin can fetch all the users within a company and an admin user can fetch everyone. In some places this is called Attribute based access control. So in way we support both. Role based access control and Attribute based access control.
GraphJin allows you to create roles dynamically using a roles_query
and match
config values.
#
Configure RBACThis configuration is relatively simple to follow the roles_query
parameter is the query that must be run to help figure out a users role. This query can be as complex as you like and include joins with other tables.
The individual roles are defined under the roles
parameter and this includes each table the role has a custom setting for. The role is dynamically matched using the match
parameter for example in the above case users.id = 1
means that when the roles_query
is executed a user with the id 1
will be assigned the admin role and those that don't match get the user
role if authenticated successfully or the anon
role.