1
0
mirror of https://github.com/oauth2-proxy/oauth2-proxy.git synced 2024-11-24 08:52:25 +02:00
oauth2-proxy/pkg/sessions/persistence/ticket.go

211 lines
6.1 KiB
Go
Raw Normal View History

package persistence
import (
"crypto/aes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/options"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/cookies"
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/encryption"
)
// saveFunc performs a persistent store's save functionality using
// a key string, value []byte & (optional) expiration time.Duration
type saveFunc func(string, []byte, time.Duration) error
// loadFunc performs a load from a persistent store using a
// string key and returning the stored value as []byte
type loadFunc func(string) ([]byte, error)
// clearFunc performs a persistent store's clear functionality using
// a string key for the target of the deletion.
type clearFunc func(string) error
Add redis lock feature (#1063) * Add sensible logging flag to default setup for logger * Add Redis lock * Fix default value flag for sensitive logging * Split RefreshSessionIfNeeded in two methods and use Redis lock * Small adjustments to doc and code * Remove sensible logging * Fix method names in ticket.go * Revert "Fix method names in ticket.go" This reverts commit 408ba1a1a5c55a3cad507a0be8634af1977769cb. * Fix methods name in ticket.go * Remove block in Redis client get * Increase lock time to 1 second * Perform retries, if session store is locked * Reverse if condition, because it should return if session does not have to be refreshed * Update go.sum * Update MockStore * Return error if loading session fails * Fix and update tests * Change validSession to session in docs and strings * Change validSession to session in docs and strings * Fix docs * Fix wrong field name * Fix linting * Fix imports for linting * Revert changes except from locking functionality * Add lock feature on session state * Update from master * Remove errors package, because it is not used * Only pass context instead of request to lock * Use lock key * By default use NoOpLock * Remove debug output * Update ticket_test.go * Map internal error to sessions error * Add ErrLockNotObtained * Enable lock peek for all redis clients * Use lock key prefix consistent * Fix imports * Use exists method for peek lock * Fix imports * Fix imports * Fix imports * Remove own Dockerfile * Fix imports * Fix tests for ticket and session store * Fix session store test * Update pkg/apis/sessions/interfaces.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Do not wrap lock method Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Use errors package for lock constants * Use better naming for initLock function * Add comments * Add session store lock test * Fix tests * Fix tests * Fix tests * Fix tests * Add cookies after saving session * Add mock lock * Fix imports for mock_lock.go * Store mock lock for key * Apply elapsed time on mock lock * Check if lock is initially applied * Reuse existing lock * Test all lock methods * Update CHANGELOG.md * Use redis client methods in redis.lock for release an refresh * Use lock key suffix instead of prefix for lock key * Add comments for Lock interface * Update comment for Lock interface * Update CHANGELOG.md * Change LockSuffix to const * Check lock on already loaded session * Use global var for loadedSession in lock tests * Use lock instance for refreshing and releasing of lock * Update possible error type for Refresh Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2021-06-02 20:08:19 +02:00
// initLockFunc returns a lock object for a persistent store using a
// string key
type initLockFunc func(string) sessions.Lock
// ticket is a structure representing the ticket used in server based
// session storage. It provides a unique per session decryption secret giving
// more security than the shared CookieSecret.
type ticket struct {
id string
secret []byte
options *options.Cookie
}
// newTicket creates a new ticket. The ID & secret will be randomly created
// with 16 byte sizes. The ID will be prefixed & hex encoded.
func newTicket(cookieOpts *options.Cookie) (*ticket, error) {
rawID := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, rawID); err != nil {
return nil, fmt.Errorf("failed to create new ticket ID: %v", err)
}
// ticketID is hex encoded
ticketID := fmt.Sprintf("%s-%s", cookieOpts.Name, hex.EncodeToString(rawID))
secret := make([]byte, aes.BlockSize)
if _, err := io.ReadFull(rand.Reader, secret); err != nil {
return nil, fmt.Errorf("failed to create encryption secret: %v", err)
}
return &ticket{
id: ticketID,
secret: secret,
options: cookieOpts,
}, nil
}
// encodeTicket encodes the Ticket to a string for usage in cookies
func (t *ticket) encodeTicket() string {
return fmt.Sprintf("%s.%s", t.id, base64.RawURLEncoding.EncodeToString(t.secret))
}
// decodeTicket decodes an encoded ticket string
func decodeTicket(encTicket string, cookieOpts *options.Cookie) (*ticket, error) {
ticketParts := strings.Split(encTicket, ".")
if len(ticketParts) != 2 {
return nil, errors.New("failed to decode ticket")
}
ticketID, secretBase64 := ticketParts[0], ticketParts[1]
secret, err := base64.RawURLEncoding.DecodeString(secretBase64)
if err != nil {
return nil, fmt.Errorf("failed to decode encryption secret: %v", err)
}
return &ticket{
id: ticketID,
secret: secret,
options: cookieOpts,
}, nil
}
// decodeTicketFromRequest retrieves a potential ticket cookie from a request
// and decodes it to a ticket.
func decodeTicketFromRequest(req *http.Request, cookieOpts *options.Cookie) (*ticket, error) {
requestCookie, err := req.Cookie(cookieOpts.Name)
if err != nil {
// Don't wrap this error to allow `err == http.ErrNoCookie` checks
return nil, err
}
// An existing cookie exists, try to retrieve the ticket
val, _, ok := encryption.Validate(requestCookie, cookieOpts.Secret, cookieOpts.Expire)
if !ok {
return nil, fmt.Errorf("session ticket cookie failed validation: %v", err)
}
// Valid cookie, decode the ticket
return decodeTicket(string(val), cookieOpts)
}
// saveSession encodes the SessionState with the ticket's secret and persists
// it to disk via the passed saveFunc.
func (t *ticket) saveSession(s *sessions.SessionState, saver saveFunc) error {
c, err := t.makeCipher()
if err != nil {
return err
}
ciphertext, err := s.EncodeSessionState(c, false)
if err != nil {
return fmt.Errorf("failed to encode the session state with the ticket: %v", err)
}
return saver(t.id, ciphertext, t.options.Expire)
}
// loadSession loads a session from the disk store via the passed loadFunc
// using the ticket.id as the key. It then decodes the SessionState using
// ticket.secret to make the AES-GCM cipher.
Add redis lock feature (#1063) * Add sensible logging flag to default setup for logger * Add Redis lock * Fix default value flag for sensitive logging * Split RefreshSessionIfNeeded in two methods and use Redis lock * Small adjustments to doc and code * Remove sensible logging * Fix method names in ticket.go * Revert "Fix method names in ticket.go" This reverts commit 408ba1a1a5c55a3cad507a0be8634af1977769cb. * Fix methods name in ticket.go * Remove block in Redis client get * Increase lock time to 1 second * Perform retries, if session store is locked * Reverse if condition, because it should return if session does not have to be refreshed * Update go.sum * Update MockStore * Return error if loading session fails * Fix and update tests * Change validSession to session in docs and strings * Change validSession to session in docs and strings * Fix docs * Fix wrong field name * Fix linting * Fix imports for linting * Revert changes except from locking functionality * Add lock feature on session state * Update from master * Remove errors package, because it is not used * Only pass context instead of request to lock * Use lock key * By default use NoOpLock * Remove debug output * Update ticket_test.go * Map internal error to sessions error * Add ErrLockNotObtained * Enable lock peek for all redis clients * Use lock key prefix consistent * Fix imports * Use exists method for peek lock * Fix imports * Fix imports * Fix imports * Remove own Dockerfile * Fix imports * Fix tests for ticket and session store * Fix session store test * Update pkg/apis/sessions/interfaces.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Do not wrap lock method Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Use errors package for lock constants * Use better naming for initLock function * Add comments * Add session store lock test * Fix tests * Fix tests * Fix tests * Fix tests * Add cookies after saving session * Add mock lock * Fix imports for mock_lock.go * Store mock lock for key * Apply elapsed time on mock lock * Check if lock is initially applied * Reuse existing lock * Test all lock methods * Update CHANGELOG.md * Use redis client methods in redis.lock for release an refresh * Use lock key suffix instead of prefix for lock key * Add comments for Lock interface * Update comment for Lock interface * Update CHANGELOG.md * Change LockSuffix to const * Check lock on already loaded session * Use global var for loadedSession in lock tests * Use lock instance for refreshing and releasing of lock * Update possible error type for Refresh Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2021-06-02 20:08:19 +02:00
// finally it appends a lock implementation
func (t *ticket) loadSession(loader loadFunc, initLock initLockFunc) (*sessions.SessionState, error) {
ciphertext, err := loader(t.id)
if err != nil {
return nil, fmt.Errorf("failed to load the session state with the ticket: %v", err)
}
c, err := t.makeCipher()
if err != nil {
return nil, err
}
2020-11-08 00:58:47 +02:00
Add redis lock feature (#1063) * Add sensible logging flag to default setup for logger * Add Redis lock * Fix default value flag for sensitive logging * Split RefreshSessionIfNeeded in two methods and use Redis lock * Small adjustments to doc and code * Remove sensible logging * Fix method names in ticket.go * Revert "Fix method names in ticket.go" This reverts commit 408ba1a1a5c55a3cad507a0be8634af1977769cb. * Fix methods name in ticket.go * Remove block in Redis client get * Increase lock time to 1 second * Perform retries, if session store is locked * Reverse if condition, because it should return if session does not have to be refreshed * Update go.sum * Update MockStore * Return error if loading session fails * Fix and update tests * Change validSession to session in docs and strings * Change validSession to session in docs and strings * Fix docs * Fix wrong field name * Fix linting * Fix imports for linting * Revert changes except from locking functionality * Add lock feature on session state * Update from master * Remove errors package, because it is not used * Only pass context instead of request to lock * Use lock key * By default use NoOpLock * Remove debug output * Update ticket_test.go * Map internal error to sessions error * Add ErrLockNotObtained * Enable lock peek for all redis clients * Use lock key prefix consistent * Fix imports * Use exists method for peek lock * Fix imports * Fix imports * Fix imports * Remove own Dockerfile * Fix imports * Fix tests for ticket and session store * Fix session store test * Update pkg/apis/sessions/interfaces.go Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Do not wrap lock method Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk> * Use errors package for lock constants * Use better naming for initLock function * Add comments * Add session store lock test * Fix tests * Fix tests * Fix tests * Fix tests * Add cookies after saving session * Add mock lock * Fix imports for mock_lock.go * Store mock lock for key * Apply elapsed time on mock lock * Check if lock is initially applied * Reuse existing lock * Test all lock methods * Update CHANGELOG.md * Use redis client methods in redis.lock for release an refresh * Use lock key suffix instead of prefix for lock key * Add comments for Lock interface * Update comment for Lock interface * Update CHANGELOG.md * Change LockSuffix to const * Check lock on already loaded session * Use global var for loadedSession in lock tests * Use lock instance for refreshing and releasing of lock * Update possible error type for Refresh Co-authored-by: Joel Speed <Joel.speed@hotmail.co.uk>
2021-06-02 20:08:19 +02:00
sessionState, err := sessions.DecodeSessionState(ciphertext, c, false)
if err != nil {
return nil, err
}
lock := initLock(t.id)
sessionState.Lock = lock
return sessionState, nil
}
// clearSession uses the passed clearFunc to delete a session stored with a
// key of ticket.id
func (t *ticket) clearSession(clearer clearFunc) error {
return clearer(t.id)
}
// setCookie sets the encoded ticket as a cookie
2020-07-21 03:18:17 +02:00
func (t *ticket) setCookie(rw http.ResponseWriter, req *http.Request, s *sessions.SessionState) error {
ticketCookie, err := t.makeCookie(
req,
t.encodeTicket(),
t.options.Expire,
*s.CreatedAt,
)
2020-07-21 03:18:17 +02:00
if err != nil {
return err
}
http.SetCookie(rw, ticketCookie)
2020-07-21 03:18:17 +02:00
return nil
}
// clearCookie removes any cookies that would be where this ticket
// would set them
func (t *ticket) clearCookie(rw http.ResponseWriter, req *http.Request) {
2020-07-21 03:18:17 +02:00
http.SetCookie(rw, cookies.MakeCookieFromOptions(
req,
2020-07-21 03:18:17 +02:00
t.options.Name,
"",
2020-07-21 03:18:17 +02:00
t.options,
time.Hour*-1,
time.Now(),
2020-07-21 03:18:17 +02:00
))
}
// makeCookie makes a cookie, signing the value if present
2020-07-21 03:18:17 +02:00
func (t *ticket) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) (*http.Cookie, error) {
if value != "" {
2020-07-21 03:18:17 +02:00
var err error
value, err = encryption.SignedValue(t.options.Secret, t.options.Name, []byte(value), now)
if err != nil {
return nil, err
}
}
return cookies.MakeCookieFromOptions(
req,
t.options.Name,
value,
t.options,
expires,
now,
2020-07-21 03:18:17 +02:00
), nil
}
// makeCipher makes a AES-GCM cipher out of the ticket's secret
func (t *ticket) makeCipher() (encryption.Cipher, error) {
c, err := encryption.NewGCMCipher(t.secret)
if err != nil {
return nil, fmt.Errorf("failed to make an AES-GCM cipher from the ticket secret: %v", err)
}
return c, nil
}