PostgREST / postgrest

REST API for any Postgres database
https://postgrest.org
MIT License
23.12k stars 1.02k forks source link

Configuration setting to use Authorization header (default) or cookie name as JWT source #3033

Open seankerr opened 10 months ago

seankerr commented 10 months ago

I have read a previous issue regarding my same concern, but any consideration was immediately shot down in favor of installing OpenResty on a proxy: https://github.com/PostgREST/postgrest/issues/773

Coming from a web application perspective, especially in 2023 when single page applications are all over the internet, I think it would be best to reconsider the cookie option. There is also a growing effort by open source developers to create No-Code or Low-Code packages that include software like PostgREST. Look no further than to SupaBase which is a fantastic product.

Use of the Authorization header is concern because of one specific reason: XSS. This is easily preventable if when authentication happens by a server, it sets an HttpOnly cookie containing the JWT. From the frontend perspective it also eliminates the need to set the header entirely relying on the browser to send the cookie behind the scenes. Security risk averted. There are hacky work-arounds that developers try to make working with JWT in a browser more secure, but in the end the solutions are more trouble than anything. It's a concern.

I do have a use-case myself. My company runs on AWS using an application load balancer. The load balancer does not support updating headers, so I cannot simply copy the cookie to a header and move forward, hence why I'm here. And it is not appealing to setup another piece of publicly accessible hardware to do one simple task of rewriting a cookie to a header, not to mention the security factors involved in that.

I'm simply asking if anybody is interested in reconsidering how to go about this and if the project is interested at all in revisiting the security importance HttpOnly cookies.

wolfgangwalther commented 10 months ago

Even with HttpOnly and SameSite a cookie is still potentially a surface for a CSRF attack, while the Authorization header is not.

Use of the Authorization header is concern because of one specific reason: XSS.

Can you explain why you think that cookies are not exploitable here? The fact that you can't read the token from a HttpOnly cookie still doesn't stop you from making requests with that token to the API, once you are able inject code.

In fact using the Authorization header is potentially safer, because you don't need to store the token in localStorage. You could just store it in a variable in the scope where it's used. To be able to make any use of that when injecting code, you will need to have either the token itself or some api helpers which make authorized requests possible in the same scope as your injected code. This is much better to protect against.

mckinlde commented 10 months ago

What protects the Authorization header from CSRF that doesn't apply to a SameSite cookie?

My current understanding is that cors is the primary mitigation for CSRF, and pgRest can be configured to use strict cors globally. This, combined with use of an HttpOnly and SameSite cookie would effectively allow pgRest to ensure any instance of a client SPA can only have a valid token that was set by pgRest, short of a client knowing the secret used to generate the token.

I'm a novice end user, and may be mistaken on the surface of CSRF, as well as the requirements for persistent sessions, but based on the understanding I have now my use case mirrors @seankerr's--if I were to add a reverse proxy, its sole purpose as of now would be moving JWTs between the Authorization header and a HttpOnly cookie, and I wish I could simply change a config setting somewhere in Tutorial 1 instead. This approach also seems to be suggested by the wording in JWT Security, specifically

PostgREST uses JWT mainly for authentication and authorization purposes and encourages users to do the same. For web sessions, using cookies over HTTPS is good enough and well catered for by standard web frameworks.

Am I misreading that paragraph? My reading is "for uses of the JWT to auth/auth client-webapp-users, putting the JWT in a cookie over HTTPS is good enough and ..." Is it possible to have refresh-persistent sessions when storing pgRest's JWT in a variable?

wolfgangwalther commented 9 months ago

What protects the Authorization header from CSRF that doesn't apply to a SameSite cookie?

Assume that either of the following is true:

Then:

Storing the token in memory in the app is not per-se secure - but at least you have a chance to make it so. You can't prevent the SameSite cookies from being sent, though.

My current understanding is that cors is the primary mitigation for CSRF

CORS does nothing to prevent CSRF. CORS prevents your own app from sending out malicious requests after XSS to other servers.

Is it possible to have refresh-persistent sessions when storing pgRest's JWT in a variable?

No. You need to store something somewhere. That's the definition of "persistent".


I am not arguing against using HttpOnly, SameSite cookies. I am just not buying the argument that using those cookies is actually any safer than the Authorization header.

Are we talking convenience? Yeah - I see that point. I need cookies to load images directly from <img> tags through the API - you can't easily use an Authorization header there, yet. But I just use nginx for that.

And it is not appealing to setup another piece of publicly accessible hardware to do one simple task of rewriting a cookie to a header, not to mention the security factors involved in that.

That's another thing that I am not buying. From where are you serving your web app? I have never had a use-case where I did not deploy nginx next to PostgREST anyway to serve the main app etc. - all my api traffic routed through that instance anyway. So it's just a simple config - done.

mckinlde commented 9 months ago

I am just not buying the argument that using those cookies is actually any safer than the Authorization header.

I clearly have more to learn here; the 'safer' that I've been convinced of is httponly cookies are hidden from client-side javascript. It occurs to me that any request sent via an XSS vulnerability would also include those cookies, particularly when that XSS attack is also attaching samesite cookies.

The Authorization header needs to be added actively, so you need to possess the token first.

If I'm storing this header in local or session storage instead of a cookie, isn't it trivial for an XSS attack to possess?

I need cookies to load images directly from tags through the API - you can't easily use an Authorization header there, yet. But I just use nginx for that.

My use case doesn't cover tags, but 'just use nginx' is the thing I'm trying to avoid, because=>

have never had a use-case where I did not deploy nginx next to PostgREST anyway to serve the main app etc. - all my api traffic routed through that instance anyway.

My use-case is using PostgREST to serve the main app directly. Routing all my api traffic through a second nginx server will effectively double the routing & api request overhead, vs current architecture that only uses PostGREST.

It may be the case that 'just use nginx' is in fact a trivial increase in server costs and an actually secure result, like I said I'm a novice end-user here. My current understanding is that using an httponly cookie to store the Auth token will hide it from client-side javascript, and storing it elsewhere does not. That said, while I can set a cookie directly from PostGREST, to read and use that cookie requires defining a /rpc. If I were able to configure PostGREST at server-scope to check an Authorization cookie for the access_token instead of an Authorization header, I would not need nginx.

wolfgangwalther commented 9 months ago

If I'm storing this header in local or session storage instead of a cookie, isn't it trivial for an XSS attack to possess?

Yes, absolutely.

But assume you want to build a highly secure application, you might consider not persisting sessions. In this case you could store the token in a variable only - which might not be readable, depending on the scope that the XSS is happening in.

My use-case is using PostgREST to serve the main app directly.

Oh, really? You serve all the html and javascript files from within PostgREST? That's interesting, I'd like to know more. How do you do that?

mckinlde commented 9 months ago

If I'm storing this header in local or session storage instead of a cookie, isn't it trivial for an XSS attack to possess?

Yes, absolutely.

This is precisely the surface I'm trying to protect. Is there a reference you'd recommend re:cookies being attached to XSS attacks? I thought that storing tokens in a cookie instead of local/session storage would separate the token from the request when a malicious request is made.

But assume you want to build a highly secure application, you might consider not persisting sessions. In this case you could store the token in a variable only - which might not be readable, depending on the scope that the XSS is happening in.

This is unfortunately not practical for my use case. For others where it can be, why not store/require auth be passed in all requests? When is returning/storing a token necessary while sessions aren't persisted?

Oh, really? You serve all the html and javascript files from within PostgREST? That's interesting, I'd like to know more. How do you do that?

To be clear, my use case is not serving html/JS via PostgREST; those are built into a static bundle that exposes a UI whose api is in turn entirely served via PostgREST. That said, couldn't html/js be stored as type text? It's interesting to imagine a table of react components; could you could effectively expose a different bundle to different Postgres roles after auth? How/could a npm build command be invoked in the browser?

yevon commented 7 months ago

I do exactly the same, I just store the cookie in an httponly samesite cookie, and proper cors and cookie configuration, so the cookie will only be sent to the specified domain even in a malicious attack (SameSite=Strict). Is this considered unsafe? I do consider unsafe storing a jwt token in localstorage or memory, as they are accessible with javascript and the httponly cookie is not and is designed for that purpose. And for added security, you can add a short term token and a refresh token, so the token rotates every few minutes. If you have full control of the domain, or you just use the domain for a unique application, or all domains are under your control I don't see any issue with it, I don't know any other way more secure to achieve this. It would be nice supporting reading the jwt token directly from a cookie optionally, as sometimes is not easy to read a cookie and put in the authorization header with proxies. Right now I was trying to implement an istio mesh and I'm getting crazy just to achieve this as I don't quite control istio and the documentation is not quite complete.

wolfgangwalther commented 7 months ago

@mckinlde

To be clear, my use case is not serving html/JS via PostgREST; those are built into a static bundle that exposes a UI whose api is in turn entirely served via PostgREST.

So you are using nginx already to serve the static files. That means you don't need to set up a second nginx instance just for the cookie processing, you already have one.

@yevon

I do exactly the same, I just store the cookie in an httponly samesite cookie, and proper cors and cookie configuration, so the cookie will only be sent to the specified domain even in a malicious attack (SameSite=Strict). Is this considered unsafe?

No, this is certainly not considered unsafe.

The argument that is made in this thread is, that:

My argument is:

The point of an XSS attack is that somebody is running code in the context of your application. If this codes sends a request to the API - it will always use the same cookie that your regular requests are using. Yes, the attacker might not be able to read the token... but they can still use it.

TLDR: SameSite Cookies do not protect in case of XSS. They protect against CSRF, but only if you control the full domain or your domain is on the above mentioned Public Suffix List.


I'm not saying that we shouldn't add support for cookies. I'm actually undecided about that. The point I'm making is that I don't think this is buying us anything in terms of security. IMHO, adding support for cookies is only about usability, because you don't need to use a reverse proxy to achieve the same. I would love to be proven wrong, though.

jhf commented 7 months ago

I'd very much like to see support for JWT's in cookies to allow a minimal setup with PostgREST using less resources. Also it simplifies services to void having a rewrite in the proxy of your choice. Furthermore, with a bearer and refresh token, there has to be client side logic to refresh, if there was a Bearer and RefreshBearer cookie, then that allows for transparent refresh on the server side.

I don't think this is improving security, but it simplifies usage.

wolfgangwalther commented 7 months ago

Furthermore, with a bearer and refresh token, there has to be client side logic to refresh, if there was a Bearer and RefreshBearer cookie, then that allows for transparent refresh on the server side.

How are two automatically refreshed tokens/cookies different from just a single cookie/token with longer expiry?

Doesn't it kind of defeat the purpose of a refresh token when you send it with every request?

jhf commented 7 months ago

Furthermore, with a bearer and refresh token, there has to be client side logic to refresh, if there was a Bearer and RefreshBearer cookie, then that allows for transparent refresh on the server side.

How are two automatically refreshed tokens/cookies different from just a single cookie/token with longer expiry?

I think the difference is in the handling, the refresh token has a longer expiry, and is only good ONCE to get you another bearer token. Hence the refresh token is consumed when the bearer token expires.

Doesn't it kind of defeat the purpose of a refresh token when you send it with every request?

That's why I say it may not be related to security, at least for PostgREST. Looking at this thread, the refresh token allows a central auth server to decide if a new auth/bearer token is provided, while an auth/bearer token is blindly accepted by all services. This is given that only a central server can process the refresh token. For PostgREST it is the same server, so there would be no difference, but why go against the standard?

wolfgangwalther commented 7 months ago

I think the difference is in the handling, the refresh token has a longer expiry, and is only good ONCE to get you another bearer token.

Unfortunately this is impossible given the stateless authentication we have implemented, I think. At least I can't imagine a way to construct a token in a way so that it can only be consumed once, without tracking state.

yevon commented 7 months ago

I have this implemented in postgrest but it requires to have a table to control the state of the refresh tokens. The idea is being able to keep longer sessions securely by rotating the access token every few minutes, so if one is compromised if it is not used in time is useless. Also you can revoke refresh tokens in case of any security issue is detected and lock the account. Or even if someone tries to refresh the token before the access token expires, you can do things like blocking the ip, lock the account, or send an alert. You can also implement device trust etc, but all this would require a state and tables and right now postgrest is fully stateless and doesn't create tables in the database.

jhf commented 7 months ago

So, yes, instead of a short living auth token and a long living refresh token, one could have a long living auth token, But how does one decide when to refresh the auth token? Currently, if the auth token has expired, and the refresh token is valid, the refresh token can create another auth token. One would instead need some logic for how long before expire of an auth token it should be refreshed. My case was more like, why bother with special casing it, let's just use auth token and refresh token in a server only cookie, fully compatible with using a bearer and refresh token.

wolfgangwalther commented 7 months ago

My case was more like, why bother with special casing it, let's just use auth token and refresh token in a server only cookie, fully compatible with using a bearer and refresh token.

I still don't understand what this is supposed to do. When you have both an auth token and a refresh token in a server only cookie, this means both of them will be send with every request. How do you decide when to actually refresh the token on the server? Surely not just by "presence of the refresh token", because then you would be refreshing on every request... which is exactly the same as just using one token and extending that on every request. Why two tokens?

I can see a refresh token being useful somehow in a scenario as described in https://github.com/PostgREST/postgrest/issues/3033#issuecomment-1908912430 (with state). @yevon how do you implement that on the client? You are not sending the refresh token with every request, do you?

yevon commented 7 months ago

No, i'm not sending it at every request because as you say it won't make sense, i don't remember how I do that exactly, I will check it later. I remember that it is only sent against the refresh token api endpoint, and only when the access token has expired. Maybe I used a subdomain just for that, I'll check it.

jhf commented 7 months ago

I still don't understand what this is supposed to do. When you have both an auth token and a refresh token in a server only cookie, this means both of them will be send with every request. How do you decide when to actually refresh the token on the server? Surely not just by "presence of the refresh token", because then you would be refreshing on every request... which is exactly the same as just using one token and extending that on every request. Why two tokens?

The refresh only happens when the auth token is expired. So most requests only check the auth token, even if both are provided.

I agree, this can be done in another way, but my goal was to make it behave exactly like if you hade a Bearer and a Refresh token in a header, just that they are in a cookie, so a minimal amount of changes.

rotty3000 commented 7 months ago

Sorry to stick my nose in here but I also don't understand what is being asked.

According to OAuth2 refresh tokens are for "clients".

An OAuth Refresh Token is a string that the OAuth client can use to get a new access token without the user's interaction.

reference

Is the OP asking about PostgREST acting as a "client", using the user's access_token to perform operations on behalf of the user when outside a normal request (like during a machine triggered event)?

If the answer to the above question is, "no" then PostgREST should not ever care about refresh tokens.

However if the answer is "yes", then that's some specialized behaviour that I don't see being in the realm of vanilla PostgREST (at least not from my short experience with it).

A typical example, however, of where a refresh token might be used in relation to PostgREST is where you have a SPA application where a user logs in (an id/access_token is issued) and that token is used to make requests to a PostgREST backend on behalf of the user. If the user's session in the browser lasts a really long time (beyond the expiration of the id/access_token) then the SPA can use the refresh token to avoid asking the user to login again. However, the SPA itself will use the refresh token to get an updated id/access_token from the Authorization server and then send the access_token to PostgREST.

In those terms PostgREST will never care about a refresh token because there's no need for it to be stateful because it expects every request to be accompanied by an access_token. This is pretty typical of stateless backend architecture.

My 2 cents.

jhf commented 7 months ago

There are two distinct flows here.

  1. Classical Auth token + Refresh token. All requests provide an Auth token. If the server says the auth token is expired, then the client can send the refresh token to get another auth token, possibly an extended refresh token. This is handled by client side code to avoid bothering the user.

This is great and works well today! Some people have however noticed that it is simpler to interact with an API when server side cookies are used, in particular from scripts. In that case one thinks that the whole machinery from can be adapted to cookies.

  1. Cookie Auth token + Refresh Token. After login two server side http cookies are set auth-token and refresh-token. Every request provides both auth-token and refresh-token. If auth-token is valid, then it works as for case (1). If the auth-token is expired, then there is server side logic that behaves the same as for case (1). The refresh-token is checked, and if valid, a new auth-token and refresh-token is issued, if not, then both tokens are cleared.

By implementing a setting, one can switch between 1 and 2, and case (2) is pretty convenient, especially from dumb clients/scripts. By using the same information and logic as case (1), only on the server side, it is also pretty consistent.

Both case (1) and (2) can be stateless. I guess supporting case (2) is about allowing cookie based tokens, but it would be up a practical implementation how it is done. Currently there is no default auth shipped with PostgREST anyway.

I was hoping it was also easy to explain, but alas, it is demonstrably not so.

wolfgangwalther commented 7 months ago
  1. Cookie Auth token + Refresh Token. After login two server side http cookies are set auth-token and refresh-token. Every request provides both auth-token and refresh-token. If auth-token is valid, then it works as for case (1). If the auth-token is expired, then there is server side logic that behaves the same as for case (1). The refresh-token is checked, and if valid, a new auth-token and refresh-token is issued, if not, then both tokens are cleared.

There is no difference compared to just sending the refresh token only. You don't need the "auth-token" at all in this scenario. There is no point in sending two cookies automatically. The refresh token overrules the auth token anyway.

mckinlde commented 7 months ago

@jhf :

Cookie Auth token + Refresh Token. After login two server side http cookies are set auth-token and refresh-token. Every request provides both auth-token and refresh-token. If auth-token is valid, then it works as for case (1). If the auth-token is expired, then there is server side logic that behaves the same as for case (1). The refresh-token is checked, and if valid, a new auth-token and refresh-token is issued, if not, then both tokens are cleared.

@rotty3000 :

A typical example, however, of where a refresh token might be used in relation to PostgREST is where you have a SPA application where a user logs in (an id/access_token is issued) and that token is used to make requests to a PostgREST backend on behalf of the user. If the user's session in the browser lasts a really long time (beyond the expiration of the id/access_token) then the SPA can use the refresh token to avoid asking the user to login again. However, the SPA itself will use the refresh token to get an updated id/access_token from the Authorization server and then send the access_token to PostgREST.

These comments approximate my use case--it would be more accurate to say "Classical Auth token + Cookie Refresh token."

I have a SPA that uses pgrst for the backend, following the user-auth examples from Tutorial 1 and SQL User Management. To that featureset, I've added a rpc/access_token_refresh, that uses a refresh token (which was issued as a cookie using PERFORM set_config('response.headers', _cookie, true); in the login() rpc) to issue a new access token as a Bearer header.

@wolfgangwalther : In my use case, [classical (header) auth, cookie refresh], the refresh token is in fact sent with every request, and generally ignored as pgrst uses the access token as normal. The exception to this rule is a call to rpc/access_token_refresh, which checks the refresh cookie (1) and then issues a new access token if valid.

(1): SELECT current_setting('request.cookies', true)::json->>'refresh_token' INTO refresh_token;

@jhf @rotty3000 @wolfgangwalther : Given that I want a user's ability to get an access token to persist across browser sessions, but don't care (and maybe don't want) any specific access token to persist, I think the [header access token, cookie refresh token] may be the ideal implementation. If I were to write a how-to chapter "SQL User Management 2: refresh cookies and rpc/access_token_refresh" would pgrst be interested in publishing? My concern is that Oauth2 obviates this; I don't understand it well enough to know if adding pgrst as an "Open Source OAuth Provider" would be appropriate.

rotty3000 commented 7 months ago

Ok, the part I was missing is that PostgREST IS the Authorization server in this story. In that case it stands to reason that it needs to be able to use the fresh token to issue a new access token. The matter is that you merely want to be able to "get" the refresh token from cookies on the request to rpc/access_token_refresh. That makes total sense to me now.

mckinlde commented 7 months ago

Ok, the part I was missing is that PostgREST IS the Authorization server in this story. In that case it stands to reason that it needs to be able to use the fresh token to issue a new access token. The matter is that you merely want to be able to "get" the refresh token from cookies on the request to rpc/access_token_refresh. That makes total sense to me now.

Yes; >PostgREST IS the Authorization server.

This comment is directly from a novice end user implementing this feature (refresh tokens / read-write-cookies with pgrst).

This closed issue is the most exhaustive/direct documentation for that user that I'm aware of.

This open issue is why I mention contributing to a tutorial.

wolfgangwalther commented 7 months ago

In my use case, [classical (header) auth, cookie refresh], the refresh token is in fact sent with every request, and generally ignored as pgrst uses the access token as normal. The exception to this rule is a call to rpc/access_token_refresh, which checks the refresh cookie (1) and then issues a new access token if valid.

Sorry, I still don't understand the advantage of using two cookies here. If the refresh token is sent with every request, you could just as well parse this cookie on every request and drop the Bearer token entirely. What additional value do you get from using this second, short-lived token?

yevon commented 7 months ago

In my use case, [classical (header) auth, cookie refresh], the refresh token is in fact sent with every request, and generally ignored as pgrst uses the access token as normal. The exception to this rule is a call to rpc/access_token_refresh, which checks the refresh cookie (1) and then issues a new access token if valid.

Sorry, I still don't understand the advantage of using two cookies here. If the refresh token is sent with every request, you could just as well parse this cookie on every request and drop the Bearer token entirely. What additional value do you get from using this second, short-lived token?

I do agree with it, both tokens are not sent on every request as it looses all security advantages of it. You would need a subdomain with a specific cookie for the refresh token that only is valid for that subdomain. The application redirects to the subdomain token refresh endpoint when the access token is not valid. And if even the refresh token is not valid, then you redirect the user to the login screen. This way the access token is used for all requests except for the refresh token.

wolfgangwalther commented 7 months ago

refresh token

In general, the only case I can imagine where a refresh token + auth token makes any sense is the following:

In this scenario, it would still be useful to keep sending both tokens all the time - but only use the refresh token sparingly.

But given that PostgREST itself does not keep state, certainly does not talk to another upstream server and checking the JWT + SET ROLE is really cheap... I don't see this applying, especially not in a stock-PostgREST setup. Certainly there is no way to automate the more complex part (state, complex queries, upstream server, ...) that is behind the refresh logic via PostgREST - this will always have to be a customized solution.


The only takeaway from this thread that I can actually see as useful is that switching from Authorization header to cookie for the access token can make deploying and using PostgREST easier in some scenarios.

But:

Some people have however noticed that it is simpler to interact with an API when server side cookies are used, in particular from scripts.

My experience is exactly the opposite. I always found it easier to write scripts when I could handle the token myself explicitly via header and not have to rely on whatever request-library I use to maintain state (cookies) for me - that's often a blackbox.

So "easier to use" is certainly subjective as well.