PostgREST / postgrest

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

Use JWT through cookies instead of Bearer scheme in headers #773

Closed tocttou closed 7 years ago

tocttou commented 7 years ago

I could not find a way in documentation to use JWT authentication using cookies instead of the Bearer scheme in headers which requires you to store the JWT in localStorage (which can be a fatal in case of an XSS vulnerability). For reference: Where to store JWTs.

This would require recognising the token as:

GET /login
Host: postgrest.com

Cookie: access_token=eyJhbGciOiJIUzI1NiIsI.eyJpc3MiOiJodHRwczotcGxlL.mFrs3Zo8eaSNcxiNfvRh9dqKP4F1cB;
SebAlbert commented 7 years ago

Seems like a perfect fit to rewrite this in the reverse proxy layer. nginx, apache etc. are all able to read and write HTTP headers while proxying a request.

But, how are Cookies less vulnerable to XSS than localstorage? Edit: I see in your link, HttpOnly. Cookie technology must have moved on from what I knew back in the days...

tocttou commented 7 years ago

Using a reverse proxy is a fair point, thanks. But it would be better if this could be integrated in the main project as it is a valid use case and easy to implement (probably).

begriffs commented 7 years ago

@tocttou have you investigated doing the proxy thing? If so I'd love an nginx snippet to include in the docs.

tocttou commented 7 years ago

Hello

Yes I was able to get it working using lua-nginx-module to rewrite the request.

If your Postgrest API server is on http://localhost:3000 and your Nginx Proxy is on http://localhost:3001, you can use this nginx proxy config and make request to your nginx proxy with a cookie access_token that contains the jwt (it rewrites the headers to include a Authorization: Bearer <jwt> header:

server {
  listen  0.0.0.0:3001;

  location / {
     rewrite_by_lua_block {
       local cookie_value = ngx.req.get_headers()["Cookie"]
       if cookie_value ~= nil then
         local jwt = cookie_value:match("access_token=([^ ]+)")
         ngx.req.set_header("Authorization", "Bearer " .. jwt)
       end
       ngx.req.clear_header("Cookie")
     }
     proxy_pass http://0.0.0.0:3000;
  }
}

Actual request (to nginx proxy by the client):

GET  HTTP/1.1
Host: localhost:3001
Cookie: access_token=mah.osum.token

Request relayed to Postgrest:

GET  HTTP/1.1
Host: localhost:3000
Authorization: Bearer mah.osum.token

Note that the regex used to extract the access_token only works correctly when there is a single cookie.

ruslantalpa commented 7 years ago

Closing this since implementation in OpenResty is the preferred way. The only way to add is there is a better way to read the cookie like this

local ck = require 'resty.cookie'
local cookie = ck:new()
local token = cookie:get('COOKIE_NAME_HERE')
TonnyLTP commented 5 years ago

What I did is setting local config in login plpgsql function, and do the transfer job in nginx (without lua).

_cookie := format('[{"set-cookie": "access_token=%s; path=/; HttpOnly; max-age=86400"}]', _token);

PERFORM set_config('response.headers', _cookie, true);

expected postgrest would support to read token from cookie, but currently seems not. So I have to transfer token from cookie header to authorizaition header.

if ($cookie_access_token) {
  set $auth "Bearer $cookie_access_token";
}

if ($http_authorization) {
  set $auth $http_authorization;
}

proxy_set_header Authorization $auth;

I hope it could support to read access_token or session_token in cookie header as jwt token.

chamini2 commented 5 years ago

expected postgrest would support to read token from cookie, but currently seems not

I would appreciate this option too

swuecho commented 3 years ago

for anyone who using envoy proxy.

 - name: envoy.filters.http.lua
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
                inline_code: |
                  -- called on request path
                  function envoy_on_request(request_handle)
                          local headers = request_handle:headers()
                          local cookieString = headers:get("cookie")
                          if cookieString ~= nil then
                              local jwt = cookieString:match("jwt=([^; ]+)")
                              headers:add("authorization", "Bearer " .. jwt)
                          end
                  end
W1M0R commented 3 years ago

For anyone using Caddy 2.4.5, this Caddyfile can be a helpful starting point:

{
    debug
}

:8080

reverse_proxy /rpc/login 127.0.0.1:3000 {

    # Include the AuthToken cookie in future requests, in case the database needs
    # to perform auditing, automatic logout etc. If the AuthToken cookie has no
    # value, then PostgREST will default to the anonymous role.
    header_up Authorization "Bearer {http.request.cookie.AuthToken}"

    # The /rpc/login endpoint will create a temporary response header called
    # App-Auth-Token, that will contain the JWT bearer authorization token. We
    # will use that header value to set the AuthToken cookie.
    # todo: For HTTPS, use Set-Cookie "__Host-Token=3h93…;Path=/;Secure;HttpOnly;SameSite=Strict"
    @match_status_ok status 2xx
    handle_response @match_status_ok {
        header Content-Type application/json
        header Set-Cookie "AuthToken={http.reverse_proxy.header.App-Auth-Token};HttpOnly;Path=/;SameSite=Strict"
        respond `{
    "auth_role" : "{http.reverse_proxy.header.App-Auth-Role}",
    "auth_session_id" : "{http.reverse_proxy.header.App-Auth-Session}",
    "auth_user_id" : "{http.reverse_proxy.header.App-Auth-User}",
}` {http.reverse_proxy.status_code}
    }

    # In case login fails, let's clear the AuthToken cookie. Login would fail if
    # there is an incorrect username/password, or if the existing authorization
    # token somehow caused validation to fail.
        # Uncomment these lines if you want to clear the cookie if login fails.
    # @match_status_errors status 4xx
    # handle_response @match_status_errors {
    #   header Content-Type application/json
    #   header Set-Cookie "AuthToken=;HttpOnly;Path=/;SameSite=Strict"
    #   respond `{ "message" : "{http.reverse_proxy.status_text}" }` {http.reverse_proxy.status_code}
    # }

    # Now that we are done with these temporary headers, we can remove them.
    header_down -App-Auth-Token
    header_down -App-Auth-Session
    header_down -App-Auth-User
    header_down -App-Auth-Role
}

reverse_proxy /rpc/logout 127.0.0.1:3000 {
    header_up Authorization "Bearer {http.request.cookie.AuthToken}"
    header_down Set-Cookie "AuthToken=;HttpOnly;Path=/;SameSite=Strict"
}

reverse_proxy 127.0.0.1:3000 {
    header_up Authorization "Bearer {http.request.cookie.AuthToken}"
}
yevon commented 2 years ago

Anybody implemented this in ingress nginx on kubernetes?

yevon commented 2 years ago

Solved in kubernetes nginx ingress by doing this, inspired in @TonnyLTP solution:

This solution includes an access token and a refresh token for more secure login implementations.

nginx.ingress.kubernetes.io/configuration-snippet: |
      if ($cookie_access_token) {
        set $auth "Bearer $cookie_access_token";
      }
      if ($http_authorization) {
        set $auth $http_authorization;
      }
      if ($cookie_refresh_token) {
        set $auth "Bearer $cookie_refresh_token";
      }      
      proxy_set_header Authorization "${auth}";
mckinlde commented 11 months ago

The ability to set custom headers was added in PR 986, which I found in this discussion.

@TonnyLTP and @yevon have the closest approach for my use case--as of now I am pretty confident I can change the /login endpoint to respond with a set-cookie header to store the JWT, but once I've done that how do I ensure the rest of the endpoints in postgrest are checking the cookie header for a JWT?

yevon commented 11 months ago

Postgrest always validates if there is an auth header present for all endpoints, you have to explicity specify which ones do not require any auth, like the login. Once you specify your token http only cookie is valid for your api.example.com, the web browser will always send that cookie to the server automatically for every request against api.example.com. And with the nginx / proxy approach, you transform the cookie to an auth header before passing it to the postgrest backend.

mckinlde commented 11 months ago

Thank you @yevon; so there isn't a way to configure pgRest to check the cookie directly instead of the Auth header? I was hoping I could simply change a config setting instead of running a reverse proxy--if transforming is required then I'll be running another server for that purpose alone.

I'm a novice end user, and I really appreciate the help; my goal is to use the JWT auth/auth as described in SQL User Management and Tutorial 1, with the sole difference of storing the JWT as an Http Only SameSite cookie.

yevon commented 11 months ago

You could achieve this by implementing a pre-validation pgsql script and validate the token yourself, but this would be even more complicated than the proxy approach. I think that right now there is no way to accept tokens via cookie officially, or compatibility with a short term token with long term refresh tokens. It would be nice having this kind of cookie based support.

mckinlde commented 11 months ago

@yevon That is the case. I've opened a new issue requesting the feature, it has been previously discussed many times and I include links to those discussions as well as relevant documentation and snippets--I'm a bit wordy, I'd appreciate editorial suggestions there!