Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get JWT from cookies and customize response headers from stored procedure #501

Closed
chadfurman opened this issue Jul 7, 2017 · 46 comments
Closed

Comments

@chadfurman
Copy link
Collaborator

chadfurman commented Jul 7, 2017

Cookies have the ability to be HttpOnly and can require HTTPS -- things that local storage cannot do. If the client must know the JWT to send the request, that means the JWT is accessible via JavaScript. Postgraph should be able to pull the jwt out of a cookie with a name of your choosing, rather than relying on the Authorization header.

To get the JWT from a cookie we'd need to change getJwt:
https://github.com/postgraphql/postgraphql/blob/607f6629986735139e2b77d5a9b1143846c4f691/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js#L548

Getting the JWT into an HttpOnly cookie is another can of worms. Maybe we could have it so if we return an optional, configurable field (i.e. select 'Cookie: ........' as responseHeaders) then the response header is automatically set and in this way we can support all sorts of response-header types including mime-types for dumping binary data straight out of the database.

@chadfurman chadfurman changed the title Send JWT as cookie instead of Authorization header Customize response headers Jul 7, 2017
@chadfurman chadfurman changed the title Customize response headers get JWT from cookies and customize response headers from stored procedure Jul 7, 2017
@benjie
Copy link
Member

benjie commented Jul 7, 2017

Passing JWT via the Authorization header is in line with the de-facto standards; if you wish to handle JWT tokens via cookies then I think that might be best for you to implement outside of PostGraphQL core; e.g. like this:

#359 (comment)

@chadfurman
Copy link
Collaborator Author

If the standard encourages localstorage over HttpOnly cookies, that's silly. Cookies are simply more secure. See here for example: https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage

Thank you for the example of how to pack my own JWTs

@benjie
Copy link
Member

benjie commented Jul 8, 2017

I don't think it recommends any storage medium, only a transport medium (the Authorization header).

You're right that I would not recommend storing the JWT to localStorage, and especially not to client-side cookies; but HttpOnly cookies is a definite option. Combined with credentials: same-origin in the .fetch(...) call this would also mean clients don't need to handle the JWT token at all - they can simply discard the payload since they know the client/browser has dealt with it for them.

So if I understand your intention correctly, all we'd need to do is when a JWT is generated, in addition to sending it how we currently do (in the payload), also set it in an HttpOnly cookie. Similarly when looking for a JWT we'd look first in the Authorization header, falling back to the named cookie. Or maybe we'd check both and throw an error if they're both set but to different values?

Does this sound in line with what you're suggesting?

@chadfurman
Copy link
Collaborator Author

Yes, HttpOnly and Secure -- will make it really hard to steal the JWTs

@chadfurman
Copy link
Collaborator Author

I really like the idea of checking both, because that builds in a degree of CSRF protection, also. Ideally, though, the double-check would be on a separate token, like double-send cookie nonces so they can be generated on a front-end server separately from the API, rather than requested from the API like the JWT is. Also, this prevents the need for the API to ever send the JWT to the client, so no amount of XSS would be able to steal the session.

@chadfurman
Copy link
Collaborator Author

Cookie nonces are a separate ticket, but it could be argued that implementing cookie-based auth would be less secure without some form of CSRF protection.

@chadfurman
Copy link
Collaborator Author

chadfurman commented Jul 8, 2017

@benjie How could I build this as a plugin for PostGraph4?

@benjie
Copy link
Member

benjie commented Jul 8, 2017

The plugin system of PostGraphQL4 currently only deals with the GraphQL schema generation... extending it to cover the HTTP server too is an interesting idea. For now; we'll probably just carry over the HTTP handling from PGQL3 so implementing it on master would work.

I wasn't suggesting that we'd require both to be present (unlike a double-submit cookie), I think if we send the token through both mechanisms then if you're a browser you'll automatically use the cookie (no other code required), but if you're a non-browser client (like a native or command line application) then you can use the traditional Authorization header approach. What I was attempting to say is if both cookie and Authorization header were present (i.e. existing clients will already be using the Authorization header) we should probably throw an error if they differed to help the application developer know of the issue; otherwise we'd just take whichever one was set.

@chadfurman
Copy link
Collaborator Author

This is why I was thinking to just set custom headers from stored procedures, somehow. I could optionally make a non-cookie-based auth endpoint for the non-browser client with a JWT and then also make a cookie-only one for the browser where I'm worried about XSS. It'd honestly be cool to have access to the headers going into some of the stored procedures as well. Exposing a --jwt-cookie auth_cookie_name option similar to --jwt-token would make it easy to know what cookie to check for a JWT, and then auth could be otherwise normal.

@benjie
Copy link
Member

benjie commented Jul 8, 2017

Could you demonstrate a tiny proof of concept? I can't quite see how it'd work, since it's kind of bypassing the GraphQL<->PostgreSQL two way thing.

(I'm not familiar with the JWT generation features of PostGraphQL yet - I've not used them.)

@chadfurman
Copy link
Collaborator Author

yeah the "somehow" for the custom headers is a trick. Basic cookie auth with a flag is much easier. I'll think about a PoC for the custom headers from the stored procedure. You probably don't need a PoC for basic cookie auth ;)

@chadfurman
Copy link
Collaborator Author

Here's a talk by an AppSec professional about, among other things, double-submit cookies: https://youtu.be/cSOKJRfkTDc?t=21m4s

@chadfurman
Copy link
Collaborator Author

@benjie I'm going to want to add this in before my September 15th launch -- can you point me to parts of graphile / postgraphile that facilitate HTTP Header exchange, and points of Graphile / postgraphile where a field returned from a stored procedure might be manipulated and handed off to the HTTP header exchange code?

@benjie
Copy link
Member

benjie commented Aug 10, 2017

I'm still not sure off-hand the best way to do this; but here's a couple pointers that may help you get started.

JWT -> Postgres

The jwtToken is extracted from the request here:

https://github.com/postgraphql/postgraphql/blob/2c80a1801ae555681412aebb74993e6df930c0b1/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js#L394

That's then passed to withPostGraphQLContext which is responsible for setting the relevant settings here:

https://github.com/postgraphql/postgraphql/blob/2c80a1801ae555681412aebb74993e6df930c0b1/src/postgraphql/withPostGraphQLContext.ts#L172-L180

It should be quite easy to extend the above to pass arbitrary headers through to the DB. (If you choose to do so I would like the specific headers explicitly listed (in the same way --extended-errors lists the error properties to expose) otherwise we'd become vulnerable to DOS attacks - I'd also like a sensible default on the maximum length of headers and perhaps the ability to extend beyond this using custom syntax, such as --ingest-headers authorization,x-really-long-header:100000,x-other-header.)

Postgres -> JWT

This is more challenging because we don't actually DO anything with the JWT, we just generate it and let the client interpret it as part of the JSON payload from graphql. The V4 code for performing this is here:

https://github.com/graphile/graphile-build/blob/7ecec2c3a851c5ef683f87332ac163c8690c1625/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js#L73-L115

Basically it says "whenever you see the composite type the settings have specified is the JWT type, instead of processing it as a composite type convert it as to a scalar string by signing it as a JWT token using the specified secret".

This isn't even directly related to a resolver (the resolver calls pg2gql which calls the above code), so without a bit of wrangling we can't even call a callback that's passed through on context or something like that :(

Alternative

Could we (ab)use RAISE INFO for this instead? I considered LISTEN/NOTIFY but I'm concerned the alert might not be received synchronously (have not checked this, maybe it will be?); however with RAISE INFO you could potentially capture the message that's emitted by the procedure and if it matches a specific format parse it and use it to set arbitrary headers on the response.

What are your thoughts?

@benjie
Copy link
Member

benjie commented Aug 10, 2017

(--ingest-headers is not a very good flag, but I can't think of a better one right now.)

@chadfurman
Copy link
Collaborator Author

For Postgres -> JWT I want to be able to return either a custom type or a custom field name (something like __HEADER__Authorization) from a stored procedure and have it just be a header. With a custom type, I've had some issues nesting custom types, so a type like Headers( header, header, ... ) being returned from a stored procedure would be ideal but may be challenging for currently unknown reasons.

amazing writeup above... thank you so much @benjie

@chadfurman
Copy link
Collaborator Author

chadfurman commented Aug 10, 2017

Oh, man, no you know what...

The best solution would be to just use select to populate out a filed called __HEADERS__ with any custom headers you want returned -- complete with newlines, colons, and spaces. Good old fashion document.cookie style.

What do you think? :)

@chadfurman
Copy link
Collaborator Author

So, beyond just setting custom headers, the question of how do we get the JWT value to set the header is still outstanding...

Trying to better grok https://github.com/graphile/graphile-build/blob/7ecec2c3a851c5ef683f87332ac163c8690c1625/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js#L73-L115

Basically, we're making a GraphQL type for the JWT currently? With a --cookie-auth CLI flag enabled, a CookieAuth plugin should load in that intercepts the incoming request, headers and all, check for a JWT in the cookie, if present validate, if invalid error as usual. Also, the plugin should recognize an "authentication" endpoint and expect it to return a type matching the JWT type. When the CookieAuth plugin detects a call to the Authentication stored procedure, the CookieAuth plugin intercepts the stored procedure's return value and hijacks the response chain to send only the JWT in a "set-cookie httpOnly" header

Ultimately, there will be origin restrictions as well, but this is more than enough of a good start. Getting the token out of the front-end JavaScript to subvert possible XSS session hijacking attacks is what this is all about.

@chadfurman
Copy link
Collaborator Author

@chadfurman
Copy link
Collaborator Author

I think the problem here is that the HttpRequestHandler is tightly coupled with https://github.com/graphile/graphile-build/blob/7ecec2c3a851c5ef683f87332ac163c8690c1625/packages/graphile-build-pg/src/plugins/PgJWTPlugin.js#L73-L115

Or, rather, vice versa?

Ultimately, the PgJWTPlugin should be doing its own JWT extraction, validation, and response forming. Is this more a conversation about plugin architechture and/or additional necessary hooks?

@benjie
Copy link
Member

benjie commented Aug 10, 2017

Really the issue is that the parts are independent rather than coupled:

HttpRequestHandler parses the incoming JWT and sets those values on the PG connection - i.e. the GraphQL schema itself knows nothing of them (but they do influence the results it gets, because PG has been told about them).

PgJWTPlugin looks for a particular type being returned from Postgres and intercepts it, instead converting it to a JWT string which it then returns via GraphQL; HttpRequestHandler knows nothing of this currently (it just passes through the JSON blob returned by GraphQL).

A potential solution arises...

With graphile-build you can write a plugin that wraps an existing resolver. And because it works a little like CSS selectors (i.e. it affects everything unless you specifically lock it down) we can have this plugin wrap ALL resolvers for fields whose types are the JWT type.

So we can wrap those resolvers with something like this:

const oldResolver = field.resolve;
field.resolve = async function(data, args, context, resolveInfo) {
  const jwtString = await oldResolver.call(this, data, args, context, resolveInfo);
  if (jwtString != null && typeof context.pgHandleJWT === 'function') {
    context.pgHandleJWT(jwtString);
  }
  return jwtString;
}

Then in withPostGraphQLContext where we pass the pgClient and role onto the context object, we can also pass a pgHandleJWT function:

https://github.com/postgraphql/postgraphql/blob/2c80a1801ae555681412aebb74993e6df930c0b1/src/postgraphql/withPostGraphQLContext.ts#L74-L77

Which can be passed through from HttpRequestHandler:

https://github.com/postgraphql/postgraphql/blob/2c80a1801ae555681412aebb74993e6df930c0b1/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js#L396-L415

And thus can do what it likes including res.setHeader.

HOWEVER, the issue with this is that the user HAS to request the jwtToken on the response because if they don't then the resolver won't fire and thus the cookie will not be set. So it's not perfect...


I'm still thinking the RAISE INFO is a viable option that would work from arbitrary functions in the Postgres database.

@chadfurman
Copy link
Collaborator Author

RAISE INFO might be a possibility, but as you've mentioned yourself we might see async issues -- any idea if this is real or not? That's the biggest problem with NOTIFY / SUBSCRIBE. Also, using RAISE INFO is quite hack-ish and I'd really like to stick with the original approach of intercepting responses, watching types, etc.

I say that they're coupled because, ideally, there'd be a JWT plugin which gets the req and pulls out the JWT -- the HTTPRequestHandler has an extra responsibility.

I need to better understand what you've said above, might take me a couple read-overs.

Thank you again for your time :)

@benjie
Copy link
Member

benjie commented Aug 10, 2017

Yeah, it takes quite a bit to grok exactly how it works and why it works like that - have to have quite a fundamental understanding of how GraphQL resolvers work.

@benjie
Copy link
Member

benjie commented Aug 17, 2017

I'm pretty sure that RAISE INFO is synchronous FWIW - like error handling it would have to be?

@chadfurman
Copy link
Collaborator Author

chadfurman commented Aug 17, 2017 via email

@benjie
Copy link
Member

benjie commented Aug 18, 2017

Rather than adding pgHandleJWT to the context as I suggested above, I think a generic EventListener mechanism might be good here. So to

https://github.com/postgraphql/postgraphql/blob/2c80a1801ae555681412aebb74993e6df930c0b1/src/postgraphql/withPostGraphQLContext.ts#L74-L77

we'd add a new EventEmitter instance (postgraphqlEventListener) which we can subscribe to (maybe it's passed in from the library settings?); this would then be passed down via the GraphQL context to the resolver which can then postgraphqlEventListener.emit(name, value) inside the resolver; you could use this for example to do postgraphqlEventListener.emit('set-cookie', 'foo=bar') which could be interpreted by your application to set a cookie. This would require a Graphile Build plugin to wrap the existing resolver for the object(s) you wish to trigger this.

@chadfurman
Copy link
Collaborator Author

chadfurman commented Sep 5, 2017

@benjie "Maybe it's passed in from the library settings" -- can you elaborate?

"Passed down via the GraphQL context" -- could you show an example of pulling this into a mock plugin quick?

"you could use this for example to do postgraphqlEventListener.emit('set-cookie', 'foo=bar') which could be interpreted by your application to set a cookie." -- When you say "interpreted by your application to set a cookie", where in the Postgraphile app would I have access to the request object to be able to set cookies, and how could I subscribe to the event at that point?

@benjie
Copy link
Member

benjie commented Sep 5, 2017

"Maybe it's passed in from the library settings" -- can you elaborate?

I mean something like this:

export postgraphql(process.env.DATABASE_URL, schemaName, {
  eventListenerFactory: (req, res) => {
    const emitter = new EventEmitter();
    emitter.on('set-cookie', (cookie) => { res.setCookie(...) });
    //...

    return emitter;
  }
})

"Passed down via the GraphQL context" -- could you show an example of pulling this into a mock plugin quick?

This would not be possible via a plugin as the context exists outside of the GraphQL schema. It would be a mod to withPostGraphQLContext:

https://github.com/postgraphql/postgraphql/blob/b507e04de0e54f2ae203633803bd8efbce30e3db/src/postgraphql/withPostGraphQLContext.ts#L74-L77

But once that mod is in place, you could then fire it from an arbitrary resolve method:

module.exports = function CreateLinkWrapPlugin(builder) {
  builder.hook(
    "GraphQLObjectType:fields:field",
    (field, _, { scope: { isRootMutation, fieldName } }) => {
      if (!isRootMutation || fieldName !== "someMutationToMonitor") return field;
      const defaultResolver = obj => obj[fieldName];
      const { resolve: oldResolve = defaultResolver, ...rest } = field;
      return {
        ...rest,
        async resolve(resolveParams) {
          const RESOLVE_CONTEXT_INDEX = 2;
          const { pgClient, postgraphqlEventListener } = resolveParams[RESOLVE_CONTEXT_INDEX];
          const callback = something => postgraphqlEventListener.emit('something', ...);
          pgClient.on('whatever', callback);
          try {
            return await oldResolve(...resolveParams);
          } finally {
            pgClient.removeListener('whatever', callback);
          }
        }
      };
    }
  )
};

When you say "interpreted by your application to set a cookie", where in the Postgraphile app would I have access to the request object to be able to set cookies, and how could I subscribe to the event at that point?

Hopefully the first example makes this clearer

@chadfurman
Copy link
Collaborator Author

Yes! Thank you @benjie 👯‍♂️

@benjie
Copy link
Member

benjie commented Sep 6, 2017

We should be collecting things like this on the wiki I guess...

@chadfurman
Copy link
Collaborator Author

@benjie
Copy link
Member

benjie commented Aug 15, 2018

[semi-automated message] Hi, there has been no activity in this issue for over 8 weeks so I'm closing it to keep the issues/pull request manageable. If this is still an issue, please re-open with a comment explaining why.

@metarama
Copy link

metarama commented May 6, 2019

It’s been a while since the original topic “get JWT from cookies and customize response headers from stored procedure“ was raised. Is this feature now supported?

@srghma
Copy link
Contributor

srghma commented Oct 13, 2020

@benjie @chadfurman interested in this too

so, what was the result of this issue?

is there exists plugin / is it possible to enable retrieving jwt from a cookie also

  • IF Authorisation: Basic encoded_jwt right THEN jwt info is availeable in postgres session (currently done)
  • ELSE IF Cookie: jwt_cookie_key_defined_by_user=encoded_jwt THEN jwt info avail in postgres (not done, should be optionally enabled)

@benjie
Copy link
Member

benjie commented Oct 13, 2020

See pgSettings function referenced above.

@Pfurr
Copy link

Pfurr commented Apr 15, 2021

@srghma @benjie is there any documentation that explains how to enable retrieving JWT in a cookie http only? Thank you

@benjie
Copy link
Member

benjie commented Apr 16, 2021

Use pgSettings, the jsonwebtoken module, and a cookie parser in your server stack (e.g. express’ cookie parser).

@Pfurr
Copy link

Pfurr commented Apr 20, 2021

Use pgSettings, the jsonwebtoken module, and a cookie parser in your server stack (e.g. express’ cookie parser).

@benjie thank you. You could give me an example, I'll attach code. I'm working with Next Js.

// postgraphile.ts

import { config } from "src/server/config";
import { Pool } from "pg";
import { postgraphile } from "postgraphile";

const pool = new Pool({
  host: config.DB_HOST,
  database: config.DB_DATABASE,
  user: config.DB_USER,
  password: config.DB_PASSWORD,
  port: config.DB_PORT,
});

export { pool as pg };

export default postgraphile(pool, {
  graphiql: true,
  enhanceGraphiql: true,
  graphqlRoute: "/api/graphql",
  graphiqlRoute: "/api/graphiql",
  retryOnInitFail: true,
  appendPlugins: [require("@graphile-contrib/pg-simplify-inflector")],
  exportGqlSchemaPath: "./src/generated/schema.graphql",
  pgDefaultRole: "anonymous",
  jwtPgTypeIdentifier: "public.jwt_token",
  jwtSecret: "SecretRandomBlaBlaTest",
  dynamicJson: true,
  showErrorStack: "json",
});

// runMiddleware.ts

import { NextApiRequest, NextApiResponse } from "next";

export default function runMiddleware(
  req: NextApiRequest,
  res: NextApiResponse,
  fn: any
) {
  return new Promise((resolve, reject) => {
    fn(req, res, (result: any) => {
      if (result instanceof Error) {
        return reject(result);
      }

      return resolve(result);
    });
  });
}

@benjie
Copy link
Member

benjie commented Apr 20, 2021

const { postgraphile } = require('postgraphile');
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');

const app = express();
app.use(cookieParser());
app.use(postgraphile(DB, SCHEMA, {
  ...options,
  pgSettings(req) {
    const { myJwt } = req.cookies;
    const claims = jwt.verify(myJwt, process.env.JWT_SECRET);
    return {
      'jwt.claims.user_id': claims.user_id,
    };
  }
}));
app.listen(5000);

@Pfurr
Copy link

Pfurr commented Apr 20, 2021

@benjie i'm not working with express js, only next js. do I have to add express js necessarily for this? Ps. I'd like to memorize JWT in cookies, but only HTTP

@benjie
Copy link
Member

benjie commented Apr 21, 2021

I cannot advise you on how to do it with other frameworks; you’ll have to figure that part out yourself.

@Rajat-Jain29
Copy link

Can I pass JWT Token in cookie rather than passing it to Header

@benjie
Copy link
Member

benjie commented May 26, 2021

Use the pgSettings function and the jsonwebtoken to allow receiving the JWT however you want.

@Rajat-Jain29
Copy link

Sir I have one doubt like I am generating JWT token through Spring Boot and it is generating fine with my username and password . But I want to ask that is it possible to convert that token into Cookie or pass that token in cookies because in postman Cookies option is also disable thats what I am asking that how can I implement this please sir help me?

@benjie
Copy link
Member

benjie commented May 26, 2021

Sorry, I am not familiar with postman. Try using the bundled PostGraphiQL; use the --enhance-graphiql flag if you're using PostGraphile CLI (or equivalent for library usage) - see the "Recommended options" section in the docs.

@zlwu
Copy link

zlwu commented Feb 21, 2022

Cookie first, fail back to header, with default role when no token found (in case pgDefaultRole can not be set when pgSettings enabled)

const pgSettings = async (req) => {
  const token = req.cookies.jwttoken || req.get("Authorization")?.split(" ")[1];
  if (token) {
    const claims = jwt.verify(token, process.env.JWT_SECRET);
    return {
      "jwt.claims.user_id": claims.user_id,
      role: claims.role,
    };
  } else {
    return {
      role: "forum_example_anonymous",  // please change it to your default role
    };
  }
};
const { postgraphile } = require('postgraphile');
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');

const app = express();
app.use(cookieParser());
app.use(postgraphile(DB, SCHEMA, {
  ...options,
  pgSettings(req) {
    const { myJwt } = req.cookies;
    const claims = jwt.verify(myJwt, process.env.JWT_SECRET);
    return {
      'jwt.claims.user_id': claims.user_id,
    };
  }
}));
app.listen(5000);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants