mirror of
https://github.com/oauth2-proxy/oauth2-proxy.git
synced 2024-11-24 08:52:25 +02:00
f648c54d87
* 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 408ba1a1a5
.
* 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>
211 lines
6.1 KiB
Go
211 lines
6.1 KiB
Go
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
|
|
|
|
// 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.
|
|
// 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
|
|
}
|
|
|
|
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
|
|
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,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
http.SetCookie(rw, ticketCookie)
|
|
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) {
|
|
http.SetCookie(rw, cookies.MakeCookieFromOptions(
|
|
req,
|
|
t.options.Name,
|
|
"",
|
|
t.options,
|
|
time.Hour*-1,
|
|
time.Now(),
|
|
))
|
|
}
|
|
|
|
// makeCookie makes a cookie, signing the value if present
|
|
func (t *ticket) makeCookie(req *http.Request, value string, expires time.Duration, now time.Time) (*http.Cookie, error) {
|
|
if value != "" {
|
|
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,
|
|
), 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
|
|
}
|