Giter VIP home page Giter VIP logo

Comments (3)

AthinaPl avatar AthinaPl commented on June 14, 2024

Context and CSRF implementation

CSRF Token and Session State

Session in oauth2-proxy is maintained in the SessionState struct either:

  • by encoding it and storing it in a cookie
  • by storing it in a Redis instance

There is no explicit session ID in either case, however there is the Nonce field for each session, which is a byte slice (randomly generated). This is apparently used in the login CSRF validation that oauth2-proxy implements using the state parameter.

Using the Nonce as the session ID would be a solution, however the Nonce is sent to the identity provider and therefore can be leaked:

OIDCNonce holds the OIDC nonce parameter used in the initial authentication and then set in all subsequent OIDC ID Tokens as the nonce claim. This is used to mitigate replay attacks.

So is seems that the Nonce can't be used as a session ID.

Therefore we introduced a new CSRFToken field inside the SessionState, which will be a random unique string, as per the token requirements in the specification.

Apart from the above, SessionState seems a fitting place to store the CSRFToken, since it already contains fields directly associated with the session and derived from the provider, i.e. id/access/refresh tokens.

For this purpose, we exposed the --csrftoken argument, which is a boolean flag to enable/disable CSRF token generation on the session state upon login. This is disabled by default, to avoid breaking changes.

Returning the CSRF token to the client application

Once the CSRF token is generated and saved along with the session, it is necessary for the token to reach the client application. The ways to return a CSRF token to the client application are:

  • with a cookie
  • with a custom header
  • with a JSON response on a designated endpoint

Note that the above can be used safely to return a CSRF token to the client, given that the proxy will expect client tokens in a custom header. Token value cannot be exploited e.g. by SameSite when returning as a cookie, or by reading the request content - headers, response - when using the other methods, since cross-origin requests in Javascript don't expose the request and response objects.

Below I will describe the implementation for each.

Return as cookie

To return the CSRF token as cookie, we need to first construct the cookie. oauth2-proxy has the following Cookie struct:
 

// Cookie contains configuration options relating to Cookie configuration
type Cookie struct {
Name           string        `flag:"cookie-name" cfg:"cookie_name"`
Secret         string        `flag:"cookie-secret" cfg:"cookie_secret"` 
Domains        []string      `flag:"cookie-domain" cfg:"cookie_domains"` 
Path           string        `flag:"cookie-path" cfg:"cookie_path"` 
Expire         time.Duration `flag:"cookie-expire" cfg:"cookie_expire"` 
Refresh        time.Duration `flag:"cookie-refresh" cfg:"cookie_refresh"` 
Secure         bool          `flag:"cookie-secure" cfg:"cookie_secure"` 
HTTPOnly       bool          `flag:"cookie-httponly" cfg:"cookie_httponly"` 
SameSite       string        `flag:"cookie-samesite" cfg:"cookie_samesite"` 
CSRFPerRequest bool          `flag:"cookie-csrf-per-request" cfg:"cookie_csrf_per_request"` 
CSRFExpire     time.Duration `flag:"cookie-csrf-expire" cfg:"cookie_csrf_expire"` 
}

which is used to hold the necessary options to construct the session cookie using function MakeCookieFromOptions (https://github.com/AthinaPl/oauth2-proxy/blob/master/pkg/cookies/cookies.go#L17).
 
We obviously can't use the same for the CSRF cookie, since it contains attributes CSRFPerRequest and CSRFExpire used for the CSRF cookie during login.
 
So we decided to create a new file and struct, in pkg/apis/options/csrftoken.go to introduce our cookie, to also take advantage of the cfg tags that populate the struct with the values taken from the configuration file or the CLI. This is done by using viper and pflag packages in file https://github.com/AthinaPl/oauth2-proxy/blob/master/pkg/apis/options/load.go.

This is the CSRF token struct

type CSRFToken struct { 
	CSRFToken      bool          `flag:"csrftoken" cfg:"csrftoken"`
	CookieName     string        `flag:"csrftoken-cookie-name" cfg:"csrftoken_cookie_name"`
	CookieDomains  []string      `flag:"csrftoken-cookie-domain" cfg:"csrftoken_cookie_domain"`
	CookiePath     string        `flag:"csrftoken-cookie-path" cfg:"csrftoken_cookie_path"`
	CookieExpire   time.Duration `flag:"csrftoken-cookie-expire" cfg:"csrftoken_cookie_expire"`
	CookieSecure   bool          `flag:"csrftoken-cookie-secure" cfg:"csrftoken_cookie_secure"`
	CookieHTTPOnly bool          `flag:"csrftoken-cookie-httponly" cfg:"csrftoken_cookie_httponly"`
	CookieSameSite string        `flag:"csrftoken-cookie-samesite" cfg:"csrftoken_cookie_samesite"`
}

We have also implemented a CSRFTokenDefaults() function that sets default values to a CSRFToken, and a MakeCSRFTokenFromOptions function to create a CSRF token cookie from default values.
 
The above cookie attributes are controlled by the new flags:

  • --csrftoken-cookie-name (string): The name of the cookie holding the CSRF token for the session. If set to empty string, the proxy will disable returning CSRF token as cookie. Default is _oauth2_proxy_csrftoken.
  • --csrftoken-cookie-domain (string | list): The domain(s) (optional) of the CSRF token cookie. Default: ""
  • --csrftoken-cookie-path (string): The path of the CSRF token cookie. Default: /
  • --csrftoken-cookie-expire (duration): The duration of the CSRF token cookie. Default is equal to the session cookie expiration, i.e. 168 hours.
  • --csrftoken-cookie-secure (bool): Set secure (HTTPS) cookie flag for CSRF token cookie. Default: true.
  • --csrftoken-cookie-httponly (bool): Set HttpOnly cookie flag for CSRF token cookie. Default: false
  • --csrftoken-cookie-samesite (string): Set SameSite cookie attribute for CSRF token cookie (ie: lax, strict, none, or ""). Default is "strict".
     
    The cookie, if --csrftoken=true and --csrftoken-cookie-name!="", will be set during the handling of the callback function in the OAuth2 cycle.
     

Return as header

To return the CSRF token as a custom header, we introduced the:

  • --csrftoken-response-header (string): The name of the actual CSRF token header to return to client. Default: X-CSRF-Token.

that injects the header with the CSRF token value from SessionState in each response of the oauth2-proxy. If the flag is set to an empty string, the proxy will disable returning the CSRF token as header.

Return as JSON response

To return the CSRF token as a JSON response, we exposed the /oauth2/csrftoken endpoint in the proxy. This is an authenticated endpoint (i.e. needs a session cookie) serving GET requests, and returns a simple JSON containing the CSRF token.

The purpose of this endpoint is to use right after the finish of the OAuth2 cycle, before making any requests to the upstream server. A frontend application can place an Ajax request to fetch the CSRF token and then include it in subsequent requests to the server.

CSRF validation

The validation of CSRF tokens is necessary if all of the following apply:

  • the proxy operates on CSRF mode (--csrftoken=true)
  • a request is using an unsafe method according to RFC 7231
  • a request is authenticated via a session cookie

Focusing on the last item, CSRF vulnerabilities happen in client applications (UI) and don't concern programmatic client requests, that authenticate with bearer tokens via authorization headers. This information is necessary when deciding to apply CSRF validation and is currently not implemented in oauth2-proxy.

Authentication in oauth2-proxy happens with chained middlewares, and the request context is passed to the proxy, which will check for a valid session before doing any more actions. Validation of the CSRF token takes place right after ensuring a session in the proxy. So it seems optimal for the request context to contain the authentication method information, to allow the proxy to decide if CSRF validation is necessary.

The above summarize the necessity of the following newly introduced argument: 

  • --csrftoken-header (string): The name of the header holding the CSRF token in incoming client requests. Default: X-CSRF-Token

Furthermore, RequestScope now has a AuthMethod field, that is set to:

  • cookie, if request was authenticated via cookie
  • header, if request was authenticated via the authorization header

Skip validation

In many setups, is it necessary to skip validation for some routes. To allow this functionality, we exposed the:

  • --skip-csrftoken-route (string | list): Format: method=path_regex OR method!=path_regex. For all methods: path_regex OR !=path_regex. Default: ""

This will construct a list of method/path combinations that requests should be allowed without CSRF validation. Note that we can completely disable CSRF validation, but not CSRF token generation, by including all paths in this argument.

from oauth2-proxy.

AthinaPl avatar AthinaPl commented on June 14, 2024

Pending items

The above summarize the operation of the CSRF implementation on the proxy. However, since there are cases that oauth2-proxy is configured with upstreams, it seems necessary to allow:

  • forwarding/removing the client CSRF token header: this was the intention of the proposed --pass-csrftoken-header, which after implementing the main functionality might be unnecessary, since we can simply use --skip-auth-strip-headers flag to remove the client CSRF token along with the other authentication headers (X-Forwarded-* and Authorization)
  • forwarding the authentication method: implement a new header that will be proxied upstream to relay the authentication method.

to the upstream. This information is necessary if the upstream server implements CSRF and doesn't need oauth2-proxy to do so.

We have already started working on the above and will add the commits to the PR as soon as they're ready.

from oauth2-proxy.

AthinaPl avatar AthinaPl commented on June 14, 2024

Authentication method header

If CSRF validation is implemented on the upstream, there is no need to enable the feature in the proxy. However, not all requests are subject to CSRF checks - as we mentioned in previous comments:

The validation of CSRF tokens is necessary if all of the following apply:

  • the proxy operates on CSRF mode (--csrftoken=true)
  • a request is using an unsafe method according to RFC 7231
  • a request is authenticated via a session cookie

Since we have added AuthMethod to the proxy's RequestScope, we need to allow passing this information to the upstream.

Currently the headers middleware that handles adding headers to requests forwarded to upstreams can get header values either from the session (ClaimSource at https://github.com/oauth2-proxy/oauth2-proxy/blob/master/pkg/apis/options/header.go#L31) or from env/file (SecretSource at https://github.com/oauth2-proxy/oauth2-proxy/blob/master/pkg/apis/options/common.go#L11). To allow fetching the authentication method from the RequestScope, we implemented a ScopeSource that fetches fields from the RequestScope.

Furthermore, it is necessary for the header injector interface to have access to the scope. Currently the injector uses the SessionState (which is fetched from the request scope). It seems logical for the injector to manipulate the RequestScope instead of the SessionState, and use RequestScope.Session specifically when it needs it.

Remove client CSRF token header from request if --skip-auth-strip-headers=true

Client headers are normally included in the requests forwarded to the upstream. It is necessary for allowing CSRF validation on the upstream to pass the client CSRF token, but it also useful to remove it (if CSRF is disabled or if validation happens on the proxy or if we don't trust the upstream).

For the above reasons we opted to remove the header from the request when proxy is configured with --skip-auth-strip-headers=true.

from oauth2-proxy.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.