Fix PKCE code verifier generation to never use UTF-8 characters
- This could result in intermittent/random failures of PKCE enabled IdP's
This commit is contained in:
		
							parent
							
								
									fd2807c091
								
							
						
					
					
						commit
						f4f5b7756c
					
				|  | @ -11,6 +11,7 @@ | ||||||
| - [#1873](https://github.com/oauth2-proxy/oauth2-proxy/pull/1873) Fix empty users with some OIDC providers (@babs) | - [#1873](https://github.com/oauth2-proxy/oauth2-proxy/pull/1873) Fix empty users with some OIDC providers (@babs) | ||||||
| - [#1882](https://github.com/oauth2-proxy/oauth2-proxy/pull/1882) Make `htpasswd.GetUsers` racecondition safe | - [#1882](https://github.com/oauth2-proxy/oauth2-proxy/pull/1882) Make `htpasswd.GetUsers` racecondition safe | ||||||
| - [#1883](https://github.com/oauth2-proxy/oauth2-proxy/pull/1883) Ensure v8 manifest variant is set on docker images | - [#1883](https://github.com/oauth2-proxy/oauth2-proxy/pull/1883) Ensure v8 manifest variant is set on docker images | ||||||
|  | - [#1906](https://github.com/oauth2-proxy/oauth2-proxy/pull/1906) Fix PKCE code verifier generation to never use UTF-8 characters | ||||||
| 
 | 
 | ||||||
| # V7.4.0 | # V7.4.0 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"encoding/base64" |  | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | @ -737,16 +736,18 @@ func (p *OAuthProxy) doOAuthStart(rw http.ResponseWriter, req *http.Request, ove | ||||||
| 	extraParams := p.provider.Data().LoginURLParams(overrides) | 	extraParams := p.provider.Data().LoginURLParams(overrides) | ||||||
| 	prepareNoCache(rw) | 	prepareNoCache(rw) | ||||||
| 
 | 
 | ||||||
| 	var codeChallenge, codeVerifier, codeChallengeMethod string | 	var ( | ||||||
|  | 		err                                              error | ||||||
|  | 		codeChallenge, codeVerifier, codeChallengeMethod string | ||||||
|  | 	) | ||||||
| 	if p.provider.Data().CodeChallengeMethod != "" { | 	if p.provider.Data().CodeChallengeMethod != "" { | ||||||
| 		codeChallengeMethod = p.provider.Data().CodeChallengeMethod | 		codeChallengeMethod = p.provider.Data().CodeChallengeMethod | ||||||
| 		preEncodedCodeVerifier, err := encryption.Nonce(96) | 		codeVerifier, err = encryption.GenerateRandomASCIIString(96) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			logger.Errorf("Unable to build random string: %v", err) | 			logger.Errorf("Unable to build random ASCII string for code verifier: %v", err) | ||||||
| 			p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) | 			p.ErrorPage(rw, req, http.StatusInternalServerError, err.Error()) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
| 		codeVerifier = base64.RawURLEncoding.EncodeToString(preEncodedCodeVerifier) |  | ||||||
| 
 | 
 | ||||||
| 		codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier) | 		codeChallenge, err = encryption.GenerateCodeChallenge(p.provider.Data().CodeChallengeMethod, codeVerifier) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  |  | ||||||
|  | @ -2,10 +2,12 @@ package encryption | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"crypto/hmac" | 	"crypto/hmac" | ||||||
|  | 	"crypto/rand" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"hash" | 	"hash" | ||||||
|  | 	"math/big" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | @ -15,6 +17,7 @@ import ( | ||||||
| const ( | const ( | ||||||
| 	CodeChallengeMethodPlain = "plain" | 	CodeChallengeMethodPlain = "plain" | ||||||
| 	CodeChallengeMethodS256  = "S256" | 	CodeChallengeMethodS256  = "S256" | ||||||
|  | 	asciiCharset             = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
 | // SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary
 | ||||||
|  | @ -80,6 +83,19 @@ func SignedValue(seed string, key string, value []byte, now time.Time) (string, | ||||||
| 	return cookieVal, nil | 	return cookieVal, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func GenerateRandomASCIIString(length int) (string, error) { | ||||||
|  | 	b := make([]byte, length) | ||||||
|  | 	charsetLen := new(big.Int).SetInt64(int64(len(asciiCharset))) | ||||||
|  | 	for i := range b { | ||||||
|  | 		character, err := rand.Int(rand.Reader, charsetLen) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 		b[i] = asciiCharset[character.Int64()] | ||||||
|  | 	} | ||||||
|  | 	return string(b), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func GenerateCodeChallenge(method, codeVerifier string) (string, error) { | func GenerateCodeChallenge(method, codeVerifier string) (string, error) { | ||||||
| 	switch method { | 	switch method { | ||||||
| 	case CodeChallengeMethodPlain: | 	case CodeChallengeMethodPlain: | ||||||
|  |  | ||||||
|  | @ -7,7 +7,9 @@ import ( | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"unicode" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| ) | ) | ||||||
|  | @ -100,3 +102,20 @@ func TestSignAndValidate(t *testing.T) { | ||||||
| 	assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) | 	assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) | ||||||
| 	assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) | 	assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestGenerateRandomASCIIString(t *testing.T) { | ||||||
|  | 	randomString, err := GenerateRandomASCIIString(96) | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 
 | ||||||
|  | 	// Only 8-bit characters
 | ||||||
|  | 	assert.Equal(t, 96, len([]byte(randomString))) | ||||||
|  | 
 | ||||||
|  | 	// All non-ascii characters removed should still be the original string
 | ||||||
|  | 	removedChars := strings.Map(func(r rune) rune { | ||||||
|  | 		if r > unicode.MaxASCII { | ||||||
|  | 			return -1 | ||||||
|  | 		} | ||||||
|  | 		return r | ||||||
|  | 	}, randomString) | ||||||
|  | 	assert.Equal(t, removedChars, randomString) | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue