From f9025a8f8f92e61148fa01c66fae82b28e5e23c6 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 4 May 2020 11:21:25 -0700 Subject: [PATCH 01/15] Add binary native AES CFB encryption helpers. These will take in []byte and not automatically Base64 encode/decode. --- pkg/encryption/cipher.go | 52 +++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index 4eb42b03..dfdf036b 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -125,15 +125,12 @@ func NewCipher(secret []byte) (*Cipher, error) { // Encrypt a value for use in a cookie func (c *Cipher) Encrypt(value string) (string, error) { - ciphertext := make([]byte, aes.BlockSize+len(value)) - iv := ciphertext[:aes.BlockSize] - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return "", fmt.Errorf("failed to create initialization vector %s", err) + encrypted, err := c.EncryptCFB([]byte(value)) + if err != nil { + return "", err } - stream := cipher.NewCFBEncrypter(c.Block, iv) - stream.XORKeyStream(ciphertext[aes.BlockSize:], []byte(value)) - return base64.StdEncoding.EncodeToString(ciphertext), nil + return base64.StdEncoding.EncodeToString(encrypted), nil } // Decrypt a value from a cookie to it's original string @@ -143,18 +140,41 @@ func (c *Cipher) Decrypt(s string) (string, error) { return "", fmt.Errorf("failed to decrypt cookie value %s", err) } - if len(encrypted) < aes.BlockSize { - return "", fmt.Errorf("encrypted cookie value should be "+ - "at least %d bytes, but is only %d bytes", - aes.BlockSize, len(encrypted)) + decrypted, err := c.DecryptCFB(encrypted) + if err != nil { + return "", err } - iv := encrypted[:aes.BlockSize] - encrypted = encrypted[aes.BlockSize:] - stream := cipher.NewCFBDecrypter(c.Block, iv) - stream.XORKeyStream(encrypted, encrypted) + return string(decrypted), nil +} - return string(encrypted), nil +// Encrypt with AES CFB on raw bytes +func (c *Cipher) EncryptCFB(value []byte) ([]byte, error) { + ciphertext := make([]byte, aes.BlockSize+len(value)) + iv := ciphertext[:aes.BlockSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, fmt.Errorf("failed to create initialization vector %s", err) + } + + stream := cipher.NewCFBEncrypter(c.Block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], value) + return ciphertext, nil +} + +// Decrypt a AES CFB ciphertext +func (c *Cipher) DecryptCFB(ciphertext []byte) ([]byte, error) { + if len(ciphertext) < aes.BlockSize { + return nil, fmt.Errorf("encrypted value should be "+ + "at least %d bytes, but is only %d bytes", + aes.BlockSize, len(ciphertext)) + } + + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + stream := cipher.NewCFBDecrypter(c.Block, iv) + stream.XORKeyStream(ciphertext, ciphertext) + + return ciphertext, nil } // EncryptInto encrypts the value and stores it back in the string pointer From b4530b92927d23c3df986e4b25b6e2046a047f61 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 4 May 2020 11:34:01 -0700 Subject: [PATCH 02/15] Allow binary values in signed cookies Make signedValue & Validate operate on []byte by default and not assume/cast string. Any casting will be done from callers. --- pkg/encryption/cipher.go | 8 ++++---- pkg/sessions/cookie/session_store.go | 4 ++-- pkg/sessions/redis/redis_store.go | 8 ++++---- pkg/sessions/session_store_test.go | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index dfdf036b..d23dd206 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -39,7 +39,7 @@ func SecretBytes(secret string) []byte { // additionally, the 'value' is encrypted so it's opaque to the browser // Validate ensures a cookie is properly signed -func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value string, t time.Time, ok bool) { +func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value []byte, t time.Time, ok bool) { // value, timestamp, sig parts := strings.Split(cookie.Value, "|") if len(parts) != 3 { @@ -59,7 +59,7 @@ func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value // it's a valid cookie. now get the contents rawValue, err := base64.URLEncoding.DecodeString(parts[0]) if err == nil { - value = string(rawValue) + value = rawValue ok = true return } @@ -69,8 +69,8 @@ func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value } // SignedValue returns a cookie that is signed and can later be checked with Validate -func SignedValue(seed string, key string, value string, now time.Time) string { - encodedValue := base64.URLEncoding.EncodeToString([]byte(value)) +func SignedValue(seed string, key string, value []byte, now time.Time) string { + encodedValue := base64.URLEncoding.EncodeToString(value) timeStr := fmt.Sprintf("%d", now.Unix()) sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr) cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index 1b88e027..65fd237c 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -59,7 +59,7 @@ func (s *SessionStore) Load(req *http.Request) (*sessions.SessionState, error) { return nil, errors.New("cookie signature not valid") } - session, err := sessionFromCookie(val, s.CookieCipher) + session, err := sessionFromCookie(string(val), s.CookieCipher) if err != nil { return nil, err } @@ -104,7 +104,7 @@ func (s *SessionStore) setSessionCookie(rw http.ResponseWriter, req *http.Reques // authentication details func (s *SessionStore) makeSessionCookie(req *http.Request, value string, now time.Time) []*http.Cookie { if value != "" { - value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, value, now) + value = encryption.SignedValue(s.CookieOptions.Secret, s.CookieOptions.Name, []byte(value), now) } c := s.makeCookie(req, s.CookieOptions.Name, value, s.CookieOptions.Expire, now) if len(c.Value) > 4096-len(s.CookieOptions.Name) { diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index 472ac3c9..51f31d51 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -175,7 +175,7 @@ func (store *SessionStore) Load(req *http.Request) (*sessions.SessionState, erro return nil, fmt.Errorf("cookie signature not valid") } ctx := req.Context() - session, err := store.loadSessionFromString(ctx, val) + session, err := store.loadSessionFromString(ctx, string(val)) if err != nil { return nil, fmt.Errorf("error loading session: %s", err) } @@ -237,7 +237,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro // We only return an error if we had an issue with redis // If there's an issue decoding the ticket, ignore it - ticket, _ := decodeTicket(store.CookieOptions.Name, val) + ticket, _ := decodeTicket(store.CookieOptions.Name, string(val)) if ticket != nil { ctx := req.Context() err := store.Client.Del(ctx, ticket.asHandle(store.CookieOptions.Name)) @@ -251,7 +251,7 @@ func (store *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) erro // makeCookie makes a cookie, signing the value if present func (store *SessionStore) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) *http.Cookie { if value != "" { - value = encryption.SignedValue(store.CookieOptions.Secret, store.CookieOptions.Name, value, now) + value = encryption.SignedValue(store.CookieOptions.Secret, store.CookieOptions.Name, []byte(value), now) } return cookies.MakeCookieFromOptions( req, @@ -302,7 +302,7 @@ func (store *SessionStore) getTicket(requestCookie *http.Cookie) (*TicketData, e } // Valid cookie, decode the ticket - ticket, err := decodeTicket(store.CookieOptions.Name, val) + ticket, err := decodeTicket(store.CookieOptions.Name, string(val)) if err != nil { // If we can't decode the ticket we have to create a new one return newTicket() diff --git a/pkg/sessions/session_store_test.go b/pkg/sessions/session_store_test.go index 60a86cef..f1919eb0 100644 --- a/pkg/sessions/session_store_test.go +++ b/pkg/sessions/session_store_test.go @@ -170,7 +170,7 @@ var _ = Describe("NewSessionStore", func() { BeforeEach(func() { By("Using a valid cookie with a different providers session encoding") broken := "BrokenSessionFromADifferentSessionImplementation" - value := encryption.SignedValue(cookieOpts.Secret, cookieOpts.Name, broken, time.Now()) + value := encryption.SignedValue(cookieOpts.Secret, cookieOpts.Name, []byte(broken), time.Now()) cookie := cookiesapi.MakeCookieFromOptions(request, cookieOpts.Name, value, cookieOpts, cookieOpts.Expire, time.Now()) request.AddCookie(cookie) From f7cca1d0b3a2de56ec2121eff3ffaf8cf76bc176 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sat, 9 May 2020 17:01:51 -0700 Subject: [PATCH 03/15] Refactor encryption.Cipher to be an Encrypt/Decrypt Interface All Encrypt/Decrypt Cipher implementations will now take and return []byte to set up usage in future binary compatible encoding schemes to fix issues with bloat encrypting to strings (which requires base64ing adding 33% size) --- pkg/apis/options/sessions.go | 2 +- pkg/apis/sessions/session_state.go | 17 +++- pkg/apis/sessions/session_state_test.go | 2 +- pkg/encryption/cipher.go | 109 +++++++++++++++-------- pkg/encryption/cipher_test.go | 113 ++++++++++++++++++++++-- pkg/sessions/cookie/session_store.go | 6 +- pkg/sessions/redis/redis_store.go | 2 +- pkg/validation/options.go | 2 +- 8 files changed, 198 insertions(+), 55 deletions(-) diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index b490baf7..16b419d6 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -5,7 +5,7 @@ import "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" // SessionOptions contains configuration options for the SessionStore providers. type SessionOptions struct { Type string `flag:"session-store-type" cfg:"session_store_type"` - Cipher *encryption.Cipher `cfg:",internal"` + Cipher encryption.Cipher `cfg:",internal"` Redis RedisStoreOptions `cfg:",squash"` } diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index f2e6633e..c014aee2 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -60,7 +60,7 @@ func (s *SessionState) String() string { } // EncodeSessionState returns string representation of the current session -func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) { +func (s *SessionState) EncodeSessionState(c encryption.Cipher) (string, error) { var ss SessionState if c == nil { // Store only Email and User when cipher is unavailable @@ -89,7 +89,7 @@ func (s *SessionState) EncodeSessionState(c *encryption.Cipher) (string, error) } // DecodeSessionState decodes the session cookie string into a SessionState -func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { +func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) { var ss SessionState err := json.Unmarshal([]byte(v), &ss) if err != nil { @@ -106,7 +106,7 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { } else { // Backward compatibility with using unencrypted Email if ss.Email != "" { - decryptedEmail, errEmail := c.Decrypt(ss.Email) + decryptedEmail, errEmail := stringDecrypt(ss.Email, c) if errEmail == nil { if !utf8.ValidString(decryptedEmail) { return nil, errors.New("invalid value for decrypted email") @@ -116,7 +116,7 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { } // Backward compatibility with using unencrypted User if ss.User != "" { - decryptedUser, errUser := c.Decrypt(ss.User) + decryptedUser, errUser := stringDecrypt(ss.User, c) if errUser == nil { if !utf8.ValidString(decryptedUser) { return nil, errors.New("invalid value for decrypted user") @@ -139,3 +139,12 @@ func DecodeSessionState(v string, c *encryption.Cipher) (*SessionState, error) { } return &ss, nil } + +// stringDecrypt wraps a Base64Cipher to make it string => string +func stringDecrypt(ciphertext string, c encryption.Cipher) (string, error) { + value, err := c.Decrypt([]byte(ciphertext)) + if err != nil { + return "", err + } + return string(value), nil +} diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go index 150e9c9d..349bd734 100644 --- a/pkg/apis/sessions/session_state_test.go +++ b/pkg/apis/sessions/session_state_test.go @@ -145,7 +145,7 @@ func TestExpired(t *testing.T) { type testCase struct { sessions.SessionState Encoded string - Cipher *encryption.Cipher + Cipher encryption.Cipher Error bool } diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index d23dd206..e2dbfa1a 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -109,47 +109,79 @@ func checkHmac(input, expected string) bool { return false } -// Cipher provides methods to encrypt and decrypt cookie values -type Cipher struct { - cipher.Block +// Cipher provides methods to encrypt and decrypt +type Cipher interface { + Encrypt(value []byte) ([]byte, error) + Decrypt(ciphertext []byte) ([]byte, error) + EncryptInto(s *string) error + DecryptInto(s *string) error } // NewCipher returns a new aes Cipher for encrypting cookie values -func NewCipher(secret []byte) (*Cipher, error) { +// This defaults to the Base64 Cipher to align with legacy Encrypt/Decrypt functionality +func NewCipher(secret []byte) (*Base64Cipher, error) { + cfb, err := NewCFBCipher(secret) + if err != nil { + return nil, err + } + return NewBase64Cipher(cfb) +} + +type Base64Cipher struct { + Cipher Cipher +} + +// NewBase64Cipher returns a new AES CFB Cipher for encrypting cookie values +// And wrapping them in Base64 -- Supports Legacy encryption scheme +func NewBase64Cipher(c Cipher) (*Base64Cipher, error) { + return &Base64Cipher{Cipher: c}, nil +} + +// Encrypt encrypts a value with AES CFB & base64 encodes it +func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { + encrypted, err := c.Cipher.Encrypt([]byte(value)) + if err != nil { + return nil, err + } + + return []byte(base64.StdEncoding.EncodeToString(encrypted)), nil +} + +// Decrypt Base64 decodes a value & decrypts it with AES CFB +func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { + encrypted, err := base64.StdEncoding.DecodeString(string(ciphertext)) + if err != nil { + return nil, fmt.Errorf("failed to decrypt cookie value %s", err) + } + + return c.Cipher.Decrypt(encrypted) +} + +// EncryptInto encrypts the value and stores it back in the string pointer +func (c *Base64Cipher) EncryptInto(s *string) error { + return into(c.Encrypt, s) +} + +// DecryptInto decrypts the value and stores it back in the string pointer +func (c *Base64Cipher) DecryptInto(s *string) error { + return into(c.Decrypt, s) +} + +type CFBCipher struct { + cipher.Block +} + +// NewCFBCipher returns a new AES CFB Cipher +func NewCFBCipher(secret []byte) (*CFBCipher, error) { c, err := aes.NewCipher(secret) if err != nil { return nil, err } - return &Cipher{Block: c}, err + return &CFBCipher{Block: c}, err } -// Encrypt a value for use in a cookie -func (c *Cipher) Encrypt(value string) (string, error) { - encrypted, err := c.EncryptCFB([]byte(value)) - if err != nil { - return "", err - } - - return base64.StdEncoding.EncodeToString(encrypted), nil -} - -// Decrypt a value from a cookie to it's original string -func (c *Cipher) Decrypt(s string) (string, error) { - encrypted, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return "", fmt.Errorf("failed to decrypt cookie value %s", err) - } - - decrypted, err := c.DecryptCFB(encrypted) - if err != nil { - return "", err - } - - return string(decrypted), nil -} - -// Encrypt with AES CFB on raw bytes -func (c *Cipher) EncryptCFB(value []byte) ([]byte, error) { +// Encrypt with AES CFB +func (c *CFBCipher) Encrypt(value []byte) ([]byte, error) { ciphertext := make([]byte, aes.BlockSize+len(value)) iv := ciphertext[:aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { @@ -162,7 +194,7 @@ func (c *Cipher) EncryptCFB(value []byte) ([]byte, error) { } // Decrypt a AES CFB ciphertext -func (c *Cipher) DecryptCFB(ciphertext []byte) ([]byte, error) { +func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { if len(ciphertext) < aes.BlockSize { return nil, fmt.Errorf("encrypted value should be "+ "at least %d bytes, but is only %d bytes", @@ -178,17 +210,18 @@ func (c *Cipher) DecryptCFB(ciphertext []byte) ([]byte, error) { } // EncryptInto encrypts the value and stores it back in the string pointer -func (c *Cipher) EncryptInto(s *string) error { +func (c *CFBCipher) EncryptInto(s *string) error { return into(c.Encrypt, s) } // DecryptInto decrypts the value and stores it back in the string pointer -func (c *Cipher) DecryptInto(s *string) error { +func (c *CFBCipher) DecryptInto(s *string) error { return into(c.Decrypt, s) } // codecFunc is a function that takes a string and encodes/decodes it -type codecFunc func(string) (string, error) +type codecFunc func([]byte) ([]byte, error) + func into(f codecFunc, s *string) error { // Do not encrypt/decrypt nil or empty strings @@ -196,10 +229,10 @@ func into(f codecFunc, s *string) error { return nil } - d, err := f(*s) + d, err := f([]byte(*s)) if err != nil { return err } - *s = d + *s = string(d) return nil } diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index aed529f3..8abfcb2e 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -105,14 +105,14 @@ func TestEncodeAndDecodeAccessToken(t *testing.T) { c, err := NewCipher([]byte(secret)) assert.Equal(t, nil, err) - encoded, err := c.Encrypt(token) + encoded, err := c.Encrypt([]byte(token)) assert.Equal(t, nil, err) decoded, err := c.Decrypt(encoded) assert.Equal(t, nil, err) - assert.NotEqual(t, token, encoded) - assert.Equal(t, token, decoded) + assert.NotEqual(t, []byte(token), encoded) + assert.Equal(t, []byte(token), decoded) } func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { @@ -124,14 +124,115 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { c, err := NewCipher([]byte(secret)) assert.Equal(t, nil, err) - encoded, err := c.Encrypt(token) + encoded, err := c.Encrypt([]byte(token)) assert.Equal(t, nil, err) decoded, err := c.Decrypt(encoded) assert.Equal(t, nil, err) - assert.NotEqual(t, token, encoded) - assert.Equal(t, token, decoded) + assert.NotEqual(t, []byte(token), encoded) + assert.Equal(t, []byte(token), decoded) +} + +func TestEncryptAndDecryptBase64(t *testing.T) { + var err error + + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + secret := make([]byte, secretSize) + _, err = io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + // NewCipher creates a Base64 wrapper of CFBCipher + c, err := NewCipher(secret) + assert.Equal(t, nil, err) + + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + } + } +} + +func TestDecryptBase64WrongSecret(t *testing.T) { + var err error + + secret1 := []byte("0123456789abcdefghijklmnopqrstuv") + secret2 := []byte("9876543210abcdefghijklmnopqrstuv") + + c1, err := NewCipher(secret1) + assert.Equal(t, nil, err) + + c2, err := NewCipher(secret2) + assert.Equal(t, nil, err) + + data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") + + ciphertext, err := c1.Encrypt(data) + assert.Equal(t, nil, err) + + wrongData, err := c2.Decrypt(ciphertext) + assert.Equal(t, nil, err) + assert.NotEqual(t, data, wrongData) +} + +func TestEncryptAndDecryptCFB(t *testing.T) { + var err error + + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + secret := make([]byte, secretSize) + _, err = io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + c, err := NewCFBCipher(secret) + assert.Equal(t, nil, err) + + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + } + } +} + +func TestDecryptCFBWrongSecret(t *testing.T) { + var err error + + secret1 := []byte("0123456789abcdefghijklmnopqrstuv") + secret2 := []byte("9876543210abcdefghijklmnopqrstuv") + + c1, err := NewCFBCipher(secret1) + assert.Equal(t, nil, err) + + c2, err := NewCFBCipher(secret2) + assert.Equal(t, nil, err) + + data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") + + ciphertext, err := c1.Encrypt(data) + assert.Equal(t, nil, err) + + wrongData, err := c2.Decrypt(ciphertext) + assert.Equal(t, nil, err) + assert.NotEqual(t, data, wrongData) } func TestEncodeIntoAndDecodeIntoAccessToken(t *testing.T) { diff --git a/pkg/sessions/cookie/session_store.go b/pkg/sessions/cookie/session_store.go index 65fd237c..62f6b348 100644 --- a/pkg/sessions/cookie/session_store.go +++ b/pkg/sessions/cookie/session_store.go @@ -28,7 +28,7 @@ var _ sessions.SessionStore = &SessionStore{} // interface that stores sessions in client side cookies type SessionStore struct { CookieOptions *options.CookieOptions - CookieCipher *encryption.Cipher + CookieCipher encryption.Cipher } // Save takes a sessions.SessionState and stores the information from it @@ -84,12 +84,12 @@ func (s *SessionStore) Clear(rw http.ResponseWriter, req *http.Request) error { } // cookieForSession serializes a session state for storage in a cookie -func cookieForSession(s *sessions.SessionState, c *encryption.Cipher) (string, error) { +func cookieForSession(s *sessions.SessionState, c encryption.Cipher) (string, error) { return s.EncodeSessionState(c) } // sessionFromCookie deserializes a session from a cookie value -func sessionFromCookie(v string, c *encryption.Cipher) (s *sessions.SessionState, err error) { +func sessionFromCookie(v string, c encryption.Cipher) (s *sessions.SessionState, err error) { return sessions.DecodeSessionState(v, c) } diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index 51f31d51..2d0fd9b0 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -32,7 +32,7 @@ type TicketData struct { // SessionStore is an implementation of the sessions.SessionStore // interface that stores sessions in redis type SessionStore struct { - CookieCipher *encryption.Cipher + CookieCipher encryption.Cipher CookieOptions *options.CookieOptions Client Client } diff --git a/pkg/validation/options.go b/pkg/validation/options.go index 2e028677..ccb96c79 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -38,7 +38,7 @@ func Validate(o *options.Options) error { msgs := make([]string, 0) - var cipher *encryption.Cipher + var cipher encryption.Cipher if o.Cookie.Secret == "" { msgs = append(msgs, "missing setting: cookie-secret") } else { From b6931aa4ea5d1edad0f181e5e3af03f38bb2b476 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sat, 9 May 2020 17:34:32 -0700 Subject: [PATCH 04/15] Add GCM Cipher support During the upcoming encoded session refactor, AES GCM is ideal to use as the Redis (and other DB like stores) encryption wrapper around the session because each session is encrypted with a distinct secret that is passed by the session ticket. --- pkg/encryption/cipher.go | 90 ++++++++++++++----- pkg/encryption/cipher_test.go | 157 +++++++++++++++++++++------------- 2 files changed, 165 insertions(+), 82 deletions(-) diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index e2dbfa1a..8eddc7d0 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -117,9 +117,27 @@ type Cipher interface { DecryptInto(s *string) error } +type DefaultCipher struct {} + +// Encrypt is a dummy method for CommonCipher.EncryptInto support +func (c *DefaultCipher) Encrypt(value []byte) ([]byte, error) { return value, nil } + +// Decrypt is a dummy method for CommonCipher.DecryptInto support +func (c *DefaultCipher) Decrypt(ciphertext []byte) ([]byte, error) { return ciphertext, nil } + +// EncryptInto encrypts the value and stores it back in the string pointer +func (c *DefaultCipher) EncryptInto(s *string) error { + return into(c.Encrypt, s) +} + +// DecryptInto decrypts the value and stores it back in the string pointer +func (c *DefaultCipher) DecryptInto(s *string) error { + return into(c.Decrypt, s) +} + // NewCipher returns a new aes Cipher for encrypting cookie values // This defaults to the Base64 Cipher to align with legacy Encrypt/Decrypt functionality -func NewCipher(secret []byte) (*Base64Cipher, error) { +func NewCipher(secret []byte) (Cipher, error) { cfb, err := NewCFBCipher(secret) if err != nil { return nil, err @@ -128,12 +146,13 @@ func NewCipher(secret []byte) (*Base64Cipher, error) { } type Base64Cipher struct { + DefaultCipher Cipher Cipher } -// NewBase64Cipher returns a new AES CFB Cipher for encrypting cookie values -// And wrapping them in Base64 -- Supports Legacy encryption scheme -func NewBase64Cipher(c Cipher) (*Base64Cipher, error) { +// NewBase64Cipher returns a new AES Cipher for encrypting cookie values +// and wrapping them in Base64 -- Supports Legacy encryption scheme +func NewBase64Cipher(c Cipher) (Cipher, error) { return &Base64Cipher{Cipher: c}, nil } @@ -157,22 +176,13 @@ func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { return c.Cipher.Decrypt(encrypted) } -// EncryptInto encrypts the value and stores it back in the string pointer -func (c *Base64Cipher) EncryptInto(s *string) error { - return into(c.Encrypt, s) -} - -// DecryptInto decrypts the value and stores it back in the string pointer -func (c *Base64Cipher) DecryptInto(s *string) error { - return into(c.Decrypt, s) -} - type CFBCipher struct { + DefaultCipher cipher.Block } // NewCFBCipher returns a new AES CFB Cipher -func NewCFBCipher(secret []byte) (*CFBCipher, error) { +func NewCFBCipher(secret []byte) (Cipher, error) { c, err := aes.NewCipher(secret) if err != nil { return nil, err @@ -193,7 +203,7 @@ func (c *CFBCipher) Encrypt(value []byte) ([]byte, error) { return ciphertext, nil } -// Decrypt a AES CFB ciphertext +// Decrypt an AES CFB ciphertext func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { if len(ciphertext) < aes.BlockSize { return nil, fmt.Errorf("encrypted value should be "+ @@ -209,20 +219,54 @@ func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { return ciphertext, nil } -// EncryptInto encrypts the value and stores it back in the string pointer -func (c *CFBCipher) EncryptInto(s *string) error { - return into(c.Encrypt, s) +type GCMCipher struct { + DefaultCipher + cipher.Block } -// DecryptInto decrypts the value and stores it back in the string pointer -func (c *CFBCipher) DecryptInto(s *string) error { - return into(c.Decrypt, s) +// NewGCMCipher returns a new AES GCM Cipher +func NewGCMCipher(secret []byte) (Cipher, error) { + c, err := aes.NewCipher(secret) + if err != nil { + return nil, err + } + return &GCMCipher{Block: c}, err +} + +// Encrypt with AES GCM on raw bytes +func (c *GCMCipher) Encrypt(value []byte) ([]byte, error) { + gcm, err := cipher.NewGCM(c.Block) + if err != nil { + return nil, err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + ciphertext := gcm.Seal(nonce, nonce, value, nil) + return ciphertext, nil +} + +// Decrypt an AES GCM ciphertext +func (c *GCMCipher) Decrypt(ciphertext []byte) ([]byte, error) { + gcm, err := cipher.NewGCM(c.Block) + if err != nil { + return nil, err + } + + nonceSize := gcm.NonceSize() + nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + return plaintext, nil } // codecFunc is a function that takes a string and encodes/decodes it type codecFunc func([]byte) ([]byte, error) - func into(f codecFunc, s *string) error { // Do not encrypt/decrypt nil or empty strings if s == nil || *s == "" { diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index 8abfcb2e..85ec5ce2 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -134,17 +134,96 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { assert.Equal(t, []byte(token), decoded) } -func TestEncryptAndDecryptBase64(t *testing.T) { +func TestEncryptAndDecrypt(t *testing.T) { var err error + // Test our 3 cipher types + for _, initCipher := range []func([]byte) (Cipher, error){NewCipher, NewCFBCipher, NewGCMCipher} { + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + secret := make([]byte, secretSize) + _, err = io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + c, err := initCipher(secret) + assert.Equal(t, nil, err) + + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + } + } + } +} + +func TestDecryptWrongSecret(t *testing.T) { + secret1 := []byte("0123456789abcdefghijklmnopqrstuv") + secret2 := []byte("9876543210abcdefghijklmnopqrstuv") + + // Test CFB & Base64 (GCM is authenticated, it errors differently) + for _, initCipher := range []func([]byte) (Cipher, error){NewCipher, NewCFBCipher} { + c1, err := initCipher(secret1) + assert.Equal(t, nil, err) + + c2, err := initCipher(secret2) + assert.Equal(t, nil, err) + + data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") + + ciphertext, err := c1.Encrypt(data) + assert.Equal(t, nil, err) + + wrongData, err := c2.Decrypt(ciphertext) + assert.Equal(t, nil, err) + assert.NotEqual(t, data, wrongData) + } +} + +func TestDecryptGCMWrongSecret(t *testing.T) { + secret1 := []byte("0123456789abcdefghijklmnopqrstuv") + secret2 := []byte("9876543210abcdefghijklmnopqrstuv") + + c1, err := NewGCMCipher(secret1) + assert.Equal(t, nil, err) + + c2, err := NewGCMCipher(secret2) + assert.Equal(t, nil, err) + + data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") + + ciphertext, err := c1.Encrypt(data) + assert.Equal(t, nil, err) + + // GCM is authenticated - this should lead to message authentication failed + _, err = c2.Decrypt(ciphertext) + assert.Error(t, err) +} + +func TestIntermixCiphersErrors(t *testing.T) { + var err error + + // Encrypt with GCM, Decrypt with CFB: Results in Garbage data // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { secret := make([]byte, secretSize) _, err = io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - // NewCipher creates a Base64 wrapper of CFBCipher - c, err := NewCipher(secret) + gcm, err := NewGCMCipher(secret) + assert.Equal(t, nil, err) + + cfb, err := NewCFBCipher(secret) assert.Equal(t, nil, err) // Test various sizes sessions might be @@ -153,48 +232,29 @@ func TestEncryptAndDecryptBase64(t *testing.T) { _, err := io.ReadFull(rand.Reader, data) assert.Equal(t, nil, err) - encrypted, err := c.Encrypt(data) + encrypted, err := gcm.Encrypt(data) assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) - decrypted, err := c.Decrypt(encrypted) + decrypted, err := cfb.Decrypt(encrypted) assert.Equal(t, nil, err) - assert.Equal(t, data, decrypted) + // Data is mangled + assert.NotEqual(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) } } -} - -func TestDecryptBase64WrongSecret(t *testing.T) { - var err error - - secret1 := []byte("0123456789abcdefghijklmnopqrstuv") - secret2 := []byte("9876543210abcdefghijklmnopqrstuv") - - c1, err := NewCipher(secret1) - assert.Equal(t, nil, err) - - c2, err := NewCipher(secret2) - assert.Equal(t, nil, err) - - data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") - - ciphertext, err := c1.Encrypt(data) - assert.Equal(t, nil, err) - - wrongData, err := c2.Decrypt(ciphertext) - assert.Equal(t, nil, err) - assert.NotEqual(t, data, wrongData) -} - -func TestEncryptAndDecryptCFB(t *testing.T) { - var err error + // Encrypt with CFB, Decrypt with GCM: Results in errors // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { secret := make([]byte, secretSize) _, err = io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - c, err := NewCFBCipher(secret) + gcm, err := NewGCMCipher(secret) + assert.Equal(t, nil, err) + + cfb, err := NewCFBCipher(secret) assert.Equal(t, nil, err) // Test various sizes sessions might be @@ -203,38 +263,17 @@ func TestEncryptAndDecryptCFB(t *testing.T) { _, err := io.ReadFull(rand.Reader, data) assert.Equal(t, nil, err) - encrypted, err := c.Encrypt(data) + encrypted, err := cfb.Encrypt(data) assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) - decrypted, err := c.Decrypt(encrypted) - assert.Equal(t, nil, err) - assert.Equal(t, data, decrypted) + // GCM is authenticated - this should lead to message authentication failed + _, err = gcm.Decrypt(encrypted) + assert.Error(t, err) } } } -func TestDecryptCFBWrongSecret(t *testing.T) { - var err error - - secret1 := []byte("0123456789abcdefghijklmnopqrstuv") - secret2 := []byte("9876543210abcdefghijklmnopqrstuv") - - c1, err := NewCFBCipher(secret1) - assert.Equal(t, nil, err) - - c2, err := NewCFBCipher(secret2) - assert.Equal(t, nil, err) - - data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") - - ciphertext, err := c1.Encrypt(data) - assert.Equal(t, nil, err) - - wrongData, err := c2.Decrypt(ciphertext) - assert.Equal(t, nil, err) - assert.NotEqual(t, data, wrongData) -} - func TestEncodeIntoAndDecodeIntoAccessToken(t *testing.T) { const secret = "0123456789abcdefghijklmnopqrstuv" c, err := NewCipher([]byte(secret)) From ce2e92bc578452a45bedf8c6413a2d8f43367469 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sun, 10 May 2020 09:44:04 -0700 Subject: [PATCH 05/15] Improve design of Base64Cipher wrapping other ciphers. Have it take in a cipher init function as an argument. Remove the confusing `newCipher` method that matched legacy behavior and returns a Base64Cipher(CFBCipher) -- instead explicitly ask for that in the uses. --- pkg/apis/sessions/session_state_test.go | 14 +++-- pkg/encryption/cipher.go | 22 +++----- pkg/encryption/cipher_test.go | 74 ++++++++++++++++--------- pkg/sessions/session_store_test.go | 2 +- pkg/validation/options.go | 2 +- 5 files changed, 67 insertions(+), 47 deletions(-) diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go index 349bd734..c6ca0c07 100644 --- a/pkg/apis/sessions/session_state_test.go +++ b/pkg/apis/sessions/session_state_test.go @@ -17,10 +17,14 @@ func timePtr(t time.Time) *time.Time { return &t } +func NewCipher(secret []byte) (encryption.Cipher, error) { + return encryption.NewBase64Cipher(encryption.NewCFBCipher, secret) +} + func TestSessionStateSerialization(t *testing.T) { - c, err := encryption.NewCipher([]byte(secret)) + c, err := NewCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := encryption.NewCipher([]byte(altSecret)) + c2, err := NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ Email: "user@domain.com", @@ -53,9 +57,9 @@ func TestSessionStateSerialization(t *testing.T) { } func TestSessionStateSerializationWithUser(t *testing.T) { - c, err := encryption.NewCipher([]byte(secret)) + c, err := NewCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := encryption.NewCipher([]byte(altSecret)) + c2, err := NewCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ User: "just-user", @@ -201,7 +205,7 @@ func TestDecodeSessionState(t *testing.T) { eJSON, _ := e.MarshalJSON() eString := string(eJSON) - c, err := encryption.NewCipher([]byte(secret)) + c, err := NewCipher([]byte(secret)) assert.NoError(t, err) testCases := []testCase{ diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index 8eddc7d0..63986312 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -135,16 +135,6 @@ func (c *DefaultCipher) DecryptInto(s *string) error { return into(c.Decrypt, s) } -// NewCipher returns a new aes Cipher for encrypting cookie values -// This defaults to the Base64 Cipher to align with legacy Encrypt/Decrypt functionality -func NewCipher(secret []byte) (Cipher, error) { - cfb, err := NewCFBCipher(secret) - if err != nil { - return nil, err - } - return NewBase64Cipher(cfb) -} - type Base64Cipher struct { DefaultCipher Cipher Cipher @@ -152,7 +142,11 @@ type Base64Cipher struct { // NewBase64Cipher returns a new AES Cipher for encrypting cookie values // and wrapping them in Base64 -- Supports Legacy encryption scheme -func NewBase64Cipher(c Cipher) (Cipher, error) { +func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Cipher, error) { + c, err := initCipher(secret) + if err != nil { + return nil, err + } return &Base64Cipher{Cipher: c}, nil } @@ -170,7 +164,7 @@ func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { encrypted, err := base64.StdEncoding.DecodeString(string(ciphertext)) if err != nil { - return nil, fmt.Errorf("failed to decrypt cookie value %s", err) + return nil, fmt.Errorf("failed to base64 decode value %s", err) } return c.Cipher.Decrypt(encrypted) @@ -206,9 +200,7 @@ func (c *CFBCipher) Encrypt(value []byte) ([]byte, error) { // Decrypt an AES CFB ciphertext func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { if len(ciphertext) < aes.BlockSize { - return nil, fmt.Errorf("encrypted value should be "+ - "at least %d bytes, but is only %d bytes", - aes.BlockSize, len(ciphertext)) + return nil, fmt.Errorf("encrypted value should be at least %d bytes, but is only %d bytes", aes.BlockSize, len(ciphertext)) } iv := ciphertext[:aes.BlockSize] diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index 85ec5ce2..074cef44 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -102,7 +102,7 @@ func TestSignAndValidate(t *testing.T) { func TestEncodeAndDecodeAccessToken(t *testing.T) { const secret = "0123456789abcdefghijklmnopqrstuv" const token = "my access token" - c, err := NewCipher([]byte(secret)) + c, err := NewBase64Cipher(NewCFBCipher, []byte(secret)) assert.Equal(t, nil, err) encoded, err := c.Encrypt([]byte(token)) @@ -121,7 +121,7 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { secret, err := base64.URLEncoding.DecodeString(secretBase64) assert.Equal(t, nil, err) - c, err := NewCipher([]byte(secret)) + c, err := NewBase64Cipher(NewCFBCipher, []byte(secret)) assert.Equal(t, nil, err) encoded, err := c.Encrypt([]byte(token)) @@ -135,14 +135,12 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { } func TestEncryptAndDecrypt(t *testing.T) { - var err error - - // Test our 3 cipher types - for _, initCipher := range []func([]byte) (Cipher, error){NewCipher, NewCFBCipher, NewGCMCipher} { + // Test our 2 cipher types + for _, initCipher := range []func([]byte) (Cipher, error){NewCFBCipher, NewGCMCipher} { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { secret := make([]byte, secretSize) - _, err = io.ReadFull(rand.Reader, secret) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) c, err := initCipher(secret) @@ -167,27 +165,55 @@ func TestEncryptAndDecrypt(t *testing.T) { } } -func TestDecryptWrongSecret(t *testing.T) { +func TestEncryptAndDecryptBase64(t *testing.T) { + // Test our cipher types wrapped in Base64 encoder + for _, initCipher := range []func([]byte) (Cipher, error){NewCFBCipher, NewGCMCipher} { + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + c, err := NewBase64Cipher(initCipher, secret) + assert.Equal(t, nil, err) + + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + } + } + } +} + +func TestDecryptCFBWrongSecret(t *testing.T) { secret1 := []byte("0123456789abcdefghijklmnopqrstuv") secret2 := []byte("9876543210abcdefghijklmnopqrstuv") - // Test CFB & Base64 (GCM is authenticated, it errors differently) - for _, initCipher := range []func([]byte) (Cipher, error){NewCipher, NewCFBCipher} { - c1, err := initCipher(secret1) - assert.Equal(t, nil, err) + c1, err := NewCFBCipher(secret1) + assert.Equal(t, nil, err) - c2, err := initCipher(secret2) - assert.Equal(t, nil, err) + c2, err := NewCFBCipher(secret2) + assert.Equal(t, nil, err) - data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") + data := []byte("f3928pufm982374dj02y485dsl34890u2t9nd4028s94dm58y2394087dhmsyt29h8df") - ciphertext, err := c1.Encrypt(data) - assert.Equal(t, nil, err) + ciphertext, err := c1.Encrypt(data) + assert.Equal(t, nil, err) - wrongData, err := c2.Decrypt(ciphertext) - assert.Equal(t, nil, err) - assert.NotEqual(t, data, wrongData) - } + wrongData, err := c2.Decrypt(ciphertext) + assert.Equal(t, nil, err) + assert.NotEqual(t, data, wrongData) } func TestDecryptGCMWrongSecret(t *testing.T) { @@ -211,13 +237,11 @@ func TestDecryptGCMWrongSecret(t *testing.T) { } func TestIntermixCiphersErrors(t *testing.T) { - var err error - // Encrypt with GCM, Decrypt with CFB: Results in Garbage data // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { secret := make([]byte, secretSize) - _, err = io.ReadFull(rand.Reader, secret) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) gcm, err := NewGCMCipher(secret) @@ -248,7 +272,7 @@ func TestIntermixCiphersErrors(t *testing.T) { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { secret := make([]byte, secretSize) - _, err = io.ReadFull(rand.Reader, secret) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) gcm, err := NewGCMCipher(secret) diff --git a/pkg/sessions/session_store_test.go b/pkg/sessions/session_store_test.go index f1919eb0..1a60aa0d 100644 --- a/pkg/sessions/session_store_test.go +++ b/pkg/sessions/session_store_test.go @@ -367,7 +367,7 @@ var _ = Describe("NewSessionStore", func() { _, err := rand.Read(secret) Expect(err).ToNot(HaveOccurred()) cookieOpts.Secret = base64.URLEncoding.EncodeToString(secret) - cipher, err := encryption.NewCipher(encryption.SecretBytes(cookieOpts.Secret)) + cipher, err := encryption.NewBase64Cipher(encryption.NewCFBCipher, encryption.SecretBytes(cookieOpts.Secret)) Expect(err).ToNot(HaveOccurred()) Expect(cipher).ToNot(BeNil()) opts.Cipher = cipher diff --git a/pkg/validation/options.go b/pkg/validation/options.go index ccb96c79..b22882d0 100644 --- a/pkg/validation/options.go +++ b/pkg/validation/options.go @@ -62,7 +62,7 @@ func Validate(o *options.Options) error { len(encryption.SecretBytes(o.Cookie.Secret)), suffix)) } else { var err error - cipher, err = encryption.NewCipher(encryption.SecretBytes(o.Cookie.Secret)) + cipher, err = encryption.NewBase64Cipher(encryption.NewCFBCipher, encryption.SecretBytes(o.Cookie.Secret)) if err != nil { msgs = append(msgs, fmt.Sprintf("cookie-secret error: %v", err)) } From f60e24d9c3582de0eccafd008541ae9a8c386aac Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sun, 10 May 2020 09:48:35 -0700 Subject: [PATCH 06/15] Split non-cipher code to utils.go out of ciphers.go --- pkg/encryption/cipher.go | 100 ------------------------------- pkg/encryption/cipher_test.go | 90 ---------------------------- pkg/encryption/utils.go | 107 ++++++++++++++++++++++++++++++++++ pkg/encryption/utils_test.go | 100 +++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 190 deletions(-) create mode 100644 pkg/encryption/utils.go create mode 100644 pkg/encryption/utils_test.go diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index 63986312..896031df 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -3,112 +3,12 @@ package encryption import ( "crypto/aes" "crypto/cipher" - "crypto/hmac" "crypto/rand" - "crypto/sha1" - "crypto/sha256" "encoding/base64" "fmt" - "hash" "io" - "net/http" - "strconv" - "strings" - "time" ) -// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary -func SecretBytes(secret string) []byte { - b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "=")) - if err == nil { - // Only return decoded form if a valid AES length - // Don't want unintentional decoding resulting in invalid lengths confusing a user - // that thought they used a 16, 24, 32 length string - for _, i := range []int{16, 24, 32} { - if len(b) == i { - return b - } - } - } - // If decoding didn't work or resulted in non-AES compliant length, - // assume the raw string was the intended secret - return []byte(secret) -} - -// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set. -// additionally, the 'value' is encrypted so it's opaque to the browser - -// Validate ensures a cookie is properly signed -func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value []byte, t time.Time, ok bool) { - // value, timestamp, sig - parts := strings.Split(cookie.Value, "|") - if len(parts) != 3 { - return - } - if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) { - ts, err := strconv.Atoi(parts[1]) - if err != nil { - return - } - // The expiration timestamp set when the cookie was created - // isn't sent back by the browser. Hence, we check whether the - // creation timestamp stored in the cookie falls within the - // window defined by (Now()-expiration, Now()]. - t = time.Unix(int64(ts), 0) - if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) { - // it's a valid cookie. now get the contents - rawValue, err := base64.URLEncoding.DecodeString(parts[0]) - if err == nil { - value = rawValue - ok = true - return - } - } - } - return -} - -// SignedValue returns a cookie that is signed and can later be checked with Validate -func SignedValue(seed string, key string, value []byte, now time.Time) string { - encodedValue := base64.URLEncoding.EncodeToString(value) - timeStr := fmt.Sprintf("%d", now.Unix()) - sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr) - cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) - return cookieVal -} - -func cookieSignature(signer func() hash.Hash, args ...string) string { - h := hmac.New(signer, []byte(args[0])) - for _, arg := range args[1:] { - h.Write([]byte(arg)) - } - var b []byte - b = h.Sum(b) - return base64.URLEncoding.EncodeToString(b) -} - -func checkSignature(signature string, args ...string) bool { - checkSig := cookieSignature(sha256.New, args...) - if checkHmac(signature, checkSig) { - return true - } - - // TODO: After appropriate rollout window, remove support for SHA1 - legacySig := cookieSignature(sha1.New, args...) - return checkHmac(signature, legacySig) -} - -func checkHmac(input, expected string) bool { - inputMAC, err1 := base64.URLEncoding.DecodeString(input) - if err1 == nil { - expectedMAC, err2 := base64.URLEncoding.DecodeString(expected) - if err2 == nil { - return hmac.Equal(inputMAC, expectedMAC) - } - } - return false -} - // Cipher provides methods to encrypt and decrypt type Cipher interface { Encrypt(value []byte) ([]byte, error) diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index 074cef44..c66e94c0 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -2,103 +2,13 @@ package encryption import ( "crypto/rand" - "crypto/sha1" - "crypto/sha256" "encoding/base64" - "fmt" "io" "testing" "github.com/stretchr/testify/assert" ) -func TestSecretBytesEncoded(t *testing.T) { - for _, secretSize := range []int{16, 24, 32} { - t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - // We test both padded & raw Base64 to ensure we handle both - // potential user input routes for Base64 - base64Padded := base64.URLEncoding.EncodeToString(secret) - sb := SecretBytes(base64Padded) - assert.Equal(t, secret, sb) - assert.Equal(t, len(sb), secretSize) - - base64Raw := base64.RawURLEncoding.EncodeToString(secret) - sb = SecretBytes(base64Raw) - assert.Equal(t, secret, sb) - assert.Equal(t, len(sb), secretSize) - }) - } -} - -// A string that isn't intended as Base64 and still decodes (but to unintended length) -// will return the original secret as bytes -func TestSecretBytesEncodedWrongSize(t *testing.T) { - for _, secretSize := range []int{15, 20, 28, 33, 44} { - t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - // We test both padded & raw Base64 to ensure we handle both - // potential user input routes for Base64 - base64Padded := base64.URLEncoding.EncodeToString(secret) - sb := SecretBytes(base64Padded) - assert.NotEqual(t, secret, sb) - assert.NotEqual(t, len(sb), secretSize) - // The given secret is returned as []byte - assert.Equal(t, base64Padded, string(sb)) - - base64Raw := base64.RawURLEncoding.EncodeToString(secret) - sb = SecretBytes(base64Raw) - assert.NotEqual(t, secret, sb) - assert.NotEqual(t, len(sb), secretSize) - // The given secret is returned as []byte - assert.Equal(t, base64Raw, string(sb)) - }) - } -} - -func TestSecretBytesNonBase64(t *testing.T) { - trailer := "equals==========" - assert.Equal(t, trailer, string(SecretBytes(trailer))) - - raw16 := "asdflkjhqwer)(*&" - sb16 := SecretBytes(raw16) - assert.Equal(t, raw16, string(sb16)) - assert.Equal(t, 16, len(sb16)) - - raw24 := "asdflkjhqwer)(*&CJEN#$%^" - sb24 := SecretBytes(raw24) - assert.Equal(t, raw24, string(sb24)) - assert.Equal(t, 24, len(sb24)) - - raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&" - sb32 := SecretBytes(raw32) - assert.Equal(t, raw32, string(sb32)) - assert.Equal(t, 32, len(sb32)) -} - -func TestSignAndValidate(t *testing.T) { - seed := "0123456789abcdef" - key := "cookie-name" - value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded")) - epoch := "123456789" - - sha256sig := cookieSignature(sha256.New, seed, key, value, epoch) - sha1sig := cookieSignature(sha1.New, seed, key, value, epoch) - - assert.True(t, checkSignature(sha256sig, seed, key, value, epoch)) - // This should be switched to False after fully deprecating SHA1 - assert.True(t, checkSignature(sha1sig, seed, key, value, epoch)) - - assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) - assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) -} - func TestEncodeAndDecodeAccessToken(t *testing.T) { const secret = "0123456789abcdefghijklmnopqrstuv" const token = "my access token" diff --git a/pkg/encryption/utils.go b/pkg/encryption/utils.go new file mode 100644 index 00000000..0d6bd86a --- /dev/null +++ b/pkg/encryption/utils.go @@ -0,0 +1,107 @@ +package encryption + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "fmt" + "hash" + "net/http" + "strconv" + "strings" + "time" +) + +// SecretBytes attempts to base64 decode the secret, if that fails it treats the secret as binary +func SecretBytes(secret string) []byte { + b, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(secret, "=")) + if err == nil { + // Only return decoded form if a valid AES length + // Don't want unintentional decoding resulting in invalid lengths confusing a user + // that thought they used a 16, 24, 32 length string + for _, i := range []int{16, 24, 32} { + if len(b) == i { + return b + } + } + } + // If decoding didn't work or resulted in non-AES compliant length, + // assume the raw string was the intended secret + return []byte(secret) +} + +// cookies are stored in a 3 part (value + timestamp + signature) to enforce that the values are as originally set. +// additionally, the 'value' is encrypted so it's opaque to the browser + +// Validate ensures a cookie is properly signed +func Validate(cookie *http.Cookie, seed string, expiration time.Duration) (value []byte, t time.Time, ok bool) { + // value, timestamp, sig + parts := strings.Split(cookie.Value, "|") + if len(parts) != 3 { + return + } + if checkSignature(parts[2], seed, cookie.Name, parts[0], parts[1]) { + ts, err := strconv.Atoi(parts[1]) + if err != nil { + return + } + // The expiration timestamp set when the cookie was created + // isn't sent back by the browser. Hence, we check whether the + // creation timestamp stored in the cookie falls within the + // window defined by (Now()-expiration, Now()]. + t = time.Unix(int64(ts), 0) + if t.After(time.Now().Add(expiration*-1)) && t.Before(time.Now().Add(time.Minute*5)) { + // it's a valid cookie. now get the contents + rawValue, err := base64.URLEncoding.DecodeString(parts[0]) + if err == nil { + value = rawValue + ok = true + return + } + } + } + return +} + +// SignedValue returns a cookie that is signed and can later be checked with Validate +func SignedValue(seed string, key string, value []byte, now time.Time) string { + encodedValue := base64.URLEncoding.EncodeToString(value) + timeStr := fmt.Sprintf("%d", now.Unix()) + sig := cookieSignature(sha256.New, seed, key, encodedValue, timeStr) + cookieVal := fmt.Sprintf("%s|%s|%s", encodedValue, timeStr, sig) + return cookieVal +} + +func cookieSignature(signer func() hash.Hash, args ...string) string { + h := hmac.New(signer, []byte(args[0])) + for _, arg := range args[1:] { + h.Write([]byte(arg)) + } + var b []byte + b = h.Sum(b) + return base64.URLEncoding.EncodeToString(b) +} + +func checkSignature(signature string, args ...string) bool { + checkSig := cookieSignature(sha256.New, args...) + if checkHmac(signature, checkSig) { + return true + } + + // TODO: After appropriate rollout window, remove support for SHA1 + legacySig := cookieSignature(sha1.New, args...) + return checkHmac(signature, legacySig) +} + +func checkHmac(input, expected string) bool { + inputMAC, err1 := base64.URLEncoding.DecodeString(input) + if err1 == nil { + expectedMAC, err2 := base64.URLEncoding.DecodeString(expected) + if err2 == nil { + return hmac.Equal(inputMAC, expectedMAC) + } + } + return false +} + diff --git a/pkg/encryption/utils_test.go b/pkg/encryption/utils_test.go new file mode 100644 index 00000000..15bc83fe --- /dev/null +++ b/pkg/encryption/utils_test.go @@ -0,0 +1,100 @@ +package encryption + +import ( + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "encoding/base64" + "fmt" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSecretBytesEncoded(t *testing.T) { + for _, secretSize := range []int{16, 24, 32} { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + // We test both padded & raw Base64 to ensure we handle both + // potential user input routes for Base64 + base64Padded := base64.URLEncoding.EncodeToString(secret) + sb := SecretBytes(base64Padded) + assert.Equal(t, secret, sb) + assert.Equal(t, len(sb), secretSize) + + base64Raw := base64.RawURLEncoding.EncodeToString(secret) + sb = SecretBytes(base64Raw) + assert.Equal(t, secret, sb) + assert.Equal(t, len(sb), secretSize) + }) + } +} + +// A string that isn't intended as Base64 and still decodes (but to unintended length) +// will return the original secret as bytes +func TestSecretBytesEncodedWrongSize(t *testing.T) { + for _, secretSize := range []int{15, 20, 28, 33, 44} { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + // We test both padded & raw Base64 to ensure we handle both + // potential user input routes for Base64 + base64Padded := base64.URLEncoding.EncodeToString(secret) + sb := SecretBytes(base64Padded) + assert.NotEqual(t, secret, sb) + assert.NotEqual(t, len(sb), secretSize) + // The given secret is returned as []byte + assert.Equal(t, base64Padded, string(sb)) + + base64Raw := base64.RawURLEncoding.EncodeToString(secret) + sb = SecretBytes(base64Raw) + assert.NotEqual(t, secret, sb) + assert.NotEqual(t, len(sb), secretSize) + // The given secret is returned as []byte + assert.Equal(t, base64Raw, string(sb)) + }) + } +} + +func TestSecretBytesNonBase64(t *testing.T) { + trailer := "equals==========" + assert.Equal(t, trailer, string(SecretBytes(trailer))) + + raw16 := "asdflkjhqwer)(*&" + sb16 := SecretBytes(raw16) + assert.Equal(t, raw16, string(sb16)) + assert.Equal(t, 16, len(sb16)) + + raw24 := "asdflkjhqwer)(*&CJEN#$%^" + sb24 := SecretBytes(raw24) + assert.Equal(t, raw24, string(sb24)) + assert.Equal(t, 24, len(sb24)) + + raw32 := "asdflkjhqwer)(*&1234lkjhqwer)(*&" + sb32 := SecretBytes(raw32) + assert.Equal(t, raw32, string(sb32)) + assert.Equal(t, 32, len(sb32)) +} + +func TestSignAndValidate(t *testing.T) { + seed := "0123456789abcdef" + key := "cookie-name" + value := base64.URLEncoding.EncodeToString([]byte("I am soooo encoded")) + epoch := "123456789" + + sha256sig := cookieSignature(sha256.New, seed, key, value, epoch) + sha1sig := cookieSignature(sha1.New, seed, key, value, epoch) + + assert.True(t, checkSignature(sha256sig, seed, key, value, epoch)) + // This should be switched to False after fully deprecating SHA1 + assert.True(t, checkSignature(sha1sig, seed, key, value, epoch)) + + assert.False(t, checkSignature(sha256sig, seed, key, "tampered", epoch)) + assert.False(t, checkSignature(sha1sig, seed, key, "tampered", epoch)) +} From 559152a10f5d0eca19564ab5644ce541d4a47cdd Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sun, 10 May 2020 10:15:51 -0700 Subject: [PATCH 07/15] Add subtests inside of encryption unit test loops --- pkg/encryption/cipher_test.go | 179 +++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 79 deletions(-) diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index c66e94c0..1a6b42b8 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -3,6 +3,7 @@ package encryption import ( "crypto/rand" "encoding/base64" + "fmt" "io" "testing" @@ -46,62 +47,76 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { func TestEncryptAndDecrypt(t *testing.T) { // Test our 2 cipher types - for _, initCipher := range []func([]byte) (Cipher, error){NewCFBCipher, NewGCMCipher} { + ciphers := map[string]func([]byte) (Cipher, error){ + "CFB": NewCFBCipher, + "GCM": NewGCMCipher, + } + for name, initCipher := range ciphers { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - c, err := initCipher(secret) - assert.Equal(t, nil, err) - - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) + subTestName := fmt.Sprintf("%s::%d", name, secretSize) + t.Run(subTestName, func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - encrypted, err := c.Encrypt(data) + c, err := initCipher(secret) assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - decrypted, err := c.Decrypt(encrypted) - assert.Equal(t, nil, err) - assert.Equal(t, data, decrypted) - assert.NotEqual(t, encrypted, decrypted) - } + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + } + }) } } } func TestEncryptAndDecryptBase64(t *testing.T) { // Test our cipher types wrapped in Base64 encoder - for _, initCipher := range []func([]byte) (Cipher, error){NewCFBCipher, NewGCMCipher} { + ciphers := map[string]func([]byte) (Cipher, error){ + "CFB": NewCFBCipher, + "GCM": NewGCMCipher, + } + for name, initCipher := range ciphers { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - c, err := NewBase64Cipher(initCipher, secret) - assert.Equal(t, nil, err) - - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) + subTestName := fmt.Sprintf("%s::%d", name, secretSize) + t.Run(subTestName, func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - encrypted, err := c.Encrypt(data) + c, err := NewBase64Cipher(initCipher, secret) assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - decrypted, err := c.Decrypt(encrypted) - assert.Equal(t, nil, err) - assert.Equal(t, data, decrypted) - assert.NotEqual(t, encrypted, decrypted) - } + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + } + }) } } } @@ -150,61 +165,67 @@ func TestIntermixCiphersErrors(t *testing.T) { // Encrypt with GCM, Decrypt with CFB: Results in Garbage data // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - gcm, err := NewGCMCipher(secret) - assert.Equal(t, nil, err) - - cfb, err := NewCFBCipher(secret) - assert.Equal(t, nil, err) - - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) + subTestName := fmt.Sprintf("GCM->CFB::%d", secretSize) + t.Run(subTestName, func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - encrypted, err := gcm.Encrypt(data) + gcm, err := NewGCMCipher(secret) assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - decrypted, err := cfb.Decrypt(encrypted) + cfb, err := NewCFBCipher(secret) assert.Equal(t, nil, err) - // Data is mangled - assert.NotEqual(t, data, decrypted) - assert.NotEqual(t, encrypted, decrypted) - } + + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := gcm.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + decrypted, err := cfb.Decrypt(encrypted) + assert.Equal(t, nil, err) + // Data is mangled + assert.NotEqual(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + } + }) } // Encrypt with CFB, Decrypt with GCM: Results in errors // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - gcm, err := NewGCMCipher(secret) - assert.Equal(t, nil, err) - - cfb, err := NewCFBCipher(secret) - assert.Equal(t, nil, err) - - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) + subTestName := fmt.Sprintf("CFB->GCM::%d", secretSize) + t.Run(subTestName, func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - encrypted, err := cfb.Encrypt(data) + gcm, err := NewGCMCipher(secret) assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - // GCM is authenticated - this should lead to message authentication failed - _, err = gcm.Decrypt(encrypted) - assert.Error(t, err) - } + cfb, err := NewCFBCipher(secret) + assert.Equal(t, nil, err) + + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := cfb.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + // GCM is authenticated - this should lead to message authentication failed + _, err = gcm.Decrypt(encrypted) + assert.Error(t, err) + } + }) } } From e823d874b0e08a037c14f79f58108bc9d09dfac2 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Sun, 10 May 2020 13:24:29 -0700 Subject: [PATCH 08/15] Improve cipher_test.go organization with subtests --- pkg/encryption/cipher.go | 2 +- pkg/encryption/cipher_test.go | 161 +++++++++++++++------------------- 2 files changed, 72 insertions(+), 91 deletions(-) diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index 896031df..1ae521ea 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -50,7 +50,7 @@ func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Ci return &Base64Cipher{Cipher: c}, nil } -// Encrypt encrypts a value with AES CFB & base64 encodes it +// Encrypt encrypts a value with AES CFB & Base64 encodes it func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { encrypted, err := c.Cipher.Encrypt([]byte(value)) if err != nil { diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index 1a6b42b8..f146017f 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -3,7 +3,6 @@ package encryption import ( "crypto/rand" "encoding/base64" - "fmt" "io" "testing" @@ -47,77 +46,55 @@ func TestEncodeAndDecodeAccessTokenB64(t *testing.T) { func TestEncryptAndDecrypt(t *testing.T) { // Test our 2 cipher types - ciphers := map[string]func([]byte) (Cipher, error){ + cipherInits := map[string]func([]byte) (Cipher, error){ "CFB": NewCFBCipher, "GCM": NewGCMCipher, } - for name, initCipher := range ciphers { - // Test all 3 valid AES sizes - for _, secretSize := range []int{16, 24, 32} { - subTestName := fmt.Sprintf("%s::%d", name, secretSize) - t.Run(subTestName, func(t *testing.T) { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - c, err := initCipher(secret) - assert.Equal(t, nil, err) - - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) + for name, initCipher := range cipherInits { + t.Run(name, func(t *testing.T) { + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + t.Run(string(secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) - encrypted, err := c.Encrypt(data) - assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - - decrypted, err := c.Decrypt(encrypted) - assert.Equal(t, nil, err) - assert.Equal(t, data, decrypted) - assert.NotEqual(t, encrypted, decrypted) - } - }) - } - } -} - -func TestEncryptAndDecryptBase64(t *testing.T) { - // Test our cipher types wrapped in Base64 encoder - ciphers := map[string]func([]byte) (Cipher, error){ - "CFB": NewCFBCipher, - "GCM": NewGCMCipher, - } - for name, initCipher := range ciphers { - // Test all 3 valid AES sizes - for _, secretSize := range []int{16, 24, 32} { - subTestName := fmt.Sprintf("%s::%d", name, secretSize) - t.Run(subTestName, func(t *testing.T) { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - c, err := NewBase64Cipher(initCipher, secret) - assert.Equal(t, nil, err) - - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) + // Test Standard & Base64 wrapped + cstd, err := initCipher(secret) assert.Equal(t, nil, err) - encrypted, err := c.Encrypt(data) + cb64, err := NewBase64Cipher(initCipher, secret) assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - decrypted, err := c.Decrypt(encrypted) - assert.Equal(t, nil, err) - assert.Equal(t, data, decrypted) - assert.NotEqual(t, encrypted, decrypted) - } - }) - } + ciphers := map[string]Cipher{ + "Standard": cstd, + "Base64": cb64, + } + + for cName, c := range ciphers { + t.Run(cName, func(t *testing.T) { + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + t.Run(string(dataSize), func(t *testing.T) { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + assert.Equal(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + }) + } + }) + } + }) + } + }) } } @@ -161,12 +138,11 @@ func TestDecryptGCMWrongSecret(t *testing.T) { assert.Error(t, err) } -func TestIntermixCiphersErrors(t *testing.T) { - // Encrypt with GCM, Decrypt with CFB: Results in Garbage data +// Encrypt with GCM, Decrypt with CFB: Results in Garbage data +func TestGCMtoCFBErrors(t *testing.T) { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - subTestName := fmt.Sprintf("GCM->CFB::%d", secretSize) - t.Run(subTestName, func(t *testing.T) { + t.Run(string(secretSize), func(t *testing.T) { secret := make([]byte, secretSize) _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) @@ -179,28 +155,31 @@ func TestIntermixCiphersErrors(t *testing.T) { // Test various sizes sessions might be for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) - assert.Equal(t, nil, err) + t.Run(string(dataSize), func(t *testing.T) { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) - encrypted, err := gcm.Encrypt(data) - assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) + encrypted, err := gcm.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) - decrypted, err := cfb.Decrypt(encrypted) - assert.Equal(t, nil, err) - // Data is mangled - assert.NotEqual(t, data, decrypted) - assert.NotEqual(t, encrypted, decrypted) + decrypted, err := cfb.Decrypt(encrypted) + assert.Equal(t, nil, err) + // Data is mangled + assert.NotEqual(t, data, decrypted) + assert.NotEqual(t, encrypted, decrypted) + }) } }) } +} - // Encrypt with CFB, Decrypt with GCM: Results in errors +// Encrypt with CFB, Decrypt with GCM: Results in errors +func TestCFBtoGCMErrors(t *testing.T) { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - subTestName := fmt.Sprintf("CFB->GCM::%d", secretSize) - t.Run(subTestName, func(t *testing.T) { + t.Run(string(secretSize), func(t *testing.T) { secret := make([]byte, secretSize) _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) @@ -213,17 +192,19 @@ func TestIntermixCiphersErrors(t *testing.T) { // Test various sizes sessions might be for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) - assert.Equal(t, nil, err) + t.Run(string(dataSize), func(t *testing.T) { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) - encrypted, err := cfb.Encrypt(data) - assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) + encrypted, err := cfb.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) - // GCM is authenticated - this should lead to message authentication failed - _, err = gcm.Decrypt(encrypted) - assert.Error(t, err) + // GCM is authenticated - this should lead to message authentication failed + _, err = gcm.Decrypt(encrypted) + assert.Error(t, err) + }) } }) } From 7bb5fc0a810165225a8a56791bcb081febae642b Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 11 May 2020 12:27:27 -0700 Subject: [PATCH 09/15] Ensure Cipher.Decrypt doesn't mangle input ciphertext []byte --- pkg/encryption/cipher.go | 14 ++++++++------ pkg/encryption/cipher_test.go | 8 ++++++++ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index 1ae521ea..d0d31882 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -50,7 +50,7 @@ func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Ci return &Base64Cipher{Cipher: c}, nil } -// Encrypt encrypts a value with AES CFB & Base64 encodes it +// Encrypt encrypts a value with the embedded Cipher & Base64 encodes it func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { encrypted, err := c.Cipher.Encrypt([]byte(value)) if err != nil { @@ -60,7 +60,7 @@ func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { return []byte(base64.StdEncoding.EncodeToString(encrypted)), nil } -// Decrypt Base64 decodes a value & decrypts it with AES CFB +// Decrypt Base64 decodes a value & decrypts it with the embedded Cipher func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { encrypted, err := base64.StdEncoding.DecodeString(string(ciphertext)) if err != nil { @@ -103,12 +103,12 @@ func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { return nil, fmt.Errorf("encrypted value should be at least %d bytes, but is only %d bytes", aes.BlockSize, len(ciphertext)) } - iv := ciphertext[:aes.BlockSize] - ciphertext = ciphertext[aes.BlockSize:] + iv, ciphertext := ciphertext[:aes.BlockSize], ciphertext[aes.BlockSize:] + plaintext := make([]byte, len(ciphertext)) stream := cipher.NewCFBDecrypter(c.Block, iv) - stream.XORKeyStream(ciphertext, ciphertext) + stream.XORKeyStream(plaintext, ciphertext) - return ciphertext, nil + return plaintext, nil } type GCMCipher struct { @@ -135,6 +135,8 @@ func (c *GCMCipher) Encrypt(value []byte) ([]byte, error) { if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return nil, err } + // Using nonce as Seal's dst argument results in it being the first + // chunk of bytes in the ciphertext. Decrypt retrieves the nonce/IV from this. ciphertext := gcm.Seal(nonce, nonce, value, nil) return ciphertext, nil } diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index f146017f..c49fb67a 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -84,9 +84,17 @@ func TestEncryptAndDecrypt(t *testing.T) { assert.Equal(t, nil, err) assert.NotEqual(t, encrypted, data) + // Ensure our Decrypt function doesn't decrypt in place + immutable := make([]byte, len(encrypted)) + copy(immutable, encrypted) + decrypted, err := c.Decrypt(encrypted) assert.Equal(t, nil, err) + // Original data back assert.Equal(t, data, decrypted) + // Decrypt didn't operate in-place on []byte + assert.Equal(t, encrypted, immutable) + // Encrypt/Decrypt actually did something assert.NotEqual(t, encrypted, decrypted) }) } From 9382293b0bcaa2cf492decf363ac588c2d696439 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 11 May 2020 12:33:11 -0700 Subject: [PATCH 10/15] Ensure Cipher.Encrypt doesn't mangle input data []byte --- pkg/encryption/cipher_test.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index c49fb67a..197611ac 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -80,20 +80,26 @@ func TestEncryptAndDecrypt(t *testing.T) { _, err := io.ReadFull(rand.Reader, data) assert.Equal(t, nil, err) + // Ensure our Encrypt function doesn't encrypt in place + immutableData := make([]byte, len(data)) + copy(immutableData, data) + encrypted, err := c.Encrypt(data) assert.Equal(t, nil, err) assert.NotEqual(t, encrypted, data) + // Encrypt didn't operate in-place on []byte + assert.Equal(t, data, immutableData) // Ensure our Decrypt function doesn't decrypt in place - immutable := make([]byte, len(encrypted)) - copy(immutable, encrypted) + immutableEnc := make([]byte, len(encrypted)) + copy(immutableEnc, encrypted) decrypted, err := c.Decrypt(encrypted) assert.Equal(t, nil, err) // Original data back assert.Equal(t, data, decrypted) // Decrypt didn't operate in-place on []byte - assert.Equal(t, encrypted, immutable) + assert.Equal(t, encrypted, immutableEnc) // Encrypt/Decrypt actually did something assert.NotEqual(t, encrypted, decrypted) }) From c6939a40c582ed0d9701b981bde48e7c64a4e186 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 11 May 2020 17:09:00 -0700 Subject: [PATCH 11/15] Move nested Encrypt/Decrypt test to helper function --- pkg/encryption/cipher_test.go | 69 +++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index 197611ac..ca8698a5 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -3,6 +3,7 @@ package encryption import ( "crypto/rand" "encoding/base64" + "fmt" "io" "testing" @@ -54,7 +55,7 @@ func TestEncryptAndDecrypt(t *testing.T) { t.Run(name, func(t *testing.T) { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - t.Run(string(secretSize), func(t *testing.T) { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { secret := make([]byte, secretSize) _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) @@ -75,33 +76,8 @@ func TestEncryptAndDecrypt(t *testing.T) { t.Run(cName, func(t *testing.T) { // Test various sizes sessions might be for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - t.Run(string(dataSize), func(t *testing.T) { - data := make([]byte, dataSize) - _, err := io.ReadFull(rand.Reader, data) - assert.Equal(t, nil, err) - - // Ensure our Encrypt function doesn't encrypt in place - immutableData := make([]byte, len(data)) - copy(immutableData, data) - - encrypted, err := c.Encrypt(data) - assert.Equal(t, nil, err) - assert.NotEqual(t, encrypted, data) - // Encrypt didn't operate in-place on []byte - assert.Equal(t, data, immutableData) - - // Ensure our Decrypt function doesn't decrypt in place - immutableEnc := make([]byte, len(encrypted)) - copy(immutableEnc, encrypted) - - decrypted, err := c.Decrypt(encrypted) - assert.Equal(t, nil, err) - // Original data back - assert.Equal(t, data, decrypted) - // Decrypt didn't operate in-place on []byte - assert.Equal(t, encrypted, immutableEnc) - // Encrypt/Decrypt actually did something - assert.NotEqual(t, encrypted, decrypted) + t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) { + runEncryptAndDecrypt(t, c, dataSize) }) } }) @@ -112,6 +88,35 @@ func TestEncryptAndDecrypt(t *testing.T) { } } +func runEncryptAndDecrypt(t *testing.T, c Cipher, dataSize int) { + data := make([]byte, dataSize) + _, err := io.ReadFull(rand.Reader, data) + assert.Equal(t, nil, err) + + // Ensure our Encrypt function doesn't encrypt in place + immutableData := make([]byte, len(data)) + copy(immutableData, data) + + encrypted, err := c.Encrypt(data) + assert.Equal(t, nil, err) + assert.NotEqual(t, encrypted, data) + // Encrypt didn't operate in-place on []byte + assert.Equal(t, data, immutableData) + + // Ensure our Decrypt function doesn't decrypt in place + immutableEnc := make([]byte, len(encrypted)) + copy(immutableEnc, encrypted) + + decrypted, err := c.Decrypt(encrypted) + assert.Equal(t, nil, err) + // Original data back + assert.Equal(t, data, decrypted) + // Decrypt didn't operate in-place on []byte + assert.Equal(t, encrypted, immutableEnc) + // Encrypt/Decrypt actually did something + assert.NotEqual(t, encrypted, decrypted) +} + func TestDecryptCFBWrongSecret(t *testing.T) { secret1 := []byte("0123456789abcdefghijklmnopqrstuv") secret2 := []byte("9876543210abcdefghijklmnopqrstuv") @@ -156,7 +161,7 @@ func TestDecryptGCMWrongSecret(t *testing.T) { func TestGCMtoCFBErrors(t *testing.T) { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - t.Run(string(secretSize), func(t *testing.T) { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { secret := make([]byte, secretSize) _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) @@ -169,7 +174,7 @@ func TestGCMtoCFBErrors(t *testing.T) { // Test various sizes sessions might be for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - t.Run(string(dataSize), func(t *testing.T) { + t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) { data := make([]byte, dataSize) _, err := io.ReadFull(rand.Reader, data) assert.Equal(t, nil, err) @@ -193,7 +198,7 @@ func TestGCMtoCFBErrors(t *testing.T) { func TestCFBtoGCMErrors(t *testing.T) { // Test all 3 valid AES sizes for _, secretSize := range []int{16, 24, 32} { - t.Run(string(secretSize), func(t *testing.T) { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { secret := make([]byte, secretSize) _, err := io.ReadFull(rand.Reader, secret) assert.Equal(t, nil, err) @@ -206,7 +211,7 @@ func TestCFBtoGCMErrors(t *testing.T) { // Test various sizes sessions might be for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - t.Run(string(dataSize), func(t *testing.T) { + t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) { data := make([]byte, dataSize) _, err := io.ReadFull(rand.Reader, data) assert.Equal(t, nil, err) From e43c65cc76eb898e54d2075f11937ca25aa1b95c Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 25 May 2020 08:16:11 -0700 Subject: [PATCH 12/15] Fix SessionOptions struct spacing --- pkg/apis/options/sessions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index 16b419d6..3b2a4d19 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -4,9 +4,9 @@ import "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" // SessionOptions contains configuration options for the SessionStore providers. type SessionOptions struct { - Type string `flag:"session-store-type" cfg:"session_store_type"` + Type string `flag:"session-store-type" cfg:"session_store_type"` Cipher encryption.Cipher `cfg:",internal"` - Redis RedisStoreOptions `cfg:",squash"` + Redis RedisStoreOptions `cfg:",squash"` } // CookieSessionStoreType is used to indicate the CookieSessionStore should be From 014fa682be348bafce6b29d0a570ee35d44d9ea2 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Mon, 1 Jun 2020 16:19:27 -0700 Subject: [PATCH 13/15] Add EncryptInto/DecryptInto Unit Tests --- CHANGELOG.md | 1 + pkg/apis/sessions/session_state.go | 35 +++------ pkg/apis/sessions/session_state_test.go | 12 +-- pkg/encryption/cipher.go | 55 ++++++++------ pkg/encryption/cipher_test.go | 97 +++++++++++++++++++------ pkg/encryption/utils.go | 1 - 6 files changed, 124 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f35a3b43..f3cca2b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ ## Changes since v5.1.1 +- [#539](https://github.com/oauth2-proxy/oauth2-proxy/pull/539) Refactor encryption ciphers and add AES-GCM support (@NickMeves) - [#601](https://github.com/oauth2-proxy/oauth2-proxy/pull/601) Ensure decrypted user/email are valid UTF8 (@JoelSpeed) - [#560](https://github.com/oauth2-proxy/oauth2-proxy/pull/560) Fallback to UserInfo is User ID claim not present (@JoelSpeed) - [#598](https://github.com/oauth2-proxy/oauth2-proxy/pull/598) acr_values no longer sent to IdP when empty (@ScottGuymer) diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index c014aee2..24377c4a 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -104,24 +104,18 @@ func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) { PreferredUsername: ss.PreferredUsername, } } else { - // Backward compatibility with using unencrypted Email - if ss.Email != "" { - decryptedEmail, errEmail := stringDecrypt(ss.Email, c) - if errEmail == nil { - if !utf8.ValidString(decryptedEmail) { - return nil, errors.New("invalid value for decrypted email") - } - ss.Email = decryptedEmail + // Backward compatibility with using unencrypted Email or User + // Decryption errors will leave original string + err = c.DecryptInto(&ss.Email) + if err == nil { + if !utf8.ValidString(ss.Email) { + return nil, errors.New("invalid value for decrypted email") } } - // Backward compatibility with using unencrypted User - if ss.User != "" { - decryptedUser, errUser := stringDecrypt(ss.User, c) - if errUser == nil { - if !utf8.ValidString(decryptedUser) { - return nil, errors.New("invalid value for decrypted user") - } - ss.User = decryptedUser + err = c.DecryptInto(&ss.User) + if err == nil { + if !utf8.ValidString(ss.User) { + return nil, errors.New("invalid value for decrypted user") } } @@ -139,12 +133,3 @@ func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) { } return &ss, nil } - -// stringDecrypt wraps a Base64Cipher to make it string => string -func stringDecrypt(ciphertext string, c encryption.Cipher) (string, error) { - value, err := c.Decrypt([]byte(ciphertext)) - if err != nil { - return "", err - } - return string(value), nil -} diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go index c6ca0c07..d48ec502 100644 --- a/pkg/apis/sessions/session_state_test.go +++ b/pkg/apis/sessions/session_state_test.go @@ -17,14 +17,14 @@ func timePtr(t time.Time) *time.Time { return &t } -func NewCipher(secret []byte) (encryption.Cipher, error) { +func newTestCipher(secret []byte) (encryption.Cipher, error) { return encryption.NewBase64Cipher(encryption.NewCFBCipher, secret) } func TestSessionStateSerialization(t *testing.T) { - c, err := NewCipher([]byte(secret)) + c, err := newTestCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := NewCipher([]byte(altSecret)) + c2, err := newTestCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ Email: "user@domain.com", @@ -57,9 +57,9 @@ func TestSessionStateSerialization(t *testing.T) { } func TestSessionStateSerializationWithUser(t *testing.T) { - c, err := NewCipher([]byte(secret)) + c, err := newTestCipher([]byte(secret)) assert.Equal(t, nil, err) - c2, err := NewCipher([]byte(altSecret)) + c2, err := newTestCipher([]byte(altSecret)) assert.Equal(t, nil, err) s := &sessions.SessionState{ User: "just-user", @@ -205,7 +205,7 @@ func TestDecodeSessionState(t *testing.T) { eJSON, _ := e.MarshalJSON() eString := string(eJSON) - c, err := NewCipher([]byte(secret)) + c, err := newTestCipher([]byte(secret)) assert.NoError(t, err) testCases := []testCase{ diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index d0d31882..34499ba6 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -13,30 +13,11 @@ import ( type Cipher interface { Encrypt(value []byte) ([]byte, error) Decrypt(ciphertext []byte) ([]byte, error) - EncryptInto(s *string) error + EncryptInto(s *string) error DecryptInto(s *string) error } -type DefaultCipher struct {} - -// Encrypt is a dummy method for CommonCipher.EncryptInto support -func (c *DefaultCipher) Encrypt(value []byte) ([]byte, error) { return value, nil } - -// Decrypt is a dummy method for CommonCipher.DecryptInto support -func (c *DefaultCipher) Decrypt(ciphertext []byte) ([]byte, error) { return ciphertext, nil } - -// EncryptInto encrypts the value and stores it back in the string pointer -func (c *DefaultCipher) EncryptInto(s *string) error { - return into(c.Encrypt, s) -} - -// DecryptInto decrypts the value and stores it back in the string pointer -func (c *DefaultCipher) DecryptInto(s *string) error { - return into(c.Decrypt, s) -} - type Base64Cipher struct { - DefaultCipher Cipher Cipher } @@ -52,7 +33,7 @@ func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Ci // Encrypt encrypts a value with the embedded Cipher & Base64 encodes it func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { - encrypted, err := c.Cipher.Encrypt([]byte(value)) + encrypted, err := c.Cipher.Encrypt(value) if err != nil { return nil, err } @@ -70,8 +51,17 @@ func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { return c.Cipher.Decrypt(encrypted) } +// EncryptInto encrypts the value and stores it back in the string pointer +func (c *Base64Cipher) EncryptInto(s *string) error { + return into(c.Encrypt, s) +} + +// DecryptInto decrypts the value and stores it back in the string pointer +func (c *Base64Cipher) DecryptInto(s *string) error { + return into(c.Decrypt, s) +} + type CFBCipher struct { - DefaultCipher cipher.Block } @@ -111,8 +101,17 @@ func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { return plaintext, nil } +// EncryptInto returns an error since the encrypted data is a []byte that isn't string cast-able +func (c *CFBCipher) EncryptInto(s *string) error { + return fmt.Errorf("CFBCipher is not a string->string compatible cipher") +} + +// EncryptInto returns an error since the encrypted data needs to be a []byte +func (c *CFBCipher) DecryptInto(s *string) error { + return fmt.Errorf("CFBCipher is not a string->string compatible cipher") +} + type GCMCipher struct { - DefaultCipher cipher.Block } @@ -158,6 +157,16 @@ func (c *GCMCipher) Decrypt(ciphertext []byte) ([]byte, error) { return plaintext, nil } +// EncryptInto returns an error since the encrypted data is a []byte that isn't string cast-able +func (c *GCMCipher) EncryptInto(s *string) error { + return fmt.Errorf("CFBCipher is not a string->string compatible cipher") +} + +// EncryptInto returns an error since the encrypted data needs to be a []byte +func (c *GCMCipher) DecryptInto(s *string) error { + return fmt.Errorf("CFBCipher is not a string->string compatible cipher") +} + // codecFunc is a function that takes a string and encodes/decodes it type codecFunc func([]byte) ([]byte, error) diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index ca8698a5..e80986d5 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "fmt" "io" + mathrand "math/rand" "testing" "github.com/stretchr/testify/assert" @@ -117,6 +118,80 @@ func runEncryptAndDecrypt(t *testing.T, c Cipher, dataSize int) { assert.NotEqual(t, encrypted, decrypted) } +func TestEncryptIntoAndDecryptInto(t *testing.T) { + // Test our 2 cipher types + cipherInits := map[string]func([]byte) (Cipher, error){ + "CFB": NewCFBCipher, + "GCM": NewGCMCipher, + } + for name, initCipher := range cipherInits { + t.Run(name, func(t *testing.T) { + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + // Test Standard & Base64 wrapped + cstd, err := initCipher(secret) + assert.Equal(t, nil, err) + + cb64, err := NewBase64Cipher(initCipher, secret) + assert.Equal(t, nil, err) + + ciphers := map[string]Cipher{ + "Standard": cstd, + "Base64": cb64, + } + + for cName, c := range ciphers { + // Check no errors with empty or nil strings + if cName == "Base64" { + empty := "" + assert.Equal(t, nil, c.EncryptInto(&empty)) + assert.Equal(t, nil, c.DecryptInto(&empty)) + assert.Equal(t, nil, c.EncryptInto(nil)) + assert.Equal(t, nil, c.DecryptInto(nil)) + } + + t.Run(cName, func(t *testing.T) { + // Test various sizes sessions might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) { + runEncryptIntoAndDecryptInto(t, c, cName, dataSize) + }) + } + }) + } + }) + } + }) + } +} + +func runEncryptIntoAndDecryptInto(t *testing.T, c Cipher, cipherType string, dataSize int) { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, dataSize) + for i := range b { + b[i] = charset[mathrand.Intn(len(charset))] + } + data := string(b) + originalData := data + + // Base64 is the only cipher that supports string->string Encrypt/Decrypt Into methods + if cipherType == "Base64" { + assert.Equal(t, nil, c.EncryptInto(&data)) + assert.NotEqual(t, originalData, data) + + assert.Equal(t, nil, c.DecryptInto(&data)) + assert.Equal(t, originalData, data) + } else { + assert.NotEqual(t, nil, c.EncryptInto(&data)) + assert.NotEqual(t, nil, c.DecryptInto(&data)) + } +} + func TestDecryptCFBWrongSecret(t *testing.T) { secret1 := []byte("0123456789abcdefghijklmnopqrstuv") secret2 := []byte("9876543210abcdefghijklmnopqrstuv") @@ -228,25 +303,3 @@ func TestCFBtoGCMErrors(t *testing.T) { }) } } - -func TestEncodeIntoAndDecodeIntoAccessToken(t *testing.T) { - const secret = "0123456789abcdefghijklmnopqrstuv" - c, err := NewCipher([]byte(secret)) - assert.Equal(t, nil, err) - - token := "my access token" - originalToken := token - - assert.Equal(t, nil, c.EncryptInto(&token)) - assert.NotEqual(t, originalToken, token) - - assert.Equal(t, nil, c.DecryptInto(&token)) - assert.Equal(t, originalToken, token) - - // Check no errors with empty or nil strings - empty := "" - assert.Equal(t, nil, c.EncryptInto(&empty)) - assert.Equal(t, nil, c.DecryptInto(&empty)) - assert.Equal(t, nil, c.EncryptInto(nil)) - assert.Equal(t, nil, c.DecryptInto(nil)) -} diff --git a/pkg/encryption/utils.go b/pkg/encryption/utils.go index 0d6bd86a..26be6b24 100644 --- a/pkg/encryption/utils.go +++ b/pkg/encryption/utils.go @@ -104,4 +104,3 @@ func checkHmac(input, expected string) bool { } return false } - From 19796275347540d561a1585d5fadab89f8220fa8 Mon Sep 17 00:00:00 2001 From: Nick Meves Date: Thu, 4 Jun 2020 14:39:31 -0700 Subject: [PATCH 14/15] Move Encrypt/Decrypt Into helper to session_state.go This helper method is only applicable for Base64 wrapped encryption since it operated on string -> string primarily. It wouldn't be used for pure CFB/GCM ciphers. After a messagePack session refactor, this method would further only be used for legacy session compatibility - making its placement in cipher.go not ideal. --- pkg/apis/sessions/session_state.go | 25 +++++- pkg/apis/sessions/session_state_test.go | 101 +++++++++++++++++------- pkg/encryption/cipher.go | 73 +++-------------- pkg/encryption/cipher_test.go | 75 ------------------ 4 files changed, 105 insertions(+), 169 deletions(-) diff --git a/pkg/apis/sessions/session_state.go b/pkg/apis/sessions/session_state.go index 24377c4a..44b91bd2 100644 --- a/pkg/apis/sessions/session_state.go +++ b/pkg/apis/sessions/session_state.go @@ -77,7 +77,7 @@ func (s *SessionState) EncodeSessionState(c encryption.Cipher) (string, error) { &ss.IDToken, &ss.RefreshToken, } { - err := c.EncryptInto(s) + err := into(s, c.Encrypt) if err != nil { return "", err } @@ -106,13 +106,13 @@ func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) { } else { // Backward compatibility with using unencrypted Email or User // Decryption errors will leave original string - err = c.DecryptInto(&ss.Email) + err = into(&ss.Email, c.Decrypt) if err == nil { if !utf8.ValidString(ss.Email) { return nil, errors.New("invalid value for decrypted email") } } - err = c.DecryptInto(&ss.User) + err = into(&ss.User, c.Decrypt) if err == nil { if !utf8.ValidString(ss.User) { return nil, errors.New("invalid value for decrypted user") @@ -125,7 +125,7 @@ func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) { &ss.IDToken, &ss.RefreshToken, } { - err := c.DecryptInto(s) + err := into(s, c.Decrypt) if err != nil { return nil, err } @@ -133,3 +133,20 @@ func DecodeSessionState(v string, c encryption.Cipher) (*SessionState, error) { } return &ss, nil } + +// codecFunc is a function that takes a []byte and encodes/decodes it +type codecFunc func([]byte) ([]byte, error) + +func into(s *string, f codecFunc) error { + // Do not encrypt/decrypt nil or empty strings + if s == nil || *s == "" { + return nil + } + + d, err := f([]byte(*s)) + if err != nil { + return err + } + *s = string(d) + return nil +} diff --git a/pkg/apis/sessions/session_state_test.go b/pkg/apis/sessions/session_state_test.go index d48ec502..3e9554c5 100644 --- a/pkg/apis/sessions/session_state_test.go +++ b/pkg/apis/sessions/session_state_test.go @@ -1,11 +1,13 @@ -package sessions_test +package sessions import ( + "crypto/rand" "fmt" + "io" + mathrand "math/rand" "testing" "time" - "github.com/oauth2-proxy/oauth2-proxy/pkg/apis/sessions" "github.com/oauth2-proxy/oauth2-proxy/pkg/encryption" "github.com/stretchr/testify/assert" ) @@ -26,7 +28,7 @@ func TestSessionStateSerialization(t *testing.T) { assert.Equal(t, nil, err) c2, err := newTestCipher([]byte(altSecret)) assert.Equal(t, nil, err) - s := &sessions.SessionState{ + s := &SessionState{ Email: "user@domain.com", PreferredUsername: "user", AccessToken: "token1234", @@ -38,7 +40,7 @@ func TestSessionStateSerialization(t *testing.T) { encoded, err := s.EncodeSessionState(c) assert.Equal(t, nil, err) - ss, err := sessions.DecodeSessionState(encoded, c) + ss, err := DecodeSessionState(encoded, c) t.Logf("%#v", ss) assert.Equal(t, nil, err) assert.Equal(t, "", ss.User) @@ -51,7 +53,7 @@ func TestSessionStateSerialization(t *testing.T) { assert.Equal(t, s.RefreshToken, ss.RefreshToken) // ensure a different cipher can't decode properly (ie: it gets gibberish) - ss, err = sessions.DecodeSessionState(encoded, c2) + ss, err = DecodeSessionState(encoded, c2) t.Logf("%#v", ss) assert.NotEqual(t, nil, err) } @@ -61,7 +63,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) { assert.Equal(t, nil, err) c2, err := newTestCipher([]byte(altSecret)) assert.Equal(t, nil, err) - s := &sessions.SessionState{ + s := &SessionState{ User: "just-user", PreferredUsername: "ju", Email: "user@domain.com", @@ -73,7 +75,7 @@ func TestSessionStateSerializationWithUser(t *testing.T) { encoded, err := s.EncodeSessionState(c) assert.Equal(t, nil, err) - ss, err := sessions.DecodeSessionState(encoded, c) + ss, err := DecodeSessionState(encoded, c) t.Logf("%#v", ss) assert.Equal(t, nil, err) assert.Equal(t, s.User, ss.User) @@ -85,13 +87,13 @@ func TestSessionStateSerializationWithUser(t *testing.T) { assert.Equal(t, s.RefreshToken, ss.RefreshToken) // ensure a different cipher can't decode properly (ie: it gets gibberish) - ss, err = sessions.DecodeSessionState(encoded, c2) + ss, err = DecodeSessionState(encoded, c2) t.Logf("%#v", ss) assert.NotEqual(t, nil, err) } func TestSessionStateSerializationNoCipher(t *testing.T) { - s := &sessions.SessionState{ + s := &SessionState{ Email: "user@domain.com", PreferredUsername: "user", AccessToken: "token1234", @@ -103,7 +105,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) { assert.Equal(t, nil, err) // only email should have been serialized - ss, err := sessions.DecodeSessionState(encoded, nil) + ss, err := DecodeSessionState(encoded, nil) assert.Equal(t, nil, err) assert.Equal(t, "", ss.User) assert.Equal(t, s.Email, ss.Email) @@ -113,7 +115,7 @@ func TestSessionStateSerializationNoCipher(t *testing.T) { } func TestSessionStateSerializationNoCipherWithUser(t *testing.T) { - s := &sessions.SessionState{ + s := &SessionState{ User: "just-user", Email: "user@domain.com", PreferredUsername: "user", @@ -126,7 +128,7 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) { assert.Equal(t, nil, err) // only email should have been serialized - ss, err := sessions.DecodeSessionState(encoded, nil) + ss, err := DecodeSessionState(encoded, nil) assert.Equal(t, nil, err) assert.Equal(t, s.User, ss.User) assert.Equal(t, s.Email, ss.Email) @@ -136,18 +138,18 @@ func TestSessionStateSerializationNoCipherWithUser(t *testing.T) { } func TestExpired(t *testing.T) { - s := &sessions.SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(-1) * time.Minute))} + s := &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(-1) * time.Minute))} assert.Equal(t, true, s.IsExpired()) - s = &sessions.SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Minute))} + s = &SessionState{ExpiresOn: timePtr(time.Now().Add(time.Duration(1) * time.Minute))} assert.Equal(t, false, s.IsExpired()) - s = &sessions.SessionState{} + s = &SessionState{} assert.Equal(t, false, s.IsExpired()) } type testCase struct { - sessions.SessionState + SessionState Encoded string Cipher encryption.Cipher Error bool @@ -163,14 +165,14 @@ func TestEncodeSessionState(t *testing.T) { testCases := []testCase{ { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "just-user", }, Encoded: `{"Email":"user@domain.com","User":"just-user"}`, }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "just-user", AccessToken: "token1234", @@ -185,7 +187,7 @@ func TestEncodeSessionState(t *testing.T) { for i, tc := range testCases { encoded, err := tc.EncodeSessionState(tc.Cipher) - t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, encoded, tc.SessionState, err) + t.Logf("i:%d Encoded:%#vSessionState:%#v Error:%#v", i, encoded, tc.SessionState, err) if tc.Error { assert.Error(t, err) assert.Empty(t, encoded) @@ -210,34 +212,34 @@ func TestDecodeSessionState(t *testing.T) { testCases := []testCase{ { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "just-user", }, Encoded: `{"Email":"user@domain.com","User":"just-user"}`, }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "", }, Encoded: `{"Email":"user@domain.com"}`, }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ User: "just-user", }, Encoded: `{"User":"just-user"}`, }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "just-user", }, Encoded: fmt.Sprintf(`{"Email":"user@domain.com","User":"just-user","AccessToken":"I6s+ml+/MldBMgHIiC35BTKTh57skGX24w==","IDToken":"xojNdyyjB1HgYWh6XMtXY/Ph5eCVxa1cNsklJw==","RefreshToken":"qEX0x6RmASxo4dhlBG6YuRs9Syn/e9sHu/+K","CreatedAt":%s,"ExpiresOn":%s}`, createdString, eString), }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "just-user", AccessToken: "token1234", @@ -250,7 +252,7 @@ func TestDecodeSessionState(t *testing.T) { Cipher: c, }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "just-user", }, @@ -268,7 +270,7 @@ func TestDecodeSessionState(t *testing.T) { Error: true, }, { - SessionState: sessions.SessionState{ + SessionState: SessionState{ Email: "user@domain.com", User: "YmFzZTY0LWVuY29kZWQtdXNlcgo=", // Base64 encoding of base64-encoded-user }, @@ -278,8 +280,8 @@ func TestDecodeSessionState(t *testing.T) { } for i, tc := range testCases { - ss, err := sessions.DecodeSessionState(tc.Encoded, tc.Cipher) - t.Logf("i:%d Encoded:%#vsessions.SessionState:%#v Error:%#v", i, tc.Encoded, ss, err) + ss, err := DecodeSessionState(tc.Encoded, tc.Cipher) + t.Logf("i:%d Encoded:%#vSessionState:%#v Error:%#v", i, tc.Encoded, ss, err) if tc.Error { assert.Error(t, err) assert.Nil(t, ss) @@ -301,7 +303,7 @@ func TestDecodeSessionState(t *testing.T) { } func TestSessionStateAge(t *testing.T) { - ss := &sessions.SessionState{} + ss := &SessionState{} // Created at unset so should be 0 assert.Equal(t, time.Duration(0), ss.Age()) @@ -310,3 +312,44 @@ func TestSessionStateAge(t *testing.T) { ss.CreatedAt = timePtr(time.Now().Add(-1 * time.Hour)) assert.Equal(t, time.Hour, ss.Age().Round(time.Minute)) } + +func TestIntoEncryptAndIntoDecrypt(t *testing.T) { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + // Test all 3 valid AES sizes + for _, secretSize := range []int{16, 24, 32} { + t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { + secret := make([]byte, secretSize) + _, err := io.ReadFull(rand.Reader, secret) + assert.Equal(t, nil, err) + + c, err := newTestCipher(secret) + assert.NoError(t, err) + + // Check no errors with empty or nil strings + empty := "" + assert.Equal(t, nil, into(&empty, c.Encrypt)) + assert.Equal(t, nil, into(&empty, c.Decrypt)) + assert.Equal(t, nil, into(nil, c.Encrypt)) + assert.Equal(t, nil, into(nil, c.Decrypt)) + + // Test various sizes tokens might be + for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { + t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) { + b := make([]byte, dataSize) + for i := range b { + b[i] = charset[mathrand.Intn(len(charset))] + } + data := string(b) + originalData := data + + assert.Equal(t, nil, into(&data, c.Encrypt)) + assert.NotEqual(t, originalData, data) + + assert.Equal(t, nil, into(&data, c.Decrypt)) + assert.Equal(t, originalData, data) + }) + } + }) + } +} diff --git a/pkg/encryption/cipher.go b/pkg/encryption/cipher.go index 34499ba6..c1158b5c 100644 --- a/pkg/encryption/cipher.go +++ b/pkg/encryption/cipher.go @@ -13,11 +13,9 @@ import ( type Cipher interface { Encrypt(value []byte) ([]byte, error) Decrypt(ciphertext []byte) ([]byte, error) - EncryptInto(s *string) error - DecryptInto(s *string) error } -type Base64Cipher struct { +type base64Cipher struct { Cipher Cipher } @@ -28,11 +26,11 @@ func NewBase64Cipher(initCipher func([]byte) (Cipher, error), secret []byte) (Ci if err != nil { return nil, err } - return &Base64Cipher{Cipher: c}, nil + return &base64Cipher{Cipher: c}, nil } // Encrypt encrypts a value with the embedded Cipher & Base64 encodes it -func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { +func (c *base64Cipher) Encrypt(value []byte) ([]byte, error) { encrypted, err := c.Cipher.Encrypt(value) if err != nil { return nil, err @@ -42,7 +40,7 @@ func (c *Base64Cipher) Encrypt(value []byte) ([]byte, error) { } // Decrypt Base64 decodes a value & decrypts it with the embedded Cipher -func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { +func (c *base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { encrypted, err := base64.StdEncoding.DecodeString(string(ciphertext)) if err != nil { return nil, fmt.Errorf("failed to base64 decode value %s", err) @@ -51,17 +49,7 @@ func (c *Base64Cipher) Decrypt(ciphertext []byte) ([]byte, error) { return c.Cipher.Decrypt(encrypted) } -// EncryptInto encrypts the value and stores it back in the string pointer -func (c *Base64Cipher) EncryptInto(s *string) error { - return into(c.Encrypt, s) -} - -// DecryptInto decrypts the value and stores it back in the string pointer -func (c *Base64Cipher) DecryptInto(s *string) error { - return into(c.Decrypt, s) -} - -type CFBCipher struct { +type cfbCipher struct { cipher.Block } @@ -71,11 +59,11 @@ func NewCFBCipher(secret []byte) (Cipher, error) { if err != nil { return nil, err } - return &CFBCipher{Block: c}, err + return &cfbCipher{Block: c}, err } // Encrypt with AES CFB -func (c *CFBCipher) Encrypt(value []byte) ([]byte, error) { +func (c *cfbCipher) Encrypt(value []byte) ([]byte, error) { ciphertext := make([]byte, aes.BlockSize+len(value)) iv := ciphertext[:aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { @@ -88,7 +76,7 @@ func (c *CFBCipher) Encrypt(value []byte) ([]byte, error) { } // Decrypt an AES CFB ciphertext -func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { +func (c *cfbCipher) Decrypt(ciphertext []byte) ([]byte, error) { if len(ciphertext) < aes.BlockSize { return nil, fmt.Errorf("encrypted value should be at least %d bytes, but is only %d bytes", aes.BlockSize, len(ciphertext)) } @@ -101,17 +89,7 @@ func (c *CFBCipher) Decrypt(ciphertext []byte) ([]byte, error) { return plaintext, nil } -// EncryptInto returns an error since the encrypted data is a []byte that isn't string cast-able -func (c *CFBCipher) EncryptInto(s *string) error { - return fmt.Errorf("CFBCipher is not a string->string compatible cipher") -} - -// EncryptInto returns an error since the encrypted data needs to be a []byte -func (c *CFBCipher) DecryptInto(s *string) error { - return fmt.Errorf("CFBCipher is not a string->string compatible cipher") -} - -type GCMCipher struct { +type gcmCipher struct { cipher.Block } @@ -121,11 +99,11 @@ func NewGCMCipher(secret []byte) (Cipher, error) { if err != nil { return nil, err } - return &GCMCipher{Block: c}, err + return &gcmCipher{Block: c}, err } // Encrypt with AES GCM on raw bytes -func (c *GCMCipher) Encrypt(value []byte) ([]byte, error) { +func (c *gcmCipher) Encrypt(value []byte) ([]byte, error) { gcm, err := cipher.NewGCM(c.Block) if err != nil { return nil, err @@ -141,7 +119,7 @@ func (c *GCMCipher) Encrypt(value []byte) ([]byte, error) { } // Decrypt an AES GCM ciphertext -func (c *GCMCipher) Decrypt(ciphertext []byte) ([]byte, error) { +func (c *gcmCipher) Decrypt(ciphertext []byte) ([]byte, error) { gcm, err := cipher.NewGCM(c.Block) if err != nil { return nil, err @@ -156,30 +134,3 @@ func (c *GCMCipher) Decrypt(ciphertext []byte) ([]byte, error) { } return plaintext, nil } - -// EncryptInto returns an error since the encrypted data is a []byte that isn't string cast-able -func (c *GCMCipher) EncryptInto(s *string) error { - return fmt.Errorf("CFBCipher is not a string->string compatible cipher") -} - -// EncryptInto returns an error since the encrypted data needs to be a []byte -func (c *GCMCipher) DecryptInto(s *string) error { - return fmt.Errorf("CFBCipher is not a string->string compatible cipher") -} - -// codecFunc is a function that takes a string and encodes/decodes it -type codecFunc func([]byte) ([]byte, error) - -func into(f codecFunc, s *string) error { - // Do not encrypt/decrypt nil or empty strings - if s == nil || *s == "" { - return nil - } - - d, err := f([]byte(*s)) - if err != nil { - return err - } - *s = string(d) - return nil -} diff --git a/pkg/encryption/cipher_test.go b/pkg/encryption/cipher_test.go index e80986d5..b552e70c 100644 --- a/pkg/encryption/cipher_test.go +++ b/pkg/encryption/cipher_test.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "fmt" "io" - mathrand "math/rand" "testing" "github.com/stretchr/testify/assert" @@ -118,80 +117,6 @@ func runEncryptAndDecrypt(t *testing.T, c Cipher, dataSize int) { assert.NotEqual(t, encrypted, decrypted) } -func TestEncryptIntoAndDecryptInto(t *testing.T) { - // Test our 2 cipher types - cipherInits := map[string]func([]byte) (Cipher, error){ - "CFB": NewCFBCipher, - "GCM": NewGCMCipher, - } - for name, initCipher := range cipherInits { - t.Run(name, func(t *testing.T) { - // Test all 3 valid AES sizes - for _, secretSize := range []int{16, 24, 32} { - t.Run(fmt.Sprintf("%d", secretSize), func(t *testing.T) { - secret := make([]byte, secretSize) - _, err := io.ReadFull(rand.Reader, secret) - assert.Equal(t, nil, err) - - // Test Standard & Base64 wrapped - cstd, err := initCipher(secret) - assert.Equal(t, nil, err) - - cb64, err := NewBase64Cipher(initCipher, secret) - assert.Equal(t, nil, err) - - ciphers := map[string]Cipher{ - "Standard": cstd, - "Base64": cb64, - } - - for cName, c := range ciphers { - // Check no errors with empty or nil strings - if cName == "Base64" { - empty := "" - assert.Equal(t, nil, c.EncryptInto(&empty)) - assert.Equal(t, nil, c.DecryptInto(&empty)) - assert.Equal(t, nil, c.EncryptInto(nil)) - assert.Equal(t, nil, c.DecryptInto(nil)) - } - - t.Run(cName, func(t *testing.T) { - // Test various sizes sessions might be - for _, dataSize := range []int{10, 100, 1000, 5000, 10000} { - t.Run(fmt.Sprintf("%d", dataSize), func(t *testing.T) { - runEncryptIntoAndDecryptInto(t, c, cName, dataSize) - }) - } - }) - } - }) - } - }) - } -} - -func runEncryptIntoAndDecryptInto(t *testing.T, c Cipher, cipherType string, dataSize int) { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, dataSize) - for i := range b { - b[i] = charset[mathrand.Intn(len(charset))] - } - data := string(b) - originalData := data - - // Base64 is the only cipher that supports string->string Encrypt/Decrypt Into methods - if cipherType == "Base64" { - assert.Equal(t, nil, c.EncryptInto(&data)) - assert.NotEqual(t, originalData, data) - - assert.Equal(t, nil, c.DecryptInto(&data)) - assert.Equal(t, originalData, data) - } else { - assert.NotEqual(t, nil, c.EncryptInto(&data)) - assert.NotEqual(t, nil, c.DecryptInto(&data)) - } -} - func TestDecryptCFBWrongSecret(t *testing.T) { secret1 := []byte("0123456789abcdefghijklmnopqrstuv") secret2 := []byte("9876543210abcdefghijklmnopqrstuv") From 43f214ce8bd0b50c9605544f6b2e9e4c7ae6a343 Mon Sep 17 00:00:00 2001 From: Evgeni Gordeev Date: Sun, 14 Jun 2020 08:06:12 -0500 Subject: [PATCH 15/15] Add Keycloak local testing environment (#604) * Adding one more example - keycloak - alongside with dex IDP. * don't expose keycloak and proxy ports to the host * specify email-domain list option in documentation * get rid of nginx and socat to simplify the example as per https://github.com/oauth2-proxy/oauth2-proxy/pull/604#issuecomment-640054390 * get rid of the scripts - use static file for keycloak startup * changelog entry * Update CHANGELOG.md Co-authored-by: Joel Speed --- CHANGELOG.md | 1 + contrib/local-environment/Makefile | 8 + .../docker-compose-keycloak.yaml | 70 + .../keycloak/master-realm.json | 1684 +++++++++++++++++ .../keycloak/master-users-0.json | 27 + .../oauth2-proxy-keycloak.cfg | 20 + docs/configuration/configuration.md | 2 +- 7 files changed, 1811 insertions(+), 1 deletion(-) create mode 100644 contrib/local-environment/docker-compose-keycloak.yaml create mode 100644 contrib/local-environment/keycloak/master-realm.json create mode 100644 contrib/local-environment/keycloak/master-users-0.json create mode 100644 contrib/local-environment/oauth2-proxy-keycloak.cfg diff --git a/CHANGELOG.md b/CHANGELOG.md index f3cca2b0..c051533d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ ## Changes since v5.1.1 +- [#604](https://github.com/oauth2-proxy/oauth2-proxy/pull/604) Add Keycloak local testing environment (@EvgeniGordeev) - [#539](https://github.com/oauth2-proxy/oauth2-proxy/pull/539) Refactor encryption ciphers and add AES-GCM support (@NickMeves) - [#601](https://github.com/oauth2-proxy/oauth2-proxy/pull/601) Ensure decrypted user/email are valid UTF8 (@JoelSpeed) - [#560](https://github.com/oauth2-proxy/oauth2-proxy/pull/560) Fallback to UserInfo is User ID claim not present (@JoelSpeed) diff --git a/contrib/local-environment/Makefile b/contrib/local-environment/Makefile index 0cfeaa66..f3df6d33 100644 --- a/contrib/local-environment/Makefile +++ b/contrib/local-environment/Makefile @@ -13,3 +13,11 @@ nginx-up: .PHONY: nginx-% nginx-%: docker-compose -f docker-compose.yaml -f docker-compose-nginx.yaml $* + +.PHONY: keycloak-up +keycloak-up: + docker-compose -f docker-compose-keycloak.yaml up -d + +.PHONY: keycloak-% +keycloak-%: + docker-compose -f docker-compose-keycloak.yaml $* diff --git a/contrib/local-environment/docker-compose-keycloak.yaml b/contrib/local-environment/docker-compose-keycloak.yaml new file mode 100644 index 00000000..f78ce0fa --- /dev/null +++ b/contrib/local-environment/docker-compose-keycloak.yaml @@ -0,0 +1,70 @@ +# This docker-compose file can be used to bring up an example instance of oauth2-proxy +# for manual testing and exploration of features. +# Alongside OAuth2-Proxy, this file also starts Keycloak to act as the identity provider, +# HTTPBin as an example upstream. +# +# This can either be created using docker-compose +# docker-compose -f docker-compose-keycloak.yaml +# Or: +# make keycloak- (eg. make keycloak-up, make keycloak-down) +# +# Access http://oauth2-proxy.localtest.me:4180 to initiate a login cycle using user=admin@example.com, password=password +# Access http://keycloak.localtest.me:9080 with the same credentials to check out the settings +version: '3.0' +services: + + oauth2-proxy: + container_name: oauth2-proxy + image: quay.io/oauth2-proxy/oauth2-proxy:v5.1.1 + command: --config /oauth2-proxy.cfg + hostname: oauth2-proxy + volumes: + - "./oauth2-proxy-keycloak.cfg:/oauth2-proxy.cfg" + restart: unless-stopped + networks: + keycloak: {} + httpbin: {} + oauth2-proxy: {} + depends_on: + - httpbin + - keycloak + ports: + - 4180:4180/tcp + + httpbin: + container_name: httpbin + image: kennethreitz/httpbin:latest + hostname: httpbin + networks: + httpbin: {} + + keycloak: + container_name: keycloak + image: jboss/keycloak:10.0.0 + hostname: keycloak + command: + [ + '-b', + '0.0.0.0', + '-Djboss.socket.binding.port-offset=1000', + '-Dkeycloak.migration.action=import', + '-Dkeycloak.migration.provider=dir', + '-Dkeycloak.migration.dir=/realm-config', + '-Dkeycloak.migration.strategy=IGNORE_EXISTING', + ] + volumes: + - ./keycloak:/realm-config + environment: + KEYCLOAK_USER: admin@example.com + KEYCLOAK_PASSWORD: password + networks: + keycloak: + aliases: + - keycloak.localtest.me + ports: + - 9080:9080/tcp + +networks: + httpbin: {} + keycloak: {} + oauth2-proxy: {} diff --git a/contrib/local-environment/keycloak/master-realm.json b/contrib/local-environment/keycloak/master-realm.json new file mode 100644 index 00000000..3b9ae7dc --- /dev/null +++ b/contrib/local-environment/keycloak/master-realm.json @@ -0,0 +1,1684 @@ +{ + "id" : "master", + "realm" : "master", + "displayName" : "Keycloak", + "displayNameHtml" : "
Keycloak
", + "notBefore" : 0, + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 60, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "32626c92-4327-40f1-b318-76a6b5c7eee5", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "master", + "attributes" : { } + }, { + "id" : "e36da570-7ae0-4323-8b39-73eb92ce722f", + "name" : "admin", + "description" : "${role_admin}", + "composite" : true, + "composites" : { + "realm" : [ "create-realm" ], + "client" : { + "master-realm" : [ "query-groups", "create-client", "query-realms", "view-authorization", "view-realm", "manage-clients", "query-users", "manage-realm", "view-events", "manage-events", "view-identity-providers", "view-users", "manage-identity-providers", "manage-authorization", "manage-users", "view-clients", "query-clients", "impersonation" ] + } + }, + "clientRole" : false, + "containerId" : "master", + "attributes" : { } + }, { + "id" : "71aca46c-6fcf-4456-ba87-6374e70108a2", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "master", + "attributes" : { } + }, { + "id" : "6ca3fee8-1a3f-4068-a311-6e81223a884b", + "name" : "create-realm", + "description" : "${role_create-realm}", + "composite" : false, + "clientRole" : false, + "containerId" : "master", + "attributes" : { } + } ], + "client" : { + "oauth2-proxy" : [ ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "2cc5e40c-0a28-4c09-85eb-20cd47ac1351", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "380985f1-61c7-4940-93ae-7a09458071ca", + "attributes" : { } + } ], + "master-realm" : [ { + "id" : "a8271c2c-6437-4ca5-ae83-49ea5fe1318d", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "5a7cb1ae-7dac-486b-bf7b-4d7fbc5adb31", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "a9e6a2fa-c31b-4959-bf8a-a46fcc9c65ec", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "1cef34e3-569a-4d2b-ba5c-aafe5c7ab423", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "efc46075-30cd-4600-aa92-2ae4a171d0c2", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "9ffacaf0-afc6-49e9-8708-ef35ac40f3f8", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "90662091-b3bc-4ae4-83c9-a4f53e7e9eeb", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "9a5fbc9d-6fae-4155-86f6-72fd399aa126", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "03f46127-9436-477d-8c7f-58569f45237c", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "f10eaea2-90ab-4310-9d5f-8d986564d061", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "2403e038-2cf7-4b06-b5cb-33a417a00d8d", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "677d057b-66f8-4163-9948-95fdbd06dfdc", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "master-realm" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "dc140fa6-bf2c-49f2-b8c9-fc34ef8a2c63", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "155bf234-4895-4855-95c2-a460518f57e8", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "5441ec71-3eac-4696-9e68-0de54fbdde98", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "2db0f052-cb91-4170-81fd-107756b162f7", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "master-realm" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "e1d7f235-8bf2-40b8-be49-49aca70a5088", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + }, { + "id" : "e743f66a-2f56-4b97-b34b-33f06ff1e739", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "7174c175-1887-4e57-b95b-969fe040deff", + "attributes" : { } + } ], + "account" : [ { + "id" : "64d8f532-839e-4386-b2eb-fe8848b0a9de", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes" : { } + }, { + "id" : "3ec22748-960f-4f96-a43e-50f54a02dc23", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes" : { } + }, { + "id" : "177d18e4-46b0-4ea3-8b70-327486ce5bb2", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes" : { } + }, { + "id" : "703643d6-0542-4e27-9737-7c442925c18c", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes" : { } + }, { + "id" : "c64f9f66-d762-4337-8833-cf31c316e8a7", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes" : { } + }, { + "id" : "611f568b-0fdd-4d2e-ba34-03136cd486c4", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRoles" : [ "offline_access", "uma_authorization" ], + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpSupportedApplications" : [ "FreeOTP", "Google Authenticator" ], + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account" ] + } ] + }, + "clients" : [ { + "id" : "a367038f-fe01-4459-9f91-7ad0cf498533", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/master/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "0896a464-da81-4454-bee9-b56bdbad9e7f", + "defaultRoles" : [ "view-profile", "manage-account" ], + "redirectUris" : [ "/realms/master/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "72f75604-1e21-407c-b967-790aafd11534", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/master/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "91f85142-ee18-4e30-9949-e5acb701bdee", + "redirectUris" : [ "/realms/master/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "2772c101-0dba-49b7-9627-5aaddc666939", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b13fd0de-3be0-4a08-bc5d-d1de34421b1a", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "4640af2e-b4a6-44eb-85ec-6278a62a4f01", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "380985f1-61c7-4940-93ae-7a09458071ca", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "65d2ba2b-bcae-49ff-9f56-77c818f55930", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7174c175-1887-4e57-b95b-969fe040deff", + "clientId" : "master-realm", + "name" : "master Realm", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "40f73851-a94c-4091-90de-aeee8ca1acf8", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "attributes" : { }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, + { + "id": "0493c7c6-6e20-49ea-9acb-627c0b52d400", + "clientId": "oauth2-proxy", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "72341b6d-7065-4518-a0e4-50ee15025608", + "redirectUris": [ + "http://oauth2-proxy.localtest.me:4180/oauth2/callback" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, { + "id" : "2a3ad1fd-a30d-4b72-89c4-bed12f178338", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/master/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "secret" : "b234b7aa-8417-410f-b3fd-c57434d3aa4a", + "redirectUris" : [ "/admin/master/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "5885b0d3-a917-4b52-8380-f37d0754a2ef", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "47ea3b67-4f0c-4c7e-8ac6-a33a3d655894", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${addressScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "4be0ca19-0ec7-4cc1-b263-845ea539ff12", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "aba72e57-540f-4825-95b7-2d143be028cc", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${emailScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "7fe82724-5748-4b6d-9708-a028f5d3b970", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "e42f334e-cfae-44a0-905d-c3ef215feaae", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "ec765598-bd71-4318-86c3-b3f81a41c99e", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "90694036-4014-4672-a2c8-c68319e9308a", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + }, { + "id" : "f7b0fcc0-6139-4158-ac45-34fd9a58a5ef", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "multivalued" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "8a09267b-3634-4a9c-baab-6f2fb4137347", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "3a48c5dd-33a8-4be0-9d2e-30fd7f98363a", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${phoneScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "5427d1b4-ba79-412a-b23c-da640a98980c", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "31d4a53f-6503-40e8-bd9d-79a7c46c4fbe", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "5921a9e9-7fec-4471-95e3-dd96eebdec58", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${profileScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "4fa92092-ee0d-4dc7-a63b-1e3b02d35ebb", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "1a5cc2e2-c983-4150-8583-23a7f5c826bf", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "67931f77-722a-492d-b581-a953e26b7d44", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "10f6ac36-3a63-4e1c-ac69-c095588f5967", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "205d9dce-b6c8-4b1d-9c9c-fa24788651cf", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "638216c8-ea8c-40e3-9429-771e9278920e", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "39c17eae-8ea7-422c-ae21-b8876bf12184", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "01c559cf-94f2-46ad-b965-3b2e1db1a2a6", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "String" + } + }, { + "id" : "1693b5ab-28eb-485d-835d-2ae070ccb3ba", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "a0e08332-954c-46d2-9795-56eb31132580", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "cea0cd9c-d085-4d19-acc3-4bb41c891b68", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "3122097d-4cba-46c2-8b3b-5d87a4cc605e", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "a3b97897-d913-4e0a-a4cf-033ce78f7d24", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "a44eeb9d-410d-49c5-b0e0-5d84787627ad", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "651408a7-6704-4198-a60f-988821b633ea", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "a8c56c7b-ccbc-4b01-8df5-3ecb6328755f", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "13ec0fd3-e64a-4d6f-9be7-c8760f2c9d6b", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "true", + "consent.screen.text" : "${rolesScopeConsentText}" + }, + "protocolMappers" : [ { + "id" : "75e741f8-dcd5-49d2-815e-8604ec1d08a1", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "06a2d506-4996-4a33-8c43-2cf64af6a630", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "3c3470df-d414-4e1c-87fc-3fb3cea34b8d", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ] + }, { + "id" : "d85aba25-c74b-49e3-9ccb-77b4bb16efa5", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false", + "consent.screen.text" : "" + }, + "protocolMappers" : [ { + "id" : "86b3f64f-1525-4500-bcbc-9b889b25f995", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { } + } ] + } ], + "defaultDefaultClientScopes" : [ "roles", "profile", "role_list", "email", "web-origins" ], + "defaultOptionalClientScopes" : [ "phone", "address", "offline_access", "microprofile-jwt" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "xXSSProtection" : "1; mode=block", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "59048b39-ad0f-4d12-8c52-7cfc2c43278a", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper" ] + } + }, { + "id" : "760559a6-a59f-4175-9ac5-6f3612e20129", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "24f4cb42-76bd-499e-812a-4e0d270c9e13", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "abbfc599-480a-44ef-8e33-73a83eaab166", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "3c6450f0-4521-402b-a247-c8165854b1fa", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "d9b64399-744b-498e-9d35-f68b1582bd7d", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "22f15f1f-3116-4348-a1e5-fc0d7576452a", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "4ad7b291-ddbb-4674-8c3d-ab8fd76d4168", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "f71cc325-9907-4d27-a0e6-88fca7450e5e", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "6c7d982e-372f-49c6-a4f3-5c451fb85eca" ], + "secret" : [ "yH6M3W7aOgh2_cKJ0srWbw" ], + "priority" : [ "100" ] + } + }, { + "id" : "7b50d0ab-dda5-4624-aa42-b4b397724ce1", + "name" : "hmac-generated", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "587f0fb5-845d-4b45-87a0-84145092aaef" ], + "secret" : [ "PuH8Lxh9GeNfGJRDk34SWIlBDdrJpC3U3SfcxqqQtlIf2DBzRKUu8VbDVrmMN5b5CoPsJhrQ2SVb-iE9Lzsb3A" ], + "priority" : [ "100" ], + "algorithm" : [ "HS256" ] + } + }, { + "id" : "547c1c71-9f97-4e12-801b-ed5c2cc61bba", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAjdo2HZ5ruNnIbkSeAfFYpbPvJw3vtz/VuKJerC4mUXYd7qRMhs3VLJZ3mFyeCuO8W81vkGrFiC9KQnX2lHj2dtA/RWEJw5bpz+JdOFr5pvXg0lQ0sa+hro9afWDygTU4FmLsEi5z98847TbH178RT6n7+JVqZ9jYU9rSpwVTC8E/4yxSuStmhGCcAkZ6dGhHNBdvGUgwxKYj7dYLRJiI+nilIdKuxPzxI/YZxZnXBHDdbNXJgDymTQPut99OnBxeZbH38CJ1MNo3VdV1fzOMGUHe+vn/EOD5E+pXC8PwvJnWU+XHUTFVZeyIXehh3pYLUsq/6bZ1MYsEaFIhznOkwwIDAQABAoIBAHB+64fVyUxRurhoRn737fuLlU/9p2xGfbHtYvNdrhnQeLB3MBGAT10K/1Gfsd6k+Q49AAsiAgGcr2HBt4nL3HohcOwOpvWsS0UIGjHFRFP6jw9+pEN+K9UJ7xObvPZnRFHMpbdNi76tYlINrbMV3h61ihR8OmSc/gKSeZjnihK5OkaNnlqGRaBM/koI+iAxUHuJPnBLBZmD4T8eIfE4S2TvUeVeQogI9Muvnb9tIPJ5XyP9iXWLdRjnek/+wTdxHHZuo06Tc0bMjRaTHiF6K9ntOM2EmQb6bS2J47zgzRLNFE22BWH7RJq659EzElkOn0C0k7dWDTur/3Lpx1+zxJECgYEA8t+J3J+9oGTFmY2VPH05Yr/R/iVDOyIOlO1CmgonOQ3KPhbfNBB3aTAJP20LOZChN4JoWuiZJg4UwzXOeY9DvdDkPO0YLlSjPAIwJNk+xcxFcp5hqMUul2db+cgEY8zp0Wg9kFOq3JmJjK4+1+fgsVnOB+B08ZYI6bZzsUVKzucCgYEAlYTrsxs6fQua0cvZNQPYNZzwF3LVwPBl/ntkdRBE3HGyZeCAhIj7e9pAUusCPsQJaCmW2UEmywD/aIxGrBkojzTKItshM3PN1PYKL8W0Zq+H67uF5KfdvsbabZWHfP/LGCpoKF8Ov7JVPPqGrZ03Z2SheeLZAtNeHN4OB1u9i8UCgYATkS7qN3Rvl67T0DRVy0D0U7/3Wckw2m2SUgsrneXLEvFYTz9sUmdMcjJMidx9pslWT4tYx6SPDFNf5tXbtU8f29SHlBJ+qRL9oq9+SIJmLS7rLRdxIXG/gPRIC3VPFRNBa8SJ/DOn0jbivqcRffz8TN/sgojpbc0KB0kK3ypHwQKBgCKVCcb1R0PgyUA4+9YNO5a647UotFPZxl1jwMpqpuKt0WtKz67X2AK/ah1DidNmmB5lcCRzsztE0c4mk7n+X6kvtoj1UeqKoFLfTV/bRGxzsOZPCxrl0J3tdFvgN+QrbZf7Rvf/dHPWFWzzLO8+66+YUNjWJQdIR/45Rdlh2KdZAoGBAMfF3ir+fe3KdQ6hAf9QyrLxJ5l+GO+IgtxXGbon7eeJBIZHHdMeDy4pC7DMcI214BmIntbyY+xS+gI3oM26EJUVmrZ6tkyIDFsCHm9rcXG9ogvffzQWM1Wqzm27hR/3s+EPWW9AOcIimiFV1UPp/mLjnrCuq58V2aJS/TT14oLe" ], + "certificate" : [ "MIICmzCCAYMCBgFygL/j4DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjAwNjA0MTkxMDU4WhcNMzAwNjA0MTkxMjM4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCN2jYdnmu42chuRJ4B8Vils+8nDe+3P9W4ol6sLiZRdh3upEyGzdUslneYXJ4K47xbzW+QasWIL0pCdfaUePZ20D9FYQnDlunP4l04Wvmm9eDSVDSxr6Guj1p9YPKBNTgWYuwSLnP3zzjtNsfXvxFPqfv4lWpn2NhT2tKnBVMLwT/jLFK5K2aEYJwCRnp0aEc0F28ZSDDEpiPt1gtEmIj6eKUh0q7E/PEj9hnFmdcEcN1s1cmAPKZNA+63306cHF5lsffwInUw2jdV1XV/M4wZQd76+f8Q4PkT6lcLw/C8mdZT5cdRMVVl7Ihd6GHelgtSyr/ptnUxiwRoUiHOc6TDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIAqydMYxa51kNEyfXyR2kStlglE4LDeLBLHDABeBPE0eN2awoH/mw3kXS4OA/C0e3c7bAwViOzOVERGeUNiBvP5rL1Amuu97nwFcxhkTaJH4ZwCGkxceaIo9LNDpAEesqHLQSdplFXIA4TbEFoKMem4k31KVU7i9/rUesrSRmxLptIOK7LLvRMYiY/t7tdAvoZAtoliuQlFKQywEuxXQrCkcoVEAARABWGt0rsWC2xK0tVxHRIrENwvMp/aUYd17sZ0403aaS9dlvfQ63ExnaHd+++RJtPku8P220Tw27YVmFAwzJgS0aUpEaDsgRNz6OMSyxEg/n7eKK08aU3szwQ=" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "3253f9b7-905d-4458-ad8a-8ada5e16d195", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 20, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "75bd854e-ab99-46f1-90ed-a8bfc1559558", + "alias" : "Authentication Options", + "description" : "Authentication options.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "basic-auth", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "basic-auth-otp", + "requirement" : "DISABLED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-spnego", + "requirement" : "DISABLED", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "9b0e6cce-62c5-4fb6-a48d-e07c950e38c3", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-otp-form", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "1c26fd14-ac06-4dc1-bdd8-8c34c1b41720", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "254f7549-51ec-4565-a736-35c07b6e25f0", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-otp-form", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "b2413da8-3de9-4bfe-b77e-643fd1964c8f", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "REQUIRED", + "priority" : 20, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "f8392bfb-8dce-4a16-8af1-b2a4d1a0a273", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-otp", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "fb69c297-b26e-44fa-aabd-d7b40eec3cd3", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 20, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "de3a41a9-7018-4931-9c4d-d04f9501b2ce", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "CONDITIONAL", + "priority" : 20, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "6526b0d1-b48e-46c6-bb08-11ebcf458def", + "alias" : "browser", + "description" : "browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "auth-spnego", + "requirement" : "DISABLED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "identity-provider-redirector", + "requirement" : "ALTERNATIVE", + "priority" : 25, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "ALTERNATIVE", + "priority" : 30, + "flowAlias" : "forms", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "92a653ba-8f2d-4283-8354-ca55f9d89181", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "requirement" : "ALTERNATIVE", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-jwt", + "requirement" : "ALTERNATIVE", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-secret-jwt", + "requirement" : "ALTERNATIVE", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "client-x509", + "requirement" : "ALTERNATIVE", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "e365be39-78db-46f0-b2e8-4e7001c2f5d0", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "direct-grant-validate-password", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "CONDITIONAL", + "priority" : 30, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "dd61caf5-a40f-48b7-9e8c-a1f3b67041dd", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "7a055643-62e1-4ac1-b126-9a8d6c299635", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "REQUIRED", + "priority" : 20, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "fe8bc7ee-6e8f-436e-8336-c60fcd350843", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "CONDITIONAL", + "priority" : 20, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "3646f08e-ab70-415b-a701-6ed2e2d214c9", + "alias" : "http challenge", + "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "no-cookie-redirect", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "REQUIRED", + "priority" : 20, + "flowAlias" : "Authentication Options", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "04176530-0972-47ad-83df-19d8534caac2", + "alias" : "registration", + "description" : "registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "requirement" : "REQUIRED", + "priority" : 10, + "flowAlias" : "registration form", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "fa0ed569-6746-439e-b07e-89f7ed918c07", + "alias" : "registration form", + "description" : "registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-profile-action", + "requirement" : "REQUIRED", + "priority" : 40, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-password-action", + "requirement" : "REQUIRED", + "priority" : 50, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "registration-recaptcha-action", + "requirement" : "DISABLED", + "priority" : 60, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + }, { + "id" : "03680917-28f3-4ccd-bdf6-4a516f7c0018", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-credential-email", + "requirement" : "REQUIRED", + "priority" : 20, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "authenticator" : "reset-password", + "requirement" : "REQUIRED", + "priority" : 30, + "userSetupAllowed" : false, + "autheticatorFlow" : false + }, { + "requirement" : "CONDITIONAL", + "priority" : 40, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false, + "autheticatorFlow" : true + } ] + }, { + "id" : "19a9d9aa-2d2b-4701-807f-c384ab921c7e", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "requirement" : "REQUIRED", + "priority" : 10, + "userSetupAllowed" : false, + "autheticatorFlow" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "534f01f4-45b3-43a0-91d1-238860cc126d", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "65bb9337-9633-4a21-8f6f-1d4129f664ac", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "terms_and_conditions", + "name" : "Terms and Conditions", + "providerId" : "terms_and_conditions", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "attributes" : { }, + "keycloakVersion" : "10.0.0", + "userManagedAccessAllowed" : false +} diff --git a/contrib/local-environment/keycloak/master-users-0.json b/contrib/local-environment/keycloak/master-users-0.json new file mode 100644 index 00000000..54d66160 --- /dev/null +++ b/contrib/local-environment/keycloak/master-users-0.json @@ -0,0 +1,27 @@ +{ + "realm" : "master", + "users" : [ { + "id" : "3356c0a0-d4d5-4436-9c5a-2299c71c08ec", + "createdTimestamp" : 1591297959169, + "username" : "admin@example.com", + "email" : "admin@example.com", + "enabled" : true, + "totp" : false, + "emailVerified" : true, + "credentials" : [ { + "id" : "a1a06ecd-fdc0-4e67-92cd-2da22d724e32", + "type" : "password", + "createdDate" : 1591297959315, + "secretData" : "{\"value\":\"6rt5zuqHVHopvd0FTFE0CYadXTtzY0mDY2BrqnNQGS51/7DfMJeGgj0roNnGMGvDv30imErNmiSOYl+cL9jiIA==\",\"salt\":\"LI0kqr09JB7J9wvr2Hxzzg==\"}", + "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "offline_access", "admin", "uma_authorization" ], + "clientRoles" : { + "account" : [ "view-profile", "manage-account" ] + }, + "notBefore" : 0, + "groups" : [ ] + } ] +} diff --git a/contrib/local-environment/oauth2-proxy-keycloak.cfg b/contrib/local-environment/oauth2-proxy-keycloak.cfg new file mode 100644 index 00000000..6620b8ad --- /dev/null +++ b/contrib/local-environment/oauth2-proxy-keycloak.cfg @@ -0,0 +1,20 @@ +http_address="0.0.0.0:4180" +cookie_secret="OQINaROshtE9TcZkNAm-5Zs2Pv3xaWytBmc5W7sPX7w=" +email_domains=["example.com"] +cookie_secure="false" +upstreams="http://httpbin" +cookie_domains=[".localtest.me"] # Required so cookie can be read on all subdomains. +whitelist_domains=[".localtest.me"] # Required to allow redirection back to original requested target. + +# keycloak provider +client_secret="72341b6d-7065-4518-a0e4-50ee15025608" +client_id="oauth2-proxy" +redirect_url="http://oauth2-proxy.localtest.me:4180/oauth2/callback" + +# in this case oauth2-proxy is going to visit +# http://keycloak.localtest.me:9080/auth/realms/master/.well-known/openid-configuration for configuration +oidc_issuer_url="http://keycloak.localtest.me:9080/auth/realms/master" +provider="oidc" +provider_display_name="Keycloak" + + diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index d4e6fc5b..dfe1e85f 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -44,7 +44,7 @@ An example [oauth2-proxy.cfg]({{ site.gitweb }}/contrib/oauth2-proxy.cfg.example | `--cookie-samesite` | string | set SameSite cookie attribute (ie: `"lax"`, `"strict"`, `"none"`, or `""`). | `""` | | `--custom-templates-dir` | string | path to custom html templates | | | `--display-htpasswd-form` | bool | display username / password login form if an htpasswd file is provided | true | -| `--email-domain` | string | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | | +| `--email-domain` | string \| list | authenticate emails with the specified domain (may be given multiple times). Use `*` to authenticate any email | | | `--extra-jwt-issuers` | string | if `--skip-jwt-bearer-tokens` is set, a list of extra JWT `issuer=audience` pairs (where the issuer URL has a `.well-known/openid-configuration` or a `.well-known/jwks.json`) | | | `--exclude-logging-paths` | string | comma separated list of paths to exclude from logging, eg: `"/ping,/path2"` |`""` (no paths excluded) | | `--flush-interval` | duration | period between flushing response buffers when streaming responses | `"1s"` |