1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2025-01-24 05:26:55 +02:00
Braunson ce750e9b30
PKCE Support (#1541)
* Add the allowed_email_domains and the allowed_groups on the auth_request endpoint + support standard wildcard char for validation with sub-domain and email-domain.

Signed-off-by: Valentin Pichard <github@w3st.fr>

* Fix provider data initialisation

* PKCE Support

Adds Code Challenge PKCE support (RFC-7636) and partial
Authorization Server Metadata (RFC-8414) for detecting PKCE support.

- Introduces new option `--force-code-challenge-method` to force a
specific code challenge method (either `S256` or `plain`) for instances
when the server has not implemented RFC-8414 in order to detect
PKCE support on the discovery document.
- In all other cases, if the PKCE support can be determined during discovery
then the `code_challenge_methods_supported` is used and S256 is always
preferred.
- The force command line argument is helpful with some providers like Azure
who supports PKCE but does not list it in their discovery document yet.
- Initial thought was given to just always attempt PKCE since according to spec
additional URL parameters should be dropped by servers which implemented
OAuth 2, however other projects found cases in the wild where this causes 500
errors by buggy implementations.
See: https://github.com/spring-projects/spring-security/pull/7804#issuecomment-578323810
- Due to the fact that the `code_verifier` must be saved between the redirect and
callback, sessions are now created when the redirect takes place with `Authenticated: false`.
The session will be recreated and marked as `Authenticated` on callback.
- Individual provider implementations can choose to include or ignore code_challenge
and code_verifier function parameters passed to them

Note: Technically speaking `plain` is not required to be implemented since
oauth2-proxy will always be able to handle S256 and servers MUST implement
S256 support.
> If the client is capable of using "S256", it MUST use "S256", as "S256"
> is Mandatory To Implement (MTI) on the server.  Clients are permitted
> to use "plain" only if they cannot support "S256" for some technical
> reason and know via out-of-band configuration that the server supports
> "plain".
Ref: RFC-7636 Sec 4.2

oauth2-proxy will always use S256 unless the user explicitly forces `plain`.

Fixes #1361

* Address PR comments by moving pkce generation

* Make PKCE opt-in, move to using the Nonce generater for code verifier

* Make PKCE opt-in, move to using the Nonce generater for code verifier

* Encrypt CodeVerifier in CSRF Token instead of Session

- Update Dex for PKCE support
- Expose HTTPBin for further use cases

* Correct the tests

* Move code challenges into extra params

* Correct typo in code challenge method

Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

* Correct the extra space in docs

Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>

* Address changelog and new line nits

* Add generated docs

Co-authored-by: Valentin Pichard <github@w3st.fr>
Co-authored-by: Joel Speed <joel.speed@hotmail.co.uk>
2022-03-13 10:08:33 +00:00

211 lines
5.4 KiB
Go

package cookies
import (
"errors"
"fmt"
"net/http"
"time"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/clock"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
"github.com/vmihailenco/msgpack/v4"
)
// CSRF manages various nonces stored in the CSRF cookie during the initial
// authentication flows.
type CSRF interface {
HashOAuthState() string
HashOIDCNonce() string
CheckOAuthState(string) bool
CheckOIDCNonce(string) bool
GetCodeVerifier() string
SetSessionNonce(s *sessions.SessionState)
SetCookie(http.ResponseWriter, *http.Request) (*http.Cookie, error)
ClearCookie(http.ResponseWriter, *http.Request)
}
type csrf struct {
// OAuthState holds the OAuth2 state parameter's nonce component set in the
// initial authentication request and mirrored back in the callback
// redirect from the IdP for CSRF protection.
OAuthState []byte `msgpack:"s,omitempty"`
// 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.
OIDCNonce []byte `msgpack:"n,omitempty"`
// CodeVerifier holds the unobfuscated PKCE code verification string
// which is used to compare the code challenge when exchanging the
// authentication code.
CodeVerifier string `msgpack:"cv,omitempty"`
cookieOpts *options.Cookie
time clock.Clock
}
// NewCSRF creates a CSRF with random nonces
func NewCSRF(opts *options.Cookie, codeVerifier string) (CSRF, error) {
state, err := encryption.Nonce(32)
if err != nil {
return nil, err
}
nonce, err := encryption.Nonce(32)
if err != nil {
return nil, err
}
return &csrf{
OAuthState: state,
OIDCNonce: nonce,
CodeVerifier: codeVerifier,
cookieOpts: opts,
}, nil
}
// LoadCSRFCookie loads a CSRF object from a request's CSRF cookie
func LoadCSRFCookie(req *http.Request, opts *options.Cookie) (CSRF, error) {
cookie, err := req.Cookie(csrfCookieName(opts))
if err != nil {
return nil, err
}
return decodeCSRFCookie(cookie, opts)
}
func (c *csrf) GetCodeVerifier() string {
return c.CodeVerifier
}
// HashOAuthState returns the hash of the OAuth state nonce
func (c *csrf) HashOAuthState() string {
return encryption.HashNonce(c.OAuthState)
}
// HashOIDCNonce returns the hash of the OIDC nonce
func (c *csrf) HashOIDCNonce() string {
return encryption.HashNonce(c.OIDCNonce)
}
// CheckOAuthState compares the OAuth state nonce against a potential
// hash of it
func (c *csrf) CheckOAuthState(hashed string) bool {
return encryption.CheckNonce(c.OAuthState, hashed)
}
// CheckOIDCNonce compares the OIDC nonce against a potential hash of it
func (c *csrf) CheckOIDCNonce(hashed string) bool {
return encryption.CheckNonce(c.OIDCNonce, hashed)
}
// SetSessionNonce sets the OIDCNonce on a SessionState
func (c *csrf) SetSessionNonce(s *sessions.SessionState) {
s.Nonce = c.OIDCNonce
}
// SetCookie encodes the CSRF to a signed cookie and sets it on the ResponseWriter
func (c *csrf) SetCookie(rw http.ResponseWriter, req *http.Request) (*http.Cookie, error) {
encoded, err := c.encodeCookie()
if err != nil {
return nil, err
}
cookie := MakeCookieFromOptions(
req,
c.cookieName(),
encoded,
c.cookieOpts,
c.cookieOpts.Expire,
c.time.Now(),
)
http.SetCookie(rw, cookie)
return cookie, nil
}
// ClearCookie removes the CSRF cookie
func (c *csrf) ClearCookie(rw http.ResponseWriter, req *http.Request) {
http.SetCookie(rw, MakeCookieFromOptions(
req,
c.cookieName(),
"",
c.cookieOpts,
time.Hour*-1,
c.time.Now(),
))
}
// encodeCookie MessagePack encodes and encrypts the CSRF and then creates a
// signed cookie value
func (c *csrf) encodeCookie() (string, error) {
packed, err := msgpack.Marshal(c)
if err != nil {
return "", fmt.Errorf("error marshalling CSRF to msgpack: %v", err)
}
encrypted, err := encrypt(packed, c.cookieOpts)
if err != nil {
return "", err
}
return encryption.SignedValue(c.cookieOpts.Secret, c.cookieName(), encrypted, c.time.Now())
}
// decodeCSRFCookie validates the signature then decrypts and decodes a CSRF
// cookie into a CSRF struct
func decodeCSRFCookie(cookie *http.Cookie, opts *options.Cookie) (*csrf, error) {
val, _, ok := encryption.Validate(cookie, opts.Secret, opts.Expire)
if !ok {
return nil, errors.New("CSRF cookie failed validation")
}
decrypted, err := decrypt(val, opts)
if err != nil {
return nil, err
}
// Valid cookie, Unmarshal the CSRF
csrf := &csrf{cookieOpts: opts}
err = msgpack.Unmarshal(decrypted, csrf)
if err != nil {
return nil, fmt.Errorf("error unmarshalling data to CSRF: %v", err)
}
return csrf, nil
}
// cookieName returns the CSRF cookie's name derived from the base
// session cookie name
func (c *csrf) cookieName() string {
return csrfCookieName(c.cookieOpts)
}
func csrfCookieName(opts *options.Cookie) string {
return fmt.Sprintf("%v_csrf", opts.Name)
}
func encrypt(data []byte, opts *options.Cookie) ([]byte, error) {
cipher, err := makeCipher(opts)
if err != nil {
return nil, err
}
return cipher.Encrypt(data)
}
func decrypt(data []byte, opts *options.Cookie) ([]byte, error) {
cipher, err := makeCipher(opts)
if err != nil {
return nil, err
}
return cipher.Decrypt(data)
}
func makeCipher(opts *options.Cookie) (encryption.Cipher, error) {
return encryption.NewCFBCipher(encryption.SecretBytes(opts.Secret))
}