Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

hapi-postgraphile Build Statusnpm version

A Postgraphile plugin for HAPI.

Installation

npm install hapi-postgraphile

Config

Here is a sample, minimal config using values that work with this tutorial. Yours will be different.

constserver=hapi.server({port: 5000});awaitserver.register({
plugin,options: {pgConfig: 'postgresql://user@localhost/db',schemaName: 'forum_example',schemaOptions: {jwtSecret: 'keyboard_kitten',jwtPgTypeIdentifier: 'forum_example.jwt_token',pgDefaultRole: 'forum_example_anonymous'}}});

Usage

This module exposes one endpoint, by default at /graphql. This endpoint will accept GraphQL queries, mutations, and will read an Authorization header with a Bearer <jwtToken> value.

You should be able to walk through the excellent schema design tutorial here and use this endpoint for all of the requests using a tool like GraphiQL.

Advanced configuration

All of the options documented here are passed through to the createPostGraphileSchema function when provided in the schemaOptions config property.

Caching

hapi-postgraphile can take advantage of your server cache. You will need to set up the cacheConfig parameters you pass to the plugin, and declare a list of allowed operation names.

Caching in this way, via the simple key/val store is very limited and can only cache queries using default options, and cannot cash requests requiring JWT authentication.

hapi-auth-jwt2

If you are using hapi-auth-jwt2 this plugin will read the token from that. In that case you'd want to be sure you are passing the same secret and necessary configuration to hapi-postgraphile, and if you're using jwt2 cookies the same security caveats as below will apply.

If you do use this approach, also remember that you likely want to allow unauthenticated calls to the graphql endpoint as well. In that case consider passing a route option to hapi-postgraphile, like:

route: {
options: {
auth: {
mode: 'optional'
}
}
}

Cookie authentication

You can also set up your endpoint to store a cookie containing your JWT.

When setting up an authentication cookie you should also review the authenticate.verifyOrigin setting.

You must provide a cookieAuthentication.name, which is the name of your cookie, and should review the authenticate.getTokenOperationName, authenticate.getTokenDataPath, and authenticate.clearTokenOperationName options to ensure your queries and responses are handled. The default settings mirror the results you'd have following this tutorial.

Security and CSRF mitigation

Using the default settings should give you a reasonable level of security against CSRF attacks. These settings rely solely on the Authorization header, and should be immune to the most common exploits. Cookies are very convenient in some settings, but come with an added security risk, especially given the level of access a GraphQL endpoint typically has to the underlying database.

If you do choose to use cookie authentication you can use the authentication.verifyOrigin checking to ensure that your request is coming from an allowed origin based on your server's CORS policy. The plugin will check hapi's request.info.cors.isOriginMatch to ensure you have a valid origin. This can happen either on every request, always, or just on requests that contain the origin header — the present setting, which is a sensible default.

For a secure setup with cookies you must do the following

  1. Ensure your route has a secure CORS policy in place either at the server level or through a route option you pass to this plugin. Read about setting your server CORS policy and / or your route CORS policy. Setting cors: true or cors: ['*'] is not secure!

  2. Set the hapi-postgraphile config option authentication.verifyOrigin to always or present. If you do not update this value and you enable cookies the value will be upgraded to present for you and a warning will be thrown.

  3. Ensure your cookie is using the isSecure and httpOnly options (both defaults) to prevent against manipulation and domain forgery.

  4. Consider also using anti-CSRF tokens like those provided by crumb.

Read the CSRF Prevention Cheat Sheet for more detail.

Token refresh support (using cookie authentication)

If you do use cookie authentication, I've included token refresh functionality. At a basic level this would allow you to create and call a refreshToken mutation, which is expected to read from the jwt_claims and return a jwtToken very similar to the reference authenticate mutation. In your PG function you might simply verify that the claimed identity still exists and is allowed, or you might check a session table to ensure they are still allowed access.

For example:

create or replacefunctionforum_example.refresh_token() returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.person_id= current_setting('jwt.claims.person_id')::text;
if FOUND and (account.suspended<> true) then
return ('forum_example_user', account.person_id)::forum_example.jwt_token;
else
return null;
end if;
end;
$$ language plpgsql strict security definer;
grant execute on function forum_example.refresh_token() to forum_example_user;

By defining the authenticate.refreshTokenOperationName and authenticate.refreshTokenDataPath you can have your new token re-stated.

Stale (jwtToken.sat support)

If you return a jwtToken with a sat ("stale at") property this plugin will compare that value with the current time and refresh if necessary. sat, like other JWT properties, should be a UNIX epoch time in seconds.

This approach assumes you have the decoded token available in your request.auth.credentials object — like the one provided by hapi-auth-jwt2. You could also create your own auth strategy to decode the token and populate this value, but be aware that postgraphile itself does not expose the decoded token itself.

The refresh will happen during the onPreResponse extension point. You will need to supply an authenticate.refreshTokenQuery GraphQL query string, which will be invoked when the stale conditions are met.

The following is an example of an authentication PG type and function that provides a valid JWT that could be refreshed sometime after it becomes stale and before it expires:

createtypeforum_example.jwt_token as (
role text,
person_id text,
exp int,
sat int
);
create or replacefunctionforum_example.authenticate(
email text,
password text
) returns forum_example.jwt_tokenas $$
declare
account forum_example_private.person_account;
epoch_time int;
expires_in int default 1800;
stale_in int default 900;
beginselect a.* into account
fromforum_example_private.person_accountas a
wherea.email= $1;
if (account.suspended<> true) and (account.password_hash= crypt(password, account.password_hash)) then
epoch_time := extract(epoch from now());
-- 30 minute expiration, 15 minutes until stale
return ('forum_example_user', account.person_id, epoch_time + expires_in, epoch_time + stale_in)::forum_example.jwt_token;
else
raise exception 'invalid login';
end if;
end;
$$ language plpgsql strict security definer;

Don't set your stale time too close to your expiration time to avoid issues.

(Nearly) All the options

Defaults shown.

{pgConfig: '',// connection string or objpgOptions: null,// object to merge with config, for pg tuning, etcpgConnectionRetry: {// Settings for the retry module, invoked on connection errors.retries: 5,// Set to 0 to disablefactor: 2,minTimeout: 1000,maxTimeout: 100000,random: false},schemaName: 'public',schemaOptions: {// options from postgraphile},route: {path: '/graphql',options: null// options to pass to your route handler, merged with (and some overwritten by) the plugin's route options},cacheAllowedOperations: null,// pass array of stringscacheConfig: {// null by defaultsegment: '',expiresIn: 0,expiresAt: '',staleIn: 0,staleTimeout: 0,generateTimeout: 500},authenticate: {verifyOrigin: 'never',// or 'always' or 'present'verifyOriginOverride: false,// By default origin will be verified if using cookie auth. This let's you keep it as 'never'.getTokenOperationName: 'getToken',// your login or operation mutationgetTokenDataPath: 'data.getToken.jwtToken',refreshTokenOperationName: 'refreshToken',// if you choose to use the refreshToken functionalityrefreshTokenDataPath: 'data.refreshToken.jwtToken',refreshTokenQuery: undefined,// if you want to use the refreshToken functionality, put your graphql mutation string hererefreshTokenVariables: undefined,// if your query requires any variables, object hereclearTokenOperationName: 'clearToken'},headerAuthentication: {headerName: 'Authorization',tokenType: 'Bearer'},cookieAuthentication: {// by default this is null, to use cookies pass a name and any hapi cookie options — default options shownname: null,options: {encoding: 'none',isSecure: true,isHttpOnly: true,clearInvalid: false,strictHeader: true,path: '/'}}}

Examples

Check out the /examples folder for a comprehensive implementation.

Native bindings

hapi-postgraphile will use the native pg bindings if you have pg-native installed as a peer.

Methods

  • postgraphile.performQuery(graphqlQuery, [options])

    • graphqlQuery: {query, variables, operationName}
    • options: {jwtToken, [schemaOptions]} — the options object can provide the JWT for the request and override any of the global schemaOptions if needed.
  • postgraphile.performQueryWithCache(graphqlQuery)

    • graphqlQuery: {query, variables, operationName}
    • cached queries cannot use options — they are ultimately uncacheable with a simple key/val lookup, and we'd also run into issues with JWT authentication.

Requirements

  • node.js >= 8.6
  • PostgreSQL >= 9.6 (tested with 9.6, developed with 10.2)
  • hapi v17 as a peer dependency
  • pg module as a peer dependency

About

A PostGraphile plugin for HAPI 17+.

Resources

Stars

20 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages